diff options
68 files changed, 1783 insertions, 569 deletions
@@ -12,7 +12,7 @@ What is GNU MediaGoblin? * Federated with OStatus! * Customizable! * A place for people to collaborate and show off original and derived - creations. Free, as in freedom. We’re a GNU project in the making, + creations. Free, as in freedom. We’re a GNU project in the making, afterall. @@ -34,5 +34,5 @@ Where is the documentation? =========================== Documentation is located in the ``docs/`` directory in a "raw" -restructured-text form. It is also mirrored at +restructured-text form. It is also available at http://docs.mediagoblin.org/ in HTML form. diff --git a/destroy_environment.py b/destroy_environment.py deleted file mode 100755 index bbdeffe9..00000000 --- a/destroy_environment.py +++ /dev/null @@ -1,22 +0,0 @@ -#!./bin/python - -import pymongo -import sys, os - -print "*** WARNING! ***" -print " Running this will destroy your mediagoblin database," -print " remove all your media files in user_dev/, etc." - -drop_it = raw_input( - 'Are you SURE you want to destroy your environment? (if so, type "yes")> ') - -if not drop_it == 'yes': - sys.exit(1) - -conn = pymongo.Connection() -conn.drop_database('mediagoblin') - -os.popen('rm -rf user_dev/media') -os.popen('rm -rf user_dev/beaker') - -print "removed all your stuff! okay, now re-run ./bin/buildout" diff --git a/docs/deploymenthowto.rst b/docs/deploymenthowto.rst index d943e276..f50edfb6 100644 --- a/docs/deploymenthowto.rst +++ b/docs/deploymenthowto.rst @@ -10,4 +10,7 @@ Step 2: ? Step 3: Write the deployment guide and profit! -But seriously, this is a stub since we're not quite there, yet. +But seriously, this is a stub since we're not quite there (yet) but if +you want to see where we are now, you can try to run the latest +development version by following the instructions at +:ref:`hacking-howto`. diff --git a/docs/git.rst b/docs/git.rst index 73e7a311..bd0f9d52 100644 --- a/docs/git.rst +++ b/docs/git.rst @@ -63,6 +63,10 @@ Further, if you isolate your changes to a branch, then you can work on multiple issues at the same time and they don't conflict with one another. +Name your branches using the isue number and something that makes it clear +what it's about. For example, if you were working on tagging, you +might name your branch ``360_tagging``. + Properly document your changes ------------------------------ diff --git a/docs/hackinghowto.rst b/docs/hackinghowto.rst index 914a5135..caafba53 100644 --- a/docs/hackinghowto.rst +++ b/docs/hackinghowto.rst @@ -60,7 +60,7 @@ requirements:: On Fedora:: yum install mongodb-server python-paste-deploy python-paste-script \ - git-core python python-devel + git-core python python-devel python-lxml .. YouCanHelp:: @@ -85,7 +85,7 @@ After installing the requirements, follow these steps: 1. Clone the repository:: - git clone http://git.gitorious.org/mediagoblin/mediagoblin.git + git clone git://gitorious.org/mediagoblin/mediagoblin.git 2. Bootstrap and run buildout:: @@ -194,7 +194,26 @@ If it's installed, check the mongodb log. On my machine, that's old lock file: /var/lib/mongodb/mongod.lock. probably means... -Then delete the lock file and relaunch mongodb. +in that case you might have had an unclean shutdown. Try:: + + sudo mongod --repair + +If that didn't work, just delete the lock file and relaunch mongodb. + +Anyway, then start the mongodb server in whatever way is appropriate +for your distro / OS. + + +pkg_resources.DistributionNotFound: distribute +---------------------------------------------- + +If you get this while running buildout:: + + pkg_resources.DistributionNotFound: distribute + +Try this commmand instead:: + + python bootstrap.py --distribute && ./bin/buildout Wiping your user data diff --git a/lazyserver.sh b/lazyserver.sh index 4f10f771..e4afdaa5 100755 --- a/lazyserver.sh +++ b/lazyserver.sh @@ -18,18 +18,18 @@ if [ "$1" = "-h" ] then - echo "$0 [-h] [-c paste.ini] ARGS_to_paster" - echo " For example:" - echo " $0 -c fcgi.ini port_number=23371" - exit 1 + echo "$0 [-h] [-c paste.ini] ARGS_to_paster" + echo " For example:" + echo " $0 -c fcgi.ini port_number=23371" + exit 1 fi PASTE_INI=paste.ini if [ "$1" = "-c" ] then - PASTE_INI="$2" - shift - shift + PASTE_INI="$2" + shift + shift fi if [ -f ./bin/paster ]; then diff --git a/maketarball.sh b/maketarball.sh index 2ee78016..5ded9671 100755 --- a/maketarball.sh +++ b/maketarball.sh @@ -1,29 +1,48 @@ #!/bin/bash -# usage: maketarball -# maketarball <tag> +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc # -# With no arguments, this creates a source tarball from git master with a -# filename based on today's date. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# With a <tag> argument, this creates a tarball of the tag. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +# usage: maketarball [-d] <rev-ish> +# +# Creates a tarball from a rev-ish. If -d is passed in, then it adds +# the date to the directory name. # # Examples: # -# ./maketarball +# ./maketarball -d master # ./maketarball v0.0.2 +if [[ -z "$1" ]]; then + echo "Usage: $0 [-d] <rev-ish>"; + exit 1; +fi + NOWDATE=`date "+%Y-%m-%d"` -if [ -z "$1" ] -then - REVISH=master +if [[ $@ == *-d* ]]; then + REVISH=$2 PREFIX="$NOWDATE-$REVISH" else REVISH=$1 PREFIX="$REVISH" fi + # convert PREFIX to all lowercase. # nix the v from tag names. PREFIX=`echo "$PREFIX" | tr '[A-Z]' '[a-z]' | sed s/v//` @@ -54,4 +73,4 @@ gzip mediagoblin-$PREFIX.tar echo "archive at mediagoblin-$PREFIX.tar.gz" -echo "done."
\ No newline at end of file +echo "done." diff --git a/mediagoblin.ini b/mediagoblin.ini index 596107dc..e889646a 100644 --- a/mediagoblin.ini +++ b/mediagoblin.ini @@ -8,6 +8,9 @@ email_sender_address = "notice@mediagoblin.example.org" # set to false to enable sending notices email_debug_mode = true +# Set to false to disable registrations +allow_registration = true + ## Uncomment this to put some user-overriding templates here #local_templates = %(here)s/user_dev/templates/ diff --git a/mediagoblin/app.py b/mediagoblin/app.py index 9454b403..c1ee3d77 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -20,18 +20,12 @@ import urllib import routes from webob import Request, exc -from mediagoblin import routing, util, storage, staticdirect -from mediagoblin.init.config import ( - read_mediagoblin_config, generate_validation_report) -from mediagoblin.db.open import setup_connection_and_db_from_config +from mediagoblin import routing, util from mediagoblin.mg_globals import setup_globals from mediagoblin.init.celery import setup_celery_from_config -from mediagoblin.init import get_jinja_loader -from mediagoblin.workbench import WorkbenchManager - - -class Error(Exception): pass -class ImproperlyConfigured(Error): pass +from mediagoblin.init import (get_jinja_loader, get_staticdirector, + setup_global_and_app_config, setup_workbench, setup_database, + setup_storage) class MediaGoblinApp(object): @@ -55,49 +49,27 @@ class MediaGoblinApp(object): ############## # Open and setup the config - global_config, validation_result = read_mediagoblin_config(config_path) - app_config = global_config['mediagoblin'] - # report errors if necessary - validation_report = generate_validation_report( - global_config, validation_result) - if validation_report: - raise ImproperlyConfigured(validation_report) + global_config, app_config = setup_global_and_app_config(config_path) ########################################## # Setup other connections / useful objects ########################################## # Set up the database - self.connection, self.db = setup_connection_and_db_from_config( - app_config) + self.connection, self.db = setup_database() # Get the template environment self.template_loader = get_jinja_loader( app_config.get('user_template_path')) # Set up storage systems - self.public_store = storage.storage_system_from_config( - app_config, 'publicstore') - self.queue_store = storage.storage_system_from_config( - app_config, 'queuestore') + self.public_store, self.queue_store = setup_storage() # set up routing self.routing = routing.get_mapper() # set up staticdirector tool - if app_config.has_key('direct_remote_path'): - self.staticdirector = staticdirect.RemoteStaticDirect( - app_config['direct_remote_path'].strip()) - elif app_config.has_key('direct_remote_paths'): - direct_remote_path_lines = app_config[ - 'direct_remote_paths'].strip().splitlines() - self.staticdirector = staticdirect.MultiRemoteStaticDirect( - dict([line.strip().split(' ', 1) - for line in direct_remote_path_lines])) - else: - raise ImproperlyConfigured( - "One of direct_remote_path or " - "direct_remote_paths must be provided") + self.staticdirector = get_staticdirector(app_config) # Setup celery, if appropriate if setup_celery and not app_config.get('celery_setup_elsewhere'): @@ -116,22 +88,11 @@ class MediaGoblinApp(object): # object. ####################################################### - setup_globals( - app_config=app_config, - global_config=global_config, - - # TODO: No need to set these two up as globals, we could - # just read them out of mg_globals.app_config - email_sender_address=app_config['email_sender_address'], - email_debug_mode=app_config['email_debug_mode'], - - # Actual, useful to everyone objects - app=self, - db_connection=self.connection, - database=self.db, - public_store=self.public_store, - queue_store=self.queue_store, - workbench_manager=WorkbenchManager(app_config['workbench_path'])) + setup_globals(app = self) + + # Workbench *currently* only used by celery, so this only + # matters in always eager mode :) + setup_workbench() def __call__(self, environ, start_response): request = Request(environ) diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py index 08bbdd16..6d1aec49 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/auth/lib.py @@ -112,7 +112,7 @@ def send_verification_email(user, request): # TODO: There is no error handling in place send_email( - mg_globals.email_sender_address, + mg_globals.app_config['email_sender_address'], [user['email']], # TODO # Due to the distributed nature of GNU MediaGoblin, we should diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py index 46c585d2..9547b3ea 100644 --- a/mediagoblin/auth/routing.py +++ b/mediagoblin/auth/routing.py @@ -19,18 +19,12 @@ from routes.route import Route auth_routes = [ Route('mediagoblin.auth.register', '/register/', controller='mediagoblin.auth.views:register'), - Route('mediagoblin.auth.register_success', '/register/success/', - template='mediagoblin/auth/register_success.html', - controller='mediagoblin.views:simple_template_render'), Route('mediagoblin.auth.login', '/login/', controller='mediagoblin.auth.views:login'), Route('mediagoblin.auth.logout', '/logout/', controller='mediagoblin.auth.views:logout'), Route('mediagoblin.auth.verify_email', '/verify_email/', controller='mediagoblin.auth.views:verify_email'), - Route('mediagoblin.auth.verify_email_notice', '/verification_required/', - template='mediagoblin/auth/verification_needed.html', - controller='mediagoblin.views:simple_template_render'), Route('mediagoblin.auth.resend_verification', '/resend_verification/', controller='mediagoblin.auth.views:resend_activation'), Route('mediagoblin.auth.resend_verification_success', diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 2450023f..fb5db870 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -19,6 +19,7 @@ import uuid from webob import exc from mediagoblin import messages +from mediagoblin import mg_globals from mediagoblin.util import render_to_response, redirect from mediagoblin.db.util import ObjectId from mediagoblin.auth import lib as auth_lib @@ -30,6 +31,14 @@ def register(request): """ Your classic registration view! """ + # Redirects to indexpage if registrations are disabled + if not mg_globals.app_config["allow_registration"]: + messages.add_message( + request, + messages.WARNING, + ('Sorry, registration is disabled on this instance.')) + return redirect(request, "index") + register_form = auth_forms.RegistrationForm(request.POST) if request.method == 'POST' and register_form.validate(): @@ -46,16 +55,25 @@ def register(request): else: # Create the user - entry = request.db.User() - entry['username'] = request.POST['username'].lower() - entry['email'] = request.POST['email'] - entry['pw_hash'] = auth_lib.bcrypt_gen_password_hash( + user = request.db.User() + user['username'] = request.POST['username'].lower() + user['email'] = request.POST['email'] + user['pw_hash'] = auth_lib.bcrypt_gen_password_hash( request.POST['password']) - entry.save(validate=True) - - send_verification_email(entry, request) + user.save(validate=True) + + # log the user in + request.session['user_id'] = unicode(user['_id']) + request.session.save() - return redirect(request, "mediagoblin.auth.register_success") + # send verification email + send_verification_email(user, request) + + # redirect the user to their homepage... there will be a + # message waiting for them to verify their email + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=user['username']) return render_to_response( request, @@ -98,13 +116,14 @@ def login(request): 'mediagoblin/auth/login.html', {'login_form': login_form, 'next': request.GET.get('next') or request.POST.get('next'), - 'login_failed': login_failed}) + 'login_failed': login_failed, + 'allow_registration': mg_globals.app_config["allow_registration"]}) def logout(request): # Maybe deleting the user_id parameter would be enough? request.session.delete() - + return redirect(request, "index") @@ -128,16 +147,16 @@ def verify_email(request): user.save() verification_successful = True messages.add_message( - request, - messages.SUCCESS, + request, + messages.SUCCESS, ('Your email address has been verified. ' 'You may now login, edit your profile, and submit images!')) else: verification_successful = False - messages.add_message(request, - messages.ERROR, - 'The verification key or user id is incorrect') - + messages.add_message(request, + messages.ERROR, + 'The verification key or user id is incorrect') + return render_to_response( request, 'mediagoblin/user_pages/user.html', @@ -156,4 +175,10 @@ def resend_activation(request): send_verification_email(request.user, request) - return redirect(request, 'mediagoblin.auth.resend_verification_success') + messages.add_message( + request, + messages.INFO, + 'Resent your verification email.') + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=request.user['username']) diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 5aae6439..a296f0c1 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -1,7 +1,7 @@ [mediagoblin] # database stuff db_host = string() -db_name = string() +db_name = string(default="mediagoblin") db_port = integer() # @@ -21,6 +21,9 @@ direct_remote_path = string(default="/mgoblin_static/") email_debug_mode = boolean(default=True) email_sender_address = string(default="notice@mediagoblin.example.org") +# Set to false to disable registrations +allow_registration = boolean(default=True) + # tag parsing tags_delimiter = string(default=",") tags_case_sensitive = boolean(default=False) @@ -78,4 +81,4 @@ celeryd_eta_scheduler_precision = float() # known lists celery_routes = string_list() -celery_imports = string_list()
\ No newline at end of file +celery_imports = string_list() diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py index d0e11311..30d43c98 100644 --- a/mediagoblin/db/indexes.py +++ b/mediagoblin/db/indexes.py @@ -45,11 +45,13 @@ REQUIRED READING: To remove deprecated indexes ---------------------------- -Removing deprecated indexes is easier, just do: +Removing deprecated indexes is the same, just move the index into the +deprecated indexes mapping. -INACTIVE_INDEXES = { - 'collection_name': [ - 'deprecated_index_identifier1', 'deprecated_index_identifier2']} +DEPRECATED_INDEXES = { + 'collection_name': { + 'deprecated_index_identifier1': { + 'index': [index_foo_goes_here]}} ... etc. diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 712f8ab4..6a8ebcf9 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -14,56 +14,41 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from mediagoblin.db.util import RegisterMigration from mediagoblin.util import cleaned_markdown_conversion -from mongokit import DocumentMigration +# Please see mediagoblin/tests/test_migrations.py for some examples of +# basic migrations. -class MediaEntryMigration(DocumentMigration): - def allmigration01_uploader_to_reference(self): - """ - Old MediaEntry['uploader'] accidentally embedded the User instead - of referencing it. Fix that! - """ - # uploader is an associative array - self.target = {'uploader': {'$type': 3}} - if not self.status: - for doc in self.collection.find(self.target): - self.update = { - '$set': { - 'uploader': doc['uploader']['_id']}} - self.collection.update( - self.target, self.update, multi=True, safe=True) - def allmigration02_add_description_html(self): - """ - Now that we can have rich descriptions via Markdown, we should - update all existing entries to record the rich description versions. - """ - self.target = {'description_html': {'$exists': False}, - 'description': {'$exists': True}} +@RegisterMigration(1) +def user_add_bio_html(database): + """ + Users now have richtext bios via Markdown, reflect appropriately. + """ + collection = database['users'] - if not self.status: - for doc in self.collection.find(self.target): - self.update = { - '$set': { - 'description_html': cleaned_markdown_conversion( - doc['description'])}} - -class UserMigration(DocumentMigration): - def allmigration01_add_bio_and_url_profile(self): - """ - User can elaborate profile with home page and biography - """ - self.target = {'url': {'$exists': False}, - 'bio': {'$exists': False}} - if not self.status: - for doc in self.collection.find(self.target): - self.update = { - '$set': {'url': '', - 'bio': ''}} - self.collection.update( - self.target, self.update, multi=True, safe=True) - - -MIGRATE_CLASSES = ['MediaEntry', 'User'] + target = collection.find( + {'bio_html': {'$exists': False}}) + + for document in target: + document['bio_html'] = cleaned_markdown_conversion( + document['bio']) + collection.save(document) + + +@RegisterMigration(2) +def mediaentry_mediafiles_main_to_original(database): + """ + Rename "main" media file to "original". + """ + collection = database['media_entries'] + target = collection.find( + {'media_files.main': {'$exists': True}}) + + for document in target: + original = document['media_files'].pop('main') + document['media_files']['original'] = original + + collection.save(document) diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 8fcbb208..4ef2d928 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -16,13 +16,16 @@ import datetime, uuid -from mongokit import Document, Set +from mongokit import Document from mediagoblin import util from mediagoblin.auth import lib as auth_lib from mediagoblin import mg_globals from mediagoblin.db import migrations from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId +from mediagoblin.util import Pagination +from mediagoblin.util import DISPLAY_IMAGE_FETCHING_ORDER + ################### # Custom validators @@ -34,6 +37,32 @@ from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId class User(Document): + """ + A user of MediaGoblin. + + Structure: + - username: The username of this user, should be unique to this instance. + - email: Email address of this user + - created: When the user was created + - plugin_data: a mapping of extra plugin information for this User. + Nothing uses this yet as we don't have plugins, but someday we + might... :) + - pw_hash: Hashed version of user's password. + - email_verified: Whether or not the user has verified their email or not. + Most parts of the site are disabled for users who haven't yet. + - status: whether or not the user is active, etc. Currently only has two + values, 'needs_email_verification' or 'active'. (In the future, maybe + we'll change this to a boolean with a key of 'active' and have a + separate field for a reason the user's been disabled if that's + appropriate... email_verified is already separate, after all.) + - verification_key: If the user is awaiting email verification, the user + will have to provide this key (which will be encoded in the presented + URL) in order to confirm their email as active. + - is_admin: Whether or not this user is an administrator or not. + - url: this user's personal webpage/website, if appropriate. + - bio: biography of this user (plaintext, in markdown) + - bio_html: biography of the user converted to proper HTML. + """ __collection__ = 'users' structure = { @@ -47,7 +76,8 @@ class User(Document): 'verification_key': unicode, 'is_admin': bool, 'url' : unicode, - 'bio' : unicode + 'bio' : unicode, # May contain markdown + 'bio_html': unicode, # May contain plaintext, or HTML } required_fields = ['username', 'created', 'pw_hash', 'email'] @@ -58,8 +88,6 @@ class User(Document): 'status': u'needs_email_verification', 'verification_key': lambda: unicode(uuid.uuid4()), 'is_admin': False} - - migration_handler = migrations.UserMigration def check_login(self, password): """ @@ -70,6 +98,80 @@ class User(Document): class MediaEntry(Document): + """ + Record of a piece of media. + + Structure: + - uploader: A reference to a User who uploaded this. + + - title: Title of this work + + - slug: A normalized "slug" which can be used as part of a URL to retrieve + this work, such as 'my-works-name-in-slug-form' may be viewable by + 'http://mg.example.org/u/username/m/my-works-name-in-slug-form/' + Note that since URLs are constructed this way, slugs must be unique + per-uploader. (An index is provided to enforce that but code should be + written on the python side to ensure this as well.) + + - created: Date and time of when this piece of work was uploaded. + + - description: Uploader-set description of this work. This can be marked + up with MarkDown for slight fanciness (links, boldness, italics, + paragraphs...) + + - description_html: Rendered version of the description, run through + Markdown and cleaned with our cleaning tool. + + - media_type: What type of media is this? Currently we only support + 'image' ;) + + - media_data: Extra information that's media-format-dependent. + For example, images might contain some EXIF data that's not appropriate + to other formats. You might store it like: + + mediaentry['media_data']['exif'] = { + 'manufacturer': 'CASIO', + 'model': 'QV-4000', + 'exposure_time': .659} + + Alternately for video you might store: + + # play length in seconds + mediaentry['media_data']['play_length'] = 340 + + ... so what's appropriate here really depends on the media type. + + - plugin_data: a mapping of extra plugin information for this User. + Nothing uses this yet as we don't have plugins, but someday we + might... :) + + - tags: A list of tags. Each tag is stored as a dictionary that has a key + for the actual name and the normalized name-as-slug, so ultimately this + looks like: + [{'name': 'Gully Gardens', + 'slug': 'gully-gardens'}, + {'name': 'Castle Adventure Time?!", + 'slug': 'castle-adventure-time'}] + + - state: What's the state of this file? Active, inactive, disabled, etc... + But really for now there are only two states: + "unprocessed": uploaded but needs to go through processing for display + "processed": processed and able to be displayed + + - queued_media_file: storage interface style filepath describing a file + queued for processing. This is stored in the mg_globals.queue_store + storage system. + + - media_files: Files relevant to this that have actually been processed + and are available for various types of display. Stored like: + {'thumb': ['dir1', 'dir2', 'pic.png'} + + - attachment_files: A list of "attachment" files, ones that aren't + critical to this piece of media but may be usefully relevant to people + viewing the work. (currently unused.) + + - thumbnail_file: Deprecated... we should remove this ;) + """ __collection__ = 'media_entries' structure = { @@ -106,12 +208,28 @@ class MediaEntry(Document): 'created': datetime.datetime.utcnow, 'state': u'unprocessed'} - migration_handler = migrations.MediaEntryMigration - def get_comments(self): return self.db.MediaComment.find({ 'media_entry': self['_id']}).sort('created', DESCENDING) + def get_display_media(self, media_map, fetch_order=DISPLAY_IMAGE_FETCHING_ORDER): + """ + Find the best media for display. + + Args: + - media_map: a dict like + {u'image_size': [u'dir1', u'dir2', u'image.jpg']} + - fetch_order: the order we should try fetching images in + + Returns: + (media_size, media_path) + """ + media_sizes = media_map.keys() + + for media_size in DISPLAY_IMAGE_FETCHING_ORDER: + if media_size in media_sizes: + return media_map[media_size] + def main_mediafile(self): pass @@ -120,7 +238,7 @@ class MediaEntry(Document): duplicate = mg_globals.database.media_entries.find_one( {'slug': self['slug']}) - + if duplicate: self['slug'] = "%s-%s" % (self['_id'], self['slug']) @@ -142,12 +260,12 @@ class MediaEntry(Document): 'mediagoblin.user_pages.media_home', user=uploader['username'], media=unicode(self['_id'])) - + def url_to_prev(self, urlgen): """ Provide a url to the previous entry from this user, if there is one """ - cursor = self.db.MediaEntry.find({'_id' : {"$gt": self['_id']}, + cursor = self.db.MediaEntry.find({'_id' : {"$gt": self['_id']}, 'uploader': self['uploader'], 'state': 'processed'}).sort( '_id', ASCENDING).limit(1) @@ -155,12 +273,12 @@ class MediaEntry(Document): return urlgen('mediagoblin.user_pages.media_home', user=self.uploader()['username'], media=unicode(cursor[0]['slug'])) - + def url_to_next(self, urlgen): """ Provide a url to the next entry from this user, if there is one """ - cursor = self.db.MediaEntry.find({'_id' : {"$lt": self['_id']}, + cursor = self.db.MediaEntry.find({'_id' : {"$lt": self['_id']}, 'uploader': self['uploader'], 'state': 'processed'}).sort( '_id', DESCENDING).limit(1) @@ -175,6 +293,18 @@ class MediaEntry(Document): class MediaComment(Document): + """ + A comment on a MediaEntry. + + Structure: + - media_entry: The media entry this comment is attached to + - author: user who posted this comment + - created: when the comment was created + - content: plaintext (but markdown'able) version of the comment's content. + - content_html: the actual html-rendered version of the comment displayed. + Run through Markdown and the HTML cleaner. + """ + __collection__ = 'media_comments' structure = { @@ -196,6 +326,7 @@ class MediaComment(Document): def author(self): return self.db.User.find_one({'_id': self['author']}) + REGISTER_MODELS = [ MediaEntry, User, diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py index cae33394..e5fde6f9 100644 --- a/mediagoblin/db/open.py +++ b/mediagoblin/db/open.py @@ -14,24 +14,42 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import pymongo import mongokit from paste.deploy.converters import asint from mediagoblin.db import models -def connect_database_from_config(app_config): - """Connect to the main database, take config from app_config""" +def connect_database_from_config(app_config, use_pymongo=False): + """ + Connect to the main database, take config from app_config + + Optionally use pymongo instead of mongokit for the connection. + """ port = app_config.get('db_port') if port: port = asint(port) - connection = mongokit.Connection( - app_config.get('db_host'), port) + + if use_pymongo: + connection = pymongo.Connection( + app_config.get('db_host'), port) + else: + connection = mongokit.Connection( + app_config.get('db_host'), port) return connection -def setup_connection_and_db_from_config(app_config): - connection = connect_database_from_config(app_config) - database_path = app_config.get('db_name', 'mediagoblin') + +def setup_connection_and_db_from_config(app_config, use_pymongo=False): + """ + Setup connection and database from config. + + Optionally use pymongo instead of mongokit. + """ + connection = connect_database_from_config(app_config, use_pymongo) + database_path = app_config['db_name'] db = connection[database_path] - models.register_models(connection) - # Could configure indexes here on db + + if not use_pymongo: + models.register_models(connection) + return (connection, db) diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 70c37945..0f3220d2 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -37,6 +37,11 @@ from mongokit import ObjectId from mediagoblin.db.indexes import ACTIVE_INDEXES, DEPRECATED_INDEXES +################ +# Indexing tools +################ + + def add_new_indexes(database, active_indexes=ACTIVE_INDEXES): """ Add any new indexes to the database. @@ -81,21 +86,206 @@ def remove_deprecated_indexes(database, deprecated_indexes=DEPRECATED_INDEXES): Args: - database: pymongo or mongokit database instance. - deprecated_indexes: the indexes to deprecate in the pattern of: - {'collection': ['index_identifier1', 'index_identifier2']} + {'collection_name': { + 'identifier': { + 'index': [index_foo_goes_here], + 'unique': True}} + + (... although we really only need the 'identifier' here, as the + rest of the information isn't used in this case. But it's kept + around so we can remember what it was) Returns: A list of indexes removed in form ('collection', 'index_name') """ indexes_removed = [] - for collection_name, index_names in deprecated_indexes.iteritems(): + for collection_name, indexes in deprecated_indexes.iteritems(): collection = database[collection_name] collection_indexes = collection.index_information().keys() - for index_name in index_names: + for index_name, index_data in indexes.iteritems(): if index_name in collection_indexes: collection.drop_index(index_name) indexes_removed.append((collection_name, index_name)) return indexes_removed + + +################# +# Migration tools +################# + +# The default migration registry... +# +# Don't set this yourself! RegisterMigration will automatically fill +# this with stuff via decorating methods in migrations.py + +class MissingCurrentMigration(Exception): pass + + +MIGRATIONS = {} + + +class RegisterMigration(object): + """ + Tool for registering migrations + + Call like: + + @RegisterMigration(33) + def update_dwarves(database): + [...] + + This will register your migration with the default migration + registry. Alternately, to specify a very specific + migration_registry, you can pass in that as the second argument. + + Note, the number of your migration should NEVER be 0 or less than + 0. 0 is the default "no migrations" state! + """ + def __init__(self, migration_number, migration_registry=MIGRATIONS): + assert migration_number > 0, "Migration number must be > 0!" + assert not migration_registry.has_key(migration_number), \ + "Duplicate migration numbers detected! That's not allowed!" + + self.migration_number = migration_number + self.migration_registry = migration_registry + + def __call__(self, migration): + self.migration_registry[self.migration_number] = migration + return migration + + +class MigrationManager(object): + """ + Migration handling tool. + + Takes information about a database, lets you update the database + to the latest migrations, etc. + """ + def __init__(self, database, migration_registry=MIGRATIONS): + """ + Args: + - database: database we're going to migrate + - migration_registry: where we should find all migrations to + run + """ + self.database = database + self.migration_registry = migration_registry + self._sorted_migrations = None + + def _ensure_current_migration_record(self): + """ + If there isn't a database[u'app_metadata'] mediagoblin entry + with the 'current_migration', throw an error. + """ + if self.database_current_migration() is None: + raise MissingCurrentMigration( + "Tried to call function which requires " + "'current_migration' set in database") + + @property + def sorted_migrations(self): + """ + Sort migrations if necessary and store in self._sorted_migrations + """ + if not self._sorted_migrations: + self._sorted_migrations = sorted( + self.migration_registry.items(), + # sort on the key... the migration number + key=lambda migration_tuple: migration_tuple[0]) + + return self._sorted_migrations + + def latest_migration(self): + """ + Return a migration number for the latest migration, or 0 if + there are no migrations. + """ + if self.sorted_migrations: + return self.sorted_migrations[-1][0] + else: + # If no migrations have been set, we start at 0. + return 0 + + def set_current_migration(self, migration_number): + """ + Set the migration in the database to migration_number + """ + # Add the mediagoblin migration if necessary + self.database[u'app_metadata'].update( + {u'_id': u'mediagoblin'}, + {u'$set': {u'current_migration': migration_number}}, + upsert=True) + + def install_migration_version_if_missing(self): + """ + Sets the migration to the latest version if no migration + version at all is set. + """ + mgoblin_metadata = self.database[u'app_metadata'].find_one( + {u'_id': u'mediagoblin'}) + if not mgoblin_metadata: + latest_migration = self.latest_migration() + self.set_current_migration(latest_migration) + + def database_current_migration(self): + """ + Return the current migration in the database. + """ + mgoblin_metadata = self.database[u'app_metadata'].find_one( + {u'_id': u'mediagoblin'}) + if not mgoblin_metadata: + return None + else: + return mgoblin_metadata[u'current_migration'] + + def database_at_latest_migration(self): + """ + See if the database is at the latest migration. + Returns a boolean. + """ + current_migration = self.database_current_migration() + return current_migration == self.latest_migration() + + def migrations_to_run(self): + """ + Get a list of migrations to run still, if any. + + Note that calling this will set your migration version to the + latest version if it isn't installed to anything yet! + """ + self._ensure_current_migration_record() + + db_current_migration = self.database_current_migration() + + return [ + (migration_number, migration_func) + for migration_number, migration_func in self.sorted_migrations + if migration_number > db_current_migration] + + def migrate_new(self, pre_callback=None, post_callback=None): + """ + Run all migrations. + + Includes two optional args: + - pre_callback: if called, this is a callback on something to + run pre-migration. Takes (migration_number, migration_func) + as arguments + - pre_callback: if called, this is a callback on something to + run post-migration. Takes (migration_number, migration_func) + as arguments + """ + # If we aren't set to any version number, presume we're at the + # latest (which means we'll do nothing here...) + self.install_migration_version_if_missing() + + for migration_number, migration_func in self.migrations_to_run(): + if pre_callback: + pre_callback(migration_number, migration_func) + migration_func(self.database) + self.set_current_migration(migration_number) + if post_callback: + post_callback(migration_number, migration_func) diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index 081eda62..2e90274e 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -38,8 +38,9 @@ def require_active_login(controller): def new_controller_func(request, *args, **kwargs): if request.user and \ request.user.get('status') == u'needs_email_verification': - return redirect(request, - 'mediagoblin.auth.verify_email_notice') + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=request.user['username']) elif not request.user or request.user.get('status') != u'active': return exc.HTTPFound( location="%s?next=%s" % ( diff --git a/mediagoblin/edit/__init__.py b/mediagoblin/edit/__init__.py index e69de29b..a8eeb5ed 100644 --- a/mediagoblin/edit/__init__.py +++ b/mediagoblin/edit/__init__.py @@ -0,0 +1,17 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index e7a86bba..a1783a72 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -24,7 +24,8 @@ class EditForm(wtforms.Form): 'Title', [wtforms.validators.Length(min=0, max=500)]) slug = wtforms.TextField( - 'Slug') + 'Slug', + [wtforms.validators.Required(message="The slug can't be empty")]) description = wtforms.TextAreaField('Description of this work') tags = wtforms.TextField( 'Tags', diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index e4ebe8d7..5cbaadb5 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -22,13 +22,11 @@ from mediagoblin import messages from mediagoblin import mg_globals from mediagoblin.util import ( render_to_response, redirect, clean_html, convert_to_tag_list_of_dicts, - media_tags_as_string) + media_tags_as_string, cleaned_markdown_conversion) from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import require_active_login, get_user_media_entry -import markdown - @get_user_media_entry @require_active_login @@ -59,11 +57,8 @@ def edit_media(request, media): media['tags'] = convert_to_tag_list_of_dicts( request.POST.get('tags')) - md = markdown.Markdown( - safe_mode = 'escape') - media['description_html'] = clean_html( - md.convert( - media['description'])) + media['description_html'] = cleaned_markdown_conversion( + media['description']) media['slug'] = request.POST['slug'] media.save() @@ -108,6 +103,9 @@ def edit_profile(request): if request.method == 'POST' and form.validate(): user['url'] = request.POST['url'] user['bio'] = request.POST['bio'] + + user['bio_html'] = cleaned_markdown_conversion(user['bio']) + user.save() messages.add_message(request, @@ -115,7 +113,7 @@ def edit_profile(request): 'Profile edited!') return redirect(request, 'mediagoblin.user_pages.user_home', - username=edit_username) + user=edit_username) return render_to_response( request, diff --git a/mediagoblin/gmg_commands/__init__.py b/mediagoblin/gmg_commands/__init__.py index 0cb4d3a2..921f0430 100644 --- a/mediagoblin/gmg_commands/__init__.py +++ b/mediagoblin/gmg_commands/__init__.py @@ -40,6 +40,10 @@ SUBCOMMAND_MAP = { 'setup': 'mediagoblin.gmg_commands.users:changepw_parser_setup', 'func': 'mediagoblin.gmg_commands.users:changepw', 'help': 'Makes admin an user'}, + 'wipealldata': { + 'setup': 'mediagoblin.gmg_commands.wipealldata:wipe_parser_setup', + 'func': 'mediagoblin.gmg_commands.wipealldata:wipe', + 'help': 'Wipes **all** the data for this MediaGoblin instance'}, } diff --git a/mediagoblin/gmg_commands/migrate.py b/mediagoblin/gmg_commands/migrate.py index ab1a267b..94adc9e0 100644 --- a/mediagoblin/gmg_commands/migrate.py +++ b/mediagoblin/gmg_commands/migrate.py @@ -14,10 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import sys -from mediagoblin.db import migrations from mediagoblin.db import util as db_util -from mediagoblin.gmg_commands import util as commands_util +from mediagoblin.db.open import setup_connection_and_db_from_config +from mediagoblin.init.config import read_mediagoblin_config + +# This MUST be imported so as to set up the appropriate migrations! +from mediagoblin.db import migrations def migrate_parser_setup(subparser): @@ -26,31 +30,41 @@ def migrate_parser_setup(subparser): help="Config file used to set up environment") +def _print_started_migration(migration_number, migration_func): + sys.stdout.write( + "Running migration %s, '%s'... " % ( + migration_number, migration_func.func_name)) + sys.stdout.flush() + + +def _print_finished_migration(migration_number, migration_func): + sys.stdout.write("done.\n") + sys.stdout.flush() + + def migrate(args): - mgoblin_app = commands_util.setup_app(args) + config, validation_result = read_mediagoblin_config(args.conf_file) + connection, db = setup_connection_and_db_from_config( + config['mediagoblin'], use_pymongo=True) + migration_manager = db_util.MigrationManager(db) # Clear old indexes print "== Clearing old indexes... ==" - removed_indexes = db_util.remove_deprecated_indexes(mgoblin_app.db) + removed_indexes = db_util.remove_deprecated_indexes(db) for collection, index_name in removed_indexes: print "Removed index '%s' in collection '%s'" % ( index_name, collection) # Migrate - print "== Applying migrations... ==" - for model_name in migrations.MIGRATE_CLASSES: - model = getattr(mgoblin_app.db, model_name) - - if not hasattr(model, 'migration_handler') or not model.collection: - continue - - migration = model.migration_handler(model) - migration.migrate_all(collection=model.collection) + print "\n== Applying migrations... ==" + migration_manager.migrate_new( + pre_callback=_print_started_migration, + post_callback=_print_finished_migration) # Add new indexes - print "== Adding new indexes... ==" - new_indexes = db_util.add_new_indexes(mgoblin_app.db) + print "\n== Adding new indexes... ==" + new_indexes = db_util.add_new_indexes(db) for collection, index_name in new_indexes: print "Added index '%s' to collection '%s'" % ( diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py index b4a6bbc1..14b6875d 100644 --- a/mediagoblin/gmg_commands/users.py +++ b/mediagoblin/gmg_commands/users.py @@ -1,3 +1,19 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + from mediagoblin.gmg_commands import util as commands_util from mediagoblin.auth import lib as auth_lib from mediagoblin import mg_globals diff --git a/mediagoblin/gmg_commands/wipealldata.py b/mediagoblin/gmg_commands/wipealldata.py new file mode 100644 index 00000000..9ad32051 --- /dev/null +++ b/mediagoblin/gmg_commands/wipealldata.py @@ -0,0 +1,51 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import pymongo +import sys +import os +import shutil + + +def wipe_parser_setup(subparser): + pass + + +def wipe(args): + print "*** WARNING! ***" + print "" + print "Running this will destroy your mediagoblin database," + print "remove all your media files in user_dev/, etc." + + drop_it = raw_input( + 'Are you **SURE** you want to destroy your environment? ' + '(if so, type "yes")> ') + + if not drop_it == 'yes': + return + + print "nixing data in mongodb...." + conn = pymongo.Connection() + conn.drop_database('mediagoblin') + + for directory in [os.path.join(os.getcwd(), "user_dev", "media"), + os.path.join(os.getcwd(), "user_dev", "beaker")]: + if os.path.exists(directory): + print "nixing %s...." % directory + shutil.rmtree(directory) + + print "removed all your stuff! okay, now re-run ./bin/buildout" diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py index b8ed2456..ff005703 100644 --- a/mediagoblin/init/__init__.py +++ b/mediagoblin/init/__init__.py @@ -15,6 +15,68 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import jinja2 +from mediagoblin import staticdirect +from mediagoblin.init.config import ( + read_mediagoblin_config, generate_validation_report) +from mediagoblin import mg_globals +from mediagoblin.mg_globals import setup_globals +from mediagoblin.db.open import setup_connection_and_db_from_config +from mediagoblin.db.util import MigrationManager +from mediagoblin.workbench import WorkbenchManager +from mediagoblin.storage import storage_system_from_config + + +class Error(Exception): pass +class ImproperlyConfigured(Error): pass + + +def setup_global_and_app_config(config_path): + global_config, validation_result = read_mediagoblin_config(config_path) + app_config = global_config['mediagoblin'] + # report errors if necessary + validation_report = generate_validation_report( + global_config, validation_result) + if validation_report: + raise ImproperlyConfigured(validation_report) + + setup_globals( + app_config=app_config, + global_config=global_config) + + return global_config, app_config + + +def setup_database(): + app_config = mg_globals.app_config + + # This MUST be imported so as to set up the appropriate migrations! + from mediagoblin.db import migrations + + # Set up the database + connection, db = setup_connection_and_db_from_config(app_config) + + # Init the migration number if necessary + migration_manager = MigrationManager(db) + migration_manager.install_migration_version_if_missing() + + # Tiny hack to warn user if our migration is out of date + if not migration_manager.database_at_latest_migration(): + db_migration_num = migration_manager.database_current_migration() + latest_migration_num = migration_manager.latest_migration() + if db_migration_num < latest_migration_num: + print ( + "*WARNING:* Your migrations are out of date, " + "maybe run ./bin/gmg migrate?") + elif db_migration_num > latest_migration_num: + print ( + "*WARNING:* Your migrations are out of date... " + "in fact they appear to be from the future?!") + + setup_globals( + db_connection = connection, + database = db) + + return connection, db def get_jinja_loader(user_template_path=None): @@ -31,3 +93,40 @@ def get_jinja_loader(user_template_path=None): jinja2.PackageLoader('mediagoblin', 'templates')]) else: return jinja2.PackageLoader('mediagoblin', 'templates') + + +def get_staticdirector(app_config): + if app_config.has_key('direct_remote_path'): + return staticdirect.RemoteStaticDirect( + app_config['direct_remote_path'].strip()) + elif app_config.has_key('direct_remote_paths'): + direct_remote_path_lines = app_config[ + 'direct_remote_paths'].strip().splitlines() + return staticdirect.MultiRemoteStaticDirect( + dict([line.strip().split(' ', 1) + for line in direct_remote_path_lines])) + else: + raise ImproperlyConfigured( + "One of direct_remote_path or " + "direct_remote_paths must be provided") + + +def setup_storage(): + app_config = mg_globals.app_config + + public_store = storage_system_from_config(app_config, 'publicstore') + queue_store = storage_system_from_config(app_config, 'queuestore') + + setup_globals( + public_store = public_store, + queue_store = queue_store) + + return public_store, queue_store + + +def setup_workbench(): + app_config = mg_globals.app_config + + workbench_manager = WorkbenchManager(app_config['workbench_path']) + + setup_globals(workbench_manager = workbench_manager) diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py index 67c3dfa0..bfae954e 100644 --- a/mediagoblin/init/celery/__init__.py +++ b/mediagoblin/init/celery/__init__.py @@ -62,7 +62,7 @@ def setup_celery_from_config(app_config, global_config, celery_mongo_settings['port'] = app_config['db_port'] if celery_settings['BROKER_BACKEND'] == 'mongodb': celery_settings['BROKER_PORT'] = app_config['db_port'] - celery_mongo_settings['database'] = app_config.get('db_name', 'mediagoblin') + celery_mongo_settings['database'] = app_config['db_name'] celery_settings['CELERY_MONGODB_BACKEND_SETTINGS'] = celery_mongo_settings diff --git a/mediagoblin/mg_globals.py b/mediagoblin/mg_globals.py index 739f44ee..80ff5ead 100644 --- a/mediagoblin/mg_globals.py +++ b/mediagoblin/mg_globals.py @@ -1,3 +1,18 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ In some places, we need to access the database, public_store, queue_store """ @@ -20,12 +35,6 @@ database = None public_store = None queue_store = None -# Dump mail to stdout instead of sending it: -email_debug_mode = False - -# Address for sending out mails -email_sender_address = None - # A WorkBenchManager workbench_manager = None diff --git a/mediagoblin/process_media/__init__.py b/mediagoblin/process_media/__init__.py index da3e887e..125b24e0 100644 --- a/mediagoblin/process_media/__init__.py +++ b/mediagoblin/process_media/__init__.py @@ -57,36 +57,43 @@ def process_media_initial(media_id): thumb.save(thumb_file, "JPEG", quality=90) """ - Create medium file, used in `media.html` + If the size of the original file exceeds the specified size of a `medium` + file, a `medium.jpg` files is created and later associated with the media + entry. """ medium = Image.open(queued_filename) - medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS) + medium_processed = False - if medium.mode != "RGB": - medium = medium.convert("RGB") + if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1]: + medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS) - medium_filepath = create_pub_filepath(entry, 'medium.jpg') + if medium.mode != "RGB": + medium = medium.convert("RGB") - medium_file = mgg.public_store.get_file(medium_filepath, 'w') - with medium_file: - medium.save(medium_file, "JPEG", quality=90) + medium_filepath = create_pub_filepath(entry, 'medium.jpg') + + medium_file = mgg.public_store.get_file(medium_filepath, 'w') + with medium_file: + medium.save(medium_file, "JPEG", quality=90) + medium_processed = True # we have to re-read because unlike PIL, not everything reads # things in string representation :) queued_file = file(queued_filename, 'rb') with queued_file: - main_filepath = create_pub_filepath(entry, queued_filepath[-1]) + original_filepath = create_pub_filepath(entry, queued_filepath[-1]) - with mgg.public_store.get_file(main_filepath, 'wb') as main_file: - main_file.write(queued_file.read()) + with mgg.public_store.get_file(original_filepath, 'wb') as original_file: + original_file.write(queued_file.read()) mgg.queue_store.delete_file(queued_filepath) entry['queued_media_file'] = [] media_files_dict = entry.setdefault('media_files', {}) media_files_dict['thumb'] = thumb_filepath - media_files_dict['main'] = main_filepath - media_files_dict['medium'] = medium_filepath + media_files_dict['original'] = original_filepath + if medium_processed: + media_files_dict['medium'] = medium_filepath entry['state'] = u'processed' entry.save() diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index 53e019f6..70db6da9 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -1,15 +1,17 @@ body { - background-color: #272727; - color: #f7f7f7; + background-color: #111; + background-image: url("../images/background.png"); + color: #999; font-family: sans-serif; - padding:none; - margin:0px; - height:100%; + padding: none; + margin: 0px; + height: 100%; + font: 16px "HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,sans-serif; } form { - margin:0px; - padding:0px; + margin: 0px; + padding: 0px; } /* Carter One font */ @@ -24,22 +26,29 @@ form { /* text styles */ h1{ - font-family: 'Carter One', arial, serif; + font-family: 'Carter One',arial,serif; margin-bottom: 15px; - margin-top:15px; + margin-top: 15px; + color: #fff; + font-size: 30px; } h2{ - margin-top:20px; + margin-top: 20px; + color: #fff; } -p { - font-family: sans-serif; - font-size:16px; +h3{ + border-bottom: 1px solid #222; + font-size: 18px; } a { - color: #86D4B1; + color: #999; +} + +a.highlight { + color: #fff; } label { @@ -49,192 +58,201 @@ label { /* website structure */ .mediagoblin_body { - position:relative; - min-height:100%; + position: relative; + min-height: 100%; } .mediagoblin_header { - width:100%; - height:36px; - background-color:#393939; - padding-top:14px; - margin-bottom:40px; + height: 36px; + padding-top: 14px; + margin-bottom: 20px; + border-bottom: 1px solid #222222; +} + +.header_submit{ + color: #272727; + background-color: #aaa; + background-image: -webkit-gradient(linear, left top, left bottom, from(##D2D2D2), to(#aaa)); + background-image: -webkit-linear-gradient(top, #D2D2D2, #aaa); + background-image: -moz-linear-gradient(top, #D2D2D2, #aaa); + background-image: -ms-linear-gradient(top, #D2D2D2, #aaa); + background-image: -o-linear-gradient(top, #D2D2D2, #aaa); + background-image: linear-gradient(top, #D2D2D2, #aaa); + box-shadow: 0px 0px 4px #000; + border-radius: 5px 5px 5px 5px; + margin: 8px; + padding: 3px 8px; + text-decoration: none; + border: medium none; + font-family: 'Carter One',arial,serif; } .mediagoblin_footer { - width:100%; - height:26px; - background-color:#393939; - bottom:0px; - padding-top:8px; - position:absolute; - text-align:center; - font-size:14px; - color:#999; + height: 30px; + border-top: 1px solid #222222; + bottom: 0px; + padding-top: 8px; + text-align: center; + font-size: 14px; + color: #999; } .mediagoblin_content { - padding-bottom:74px; -} - -a.mediagoblin_logo { - width:34px; - height:25px; - margin-right:10px; - background-image:url('../images/icon.png'); - background-position:0px 0px; - display:inline-block; -} - -a.mediagoblin_logo:hover { - background-position:0px -28px; + padding-bottom: 74px; } .mediagoblin_header_right { - float:right; + float: right; } /* common website elements */ .button { - font-family:'Carter One', arial, serif; - height:32px; - min-width:99px; - background-color:#86d4b1; + font-family: 'Carter One', arial, serif; + height: 32px; + min-width: 99px; + background-color: #86d4b1; background-image: -webkit-gradient(linear, left top, left bottom, from(#86d4b1), to(#62caa2)); background-image: -webkit-linear-gradient(top, #86d4b1, #62caa2); background-image: -moz-linear-gradient(top, #86d4b1, #62caa2); background-image: -ms-linear-gradient(top, #86d4b1, #62caa2); background-image: -o-linear-gradient(top, #86d4b1, #62caa2); background-image: linear-gradient(top, #86d4b1, #62caa2); - box-shadow:0px 0px 4px #000; - border-radius:5px; - border:none; - color:#272727; - margin:10px 0px 10px 15px; - font-size:1em; - text-align:center; - padding-left:11px; - padding-right:11px; + box-shadow: 0px 0px 4px #000; + border-radius: 5px; + border: none; + color: #272727; + margin: 10px 0px 10px 15px; + font-size: 1em; + text-align: center; + padding-left: 11px; + padding-right: 11px; + text-decoration: none; } .pagination{ -text-align:center; +text-align: center; +} + +.pagination_arrow{ + margin: 5px; } /* forms */ .form_box { - background-color:#393939; - background-image:url("../images/background_lines.png"); - background-repeat:repeat-x; - font-size:18px; - padding-bottom:30px; - padding-top:1px; - margin-left:auto; - margin-right:auto; - display:block; - float:none; + background-color: #222; + background-image: url("../images/background_lines.png"); + background-repeat: repeat-x; + font-size: 18px; + padding-bottom: 30px; + padding-top: 30px; + margin-left: auto; + margin-right: auto; + display: block; + float: none; } .edit_box { - background-image:url("../images/background_edit.png"); + background-image: url("../images/background_edit.png"); } .form_box h1 { - font-size:28px; + font-size: 28px; } .form_field_input input, .form_field_input textarea { - width:100%; - font-size:18px; + width: 100%; + font-size: 18px; } .form_field_box { - margin-bottom:24px; + margin-bottom: 24px; } .form_field_label,.form_field_input { - margin-bottom:4px; + margin-bottom: 4px; } .form_field_error { - background-color:#87453b; - border:none; - font-size:16px; - padding:9px; - margin-top:8px; - margin-bottom:8px; + background-color: #87453b; + color: #fff; + border: none; + font-size: 16px; + padding: 9px; + margin-top: 8px; + margin-bottom: 8px; } .form_submit_buttons { - text-align:right; + text-align: right; } /* comments */ .comment_author { - margin-bottom:40px; - padding-top:4px; + margin-bottom: 40px; + padding-top: 4px; + font-size: 14px; } .comment_content p { - margin-bottom:4px; + margin-bottom: 4px; } /* media galleries */ .media_thumbnail { - padding:0px; - width:180px; - height:180px; - overflow:hidden; - float:left; - margin:0px 4px 10px 4px; - text-align:center; + padding: 0px; + width: 180px; + height: 180px; + overflow: hidden; + float: left; + margin: 0px 4px 10px 4px; + text-align: center; } /* icons */ img.media_icon{ - margin:0 4px; - vertical-align:sub; + margin: 0 4px; + vertical-align: sub; } /* navigation */ .navigation_button{ width: 139px; - display:block; - float:left; + display: block; + float: left; text-align: center; - background-color: #393939; + background-color: #222; text-decoration: none; - padding: 6px 0pt; + padding: 12px 0pt; font-family: 'Carter One', arial, serif; - font-size:2em; - margin:0 0 20px + font-size: 2em; + margin: 0 0 20px } p.navigation_button{ - color:#272727; + color: #272727; } .navigation_left{ - margin-right:2px; + margin-right: 2px; } /* messages */ ul.mediagoblin_messages { - list-style:none inside; - color:#f7f7f7; + list-style: none inside; + color: #f7f7f7; } .mediagoblin_messages li { - margin:5px 0; - padding:8px; - text-align:center; + margin: 5px 0; + padding: 8px; + text-align: center; } .message_success { @@ -255,13 +273,5 @@ ul.mediagoblin_messages { .message_debug { background-color: #f7f7f7; - color:#272727; -} - -/* profile stuff */ - -.profile_content { - padding: 6px; - background-color: #393939; - margin-bottom: 10px; + color: #272727; } diff --git a/mediagoblin/static/images/background.png b/mediagoblin/static/images/background.png Binary files differnew file mode 100644 index 00000000..aa101308 --- /dev/null +++ b/mediagoblin/static/images/background.png diff --git a/mediagoblin/static/images/icon.png b/mediagoblin/static/images/icon.png Binary files differdeleted file mode 100644 index 4f4f3e9c..00000000 --- a/mediagoblin/static/images/icon.png +++ /dev/null diff --git a/mediagoblin/static/images/logo.png b/mediagoblin/static/images/logo.png Binary files differnew file mode 100644 index 00000000..cf28a6d4 --- /dev/null +++ b/mediagoblin/static/images/logo.png diff --git a/mediagoblin/static/images/navigation_end.png b/mediagoblin/static/images/navigation_end.png Binary files differnew file mode 100644 index 00000000..b2f27296 --- /dev/null +++ b/mediagoblin/static/images/navigation_end.png diff --git a/mediagoblin/static/images/navigation_left.png b/mediagoblin/static/images/navigation_left.png Binary files differnew file mode 100644 index 00000000..d1645120 --- /dev/null +++ b/mediagoblin/static/images/navigation_left.png diff --git a/mediagoblin/static/images/navigation_right.png b/mediagoblin/static/images/navigation_right.png Binary files differnew file mode 100644 index 00000000..d4caa7b8 --- /dev/null +++ b/mediagoblin/static/images/navigation_right.png diff --git a/mediagoblin/static/images/pagination_left.png b/mediagoblin/static/images/pagination_left.png Binary files differnew file mode 100644 index 00000000..56a26596 --- /dev/null +++ b/mediagoblin/static/images/pagination_left.png diff --git a/mediagoblin/static/images/pagination_right.png b/mediagoblin/static/images/pagination_right.png Binary files differnew file mode 100644 index 00000000..84f8abba --- /dev/null +++ b/mediagoblin/static/images/pagination_right.png diff --git a/mediagoblin/submit/__init__.py b/mediagoblin/submit/__init__.py index e69de29b..a8eeb5ed 100644 --- a/mediagoblin/submit/__init__.py +++ b/mediagoblin/submit/__init__.py @@ -0,0 +1,17 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + diff --git a/mediagoblin/submit/routing.py b/mediagoblin/submit/routing.py index 3edbab70..5585ecb0 100644 --- a/mediagoblin/submit/routing.py +++ b/mediagoblin/submit/routing.py @@ -18,7 +18,4 @@ from routes.route import Route submit_routes = [ Route('mediagoblin.submit.start', '/', - controller='mediagoblin.submit.views:submit_start'), - Route('mediagoblin.submit.success', '/success/', - template='mediagoblin/submit/success.html', - controller='mediagoblin.views:simple_template_render')] + controller='mediagoblin.submit.views:submit_start')] diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index c5ac8c62..87e57dda 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -101,8 +101,3 @@ def submit_start(request): request, 'mediagoblin/submit/start.html', {'submit_form': submit_form}) - - -def submit_success(request): - return render_to_response( - request, 'mediagoblin/submit/success.html', {}) diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 2303ce5c..e25783ea 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -35,7 +35,9 @@ <input type="hidden" name="next" value="{{ next }}" class="button" style="display: none;"/> {% endif %} - <p>Don't have an account yet?<br /><a href="{{ request.urlgen('mediagoblin.auth.register') }}">Create one here!</a></p> + {% if allow_registration %} + <p>Don't have an account yet?<br /><a href="{{ request.urlgen('mediagoblin.auth.register') }}">Create one here!</a></p> + {% endif %} </div> </form> {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/auth/register_success.html b/mediagoblin/templates/mediagoblin/auth/register_success.html deleted file mode 100644 index cd82a0b9..00000000 --- a/mediagoblin/templates/mediagoblin/auth/register_success.html +++ /dev/null @@ -1,25 +0,0 @@ -{# -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011 Free Software Foundation, Inc -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -#} -{% extends "mediagoblin/base.html" %} - -{% block mediagoblin_content %} - <p> - Register successful! :D <br /> - You should get a confirmation email soon. - </p> -{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/auth/verification_needed.html b/mediagoblin/templates/mediagoblin/auth/verification_needed.html deleted file mode 100644 index 4104da19..00000000 --- a/mediagoblin/templates/mediagoblin/auth/verification_needed.html +++ /dev/null @@ -1,29 +0,0 @@ -{# -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011 Free Software Foundation, Inc -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -#} -{% extends "mediagoblin/base.html" %} - -{% block mediagoblin_content %} - <p> - Verfication needed!<br /> - Please check your email to verify your account. - </p> - - <p> - Still haven't received an email? <a href="{{ request.urlgen('mediagoblin.auth.resend_verification') }}">Click here to resend it.</a> - </p> -{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index b71fca24..656fb46b 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -14,9 +14,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -#} +-#} +<!doctype html> <html> <head> + <meta charset="utf-8"> <title>{% block title %}GNU MediaGoblin{% endblock title %}</title> <link rel="stylesheet" type="text/css" href="{{ request.staticdirect('/css/contrib/reset.css') }}"/> @@ -34,23 +36,41 @@ {% block mediagoblin_body %} <div class="mediagoblin_body"> {% block mediagoblin_header %} - <div class="mediagoblin_header"> - <div class="container_16"> - <div class="grid_16"> - {% block mediagoblin_logo %} - <a class="mediagoblin_logo" href="{{ request.urlgen('index') }}"></a> - {% endblock %}{% block mediagoblin_header_title %}{% endblock %} - <div class="mediagoblin_header_right"> - {% if request.user %} + <div class="container_16"> + <div class="grid_16 mediagoblin_header"> + {% block mediagoblin_logo %} + <a class="mediagoblin_logo" + href="{{ request.urlgen('index') }}"> + <img src="{{ request.staticdirect('/images/logo.png') }}" + alt="Mediagoblin logo" /> + </a> + {% endblock %} + {% if request.user and request.user['status'] == 'active' %} + <a class="header_submit" + href="{{ request.urlgen('mediagoblin.submit.start') }}"> + Submit media + </a> + {% endif %} + {% block mediagoblin_header_title %}{% endblock %} + <div class="mediagoblin_header_right"> + {% if request.user %} + {# the following link should only appear when verification is needed #} + {% if request.user.status == "needs_email_verification" %} <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', - user= request.user['username']) }}"> - {{ request.user['username'] }}</a>'s account - (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>) - {% else %} - <a href="{{ request.urlgen('mediagoblin.auth.login') }}"> - Login</a> + user=request.user['username']) }}" + class="header_submit"> + needs verification!</a> {% endif %} - </div> + + <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', + user= request.user['username']) }}"> + {{ request.user['username'] }}</a> + + (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>) + {% else %} + <a href="{{ request.urlgen('mediagoblin.auth.login') }}"> + Login</a> + {% endif %} </div> </div> </div> @@ -63,11 +83,9 @@ </div> </div> {% block mediagoblin_footer %} - <div class="mediagoblin_footer"> - <div class="container_16"> - <div class="grid_16"> - Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU project</a> - </div> + <div class="container_16"> + <div class="grid_16 mediagoblin_footer"> + Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU project</a> </div> </div> {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/root.html b/mediagoblin/templates/mediagoblin/root.html index 5b744999..bae033c4 100644 --- a/mediagoblin/templates/mediagoblin/root.html +++ b/mediagoblin/templates/mediagoblin/root.html @@ -29,10 +29,12 @@ If you have an account, you can <a href="{{ request.urlgen('mediagoblin.auth.login') }}">Login</a>. </p> - <p> - If you don't have an account, please - <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Register</a>. - </p> + {% if allow_registration %} + <p> + If you don't have an account, please + <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Register</a>. + </p> + {% endif %} {% endif %} {# temporarily, an "image gallery" that isn't one really ;) #} diff --git a/mediagoblin/templates/mediagoblin/submit/success.html b/mediagoblin/templates/mediagoblin/submit/success.html deleted file mode 100644 index afc9f9d1..00000000 --- a/mediagoblin/templates/mediagoblin/submit/success.html +++ /dev/null @@ -1,22 +0,0 @@ -{# -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011 Free Software Foundation, Inc -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -#} -{% extends "mediagoblin/base.html" %} - -{% block mediagoblin_content %} - Woohoo! Submitted! -{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 8dd42115..7622d6e6 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -23,13 +23,8 @@ {% block mediagoblin_content %} {% if media %} <div class="grid_11 alpha"> - {% if media.media_files.medium %} - <img src="{{ request.app.public_store.file_url( - media.media_files.medium) }}" /> - {% else %} - <img src="{{ request.app.public_store.file_url( - media.media_files.main) }}" /> - {% endif %} + <img class="media_image" src="{{ request.app.public_store.file_url( + media.get_display_media(media.media_files)) }}" /> <h2> {{media.title}} @@ -55,7 +50,7 @@ <form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment', user= media.uploader().username, media=media._id) }}" method="POST"> - {{ wtforms_util.render_field_div(comment_form.comment) }} + {{ wtforms_util.render_field_div(comment_form.comment_content) }} <div class="form_submit_buttons"> <input type="submit" value="Post comment!" class="button" /> </div> @@ -65,7 +60,12 @@ {% if comments %} {% for comment in comments %} {% set comment_author = comment.author() %} - <div class="comment_wrapper" id="comment-{{ comment['_id'] }}"> + {% if pagination.active_id == comment._id %} + <div class="comment_wrapper comment_active" id="comment-{{ comment['_id'] }}"> + <a name="comment" id="comment"></a> + {% else %} + <div class="comment_wrapper" id="comment-{{ comment['_id'] }}"> + {% endif %} <div class="comment_content"> {% autoescape False %} {{ comment.content_html }} @@ -77,7 +77,10 @@ {{ comment_author['username'] }}</a> at <!--</div> <div class="comment_datetime">--> - <a href="#comment-{{ comment['_id'] }}"> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment', + comment = comment['_id'], + user = media.uploader().username, + media = media._id) }}#comment"> {{ "%4d-%02d-%02d %02d:%02d"|format(comment.created.year, comment.created.month, comment.created.day, @@ -88,7 +91,10 @@ </div> {% endfor %} - {{ render_pagination(request, pagination) }} + {{ render_pagination(request, pagination, + request.urlgen('mediagoblin.user_pages.media_home', + user = media.uploader().username, + media = media._id)) }} </div> {% endif %} <div class="grid_5 omega"> diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html index aed330c8..7769b8b3 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/user.html +++ b/mediagoblin/templates/mediagoblin/user_pages/user.html @@ -25,34 +25,67 @@ {% endblock mediagoblin_head %} {% block mediagoblin_content -%} - {% if user %} - <h1>{{ user.username }}'s profile</h1> + {# If no user... #} + {% if not user %} + <p>Sorry, no such user found.<p/> - {% include "mediagoblin/utils/profile.html" %} + {# User exists, but needs verification #} + {% elif user.status == "needs_email_verification" %} + {% if user == request.user %} + {# this should only be visible when you are this user #} + <div class="grid_6 prefix_1 suffix_1 form_box"> + <h1>Verification needed</h1> - {% if request.user['_id'] == user['_id'] or request.user['is_admin'] %} - <a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{ - user.username }}">Edit profile</a> - {% endif %} + <p>Almost done! Your account still needs to be verified.</p> + <p> + An email should arrive in a few moments with instructions + on how to do so. + </p> + <p>In case it doesn't:</p> + + <a href="{{ request.urlgen('mediagoblin.auth.resend_verification') }}" + class="button">Resend verification email</a> + </div> + {% else %} + {# if the user is not you, but still needs to verify their email #} + <div class="grid_6 prefix_1 suffix_1 form_box"> + <h1>Verification needed</h1> - {% if request.user['_id'] == user['_id'] %} - <p> - <a href="{{ request.urlgen('mediagoblin.submit.start') }}">Submit an item</a> - </p> + <p> + Someone has registered an account with this username, but it + still has to be verified. + </p> + + <p> + If you are that person but you've lost your verification + email, you can + <a href="{{ request.urlgen('mediagoblin.auth.login') }}">log in</a> + and resend it. + </p> + </div> {% endif %} - {% set pagination_base_url = user_gallery_url %} - {% include "mediagoblin/utils/object_gallery.html" %} + {# Active(?) (or at least verified at some point) user, horray! #} + {% else %} + <h1>{{ user.username }}'s profile</h1> - <div class="clear"></div> + <div class="grid_6 alpha"> + {% include "mediagoblin/utils/profile.html" %} + {% if request.user['_id'] == user['_id'] or request.user['is_admin'] %} + <a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{ + user.username }}">Edit profile</a> + {% endif %} + </div> - <p><a href="{{ user_gallery_url }}">View all of {{ user.username }}'s media</a></p> + <div class="grid_10 omega"> + {% set pagination_base_url = user_gallery_url %} + {% include "mediagoblin/utils/object_gallery.html" %} + <p><a href="{{ user_gallery_url }}">View all of {{ user.username }}'s media</a></p> + <a href={{ request.urlgen( + 'mediagoblin.user_pages.atom_feed', + user=user.username) }}>atom feed</a> + </div> - <a href={{ request.urlgen( - 'mediagoblin.user_pages.atom_feed', - user=user.username) }}>atom feed</a> - {% else %} - {# This *should* not occur as the view makes sure we pass in a user. #} - <p>Sorry, no such user found.<p/> + <div class="clear"></div> {% endif %} {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/utils/pagination.html b/mediagoblin/templates/mediagoblin/utils/pagination.html index aae50d22..23d49463 100644 --- a/mediagoblin/templates/mediagoblin/utils/pagination.html +++ b/mediagoblin/templates/mediagoblin/utils/pagination.html @@ -34,9 +34,16 @@ {% if pagination.has_prev %} <a href="{{ pagination.get_page_url_explicit( base_url, get_params, - pagination.page - 1) }}">« Prev</a> + pagination.page - 1) }}"><img class="pagination_arrow" src="/mgoblin_static/images/pagination_left.png" alt="Previous page" />Newer</a> {% endif %} - + {% if pagination.has_next %} + <a href="{{ pagination.get_page_url_explicit( + base_url, get_params, + pagination.page + 1) }}">Older<img class="pagination_arrow" src="/mgoblin_static/images/pagination_right.png" alt="Next page" /> + </a> + {% endif %} + <br /> + Go to page: {%- for page in pagination.iter_pages() %} {% if page %} {% if page != pagination.page %} @@ -50,12 +57,6 @@ <span class="ellipsis">…</span> {% endif %} {%- endfor %} - - {% if pagination.has_next %} - <a href="{{ pagination.get_page_url_explicit( - base_url, get_params, - pagination.page + 1) }}">Next »</a> - {% endif %} </p> </div> {% endif %} diff --git a/mediagoblin/templates/mediagoblin/utils/prev_next.html b/mediagoblin/templates/mediagoblin/utils/prev_next.html index 8908c298..7cf8d2a4 100644 --- a/mediagoblin/templates/mediagoblin/utils/prev_next.html +++ b/mediagoblin/templates/mediagoblin/utils/prev_next.html @@ -24,20 +24,23 @@ {# There are no previous entries for the very first media entry #} {% if prev_entry_url %} <a class="navigation_button navigation_left" href="{{ prev_entry_url }}"> - < + <img src="/mgoblin_static/images/navigation_left.png" alt="Previous image" /> </a> {% else %} {# This is the first entry. display greyed-out 'previous' image #} - <p class="navigation_button">X</p> + <p class="navigation_button navigation_left"> + <img src="/mgoblin_static/images/navigation_end.png" alt="No previous images" /> + </p> {% endif %} - {# Likewise, this could be the very last media entry #} {% if next_entry_url %} <a class="navigation_button" href="{{ next_entry_url }}"> - > + <img src="/mgoblin_static/images/navigation_right.png" alt="Next image" /> </a> {% else %} {# This is the last entry. display greyed-out 'next' image #} - <p class="navigation_button">X</p> + <p class="navigation_button"> + <img src="/mgoblin_static/images/navigation_end.png" alt="No following images" /> + </p> {% endif %} </div> diff --git a/mediagoblin/templates/mediagoblin/utils/profile.html b/mediagoblin/templates/mediagoblin/utils/profile.html index cd60bbfc..63024b77 100644 --- a/mediagoblin/templates/mediagoblin/utils/profile.html +++ b/mediagoblin/templates/mediagoblin/utils/profile.html @@ -17,19 +17,14 @@ #} {% block profile_content -%} - {% if user.url or user.bio %} - <div class="profile_content"> - {% if user.url %} - <div class="profile_homepage"> - <a href="{{ user.url }}">{{ user.url }}</a> - </div> - {% endif %} - - {% if user.bio %} - <div class="profile_bio"> - {{ user.bio }} - </div> - {% endif %} - </div> + {% if user.bio %} + {% autoescape False %} + <p>{{ user.bio_html }}</p> + {% endautoescape %} {% endif %} -{% endblock %} + {% if user.url %} + <p> + <a href="{{ user.url }}">{{ user.url }}</a> + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index ad9dd35b..f0bb183f 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -153,9 +153,9 @@ def test_register_views(test_app): ## Did we redirect to the proper page? Use the right template? assert_equal( urlparse.urlsplit(response.location)[2], - '/auth/register/success/') + '/u/happygirl/') assert util.TEMPLATE_TEST_CONTEXT.has_key( - 'mediagoblin/auth/register_success.html') + 'mediagoblin/user_pages/user.html') ## Make sure user is in place new_user = mg_globals.database.User.find_one( @@ -164,6 +164,11 @@ def test_register_views(test_app): assert new_user['status'] == u'needs_email_verification' assert new_user['email_verified'] == False + ## Make sure user is logged in + request = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/user_pages/user.html']['request'] + assert request.session['user_id'] == unicode(new_user['_id']) + ## Make sure we get email confirmation, and try verifying assert len(util.EMAIL_TEST_INBOX) == 1 message = util.EMAIL_TEST_INBOX.pop() diff --git a/mediagoblin/tests/test_migrations.py b/mediagoblin/tests/test_migrations.py new file mode 100644 index 00000000..c100a26a --- /dev/null +++ b/mediagoblin/tests/test_migrations.py @@ -0,0 +1,403 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from nose.tools import assert_raises +from pymongo import Connection + +from mediagoblin.tests.tools import ( + install_fixtures_simple, assert_db_meets_expected) +from mediagoblin.db.util import ( + RegisterMigration, MigrationManager, ObjectId, + MissingCurrentMigration) + +# This one will get filled with local migrations +TEST_MIGRATION_REGISTRY = {} +# this one won't get filled +TEST_EMPTY_MIGRATION_REGISTRY = {} + +MIGRATION_DB_NAME = u'__mediagoblin_test_migrations__' + + +###################### +# Fake test migrations +###################### + +@RegisterMigration(1, TEST_MIGRATION_REGISTRY) +def creature_add_magical_powers(database): + """ + Add lists of magical powers. + + This defaults to [], an empty list. Since we haven't declared any + magical powers, all existing monsters, setting to an empty list is + fine. + """ + database['creatures'].update( + {'magical_powers': {'$exists': False}}, + {'$set': {'magical_powers': []}}, + multi=True) + + +@RegisterMigration(2, TEST_MIGRATION_REGISTRY) +def creature_rename_num_legs_to_num_limbs(database): + """ + It turns out we want to track how many limbs a creature has, not + just how many legs. We don't care about the ambiguous distinction + between arms/legs currently. + """ + # $rename not available till 1.7.2+, Debian Stable only includes + # 1.4.4... we should do renames manually for now :( + + collection = database['creatures'] + target = collection.find( + {'num_legs': {'$exists': True}}) + + for document in target: + # A lame manual renaming. + document['num_limbs'] = document.pop('num_legs') + collection.save(document) + + +@RegisterMigration(3, TEST_MIGRATION_REGISTRY) +def creature_remove_is_demon(database): + """ + It turns out we don't care much about whether creatures are demons + or not. + """ + database['creatures'].update( + {'is_demon': {'$exists': True}}, + {'$unset': {'is_demon': 1}}, + multi=True) + + +@RegisterMigration(4, TEST_MIGRATION_REGISTRY) +def level_exits_dict_to_list(database): + """ + For the sake of the indexes we want to write, and because we + intend to add more flexible fields, we want to move level exits + from like: + + {'big_door': 'castle_level_id', + 'trapdoor': 'dungeon_level_id'} + + to like: + + [{'name': 'big_door', + 'exits_to': 'castle_level_id'}, + {'name': 'trapdoor', + 'exits_to': 'dungeon_level_id'}] + """ + collection = database['levels'] + target = collection.find( + {'exits': {'$type': 3}}) + + for level in target: + new_exits = [] + for exit_name, exits_to in level['exits'].items(): + new_exits.append( + {'name': exit_name, + 'exits_to': exits_to}) + + level['exits'] = new_exits + collection.save(level) + + +CENTIPEDE_OBJECTID = ObjectId() +WOLF_OBJECTID = ObjectId() +WIZARDSNAKE_OBJECTID = ObjectId() + +UNMIGRATED_DBDATA = { + 'creatures': [ + {'_id': CENTIPEDE_OBJECTID, + 'name': 'centipede', + 'num_legs': 100, + 'is_demon': False}, + {'_id': WOLF_OBJECTID, + 'name': 'wolf', + 'num_legs': 4, + 'is_demon': False}, + # don't ask me what a wizardsnake is. + {'_id': WIZARDSNAKE_OBJECTID, + 'name': 'wizardsnake', + 'num_legs': 0, + 'is_demon': True}], + 'levels': [ + {'_id': 'necroplex', + 'name': 'The Necroplex', + 'description': 'A complex full of pure deathzone.', + 'exits': { + 'deathwell': 'evilstorm', + 'portal': 'central_park'}}, + {'_id': 'evilstorm', + 'name': 'Evil Storm', + 'description': 'A storm full of pure evil.', + 'exits': {}}, # you can't escape the evilstorm + {'_id': 'central_park', + 'name': 'Central Park, NY, NY', + 'description': "New York's friendly Central Park.", + 'exits': { + 'portal': 'necroplex'}}]} + + +EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA = { + 'creatures': [ + {'_id': CENTIPEDE_OBJECTID, + 'name': 'centipede', + 'num_limbs': 100, + 'magical_powers': []}, + {'_id': WOLF_OBJECTID, + 'name': 'wolf', + 'num_limbs': 4, + # kept around namely to check that it *isn't* removed! + 'magical_powers': []}, + {'_id': WIZARDSNAKE_OBJECTID, + 'name': 'wizardsnake', + 'num_limbs': 0, + 'magical_powers': []}], + 'levels': [ + {'_id': 'necroplex', + 'name': 'The Necroplex', + 'description': 'A complex full of pure deathzone.', + 'exits': [ + {'name': 'deathwell', + 'exits_to': 'evilstorm'}, + {'name': 'portal', + 'exits_to': 'central_park'}]}, + {'_id': 'evilstorm', + 'name': 'Evil Storm', + 'description': 'A storm full of pure evil.', + 'exits': []}, # you can't escape the evilstorm + {'_id': 'central_park', + 'name': 'Central Park, NY, NY', + 'description': "New York's friendly Central Park.", + 'exits': [ + {'name': 'portal', + 'exits_to': 'necroplex'}]}]} + +# We want to make sure that if we're at migration 3, migration 3 +# doesn't get re-run. + +SEMI_MIGRATED_DBDATA = { + 'creatures': [ + {'_id': CENTIPEDE_OBJECTID, + 'name': 'centipede', + 'num_limbs': 100, + 'magical_powers': []}, + {'_id': WOLF_OBJECTID, + 'name': 'wolf', + 'num_limbs': 4, + # kept around namely to check that it *isn't* removed! + 'is_demon': False, + 'magical_powers': [ + 'ice_breath', 'death_stare']}, + {'_id': WIZARDSNAKE_OBJECTID, + 'name': 'wizardsnake', + 'num_limbs': 0, + 'magical_powers': [ + 'death_rattle', 'sneaky_stare', + 'slithery_smoke', 'treacherous_tremors'], + 'is_demon': True}], + 'levels': [ + {'_id': 'necroplex', + 'name': 'The Necroplex', + 'description': 'A complex full of pure deathzone.', + 'exits': { + 'deathwell': 'evilstorm', + 'portal': 'central_park'}}, + {'_id': 'evilstorm', + 'name': 'Evil Storm', + 'description': 'A storm full of pure evil.', + 'exits': {}}, # you can't escape the evilstorm + {'_id': 'central_park', + 'name': 'Central Park, NY, NY', + 'description': "New York's friendly Central Park.", + 'exits': { + 'portal': 'necroplex'}}]} + + +EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA = { + 'creatures': [ + {'_id': CENTIPEDE_OBJECTID, + 'name': 'centipede', + 'num_limbs': 100, + 'magical_powers': []}, + {'_id': WOLF_OBJECTID, + 'name': 'wolf', + 'num_limbs': 4, + # kept around namely to check that it *isn't* removed! + 'is_demon': False, + 'magical_powers': [ + 'ice_breath', 'death_stare']}, + {'_id': WIZARDSNAKE_OBJECTID, + 'name': 'wizardsnake', + 'num_limbs': 0, + 'magical_powers': [ + 'death_rattle', 'sneaky_stare', + 'slithery_smoke', 'treacherous_tremors'], + 'is_demon': True}], + 'levels': [ + {'_id': 'necroplex', + 'name': 'The Necroplex', + 'description': 'A complex full of pure deathzone.', + 'exits': [ + {'name': 'deathwell', + 'exits_to': 'evilstorm'}, + {'name': 'portal', + 'exits_to': 'central_park'}]}, + {'_id': 'evilstorm', + 'name': 'Evil Storm', + 'description': 'A storm full of pure evil.', + 'exits': []}, # you can't escape the evilstorm + {'_id': 'central_park', + 'name': 'Central Park, NY, NY', + 'description': "New York's friendly Central Park.", + 'exits': [ + {'name': 'portal', + 'exits_to': 'necroplex'}]}]} + + +class TestMigrations(object): + def setUp(self): + # Set up the connection, drop an existing possible database + self.connection = Connection() + self.connection.drop_database(MIGRATION_DB_NAME) + self.db = Connection()[MIGRATION_DB_NAME] + self.migration_manager = MigrationManager( + self.db, TEST_MIGRATION_REGISTRY) + self.empty_migration_manager = MigrationManager( + self.db, TEST_EMPTY_MIGRATION_REGISTRY) + self.run_migrations = [] + + def tearDown(self): + self.connection.drop_database(MIGRATION_DB_NAME) + + def _record_migration(self, migration_number, migration_func): + self.run_migrations.append((migration_number, migration_func)) + + def test_migrations_registered_and_sorted(self): + """ + Make sure that migrations get registered and are sorted right + in the migration manager + """ + assert TEST_MIGRATION_REGISTRY == { + 1: creature_add_magical_powers, + 2: creature_rename_num_legs_to_num_limbs, + 3: creature_remove_is_demon, + 4: level_exits_dict_to_list} + assert self.migration_manager.sorted_migrations == [ + (1, creature_add_magical_powers), + (2, creature_rename_num_legs_to_num_limbs), + (3, creature_remove_is_demon), + (4, level_exits_dict_to_list)] + assert self.empty_migration_manager.sorted_migrations == [] + + def test_run_full_migrations(self): + """ + Make sure that running the full migration suite from 0 updates + everything + """ + self.migration_manager.set_current_migration(0) + assert self.migration_manager.database_current_migration() == 0 + install_fixtures_simple(self.db, UNMIGRATED_DBDATA) + self.migration_manager.migrate_new(post_callback=self._record_migration) + + assert self.run_migrations == [ + (1, creature_add_magical_powers), + (2, creature_rename_num_legs_to_num_limbs), + (3, creature_remove_is_demon), + (4, level_exits_dict_to_list)] + + assert_db_meets_expected( + self.db, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA) + + # Make sure the migration is recorded correctly + assert self.migration_manager.database_current_migration() == 4 + + # run twice! It should do nothing the second time. + # ------------------------------------------------ + self.run_migrations = [] + self.migration_manager.migrate_new(post_callback=self._record_migration) + assert self.run_migrations == [] + assert_db_meets_expected( + self.db, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA) + assert self.migration_manager.database_current_migration() == 4 + + + def test_run_partial_migrations(self): + """ + Make sure that running full migration suite from 3 only runs + last migration + """ + self.migration_manager.set_current_migration(3) + assert self.migration_manager.database_current_migration() == 3 + install_fixtures_simple(self.db, SEMI_MIGRATED_DBDATA) + self.migration_manager.migrate_new(post_callback=self._record_migration) + + assert self.run_migrations == [ + (4, level_exits_dict_to_list)] + + assert_db_meets_expected( + self.db, EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA) + + # Make sure the migration is recorded correctly + assert self.migration_manager.database_current_migration() == 4 + + def test_migrations_recorded_as_latest(self): + """ + Make sure that if we don't have a migration_status + pre-recorded it's marked as the latest + """ + self.migration_manager.install_migration_version_if_missing() + assert self.migration_manager.database_current_migration() == 4 + + def test_no_migrations_recorded_as_zero(self): + """ + Make sure that if we don't have a migration_status + but there *are* no migrations that it's marked as 0 + """ + self.empty_migration_manager.install_migration_version_if_missing() + assert self.empty_migration_manager.database_current_migration() == 0 + + def test_migrations_to_run(self): + """ + Make sure we get the right list of migrations to run + """ + self.migration_manager.set_current_migration(0) + + assert self.migration_manager.migrations_to_run() == [ + (1, creature_add_magical_powers), + (2, creature_rename_num_legs_to_num_limbs), + (3, creature_remove_is_demon), + (4, level_exits_dict_to_list)] + + self.migration_manager.set_current_migration(3) + + assert self.migration_manager.migrations_to_run() == [ + (4, level_exits_dict_to_list)] + + self.migration_manager.set_current_migration(4) + + assert self.migration_manager.migrations_to_run() == [] + + + def test_no_migrations_raises_exception(self): + """ + If we don't have the current migration set in the database, + this should error out. + """ + assert_raises( + MissingCurrentMigration, + self.migration_manager.migrations_to_run) diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py new file mode 100644 index 00000000..22b6117c --- /dev/null +++ b/mediagoblin/tests/test_submission.py @@ -0,0 +1,157 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urlparse +import pkg_resources + +from nose.tools import assert_equal + +from mediagoblin.auth import lib as auth_lib +from mediagoblin.tests.tools import setup_fresh_app, get_test_app +from mediagoblin import mg_globals +from mediagoblin import util + +GOOD_JPG = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_submission/good.jpg') +GOOD_PNG = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_submission/good.png') +EVIL_FILE = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_submission/evil') +EVIL_JPG = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_submission/evil.jpg') +EVIL_PNG = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_submission/evil.png') + + +class TestSubmission: + def setUp(self): + self.test_app = get_test_app() + + # TODO: Possibly abstract into a decorator like: + # @as_authenticated_user('chris') + test_user = mg_globals.database.User() + test_user['username'] = u'chris' + test_user['email'] = u'chris@example.com' + test_user['email_verified'] = True + test_user['status'] = u'active' + test_user['pw_hash'] = auth_lib.bcrypt_gen_password_hash('toast') + test_user.save() + + self.test_app.post( + '/auth/login/', { + 'username': u'chris', + 'password': 'toast'}) + + def test_missing_fields(self): + # Test blank form + # --------------- + util.clear_test_template_context() + response = self.test_app.post( + '/submit/', {}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html'] + form = context['submit_form'] + assert form.file.errors == [u'You must provide a file.'] + + # Test blank file + # --------------- + util.clear_test_template_context() + response = self.test_app.post( + '/submit/', { + 'title': 'test title'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html'] + form = context['submit_form'] + assert form.file.errors == [u'You must provide a file.'] + + + def test_normal_uploads(self): + # Test JPG + # -------- + util.clear_test_template_context() + response = self.test_app.post( + '/submit/', { + 'title': 'Normal upload 1' + }, upload_files=[( + 'file', GOOD_JPG)]) + + # User should be redirected + response.follow() + assert_equal( + urlparse.urlsplit(response.location)[2], + '/u/chris/') + assert util.TEMPLATE_TEST_CONTEXT.has_key( + 'mediagoblin/user_pages/user.html') + + # Test PNG + # -------- + util.clear_test_template_context() + response = self.test_app.post( + '/submit/', { + 'title': 'Normal upload 2' + }, upload_files=[( + 'file', GOOD_PNG)]) + + response.follow() + assert_equal( + urlparse.urlsplit(response.location)[2], + '/u/chris/') + assert util.TEMPLATE_TEST_CONTEXT.has_key( + 'mediagoblin/user_pages/user.html') + + + def test_malicious_uploads(self): + # Test non-suppoerted file with non-supported extension + # ----------------------------------------------------- + util.clear_test_template_context() + response = self.test_app.post( + '/submit/', { + 'title': 'Malicious Upload 2' + }, upload_files=[( + 'file', EVIL_FILE)]) + + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html'] + form = context['submit_form'] + assert form.file.errors == ['The file doesn\'t seem to be an image!'] + + # NOTE: The following 2 tests will fail. These can be uncommented + # after http://bugs.foocorp.net/issues/324 is resolved and + # bad files are handled properly. + + # Test non-supported file with .jpg extension + # ------------------------------------------- + #util.clear_test_template_context() + #response = self.test_app.post( + # '/submit/', { + # 'title': 'Malicious Upload 2' + # }, upload_files=[( + # 'file', EVIL_JPG)]) + + #context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html'] + #form = context['submit_form'] + #assert form.file.errors == ['The file doesn\'t seem to be an image!'] + + # Test non-supported file with .png extension + # ------------------------------------------- + #util.clear_test_template_context() + #response = self.test_app.post( + # '/submit/', { + # 'title': 'Malicious Upload 3' + # }, upload_files=[( + # 'file', EVIL_PNG)]) + + #context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html'] + #form = context['submit_form'] + #assert form.file.errors == ['The file doesn\'t seem to be an image!'] + diff --git a/mediagoblin/tests/test_submission/evil b/mediagoblin/tests/test_submission/evil Binary files differnew file mode 100755 index 00000000..775da664 --- /dev/null +++ b/mediagoblin/tests/test_submission/evil diff --git a/mediagoblin/tests/test_submission/evil.jpg b/mediagoblin/tests/test_submission/evil.jpg Binary files differnew file mode 100755 index 00000000..775da664 --- /dev/null +++ b/mediagoblin/tests/test_submission/evil.jpg diff --git a/mediagoblin/tests/test_submission/evil.png b/mediagoblin/tests/test_submission/evil.png Binary files differnew file mode 100755 index 00000000..775da664 --- /dev/null +++ b/mediagoblin/tests/test_submission/evil.png diff --git a/mediagoblin/tests/test_submission/good.jpg b/mediagoblin/tests/test_submission/good.jpg Binary files differnew file mode 100644 index 00000000..936458e9 --- /dev/null +++ b/mediagoblin/tests/test_submission/good.jpg diff --git a/mediagoblin/tests/test_submission/good.png b/mediagoblin/tests/test_submission/good.png Binary files differnew file mode 100644 index 00000000..c1eadf9c --- /dev/null +++ b/mediagoblin/tests/test_submission/good.png diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index e56af4de..4b61f259 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -118,3 +118,35 @@ def setup_fresh_app(func): return func(test_app, *args, **kwargs) return _make_safe(wrapper, func) + + +def install_fixtures_simple(db, fixtures): + """ + Very simply install fixtures in the database + """ + for collection_name, collection_fixtures in fixtures.iteritems(): + collection = db[collection_name] + for fixture in collection_fixtures: + collection.insert(fixture) + + +def assert_db_meets_expected(db, expected): + """ + Assert a database contains the things we expect it to. + + Objects are found via '_id', so you should make sure your document + has an _id. + + Args: + - db: pymongo or mongokit database connection + - expected: the data we expect. Formatted like: + {'collection_name': [ + {'_id': 'foo', + 'some_field': 'some_value'},]} + """ + for collection_name, collection_data in expected.iteritems(): + collection = db[collection_name] + for expected_document in collection_data: + document = collection.find_one({'_id': expected_document['_id']}) + assert document is not None # make sure it exists + assert document == expected_document # make sure it matches diff --git a/mediagoblin/user_pages/__init__.py b/mediagoblin/user_pages/__init__.py index e69de29b..a8eeb5ed 100644 --- a/mediagoblin/user_pages/__init__.py +++ b/mediagoblin/user_pages/__init__.py @@ -0,0 +1,17 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py index 9f7d2fbd..8829b674 100644 --- a/mediagoblin/user_pages/forms.py +++ b/mediagoblin/user_pages/forms.py @@ -1,21 +1,22 @@ -# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import wtforms
-
-class MediaCommentForm(wtforms.Form):
- comment = wtforms.TextAreaField('Comment',
- [wtforms.validators.Required()])
\ No newline at end of file +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import wtforms + +class MediaCommentForm(wtforms.Form): + comment_content = wtforms.TextAreaField( + 'Comment', + [wtforms.validators.Required()]) diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index 255b6f66..3be0617d 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -24,6 +24,9 @@ user_routes = [ Route('mediagoblin.user_pages.media_home', '/{user}/m/{media}/', requirements=dict(m_id="[0-9a-fA-F]{24}"), controller="mediagoblin.user_pages.views:media_home"), + Route('mediagoblin.user_pages.media_home.view_comment', + '/{user}/m/{media}/c/{comment}/', + controller="mediagoblin.user_pages.views:media_home"), Route('mediagoblin.edit.edit_media', "/{user}/m/{media}/edit/", controller="mediagoblin.edit.views:edit_media"), Route('mediagoblin.user_pages.atom_feed', '/{user}/atom/', diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 3a8684d3..dc71b059 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -22,8 +22,8 @@ from mediagoblin.util import ( Pagination, render_to_response, redirect, cleaned_markdown_conversion) from mediagoblin.user_pages import forms as user_forms -from mediagoblin.decorators import uses_pagination, get_user_media_entry, \ - require_active_login +from mediagoblin.decorators import (uses_pagination, get_user_media_entry, + require_active_login) from werkzeug.contrib.atom import AtomFeed @@ -32,10 +32,14 @@ from werkzeug.contrib.atom import AtomFeed def user_home(request, page): """'Homepage' of a User()""" user = request.db.User.find_one({ - 'username': request.matchdict['user'], - 'status': 'active'}) + 'username': request.matchdict['user']}) if not user: return exc.HTTPNotFound() + elif user['status'] != u'active': + return render_to_response( + request, + 'mediagoblin/user_pages/user.html', + {'user': user}) cursor = request.db.MediaEntry.find( {'uploader': user['_id'], @@ -95,8 +99,14 @@ def media_home(request, media, page, **kwargs): """ 'Homepage' of a MediaEntry() """ + if ObjectId(request.matchdict.get('comment')): + pagination = Pagination( + page, media.get_comments(), MEDIA_COMMENTS_PER_PAGE, + ObjectId(request.matchdict.get('comment'))) + else: + pagination = Pagination( + page, media.get_comments(), MEDIA_COMMENTS_PER_PAGE) - pagination = Pagination(page, media.get_comments(), MEDIA_COMMENTS_PER_PAGE) comments = pagination() comment_form = user_forms.MediaCommentForm(request.POST) @@ -118,7 +128,7 @@ def media_post_comment(request): comment = request.db.MediaComment() comment['media_entry'] = ObjectId(request.matchdict['media']) comment['author'] = request.user['_id'] - comment['content'] = request.POST['comment'] + comment['content'] = request.POST['comment_content'] comment['content_html'] = cleaned_markdown_conversion(comment['content']) diff --git a/mediagoblin/util.py b/mediagoblin/util.py index f051dc50..bb9f6db4 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from __future__ import division + from email.MIMEText import MIMEText import gettext import pkg_resources @@ -21,8 +23,7 @@ import smtplib import sys import re import urllib -from math import ceil -from string import strip +from math import ceil, floor import copy import wtforms @@ -37,6 +38,10 @@ from mediagoblin import mg_globals from mediagoblin import messages from mediagoblin.db.util import ObjectId +from itertools import izip, count + +DISPLAY_IMAGE_FETCHING_ORDER = [u'medium', u'original', u'thumb'] + TESTS_ENABLED = False def _activate_testing(): """ @@ -135,7 +140,16 @@ def render_to_response(request, template, context): def redirect(request, *args, **kwargs): """Returns a HTTPFound(), takes a request and then urlgen params""" - return exc.HTTPFound(location=request.urlgen(*args, **kwargs)) + + querystring = None + if kwargs.get('querystring'): + querystring = kwargs.get('querystring') + del kwargs['querystring'] + + return exc.HTTPFound( + location=''.join([ + request.urlgen(*args, **kwargs), + querystring if querystring else ''])) def setup_user_in_request(request): @@ -254,9 +268,9 @@ def send_email(from_addr, to_addrs, subject, message_body): - message_body: email body text """ # TODO: make a mock mhost if testing is enabled - if TESTS_ENABLED or mg_globals.email_debug_mode: + if TESTS_ENABLED or mg_globals.app_config['email_debug_mode']: mhost = FakeMhost() - elif not mg_globals.email_debug_mode: + elif not mg_globals.app_config['email_debug_mode']: mhost = smtplib.SMTP() mhost.connect() @@ -269,7 +283,7 @@ def send_email(from_addr, to_addrs, subject, message_body): if TESTS_ENABLED: EMAIL_TEST_INBOX.append(message) - if getattr(mg_globals, 'email_debug_mode', False): + if mg_globals.app_config['email_debug_mode']: print u"===== Email =====" print u"From address: %s" % message['From'] print u"To addresses: %s" % message['To'] @@ -478,7 +492,8 @@ class Pagination(object): get actual data slice through __call__(). """ - def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE): + def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE, + jump_to_id=False): """ Initializes Pagination @@ -486,11 +501,25 @@ class Pagination(object): - page: requested page - per_page: number of objects per page - cursor: db cursor + - jump_to_id: ObjectId, sets the page to the page containing the object + with _id == jump_to_id. """ - self.page = page + self.page = page self.per_page = per_page self.cursor = cursor self.total_count = self.cursor.count() + self.active_id = None + + if jump_to_id: + cursor = copy.copy(self.cursor) + + for (doc, increment) in izip(cursor, count(0)): + if doc['_id'] == jump_to_id: + self.page = 1 + int(floor(increment / self.per_page)) + + self.active_id = jump_to_id + break + def __call__(self): """ diff --git a/mediagoblin/views.py b/mediagoblin/views.py index 5b6d9773..e7d9dbdd 100644 --- a/mediagoblin/views.py +++ b/mediagoblin/views.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from mediagoblin import mg_globals from mediagoblin.util import render_to_response from mediagoblin.db.util import DESCENDING @@ -23,7 +24,8 @@ def root_view(request): return render_to_response( request, 'mediagoblin/root.html', - {'media_entries': media_entries}) + {'media_entries': media_entries, + 'allow_registration': mg_globals.app_config["allow_registration"]}) def simple_template_render(request): |