diff --git a/AUTHORS b/AUTHORS index b6dd1cf2..304e14a2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,10 +8,12 @@ variety of different ways and this software wouldn't exist without them. Thank you! * Aaron Williamson +* Aditi Mittal * Aeva Ntsc * Alejandro Villanueva * Aleksandar Micovic * Aleksej Serdjukov +* Alon Levy * Alex Camelio * András Veres-Szentkirályi * Bassam Kurdali @@ -21,13 +23,17 @@ Thank you! * Corey Farwell * Chris Moylan * Christopher Allan Webber +* David Thompson * Daniel Neel * Deb Nicholson * Derek Moore * Duncan Paterson * Elrond of Samba TNG * Emily O'Leary +* Gabi Thume +* Gabriel Saldana * Greg Grossmeier +* Hans Lo * Jakob Kramer * Jef van Schendel * Jessica Tallon @@ -36,25 +42,34 @@ Thank you! * Jorge Araya Navarro * Karen Rustad * Kuno Woudt +* Laura Arjona * Larisa Hoffenbecker * Luke Slater * Manuel Urbano Santos * Mark Holmquist +* Mats Sjöberg * Matt Lee * Michele Azzolari +* Mike Linksvayer +* Natalie Foust-Pilcher * Nathan Yergler * Odin Hørthe Omdal * Osama Khalid * Pablo J. Urbano Santos +* Praveen Kumar * Rasmus Larsson +* Rodney Ewing * Runar Petursson * Sacha De'Angeli * Sam Kleinman +* Sam Tuke * Sebastian Spaeth * Shawn Khan +* Simon Fondrie-Teitler * Stefano Zacchiroli * Tiberiu C. Turbureanu * Tran Thanh Bao +* Tryggvi Björgvinsson * Shawn Khan * Will Kahn-Greene @@ -64,4 +79,4 @@ If you think your name should be on this list, let us know! We also are currently borrowing an image in mediagoblin/static/images/media_thumbs/image.png from the wonderful people at http://tango.freedesktop.org/ which is in the public -domain... thanks Tango folks! \ No newline at end of file +domain... thanks Tango folks! diff --git a/README b/README index 07f9a094..98b8e9e9 100644 --- a/README +++ b/README @@ -5,21 +5,21 @@ What is GNU MediaGoblin? ======================== -* Initially, a place to store all your photos that’s as awesome as, if - not more awesome than, existing network services (Flickr, SmugMug, - Picasa, etc) +* A place to store all your different media (photos, videos, audios, + and more!) that’s as awesome as, if not more awesome than, existing + network services (Flickr, YouTube, etc) * Customizable! * A place for people to collaborate and show off original and derived creations. Free, as in freedom. We’re a GNU project after all. -* Later, a place for all sorts of media, such as video, music, etc hosting. -* Later, federated with OStatus! +* Extensible: Plugins allow you to add new media types (3d models? + Presentations and documents? Yes, and more!) or extend old ones. +* A real community, and we'd love to have you join us! Is it ready for me to use? ========================== -Yes! But with caveats. The software is usable and there are instances -running, but it's still in its early stages. +Yes! Can I help/hang out/participate/whisper sweet nothings in your ear? diff --git a/docs/source/devel/migrations.rst b/docs/source/devel/migrations.rst new file mode 100644 index 00000000..16c02b04 --- /dev/null +++ b/docs/source/devel/migrations.rst @@ -0,0 +1,62 @@ +.. MediaGoblin Documentation + + Written in 2011, 2012 by MediaGoblin contributors + + To the extent possible under law, the author(s) have dedicated all + copyright and related and neighboring rights to this software to + the public domain worldwide. This software is distributed without + any warranty. + + You should have received a copy of the CC0 Public Domain + Dedication along with this software. If not, see + . + +========== +Migrations +========== + +So, about migrations. Every time we change the way the database +structure works, we need to add a migration so that people running +older codebases can have their databases updated to the new structure +when they run `./bin/gmg dbupdate`. + +The first time `./bin/gmg dbupdate` is run by a user, it creates the +tables at the current state that they're defined in models.py and sets +the migration number to the current migration... after all, migrations +only exist to get things to the current state of the db. After that, +every migration is run with dbupdate. + +There's a few things you need to know: + +- We use `sqlalchemy-migrate + `_. + See `their docs `_. +- `Alembic `_ might be a better + choice than sqlalchemy-migrate now or in the future, but we + originally decided not to use it because it didn't have sqlite + support. It's not clear if that's changed. +- SQLAlchemy has two parts to it, the ORM and the "core" interface. + We DO NOT use the ORM when running migrations. Think about it: the + ORM is set up with an expectation that the models already reflect a + certain pattern. But if a person is moving from their old patern + and are running tools to *get to* the current pattern, of course + their current database structure doesn't match the state of the ORM! +- How to write migrations? Maybe there will be a tutorial here in the + future... in the meanwhile, look at existing migrations in + `mediagoblin/db/migrations.py` and look in + `mediagoblin/tests/test_sql_migrations.py` for examples. +- Common pattern: use `inspect_table` to get the current state + of the table before we run alterations on it. +- Make sure you set the RegisterMigration to be the next migration in + order. +- What happens if you're adding a *totally new* table? In this case, + you should copy the table in entirety as it exists into + migrations.py then create the tables based off of that... see + add_collection_tables. This is easier than reproducing the SQL by + hand. +- If you're writing a feature branch, you don't need to keep adding + migrations every time you change things around if your database + structure is in flux. Just alter your migrations so that they're + correct for the merge into master. + +That's it for now! Good luck! diff --git a/docs/source/index.rst b/docs/source/index.rst index d71f39f8..de6c9c0d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -74,6 +74,7 @@ This guide covers writing new GNU MediaGoblin plugins. pluginwriter/database pluginwriter/api pluginwriter/tests + pluginwriter/media_type_hooks Part 4: Developer's Zone @@ -87,6 +88,7 @@ This chapter contains various information for developers. devel/codebase devel/storage devel/originaldesigndecisions + devel/migrations Indices and tables diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst index 66def173..29adb691 100644 --- a/docs/source/pluginwriter/api.rst +++ b/docs/source/pluginwriter/api.rst @@ -69,6 +69,32 @@ example might look like:: This means that when people enable your plugin in their config you'll be able to provide defaults as well as type validation. +You can access this via the app_config variables in mg_globals, or you +can use a shortcut to get your plugin's config section:: + + >>> from mediagoblin.tools import pluginapi + # Replace with the path to your plugin. + # (If an external package, it won't be part of mediagoblin.plugins) + >>> floobie_config = pluginapi.get_config('mediagoblin.plugins.floobifier') + >>> floobie_dir = floobie_config['floobie_dir'] + # This is the same as the above + >>> from mediagoblin import mg_globals + >>> config = mg_globals.global_config['plugins']['mediagoblin.plugins.floobifier'] + >>> floobie_dir = floobie_config['floobie_dir'] + +A tip: you have access to the `%(here)s` variable in your config, +which is the directory that the user's mediagoblin config is running +out of. So for example, your plugin may need a "floobie" directory to +store floobs in. You could give them a reasonable default that makes +use of the default `user_dev` location, but allow users to override +it, like so:: + + [plugin_spec] + floobie_dir = string(default="%(here)s/user_dev/floobs/") + +Note, this is relative to the user's mediagoblin config directory, +*not* your plugin directory! + Context Hooks ------------- diff --git a/docs/source/pluginwriter/media_type_hooks.rst b/docs/source/pluginwriter/media_type_hooks.rst new file mode 100644 index 00000000..498b0b54 --- /dev/null +++ b/docs/source/pluginwriter/media_type_hooks.rst @@ -0,0 +1,38 @@ +================== + Media Type hooks +================== + +This documents the hooks that are currently available for ``media_type`` plugins. + +What hooks are available? +========================= + +'sniff_handler' +--------------- + +This hook is used by ``sniff_media`` in ``mediagoblin.media_types.__init__``. +Your media type should return its ``sniff_media`` method when this hook is +called. + +.. Note:: + Your ``sniff_media`` method should return either the ``media_type`` or + ``None``. + +'get_media_type_and_manager' +---------------------------- + +This hook is used by ``get_media_type_and_manager`` in +``mediagoblin.media_types.__init__``. When this hook is called, your media type +plugin should check if it can handle the given extension. If so, your media +type plugin should return the media type and media manager. + +('media_manager', MEDIA_TYPE) +----------------------------- + +If you already know the string representing the media type of a type +of media, you can pull down the manager specifically. Note that this +hook is not a string but a tuple of two strings, the latter being the +name of the media type. + +This is used by media entries to pull down their media managers, and +so on. diff --git a/docs/source/siteadmin/deploying.rst b/docs/source/siteadmin/deploying.rst index 0ee6b5b4..50fc05c5 100644 --- a/docs/source/siteadmin/deploying.rst +++ b/docs/source/siteadmin/deploying.rst @@ -157,13 +157,14 @@ directory. Modify these commands to reflect your own environment:: mkdir -p /srv/mediagoblin.example.org/ cd /srv/mediagoblin.example.org/ -Clone the MediaGoblin repository:: +Clone the MediaGoblin repository and set up the git submodules:: git clone git://gitorious.org/mediagoblin/mediagoblin.git + cd mediagoblin + git submodule init && git submodule update And set up the in-package virtualenv:: - cd mediagoblin (virtualenv --system-site-packages . || virtualenv .) && ./bin/python setup.py develop .. note:: @@ -194,7 +195,7 @@ This concludes the initial configuration of the development environment. In the future, when you update your codebase, you should also run:: - ./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate + ./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate && git submodule fetch Note: If you are running an active site, depending on your server configuration, you may need to stop it first or the dbupdate command diff --git a/docs/source/siteadmin/media-types.rst b/docs/source/siteadmin/media-types.rst index 210094b9..3e8a94e9 100644 --- a/docs/source/siteadmin/media-types.rst +++ b/docs/source/siteadmin/media-types.rst @@ -18,16 +18,18 @@ Media Types ==================== In the future, there will be all sorts of media types you can enable, -but in the meanwhile there are three additional media types: video, audio -and ascii art. +but in the meanwhile there are five additional media types: video, audio, +ascii art, STL/3d models, PDF and Document. First, you should probably read ":doc:`configuration`" to make sure you know how to modify the mediagoblin config file. - Enabling Media Types ==================== +.. note:: + Media types are now plugins + Media types are enabled in your mediagoblin configuration file, typically it is created by copying ``mediagoblin.ini`` to ``mediagoblin_local.ini`` and then applying your changes to ``mediagoblin_local.ini``. If you don't already have a @@ -37,11 +39,13 @@ Most media types have additional dependencies that you will have to install. You will find descriptions on how to satisfy the requirements of each media type on this page. -To enable a media type, edit the ``media_types`` list in your -``mediagoblin_local.ini``. For example, if your system supported image and -video media types, then the list would look like this:: +To enable a media type, add the the media type under the ``[plugins]`` section +in you ``mediagoblin_local.ini``. For example, if your system supported image +and video media types, then it would look like this:: - media_types = mediagoblin.media_types.image, mediagoblin.media_types.video + [plugins] + [[mediagoblin.media_types.image]] + [[mediagoblin.media_types.video]] Note that after enabling new media types, you must run dbupdate like so:: @@ -83,8 +87,8 @@ good/bad/ugly). On Debianoid systems gstreamer0.10-ffmpeg -Add ``mediagoblin.media_types.video`` to the ``media_types`` list in your -``mediagoblin_local.ini`` and restart MediaGoblin. +Add ``[[mediagoblin.media_types.video]]`` under the ``[plugins]`` section in +your ``mediagoblin_local.ini`` and restart MediaGoblin. Run @@ -133,7 +137,7 @@ Then install ``scikits.audiolab`` for the spectrograms:: ./bin/pip install scikits.audiolab -Add ``mediagoblin.media_types.audio`` to the ``media_types`` list in your +Add ``[[mediagoblin.media_types.audio]]`` under the ``[plugins]`` section in your ``mediagoblin_local.ini`` and restart MediaGoblin. Run @@ -158,13 +162,8 @@ library, which is necessary for creating thumbnails of ascii art Next, modify (and possibly copy over from ``mediagoblin.ini``) your -``mediagoblin_local.ini``. In the ``[mediagoblin]`` section, add -``mediagoblin.media_types.ascii`` to the ``media_types`` list. - -For example, if your system supported image and ascii art media types, then -the list would look like this:: - - media_types = mediagoblin.media_types.image, mediagoblin.media_types.ascii +``mediagoblin_local.ini``. In the ``[plugins]`` section, add +``[[mediagoblin.media_types.ascii]]``. Run @@ -184,7 +183,7 @@ your execution path. This feature has been tested with Blender 2.63. It may work on some earlier versions, but that is not guaranteed (and is surely not to work prior to Blender 2.5X). -Add ``mediagoblin.media_types.stl`` to the ``media_types`` list in your +Add ``[[mediagoblin.media_types.stl]]`` under the ``[plugins]`` section in your ``mediagoblin_local.ini`` and restart MediaGoblin. Run @@ -199,8 +198,16 @@ will be able to present them to your wide audience of admirers! PDF and Document ================ -To enable the "PDF and Document" support plugin, you need pdftocairo, pdfinfo, -unoconv with headless support. All executables must be on your execution path. +To enable the "PDF and Document" support plugin, you need: + +1. pdftocairo and pdfinfo for pdf only support. + +2. unoconv with headless support to support converting libreoffice supported + documents as well, such as doc/ppt/xls/odf/odg/odp and more. + For the full list see mediagoblin/media_types/pdf/processing.py, + unoconv_supported. + +All executables must be on your execution path. To install this on Fedora: @@ -208,6 +215,9 @@ To install this on Fedora: sudo yum install -y poppler-utils unoconv libreoffice-headless +Note: You can leave out unoconv and libreoffice-headless if you want only pdf +support. This will result in a much smaller list of dependencies. + pdf.js relies on git submodules, so be sure you have fetched them: .. code-block:: bash @@ -222,7 +232,7 @@ This feature has been tested on Fedora with: It may work on some earlier versions, but that is not guaranteed. -Add ``mediagoblin.media_types.pdf`` to the ``media_types`` list in your +Add ``[[mediagoblin.media_types.pdf]]`` under the ``[plugins]`` section in your ``mediagoblin_local.ini`` and restart MediaGoblin. Run diff --git a/docs/source/siteadmin/relnotes.rst b/docs/source/siteadmin/relnotes.rst index 9c9d311c..35415b66 100644 --- a/docs/source/siteadmin/relnotes.rst +++ b/docs/source/siteadmin/relnotes.rst @@ -21,11 +21,28 @@ This chapter has important information for releases in it. If you're upgrading from a previous release, please read it carefully, or at least skim over it. + +0.4.1 +===== + +This is a bugfix release for 0.4.0. This only implements one major +fix in the newly released document support which prevented the +"conversion via libreoffice" feature. + +If you were running 0.4.0 you can upgrade to v0.4.1 via a simple +switch and restarting mediagoblin/celery with no other actions. + +Otherwise, follow 0.4.0 instructions. + + 0.4.0 ===== **Do this to upgrade** -1. Make sure to run ``bin/gmg dbupdate`` after upgrading. + +1. Make sure to run + ``./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate`` + after upgrading. 2. See "For Theme authors" if you have a custom theme. 3. Note that ``./bin/gmg theme assetlink`` is now just ``./bin/gmg assetlink`` and covers both plugins and assets. @@ -39,6 +56,16 @@ carefully, or at least skim over it. alias /srv/mediagoblin.example.org/mediagoblin/user_dev/plugin_static/; } + Similarly, if you've got a modified paste config, you may want to + borrow the app:plugin_static section from the default paste.ini + file. +5. We now use itsdangerous for sessions; if you had any references to + beaker in your paste config you can remove them. Again, see the + default paste.ini config +6. We also now use git submodules. Please do: + ``git submodule init && git submodule update`` + You will need to do this to use the new PDF support. + **For theme authors** If you have your own theme or you have any "user modified templates", @@ -51,7 +78,23 @@ please note the following: You can easily customize this to give a welcome page appropriate to your site. + **New features** +* PDF media type! +* Improved plugin system. More flexible, better documented, with a + new plugin authoring section of the docs. +* itsdangerous based sessions. No more beaker! +* New, experimental Piwigo-based API. This means you should be able + to use MediaGoblin with something like Shotwell. (Again, a word of + caution: this is *very experimental*!) +* Human readable timestamps, and the option to display the original + date of an image when available (available as the + "original_date_visible" variable) +* Moved unit testing system from nosetests to py.test so we can better + handle issues with sqlalchemy exploding with different database + configurations. Long story :) +* You can now disable the ability to post comments. +* Tags now can be up to length 255 characters by default. 0.3.3 diff --git a/mediagoblin.ini b/mediagoblin.ini index cc45c08d..30dacadf 100644 --- a/mediagoblin.ini +++ b/mediagoblin.ini @@ -11,19 +11,15 @@ email_sender_address = "notice@mediagoblin.example.org" ## Uncomment and change to your DB's appropiate setting. ## Default is a local sqlite db "mediagoblin.db". +## Don't forget to run `./bin/gmg dbupdate` after having changed it. # sql_engine = postgresql:///mediagoblin -# set to false to enable sending notices +# Set to false to enable sending notices email_debug_mode = true # Set to false to disable registrations allow_registration = true -## Uncomment this to turn on video or enable other media types -## You may have to install dependencies, and will have to run ./bin/gmg dbupdate -## See http://docs.mediagoblin.org/siteadmin/media-types.html for details. -# media_types = mediagoblin.media_types.image, mediagoblin.media_types.video - ## Uncomment this to put some user-overriding templates here # local_templates = %(here)s/user_dev/templates/ @@ -43,7 +39,9 @@ base_url = /mgoblin_media/ [celery] # Put celery stuff here -# place plugins here---each in their own subsection of [plugins]. see -# documentation for details. +# Place plugins here, each in their own subsection of [plugins]. +# See http://docs.mediagoblin.org/siteadmin/plugins.html for details. [plugins] [[mediagoblin.plugins.geolocation]] +[[mediagoblin.plugins.basic_auth]] +[[mediagoblin.media_types.image]] diff --git a/mediagoblin/_version.py b/mediagoblin/_version.py index 8437be8b..94629775 100644 --- a/mediagoblin/_version.py +++ b/mediagoblin/_version.py @@ -23,4 +23,4 @@ # see http://www.python.org/dev/peps/pep-0386/ -__version__ = "0.4.0.dev" +__version__ = "0.5.0.dev" diff --git a/mediagoblin/app.py b/mediagoblin/app.py index 1984ce77..57e09e49 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -29,6 +29,7 @@ from mediagoblin.tools import common, session, translate, template from mediagoblin.tools.response import render_http_exception from mediagoblin.tools.theme import register_themes from mediagoblin.tools import request as mg_request +from mediagoblin.media_types.tools import media_type_warning from mediagoblin.mg_globals import setup_globals from mediagoblin.init.celery import setup_celery_from_config from mediagoblin.init.plugins import setup_plugins @@ -37,6 +38,8 @@ from mediagoblin.init import (get_jinja_loader, get_staticdirector, setup_storage) from mediagoblin.tools.pluginapi import PluginManager, hook_transform from mediagoblin.tools.crypto import setup_crypto +from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout +from mediagoblin import notifications _log = logging.getLogger(__name__) @@ -67,6 +70,8 @@ class MediaGoblinApp(object): # Open and setup the config global_config, app_config = setup_global_and_app_config(config_path) + media_type_warning() + setup_crypto() ########################################## @@ -85,7 +90,7 @@ class MediaGoblinApp(object): setup_plugins() # Set up the database - self.db = setup_database() + self.db = setup_database(app_config['run_migrations']) # Register themes self.theme_registry, self.current_theme = register_themes(app_config) @@ -97,6 +102,11 @@ class MediaGoblinApp(object): PluginManager().get_template_paths() ) + # Check if authentication plugin is enabled and respond accordingly. + self.auth = check_auth_enabled() + if not self.auth: + app_config['allow_comments'] = False + # Set up storage systems self.public_store, self.queue_store = setup_storage() @@ -186,6 +196,11 @@ class MediaGoblinApp(object): request.urlgen = build_proxy + # Log user out if authentication_disabled + no_auth_logout(request) + + request.notifications = notifications + mg_request.setup_user_in_request(request) request.controller_name = None diff --git a/mediagoblin/auth/__init__.py b/mediagoblin/auth/__init__.py index 621845ba..be5d0eed 100644 --- a/mediagoblin/auth/__init__.py +++ b/mediagoblin/auth/__init__.py @@ -13,3 +13,32 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from mediagoblin.tools.pluginapi import hook_handle, hook_runall + + +def get_user(**kwargs): + """ Takes a kwarg such as username and returns a user object """ + return hook_handle("auth_get_user", **kwargs) + + +def create_user(register_form): + results = hook_runall("auth_create_user", register_form) + return results[0] + + +def extra_validation(register_form): + from mediagoblin.auth.tools import basic_extra_validation + + extra_validation_passes = basic_extra_validation(register_form) + if False in hook_runall("auth_extra_validation", register_form): + extra_validation_passes = False + return extra_validation_passes + + +def gen_password_hash(raw_pass, extra_salt=None): + return hook_handle("auth_gen_password_hash", raw_pass, extra_salt) + + +def check_password(raw_pass, stored_hash, extra_salt=None): + return hook_handle("auth_check_password", + raw_pass, stored_hash, extra_salt) diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index 0a391d67..865502e9 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -20,32 +20,6 @@ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.auth.tools import normalize_user_or_email_field -class RegistrationForm(wtforms.Form): - username = wtforms.TextField( - _('Username'), - [wtforms.validators.Required(), - normalize_user_or_email_field(allow_email=False)]) - password = wtforms.PasswordField( - _('Password'), - [wtforms.validators.Required(), - wtforms.validators.Length(min=5, max=1024)]) - email = wtforms.TextField( - _('Email address'), - [wtforms.validators.Required(), - normalize_user_or_email_field(allow_user=False)]) - - -class LoginForm(wtforms.Form): - username = wtforms.TextField( - _('Username or Email'), - [wtforms.validators.Required(), - normalize_user_or_email_field()]) - password = wtforms.PasswordField( - _('Password'), - [wtforms.validators.Required(), - wtforms.validators.Length(min=5, max=1024)]) - - class ForgotPassForm(wtforms.Form): username = wtforms.TextField( _('Username or email'), @@ -58,9 +32,6 @@ class ChangePassForm(wtforms.Form): 'Password', [wtforms.validators.Required(), wtforms.validators.Length(min=5, max=1024)]) - userid = wtforms.HiddenField( - '', - [wtforms.validators.Required()]) token = wtforms.HiddenField( '', [wtforms.validators.Required()]) diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py index 39b349de..596a4447 100644 --- a/mediagoblin/auth/tools.py +++ b/mediagoblin/auth/tools.py @@ -14,19 +14,20 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import uuid -import logging +import logging import wtforms from sqlalchemy import or_ from mediagoblin import mg_globals -from mediagoblin.auth import lib as auth_lib -from mediagoblin.db.models import User, Privilege +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.db.models import User from mediagoblin.tools.mail import (normalize_email, send_email, email_debug_message) from mediagoblin.tools.template import render_template from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ +from mediagoblin.tools.pluginapi import hook_handle +from mediagoblin import auth _log = logging.getLogger(__name__) @@ -62,11 +63,12 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True): EMAIL_VERIFICATION_TEMPLATE = ( - u"http://{host}{uri}?" - u"userid={userid}&token={verification_key}") + u"{uri}?" + u"token={verification_key}") -def send_verification_email(user, request): +def send_verification_email(user, request, email=None, + rendered_email=None): """ Send the verification email to users to activate their accounts. @@ -74,19 +76,24 @@ def send_verification_email(user, request): - user: a user object - request: the request """ - rendered_email = render_template( - request, 'mediagoblin/auth/verification_email.txt', - {'username': user.username, - 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format( - host=request.host, - uri=request.urlgen('mediagoblin.auth.verify_email'), - userid=unicode(user.id), - verification_key=user.verification_key)}) + if not email: + email = user.email + + if not rendered_email: + verification_key = get_timed_signer_url('mail_verification_token') \ + .dumps(user.id) + rendered_email = render_template( + request, 'mediagoblin/auth/verification_email.txt', + {'username': user.username, + 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format( + uri=request.urlgen('mediagoblin.auth.verify_email', + qualified=True), + verification_key=verification_key)}) # TODO: There is no error handling in place send_email( mg_globals.app_config['email_sender_address'], - [user.email], + [email], # TODO # Due to the distributed nature of GNU MediaGoblin, we should # find a way to send some additional information about the @@ -96,11 +103,43 @@ def send_verification_email(user, request): rendered_email) +EMAIL_FP_VERIFICATION_TEMPLATE = ( + u"{uri}?" + u"token={fp_verification_key}") + + +def send_fp_verification_email(user, request): + """ + Send the verification email to users to change their password. + + Args: + - user: a user object + - request: the request + """ + fp_verification_key = get_timed_signer_url('mail_verification_token') \ + .dumps(user.id) + + rendered_email = render_template( + request, 'mediagoblin/auth/fp_verification_email.txt', + {'username': user.username, + 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( + uri=request.urlgen('mediagoblin.auth.verify_forgot_password', + qualified=True), + fp_verification_key=fp_verification_key)}) + + # TODO: There is no error handling in place + send_email( + mg_globals.app_config['email_sender_address'], + [user.email], + 'GNU MediaGoblin - Change forgotten password!', + rendered_email) + + def basic_extra_validation(register_form, *args): users_with_username = User.query.filter_by( - username=register_form.data['username']).count() + username=register_form.username.data).count() users_with_email = User.query.filter_by( - email=register_form.data['email']).count() + email=register_form.email.data).count() extra_validation_passes = True @@ -118,17 +157,11 @@ def basic_extra_validation(register_form, *args): def register_user(request, register_form): """ Handle user registration """ - extra_validation_passes = basic_extra_validation(register_form) + extra_validation_passes = auth.extra_validation(register_form) if extra_validation_passes: # Create the user - user = User() - user.username = register_form.data['username'] - user.email = register_form.data['email'] - user.pw_hash = auth_lib.bcrypt_gen_password_hash( - register_form.password.data) - user.verification_key = unicode(uuid.uuid4()) - user.save() + user = auth.create_user(register_form) # give the user the default privileges default_privileges = [ @@ -151,17 +184,37 @@ def register_user(request, register_form): return None -def check_login_simple(username, password, username_might_be_email=False): - search = (User.username == username) - if username_might_be_email and ('@' in username): - search = or_(search, User.email == username) - user = User.query.filter(search).first() +def check_login_simple(username, password): + user = auth.get_user(username=username) if not user: _log.info("User %r not found", username) - auth_lib.fake_login_attempt() + hook_handle("auth_fake_login_attempt") return None - if not auth_lib.bcrypt_check_password(password, user.pw_hash): + if not auth.check_password(password, user.pw_hash): _log.warn("Wrong password for %r", username) return None _log.info("Logging %r in", username) return user + + +def check_auth_enabled(): + if not hook_handle('authentication'): + _log.warning('No authentication is enabled') + return False + else: + return True + + +def no_auth_logout(request): + """Log out the user if authentication_disabled, but don't delete the messages""" + if not mg_globals.app.auth and 'user_id' in request.session: + del request.session['user_id'] + request.session.save() + + +def create_basic_user(form): + user = User() + user.username = form.username.data + user.email = form.email.data + user.save() + return user diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 1c346556..90d6f5cc 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -14,36 +14,37 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import uuid -import datetime +from itsdangerous import BadSignature from mediagoblin import messages, mg_globals from mediagoblin.db.models import User, Privilege +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.decorators import auth_enabled, allow_registration from mediagoblin.tools.response import render_to_response, redirect, render_404 from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.mail import email_debug_message -from mediagoblin.auth import lib as auth_lib +from mediagoblin.tools.pluginapi import hook_handle from mediagoblin.auth import forms as auth_forms -from mediagoblin.auth.lib import send_fp_verification_email from mediagoblin.auth.tools import (send_verification_email, register_user, + send_fp_verification_email, check_login_simple) +from mediagoblin import auth +@allow_registration +@auth_enabled def register(request): """The registration view. Note that usernames will always be lowercased. Email domains are lowercased while the first part remains case-sensitive. """ - # 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") + if 'pass_auth' not in request.template_env.globals: + redirect_name = hook_handle('auth_no_pass_redirect') + return redirect(request, 'mediagoblin.plugins.{0}.register'.format( + redirect_name)) - register_form = auth_forms.RegistrationForm(request.form) + register_form = hook_handle("auth_get_registration_form", request) if request.method == 'POST' and register_form.validate(): # TODO: Make sure the user doesn't exist already @@ -59,28 +60,36 @@ def register(request): return render_to_response( request, 'mediagoblin/auth/register.html', - {'register_form': register_form}) + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.auth.register')}) +@auth_enabled def login(request): """ MediaGoblin login view. If you provide the POST with 'next', it'll redirect to that view. """ - login_form = auth_forms.LoginForm(request.form) + if 'pass_auth' not in request.template_env.globals: + redirect_name = hook_handle('auth_no_pass_redirect') + return redirect(request, 'mediagoblin.plugins.{0}.login'.format( + redirect_name)) + + login_form = hook_handle("auth_get_login_form", request) login_failed = False if request.method == 'POST': - - username = login_form.data['username'] + username = login_form.username.data if login_form.validate(): - user = check_login_simple(username, login_form.password.data, True) + user = check_login_simple(username, login_form.password.data) if user: # set up login in session + if login_form.stay_logged_in.data: + request.session['stay_logged_in'] = True request.session['user_id'] = unicode(user.id) request.session.save() @@ -97,6 +106,7 @@ def login(request): {'login_form': login_form, 'next': request.GET.get('next') or request.form.get('next'), 'login_failed': login_failed, + 'post_url': request.urlgen('mediagoblin.auth.login'), 'allow_registration': mg_globals.app_config["allow_registration"]}) @@ -115,12 +125,26 @@ def verify_email(request): you are lucky :) """ # If we don't have userid and token parameters, we can't do anything; 404 - if not 'userid' in request.GET or not 'token' in request.GET: + if not 'token' in request.GET: return render_404(request) - user = User.query.filter_by(id=request.args['userid']).first() + # Catch error if token is faked or expired + try: + token = get_timed_signer_url("mail_verification_token") \ + .loads(request.GET['token'], max_age=10*24*3600) + except BadSignature: + messages.add_message( + request, + messages.ERROR, + _('The verification key or user id is incorrect.')) - if user and user.verification_key == unicode(request.GET['token']): + return redirect( + request, + 'index') + + user = User.query.filter_by(id=int(token)).first() + + if user and user.email_verified is False: user.status = u'active' user.email_verified = True user.verification_key = None @@ -169,9 +193,6 @@ def resend_activation(request): return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username']) - request.user.verification_key = unicode(uuid.uuid4()) - request.user.save() - email_debug_message(request) send_verification_email(request.user, request) @@ -191,13 +212,16 @@ def forgot_password(request): Sends an email with an url to renew forgotten password. Use GET querystring parameter 'username' to pre-populate the input field """ + if not 'pass_auth' in request.template_env.globals: + return redirect(request, 'index') + fp_form = auth_forms.ForgotPassForm(request.form, username=request.args.get('username')) if not (request.method == 'POST' and fp_form.validate()): # Either GET request, or invalid form submitted. Display the template return render_to_response(request, - 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form}) + 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form,}) # If we are here: method == POST and form is valid. username casing # has been sanitized. Store if a user was found by email. We should @@ -238,11 +262,6 @@ def forgot_password(request): # SUCCESS. Send reminder and return to login page if user: - user.fp_verification_key = unicode(uuid.uuid4()) - user.fp_token_expire = datetime.datetime.now() + \ - datetime.timedelta(days=10) - user.save() - email_debug_message(request) send_fp_verification_email(user, request) @@ -257,31 +276,44 @@ def verify_forgot_password(request): """ # get form data variables, and specifically check for presence of token formdata = _process_for_token(request) - if not formdata['has_userid_and_token']: + if not formdata['has_token']: return render_404(request) - formdata_token = formdata['vars']['token'] - formdata_userid = formdata['vars']['userid'] formdata_vars = formdata['vars'] - # check if it's a valid user id - user = User.query.filter_by(id=formdata_userid).first() - if not user: - return render_404(request) + # Catch error if token is faked or expired + try: + token = get_timed_signer_url("mail_verification_token") \ + .loads(formdata_vars['token'], max_age=10*24*3600) + except BadSignature: + messages.add_message( + request, + messages.ERROR, + _('The verification key or user id is incorrect.')) - # check if we have a real user and correct token - if ((user and user.fp_verification_key and - user.fp_verification_key == unicode(formdata_token) and - datetime.datetime.now() < user.fp_token_expire - and user.email_verified and user.status == 'active')): + return redirect( + request, + 'index') + + # check if it's a valid user id + user = User.query.filter_by(id=int(token)).first() + + # no user in db + if not user: + messages.add_message( + request, messages.ERROR, + _('The user id is incorrect.')) + return redirect( + request, 'index') + + # check if user active and has email verified + if user.email_verified and user.status == 'active': cp_form = auth_forms.ChangePassForm(formdata_vars) if request.method == 'POST' and cp_form.validate(): - user.pw_hash = auth_lib.bcrypt_gen_password_hash( + user.pw_hash = auth.gen_password_hash( cp_form.password.data) - user.fp_verification_key = None - user.fp_token_expire = None user.save() messages.add_message( @@ -293,12 +325,22 @@ def verify_forgot_password(request): return render_to_response( request, 'mediagoblin/auth/change_fp.html', - {'cp_form': cp_form}) + {'cp_form': cp_form,}) - # in case there is a valid id but no user with that id in the db - # or the token expired - else: - return render_404(request) + if not user.email_verified: + messages.add_message( + request, messages.ERROR, + _('You need to verify your email before you can reset your' + ' password.')) + + if not user.status == 'active': + messages.add_message( + request, messages.ERROR, + _('You are no longer an active user. Please contact the system' + ' admin to reactivate your accoutn.')) + + return redirect( + request, 'index') def _process_for_token(request): @@ -316,7 +358,6 @@ def _process_for_token(request): formdata = { 'vars': formdata_vars, - 'has_userid_and_token': - 'userid' in formdata_vars and 'token' in formdata_vars} + 'has_token': 'token' in formdata_vars} return formdata diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index b213970d..d2ada163 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -5,12 +5,13 @@ html_title = string(default="GNU MediaGoblin") # link to source for this MediaGoblin site source_link = string(default="https://gitorious.org/mediagoblin/mediagoblin") -# Enabled media types -media_types = string_list(default=list("mediagoblin.media_types.image")) - # database stuff sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db") +# This flag is used during testing to allow use of in-memory SQLite +# databases. It is not recommended to be used on a running instance. +run_migrations = boolean(default=False) + # Where temporary files used in processing and etc are kept workbench_path = string(default="%(here)s/user_dev/media/workbench") @@ -22,9 +23,10 @@ direct_remote_path = string(default="/mgoblin_static/") # set to false to enable sending notices email_debug_mode = boolean(default=True) +email_smtp_use_ssl = boolean(default=False) email_sender_address = string(default="notice@mediagoblin.example.org") email_smtp_host = string(default='') -email_smtp_port = integer(default=25) +email_smtp_port = integer(default=0) email_smtp_user = string(default=None) email_smtp_pass = string(default=None) diff --git a/mediagoblin/db/base.py b/mediagoblin/db/base.py index 699a503a..c0cefdc2 100644 --- a/mediagoblin/db/base.py +++ b/mediagoblin/db/base.py @@ -24,18 +24,6 @@ Session = scoped_session(sessionmaker()) class GMGTableBase(object): query = Session.query_property() - @classmethod - def find(cls, query_dict): - return cls.query.filter_by(**query_dict) - - @classmethod - def find_one(cls, query_dict): - return cls.query.filter_by(**query_dict).first() - - @classmethod - def one(cls, query_dict): - return cls.find(query_dict).one() - def get(self, key): return getattr(self, key) diff --git a/mediagoblin/db/migration_tools.py b/mediagoblin/db/migration_tools.py index f100f47e..ad137683 100644 --- a/mediagoblin/db/migration_tools.py +++ b/mediagoblin/db/migration_tools.py @@ -29,7 +29,7 @@ class MigrationManager(object): to the latest migrations, etc. """ - def __init__(self, name, models, migration_registry, session, + def __init__(self, name, models, foundations, migration_registry, session, printer=simple_printer): """ Args: @@ -40,6 +40,7 @@ class MigrationManager(object): """ self.name = unicode(name) self.models = models + self.foundations = foundations self.session = session self.migration_registry = migration_registry self._sorted_migrations = None @@ -145,11 +146,11 @@ class MigrationManager(object): Create the table foundations (default rows) as layed out in FOUNDATIONS in mediagoblin.db.models """ - from mediagoblin.db.models import FOUNDATIONS as MAIN_FOUNDATIONS - for Model in MAIN_FOUNDATIONS.keys(): - for parameters in MAIN_FOUNDATIONS[Model]: - row = Model(*parameters) - row.save() + for Model, rows in self.foundations.items(): + print u'\n + Laying foundations for %s table' % (Model.__name__) + for parameters in rows: + new_row = Model(**parameters) + new_row.save() def create_new_migration_record(self): """ @@ -186,8 +187,7 @@ class MigrationManager(object): if self.name == u'__main__': return u"main mediagoblin tables" else: - # TODO: Use the friendlier media manager "human readable" name - return u'media type "%s"' % self.name + return u'plugin "%s"' % self.name def init_or_migrate(self): """ @@ -214,10 +214,9 @@ class MigrationManager(object): self.init_tables() # auto-set at latest migration number - self.create_new_migration_record() - if self.name==u'__main__': - self.populate_table_foundations() - + self.create_new_migration_record() + self.populate_table_foundations() + self.printer(u"done.\n") self.set_current_migration() return u'inited' diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 247298ac..53df7954 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -289,10 +289,99 @@ def unique_collections_slug(db): db.commit() -class ReportBase_v0(declarative_base()): +@RegisterMigration(11, MIGRATIONS) +def drop_token_related_User_columns(db): """ + Drop unneeded columns from the User table after switching to using + itsdangerous tokens for email and forgot password verification. + """ + metadata = MetaData(bind=db.bind) + user_table = inspect_table(metadata, 'core__users') - """ + + verification_key = user_table.columns['verification_key'] + fp_verification_key = user_table.columns['fp_verification_key'] + fp_token_expire = user_table.columns['fp_token_expire'] + + verification_key.drop() + fp_verification_key.drop() + fp_token_expire.drop() + + db.commit() + + +class CommentSubscription_v0(declarative_base()): + __tablename__ = 'core__comment_subscriptions' + id = Column(Integer, primary_key=True) + + created = Column(DateTime, nullable=False, default=datetime.datetime.now) + + media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + + notify = Column(Boolean, nullable=False, default=True) + send_email = Column(Boolean, nullable=False, default=True) + + +class Notification_v0(declarative_base()): + __tablename__ = 'core__notifications' + id = Column(Integer, primary_key=True) + type = Column(Unicode) + + created = Column(DateTime, nullable=False, default=datetime.datetime.now) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + index=True) + seen = Column(Boolean, default=lambda: False, index=True) + + +class CommentNotification_v0(Notification_v0): + __tablename__ = 'core__comment_notifications' + id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True) + + subject_id = Column(Integer, ForeignKey(MediaComment.id)) + + +class ProcessingNotification_v0(Notification_v0): + __tablename__ = 'core__processing_notifications' + + id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True) + + subject_id = Column(Integer, ForeignKey(MediaEntry.id)) + + +@RegisterMigration(12, MIGRATIONS) +def add_new_notification_tables(db): + metadata = MetaData(bind=db.bind) + + user_table = inspect_table(metadata, 'core__users') + mediaentry_table = inspect_table(metadata, 'core__media_entries') + mediacomment_table = inspect_table(metadata, 'core__media_comments') + + CommentSubscription_v0.__table__.create(db.bind) + + Notification_v0.__table__.create(db.bind) + CommentNotification_v0.__table__.create(db.bind) + ProcessingNotification_v0.__table__.create(db.bind) + + +@RegisterMigration(13, MIGRATIONS) +def pw_hash_nullable(db): + """Make pw_hash column nullable""" + metadata = MetaData(bind=db.bind) + user_table = inspect_table(metadata, "core__users") + + user_table.c.pw_hash.alter(nullable=True) + + # sqlite+sqlalchemy seems to drop this constraint during the + # migration, so we add it back here for now a bit manually. + if db.bind.url.drivername == 'sqlite': + constraint = UniqueConstraint('username', table=user_table) + constraint.create() + +class ReportBase_v0(declarative_base()): __tablename__ = 'core__reports' id = Column(Integer, primary_key=True) reporter_id = Column(Integer, ForeignKey(User.id), nullable=False) @@ -302,7 +391,6 @@ class ReportBase_v0(declarative_base()): discriminator = Column('type', Unicode(50)) __mapper_args__ = {'polymorphic_on': discriminator} - class CommentReport_v0(ReportBase_v0): __tablename__ = 'core__reports_on_comments' __mapper_args__ = {'polymorphic_identity': 'comment_report'} @@ -316,14 +404,13 @@ class MediaReport_v0(ReportBase_v0): __mapper_args__ = {'polymorphic_identity': 'media_report'} id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True) - media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False) + media_entry_id = Column(Integer, ForeignKey(MediaEntry.i class ArchivedReport_v0(ReportBase_v0): __tablename__ = 'core__reports_archived' __mapper_args__ = {'polymorphic_identity': 'archived_report'} id = Column('id',Integer, ForeignKey('core__reports.id')) - media_entry_id = Column(Integer, ForeignKey(MediaEntry.id)) comment_id = Column(Integer, ForeignKey(MediaComment.id)) resolver_id = Column(Integer, ForeignKey(User.id), nullable=False) @@ -344,7 +431,6 @@ class Privilege_v0(declarative_base()): class PrivilegeUserAssociation_v0(declarative_base()): __tablename__ = 'core__privileges_users' - group_id = Column( 'core__privilege_id', Integer, @@ -356,7 +442,7 @@ class PrivilegeUserAssociation_v0(declarative_base()): ForeignKey(Privilege.id), primary_key=True) -@RegisterMigration(11, MIGRATIONS) +@RegisterMigration(14, MIGRATIONS) def create_moderation_tables(db): ReportBase_v0.__table__.create(db.bind) CommentReport_v0.__table__.create(db.bind) diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 70c9dd41..25ce6642 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -29,13 +29,14 @@ real objects. import uuid import re -import datetime +from datetime import datetime from werkzeug.utils import cached_property from mediagoblin import mg_globals -from mediagoblin.media_types import get_media_managers, FileTypeNotSupported +from mediagoblin.media_types import FileTypeNotSupported from mediagoblin.tools import common, licenses +from mediagoblin.tools.pluginapi import hook_handle from mediagoblin.tools.text import cleaned_markdown_conversion from mediagoblin.tools.url import slugify @@ -201,14 +202,14 @@ class MediaEntryMixin(GenerateSlugMixin): Raises FileTypeNotSupported in case no such manager is enabled """ - # TODO, we should be able to make this a simple lookup rather - # than iterating through all media managers. - for media_type, manager in get_media_managers(): - if media_type == self.media_type: - return manager(self) + manager = hook_handle(('media_manager', self.media_type)) + if manager: + return manager(self) + # Not found? Then raise an error raise FileTypeNotSupported( - "MediaManager not in enabled types. Check media_types in config?") + "MediaManager not in enabled types. Check media_type plugins are" + " enabled in config?") def get_fail_exception(self): """ @@ -287,6 +288,13 @@ class MediaCommentMixin(object): """ return cleaned_markdown_conversion(self.content) + def __repr__(self): + return '<{klass} #{id} {author} "{comment}">'.format( + klass=self.__class__.__name__, + id=self.id, + author=self.get_author, + comment=self.content) + class CollectionMixin(GenerateSlugMixin): def check_slug_used(self, slug): diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index c85d546f..32d3135f 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -24,16 +24,16 @@ import datetime from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \ Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \ SmallInteger -from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm import relationship, backref, with_polymorphic from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql.expression import desc from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.util import memoized_property -from sqlalchemy.schema import Table from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded from mediagoblin.db.base import Base, DictReadAttrProxy -from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin +from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \ + MediaCommentMixin, CollectionMixin, CollectionItemMixin from mediagoblin.tools.files import delete_media_files from mediagoblin.tools.common import import_component @@ -239,8 +239,8 @@ class MediaEntry(Base, MediaEntryMixin): This will *not* automatically delete unused collections, which can remain empty... - :keyword del_orphan_tags: True/false if we delete unused Tags too - :keyword commit: True/False if this should end the db transaction""" + :param del_orphan_tags: True/false if we delete unused Tags too + :param commit: True/False if this should end the db transaction""" # User's CollectionItems are automatically deleted via "cascade". # Comments on this Media are deleted by cascade, hopefully. @@ -393,6 +393,10 @@ class MediaComment(Base, MediaCommentMixin): backref=backref("posted_comments", lazy="dynamic", cascade="all, delete-orphan")) + get_entry = relationship(MediaEntry, + backref=backref("comments", + lazy="dynamic", + cascade="all, delete-orphan")) # Cascade: Comments are somewhat owned by their MediaEntry. # So do the full thing. @@ -484,6 +488,92 @@ class ProcessingMetaData(Base): """A dict like view on this object""" return DictReadAttrProxy(self) +class CommentSubscription(Base): + __tablename__ = 'core__comment_subscriptions' + id = Column(Integer, primary_key=True) + + created = Column(DateTime, nullable=False, default=datetime.datetime.now) + + media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False) + media_entry = relationship(MediaEntry, + backref=backref('comment_subscriptions', + cascade='all, delete-orphan')) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + user = relationship(User, + backref=backref('comment_subscriptions', + cascade='all, delete-orphan')) + + notify = Column(Boolean, nullable=False, default=True) + send_email = Column(Boolean, nullable=False, default=True) + + def __repr__(self): + return ('<{classname} #{id}: {user} {media} notify: ' + '{notify} email: {email}>').format( + id=self.id, + classname=self.__class__.__name__, + user=self.user, + media=self.media_entry, + notify=self.notify, + email=self.send_email) + + +class Notification(Base): + __tablename__ = 'core__notifications' + id = Column(Integer, primary_key=True) + type = Column(Unicode) + + created = Column(DateTime, nullable=False, default=datetime.datetime.now) + + user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False, + index=True) + seen = Column(Boolean, default=lambda: False, index=True) + user = relationship( + User, + backref=backref('notifications', cascade='all, delete-orphan')) + + __mapper_args__ = { + 'polymorphic_identity': 'notification', + 'polymorphic_on': type + } + + def __repr__(self): + return '<{klass} #{id}: {user}: {subject} ({seen})>'.format( + id=self.id, + klass=self.__class__.__name__, + user=self.user, + subject=getattr(self, 'subject', None), + seen='unseen' if not self.seen else 'seen') + + +class CommentNotification(Notification): + __tablename__ = 'core__comment_notifications' + id = Column(Integer, ForeignKey(Notification.id), primary_key=True) + + subject_id = Column(Integer, ForeignKey(MediaComment.id)) + subject = relationship( + MediaComment, + backref=backref('comment_notifications', cascade='all, delete-orphan')) + + __mapper_args__ = { + 'polymorphic_identity': 'comment_notification' + } + + +class ProcessingNotification(Notification): + __tablename__ = 'core__processing_notifications' + + id = Column(Integer, ForeignKey(Notification.id), primary_key=True) + + subject_id = Column(Integer, ForeignKey(MediaEntry.id)) + subject = relationship( + MediaEntry, + backref=backref('processing_notifications', + cascade='all, delete-orphan')) + + __mapper_args__ = { + 'polymorphic_identity': 'processing_notification' + } class ReportBase(Base): """ @@ -672,20 +762,38 @@ class PrivilegeUserAssociation(Base): ForeignKey(Privilege.id), primary_key=True) +with_polymorphic( + Notification, + [ProcessingNotification, CommentNotification]) -privilege_foundations = [[u'admin'], [u'moderator'], [u'uploader'],[u'reporter'], [u'commenter'] ,[u'active']] MODELS = [ User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData, ReportBase, CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation, - ArchivedReport] + ArchivedReport, Notification, CommentNotification, + ProcessingNotification, CommentSubscription] -# Foundations are the default rows that are created immediately after the tables are initialized. Each entry to -# this dictionary should be in the format of -# ModelObject:List of Rows -# (Each Row must be a list of parameters that can create and instance of the ModelObject) -# +""" + Foundations are the default rows that are created immediately after the tables + are initialized. Each entry to this dictionary should be in the format of: + ModelConstructorObject:List of Dictionaries + (Each Dictionary represents a row on the Table to be created, containing each + of the columns' names as a key string, and each of the columns' values as a + value) + + ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE] + user_foundations = [{'name':u'Joanna', 'age':24}, + {'name':u'Andrea', 'age':41}] + + FOUNDATIONS = {User:user_foundations} +""" +privilege_foundations = [{'privilege_name':u'admin'}, + {'privilege_name':u'moderator'}, + {'privilege_name':u'uploader'}, + {'privilege_name':u'reporter'}, + {'privilege_name':u'commenter'}, + {'privilege_name':u'active'}] FOUNDATIONS = {Privilege:privilege_foundations} ###################################################### diff --git a/mediagoblin/db/models_v0.py b/mediagoblin/db/models_v0.py index ec51a1f5..bdedec2e 100644 --- a/mediagoblin/db/models_v0.py +++ b/mediagoblin/db/models_v0.py @@ -18,6 +18,29 @@ TODO: indexes on foreignkeys, where useful. """ +########################################################################### +# WHAT IS THIS FILE? +# ------------------ +# +# Upon occasion, someone runs into this file and wonders why we have +# both a models.py and a models_v0.py. +# +# The short of it is: you can ignore this file. +# +# The long version is, in two parts: +# +# - We used to use MongoDB, then we switched to SQL and SQLAlchemy. +# We needed to convert peoples' databases; the script we had would +# switch them to the first version right after Mongo, convert over +# all their tables, then run any migrations that were added after. +# +# - That script is now removed, but there is some discussion of +# writing a test that would set us at the first SQL migration and +# run everything after. If we wrote that, this file would still be +# useful. But for now, it's legacy! +# +########################################################################### + import datetime import sys diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py index 0b1679fb..4ff0945f 100644 --- a/mediagoblin/db/open.py +++ b/mediagoblin/db/open.py @@ -52,10 +52,6 @@ class DatabaseMaster(object): def load_models(app_config): import mediagoblin.db.models - for media_type in app_config['media_types']: - _log.debug("Loading %s.models", media_type) - __import__(media_type + ".models") - for plugin in mg_globals.global_config.get('plugins', {}).keys(): _log.debug("Loading %s.models", plugin) try: diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 1aa0a63c..2a99d467 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -25,7 +25,7 @@ from mediagoblin.db.models import (MediaEntry, Tag, MediaTag, Collection, \ def atomic_update(table, query_dict, update_values): - table.find(query_dict).update(update_values, + table.query.filter_by(**query_dict).update(update_values, synchronize_session=False) Session.commit() diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index b39b36f5..c9a1a78c 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -21,9 +21,11 @@ from werkzeug.exceptions import Forbidden, NotFound from werkzeug.urls import url_quote from mediagoblin import mg_globals as mgg -from mediagoblin.db.models import MediaEntry, User, MediaComment, Privilege, \ - UserBan -from mediagoblin.tools.response import redirect, render_404, render_user_banned +from mediagoblin import messages +from mediagoblin.db.models import MediaEntry, User, MediaComment, + UserBan +from mediagoblin.tools.response import redirect, render_404 +from mediagoblin.tools.translate import pass_to_ugettext as _ def require_active_login(controller): @@ -107,8 +109,8 @@ def user_may_alter_collection(controller): """ @wraps(controller) def wrapper(request, *args, **kwargs): - creator_id = request.db.User.find_one( - {'username': request.matchdict['user']}).id + creator_id = request.db.User.query.filter_by( + username=request.matchdict['user']).first().id if not (request.user.is_admin or request.user.id == creator_id): raise Forbidden() @@ -182,15 +184,15 @@ def get_user_collection(controller): """ @wraps(controller) def wrapper(request, *args, **kwargs): - user = request.db.User.find_one( - {'username': request.matchdict['user']}) + user = request.db.User.query.filter_by( + username=request.matchdict['user']).first() if not user: return render_404(request) - collection = request.db.Collection.find_one( - {'slug': request.matchdict['collection'], - 'creator': user.id}) + collection = request.db.Collection.query.filter_by( + slug=request.matchdict['collection'], + creator=user.id).first() # Still no collection? Okay, 404. if not collection: @@ -207,14 +209,14 @@ def get_user_collection_item(controller): """ @wraps(controller) def wrapper(request, *args, **kwargs): - user = request.db.User.find_one( - {'username': request.matchdict['user']}) + user = request.db.User.query.filter_by( + username=request.matchdict['user']).first() if not user: return render_404(request) - collection_item = request.db.CollectionItem.find_one( - {'id': request.matchdict['collection_item'] }) + collection_item = request.db.CollectionItem.query.filter_by( + id=request.matchdict['collection_item']).first() # Still no collection item? Okay, 404. if not collection_item: @@ -247,6 +249,21 @@ def get_media_entry_by_id(controller): return wrapper +def allow_registration(controller): + """ Decorator for if registration is enabled""" + @wraps(controller) + def wrapper(request, *args, **kwargs): + if not mgg.app_config["allow_registration"]: + messages.add_message( + request, + messages.WARNING, + _('Sorry, registration is disabled on this instance.')) + return redirect(request, "index") + + return controller(request, *args, **kwargs) + + return wrapper + def get_media_comment_by_id(controller): """ Pass in a MediaComment based off of a url component @@ -264,15 +281,26 @@ def get_media_comment_by_id(controller): return wrapper +def auth_enabled(controller): + """Decorator for if an auth plugin is enabled""" + @wraps(controller) + def wrapper(request, *args, **kwargs): + if not mgg.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this instance.')) + return redirect(request, 'index') + return controller(request, *args, **kwargs) + + return wrapper def get_workbench(func): """Decorator, passing in a workbench as kwarg which is cleaned up afterwards""" - @wraps(func) def new_func(*args, **kwargs): with mgg.workbench_manager.create() as workbench: return func(*args, workbench=workbench, **kwargs) - return new_func def require_admin_or_moderator_login(controller): diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index 3b2486de..85c243a0 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -16,9 +16,11 @@ import wtforms -from mediagoblin.tools.text import tag_length_validator, TOO_LONG_TAG_WARNING +from mediagoblin.tools.text import tag_length_validator from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.tools.licenses import licenses_as_choices +from mediagoblin.auth.forms import normalize_user_or_email_field + class EditForm(wtforms.Form): title = wtforms.TextField( @@ -59,6 +61,12 @@ class EditProfileForm(wtforms.Form): class EditAccountForm(wtforms.Form): + new_email = wtforms.TextField( + _('New email address'), + [wtforms.validators.Optional(), + normalize_user_or_email_field(allow_user=False)]) + wants_comment_notification = wtforms.BooleanField( + description=_("Email me when others comment on my media")) license_preference = wtforms.SelectField( _('License preference'), [ @@ -67,8 +75,6 @@ class EditAccountForm(wtforms.Form): ], choices=licenses_as_choices(), description=_('This will be your default license on upload forms.')) - wants_comment_notification = wtforms.BooleanField( - label=_("Email me when others comment on my media")) class EditAttachmentsForm(wtforms.Form): diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index 622729ac..3592f708 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -26,3 +26,5 @@ add_route('mediagoblin.edit.delete_account', '/edit/account/delete/', 'mediagoblin.edit.views:delete_account') add_route('mediagoblin.edit.pass', '/edit/password/', 'mediagoblin.edit.views:change_pass') +add_route('mediagoblin.edit.verify_email', '/edit/verify_email/', + 'mediagoblin.edit.views:verify_email') diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 508c380d..6aa2acd9 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -16,25 +16,31 @@ from datetime import datetime +from itsdangerous import BadSignature from werkzeug.exceptions import Forbidden from werkzeug.utils import secure_filename from mediagoblin import messages from mediagoblin import mg_globals -from mediagoblin.auth import lib as auth_lib +from mediagoblin import auth +from mediagoblin.auth import tools as auth_tools from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import (require_active_login, active_user_from_url, - get_media_entry_by_id, - user_may_alter_collection, get_user_collection) -from mediagoblin.tools.response import render_to_response, \ - redirect, redirect_obj + get_media_entry_by_id, user_may_alter_collection, + get_user_collection) +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.tools.mail import email_debug_message +from mediagoblin.tools.response import (render_to_response, + redirect, redirect_obj, render_404) from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.tools.template import render_template from mediagoblin.tools.text import ( convert_to_tag_list_of_dicts, media_tags_as_string) from mediagoblin.tools.url import slugify from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used +from mediagoblin.db.models import User import mimetypes @@ -212,6 +218,10 @@ def edit_profile(request, url_user=None): {'user': user, 'form': form}) +EMAIL_VERIFICATION_TEMPLATE = ( + u'{uri}?' + u'token={verification_key}') + @require_active_login def edit_account(request): @@ -220,27 +230,22 @@ def edit_account(request): wants_comment_notification=user.wants_comment_notification, license_preference=user.license_preference) - if request.method == 'POST': - form_validated = form.validate() + if request.method == 'POST' and form.validate(): + user.wants_comment_notification = form.wants_comment_notification.data - if form_validated and \ - form.wants_comment_notification.validate(form): - user.wants_comment_notification = \ - form.wants_comment_notification.data + user.license_preference = form.license_preference.data - if form_validated and \ - form.license_preference.validate(form): - user.license_preference = \ - form.license_preference.data + if form.new_email.data: + _update_email(request, form, user) - if form_validated and not form.errors: + if not form.errors: user.save() messages.add_message(request, - messages.SUCCESS, - _("Account settings saved")) + messages.SUCCESS, + _("Account settings saved")) return redirect(request, - 'mediagoblin.user_pages.user_home', - user=user.username) + 'mediagoblin.user_pages.user_home', + user=user.username) return render_to_response( request, @@ -300,9 +305,9 @@ def edit_collection(request, collection): form.slug.data, collection.id) # Make sure there isn't already a Collection with this title - existing_collection = request.db.Collection.find_one({ - 'creator': request.user.id, - 'title':form.title.data}) + existing_collection = request.db.Collection.query.filter_by( + creator=request.user.id, + title=form.title.data).first() if existing_collection and existing_collection.id != collection.id: messages.add_message( @@ -337,12 +342,16 @@ def edit_collection(request, collection): @require_active_login def change_pass(request): + # If no password authentication, no need to change your password + if 'pass_auth' not in request.template_env.globals: + return redirect(request, 'index') + form = forms.ChangePassForm(request.form) user = request.user if request.method == 'POST' and form.validate(): - if not auth_lib.bcrypt_check_password( + if not auth.check_password( form.old_password.data, user.pw_hash): form.old_password.errors.append( _('Wrong password')) @@ -354,7 +363,7 @@ def change_pass(request): 'user': user}) # Password matches - user.pw_hash = auth_lib.bcrypt_gen_password_hash( + user.pw_hash = auth.gen_password_hash( form.new_password.data) user.save() @@ -369,3 +378,77 @@ def change_pass(request): 'mediagoblin/edit/change_pass.html', {'form': form, 'user': user}) + + +def verify_email(request): + """ + Email verification view for changing email address + """ + # If no token, we can't do anything + if not 'token' in request.GET: + return render_404(request) + + # Catch error if token is faked or expired + token = None + try: + token = get_timed_signer_url("mail_verification_token") \ + .loads(request.GET['token'], max_age=10*24*3600) + except BadSignature: + messages.add_message( + request, + messages.ERROR, + _('The verification key or user id is incorrect.')) + + return redirect( + request, + 'index') + + user = User.query.filter_by(id=int(token['user'])).first() + + if user: + user.email = token['email'] + user.save() + + messages.add_message( + request, + messages.SUCCESS, + _('Your email address has been verified.')) + + else: + messages.add_message( + request, + messages.ERROR, + _('The verification key or user id is incorrect.')) + + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=user.username) + + +def _update_email(request, form, user): + new_email = form.new_email.data + users_with_email = User.query.filter_by( + email=new_email).count() + + if users_with_email: + form.new_email.errors.append( + _('Sorry, a user with that email address' + ' already exists.')) + + elif not users_with_email: + verification_key = get_timed_signer_url( + 'mail_verification_token').dumps({ + 'user': user.id, + 'email': new_email}) + + rendered_email = render_template( + request, 'mediagoblin/edit/verification.txt', + {'username': user.username, + 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format( + uri=request.urlgen('mediagoblin.edit.verify_email', + qualified=True), + verification_key=verification_key)}) + + email_debug_message(request) + auth_tools.send_verification_email(user, request, new_email, + rendered_email) diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py index fa25ecb2..bad3e352 100644 --- a/mediagoblin/gmg_commands/dbupdate.py +++ b/mediagoblin/gmg_commands/dbupdate.py @@ -32,17 +32,18 @@ def dbupdate_parse_setup(subparser): class DatabaseData(object): - def __init__(self, name, models, migrations): + def __init__(self, name, models, foundations, migrations): self.name = name self.models = models + self.foundations = foundations self.migrations = migrations def make_migration_manager(self, session): return MigrationManager( - self.name, self.models, self.migrations, session) + self.name, self.models, self.foundations, self.migrations, session) -def gather_database_data(media_types, plugins): +def gather_database_data(plugins): """ Gather all database data relevant to the extensions we have installed so we can do migrations and table initialization. @@ -54,17 +55,11 @@ def gather_database_data(media_types, plugins): # Add main first from mediagoblin.db.models import MODELS as MAIN_MODELS from mediagoblin.db.migrations import MIGRATIONS as MAIN_MIGRATIONS + from mediagoblin.db.models import FOUNDATIONS as MAIN_FOUNDATIONS managed_dbdata.append( DatabaseData( - u'__main__', MAIN_MODELS, MAIN_MIGRATIONS)) - - # Then get all registered media managers (eventually, plugins) - for media_type in media_types: - models = import_component('%s.models:MODELS' % media_type) - migrations = import_component('%s.migrations:MIGRATIONS' % media_type) - managed_dbdata.append( - DatabaseData(media_type, models, migrations)) + u'__main__', MAIN_MODELS, MAIN_FOUNDATIONS, MAIN_MIGRATIONS)) for plugin in plugins: try: @@ -90,13 +85,26 @@ forgotten to add it? ({1})'.format(plugin, exc)) migrations = {} except AttributeError as exc: - _log.debug('Cloud not find MIGRATIONS in {0}.migrations, have you \ + _log.debug('Could not find MIGRATIONS in {0}.migrations, have you \ forgotten to add it? ({1})'.format(plugin, exc)) migrations = {} + try: + foundations = import_component('{0}.models:FOUNDATIONS'.format(plugin)) + except ImportError as exc: + _log.debug('No foundations found for {0}: {1}'.format( + plugin, + exc)) + + foundations = [] + except AttributeError as exc: + _log.debug('Could not find FOUNDATIONS in {0}.models, have you \ +forgotten to add it? ({1})'.format(plugin, exc)) + foundations = {} + if models: managed_dbdata.append( - DatabaseData(plugin, models, migrations)) + DatabaseData(plugin, models, foundations, migrations)) return managed_dbdata @@ -110,13 +118,24 @@ def run_dbupdate(app_config, global_config): in the future, plugins) """ - # Gather information from all media managers / projects - dbdatas = gather_database_data( - app_config['media_types'], - global_config.get('plugins', {}).keys()) - # Set up the database db = setup_connection_and_db_from_config(app_config, migrations=True) + #Run the migrations + run_all_migrations(db, app_config, global_config) + + +def run_all_migrations(db, app_config, global_config): + """ + Initializes or migrates a database that already has a + connection setup and also initializes or migrates all + extensions based on the config files. + + It can be used to initialize an in-memory database for + testing. + """ + # Gather information from all media managers / projects + dbdatas = gather_database_data( + global_config.get('plugins', {}).keys()) Session = sessionmaker(bind=db.engine) diff --git a/mediagoblin/gmg_commands/import_export.py b/mediagoblin/gmg_commands/import_export.py index d51a1e3e..98ec617d 100644 --- a/mediagoblin/gmg_commands/import_export.py +++ b/mediagoblin/gmg_commands/import_export.py @@ -63,7 +63,7 @@ def _import_media(db, args): # TODO: Add import of queue files queue_cache = BasicFileStorage(args._cache_path['queue']) - for entry in db.MediaEntry.find(): + for entry in db.MediaEntry.query.filter_by(): for name, path in entry.media_files.items(): _log.info('Importing: {0} - {1}'.format( entry.title.encode('ascii', 'replace'), @@ -204,7 +204,7 @@ def _export_media(db, args): # TODO: Add export of queue files queue_cache = BasicFileStorage(args._cache_path['queue']) - for entry in db.MediaEntry.find(): + for entry in db.MediaEntry.query.filter_by(): for name, path in entry.media_files.items(): _log.info(u'Exporting {0} - {1}'.format( entry.title, diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py index 5c19d3a9..ad8263e7 100644 --- a/mediagoblin/gmg_commands/users.py +++ b/mediagoblin/gmg_commands/users.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from mediagoblin.gmg_commands import util as commands_util -from mediagoblin.auth import lib as auth_lib +from mediagoblin import auth from mediagoblin import mg_globals def adduser_parser_setup(subparser): @@ -40,9 +40,9 @@ def adduser(args): db = mg_globals.database users_with_username = \ - db.User.find({ - 'username': args.username.lower(), - }).count() + db.User.query.filter_by( + username=args.username.lower() + ).count() if users_with_username: print u'Sorry, a user with that name already exists.' @@ -52,7 +52,7 @@ def adduser(args): entry = db.User() entry.username = unicode(args.username.lower()) entry.email = unicode(args.email) - entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password) + entry.pw_hash = auth.gen_password_hash(args.password) entry.status = u'active' entry.email_verified = True default_privileges = [ @@ -78,7 +78,8 @@ def makeadmin(args): db = mg_globals.database - user = db.User.one({'username': unicode(args.username.lower())}) + user = db.User.query.filter_by( + username=unicode(args.username.lower())).one() if user: user.is_admin = True user.all_privileges.append( @@ -105,9 +106,10 @@ def changepw(args): db = mg_globals.database - user = db.User.one({'username': unicode(args.username.lower())}) + user = db.User.query.filter_by( + username=unicode(args.username.lower())).one() if user: - user.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password) + user.pw_hash = auth.gen_password_hash(args.password) user.save() print 'Password successfully changed' else: diff --git a/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.mo index a07400ae..e2fcf85d 100644 Binary files a/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.po index 4bde0095..e2147070 100644 --- a/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.po @@ -15,15 +15,15 @@ # Elrond , 2011 # Art O. Pal , 2011 # spaetz , 2012 -# Vinzenz Vietzke , 2012 -# Vinzenz Vietzke , 2011 +# Vinzenz Vietzke , 2012 +# Vinzenz Vietzke , 2011 msgid "" msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-05-28 10:43+0000\n" +"Last-Translator: Elrond \n" "Language-Team: German (http://www.transifex.com/projects/p/mediagoblin/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -47,7 +47,7 @@ msgstr "E-Mail-Adresse" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Benutzername oder E-Mail-Adresse" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -628,7 +628,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Registriere dich auf dieser Seite oder Installiere MediaGoblin auf deinem eigenen Server" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -784,7 +784,7 @@ msgstr "Bild für %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF-Datei" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -944,11 +944,11 @@ msgstr "" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Hinzugefügt" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Originaldatum" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1136,27 +1136,27 @@ msgstr "Tut uns Leid, aber unter der angegebenen Adresse gibt es keine Seite!

\n" "Language-Team: LANGUAGE \n" @@ -17,94 +17,94 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -#: mediagoblin/auth/forms.py:26 +#: mediagoblin/auth/forms.py:25 msgid "Username" msgstr "" -#: mediagoblin/auth/forms.py:30 mediagoblin/auth/forms.py:45 +#: mediagoblin/auth/forms.py:29 mediagoblin/auth/forms.py:44 #: mediagoblin/tests/test_util.py:110 msgid "Password" msgstr "" -#: mediagoblin/auth/forms.py:34 +#: mediagoblin/auth/forms.py:33 msgid "Email address" msgstr "" -#: mediagoblin/auth/forms.py:41 +#: mediagoblin/auth/forms.py:40 msgid "Username or Email" msgstr "" -#: mediagoblin/auth/forms.py:52 +#: mediagoblin/auth/forms.py:51 msgid "Username or email" msgstr "" -#: mediagoblin/auth/tools.py:31 +#: mediagoblin/auth/tools.py:42 msgid "Invalid User name or email address." msgstr "" -#: mediagoblin/auth/tools.py:32 +#: mediagoblin/auth/tools.py:43 msgid "This field does not take email addresses." msgstr "" -#: mediagoblin/auth/tools.py:33 +#: mediagoblin/auth/tools.py:44 msgid "This field requires an email address." msgstr "" -#: mediagoblin/auth/views.py:54 -msgid "Sorry, registration is disabled on this instance." -msgstr "" - -#: mediagoblin/auth/views.py:68 +#: mediagoblin/auth/tools.py:109 msgid "Sorry, a user with that name already exists." msgstr "" -#: mediagoblin/auth/views.py:72 +#: mediagoblin/auth/tools.py:113 msgid "Sorry, a user with that email address already exists." msgstr "" -#: mediagoblin/auth/views.py:182 +#: mediagoblin/auth/views.py:43 +msgid "Sorry, registration is disabled on this instance." +msgstr "" + +#: mediagoblin/auth/views.py:133 msgid "" "Your email address has been verified. You may now login, edit your " "profile, and submit images!" msgstr "" -#: mediagoblin/auth/views.py:188 +#: mediagoblin/auth/views.py:139 msgid "The verification key or user id is incorrect" msgstr "" -#: mediagoblin/auth/views.py:206 +#: mediagoblin/auth/views.py:157 msgid "You must be logged in so we know who to send the email to!" msgstr "" -#: mediagoblin/auth/views.py:214 +#: mediagoblin/auth/views.py:165 msgid "You've already verified your email address!" msgstr "" -#: mediagoblin/auth/views.py:227 +#: mediagoblin/auth/views.py:178 msgid "Resent your verification email." msgstr "" -#: mediagoblin/auth/views.py:258 +#: mediagoblin/auth/views.py:209 msgid "" "If that email address (case sensitive!) is registered an email has been " "sent with instructions on how to change your password." msgstr "" -#: mediagoblin/auth/views.py:269 +#: mediagoblin/auth/views.py:220 msgid "Couldn't find someone with that username." msgstr "" -#: mediagoblin/auth/views.py:272 +#: mediagoblin/auth/views.py:223 msgid "An email has been sent with instructions on how to change your password." msgstr "" -#: mediagoblin/auth/views.py:279 +#: mediagoblin/auth/views.py:230 msgid "" "Could not send password recovery email as your username is inactive or " "your account's email address has not been verified." msgstr "" -#: mediagoblin/auth/views.py:336 +#: mediagoblin/auth/views.py:287 msgid "You can now log in using your new password." msgstr "" @@ -634,13 +634,13 @@ msgid "Editing attachments for %(media_title)s" msgstr "" #: mediagoblin/templates/mediagoblin/edit/attachments.html:44 -#: mediagoblin/templates/mediagoblin/user_pages/media.html:182 -#: mediagoblin/templates/mediagoblin/user_pages/media.html:198 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:171 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:187 msgid "Attachments" msgstr "" #: mediagoblin/templates/mediagoblin/edit/attachments.html:57 -#: mediagoblin/templates/mediagoblin/user_pages/media.html:204 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:193 msgid "Add attachment" msgstr "" @@ -763,6 +763,17 @@ msgstr "" msgid "WebM file (Vorbis codec)" msgstr "" +#: mediagoblin/templates/mediagoblin/media_displays/image.html:36 +msgid "Created" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/image.html:39 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:132 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:152 +#, python-format +msgid "%(formatted_time)s ago" +msgstr "" + #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:59 #: mediagoblin/templates/mediagoblin/media_displays/stl.html:87 #: mediagoblin/templates/mediagoblin/media_displays/stl.html:93 @@ -928,21 +939,10 @@ msgstr "" msgid "Add this comment" msgstr "" -#: mediagoblin/templates/mediagoblin/user_pages/media.html:132 -#: mediagoblin/templates/mediagoblin/user_pages/media.html:152 -#: mediagoblin/templates/mediagoblin/user_pages/media.html:164 -#, python-format -msgid "%(formatted_time)s ago" -msgstr "" - #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" msgstr "" -#: mediagoblin/templates/mediagoblin/user_pages/media.html:161 -msgid "Created" -msgstr "" - #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 #, python-format diff --git a/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.mo index 406b269f..645af16b 100644 Binary files a/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.po index eb1ec1ec..873869f0 100644 --- a/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.po @@ -12,8 +12,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-06-01 21:16+0000\n" +"Last-Translator: aleksejrs \n" "Language-Team: Esperanto (http://www.transifex.com/projects/p/mediagoblin/language/eo/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -37,7 +37,7 @@ msgstr "Retpoŝtadreso" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Uzantonomo aŭ retpoŝtadreso" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -178,7 +178,7 @@ msgstr "Permesila prefero" #: mediagoblin/edit/forms.py:69 msgid "This will be your default license on upload forms." -msgstr "" +msgstr "Tiu ĉi permesilo estos antaŭelektita en la alŝutformularoj." #: mediagoblin/edit/forms.py:71 msgid "Email me when others comment on my media" @@ -264,7 +264,7 @@ msgstr "Malĝusta pasvorto" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Via pasvorto estas sukcese ŝanĝita" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -665,11 +665,11 @@ msgstr "Konservi ŝanĝojn" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Ŝanĝado de pasvorto de %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Konservi" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -700,7 +700,7 @@ msgstr "Ŝanĝado de kontagordoj de %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Ŝanĝi la pasvorton" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -774,7 +774,7 @@ msgstr "Bildo de «%(media_title)s»" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF-dosiero" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -930,15 +930,15 @@ msgstr "Aldoni ĉi tiun komenton" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "antaŭ %(formatted_time)s" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Aldonita" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Kreita" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1126,27 +1126,27 @@ msgstr "" #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "jaro(j)" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "monato(j)" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "semajno(j)" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "tago(j)" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "horo(j)" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "minuto(j)" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1185,7 +1185,7 @@ msgstr "komentis je via afiŝo" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Ve, komentado estas malebligita." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.mo index ececfb2e..c5e50f53 100644 Binary files a/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.po index 03e755ab..8c2f046f 100644 --- a/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.po @@ -20,8 +20,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-06-02 21:23+0000\n" +"Last-Translator: larjona \n" "Language-Team: Spanish (http://www.transifex.com/projects/p/mediagoblin/language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -45,7 +45,7 @@ msgstr "Dirección de correo electrónico" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Nombre de usuario o correo electrónico" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -272,7 +272,7 @@ msgstr "Contraseña incorrecta" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Se ha cambiado la contraseña correctamente" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -289,17 +289,17 @@ msgstr "Sin embargo, se encontró un enlace simbólico de un directorio antiguo; #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "No se pudo enlazar \"%s\": %s existe y no es un enlace simbólico\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "Omitiendo \"%s\"; ya está establecido.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "Se encontró un enlace antiguo para \"%s\"; se eliminará.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -315,7 +315,7 @@ msgstr "Lo sentidos, No soportamos ese tipo de archivo :(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "ha fallado la ejecución de unoconv, comprueba el fichero de registro (log)" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -626,7 +626,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Crear una cuenta en este sitio\n o\n Instalar MediaGoblin en tu propio servidor" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -673,11 +673,11 @@ msgstr "Guardar cambios" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Cambiando la contraseña de %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Guardar" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -708,7 +708,7 @@ msgstr "Cambio de %(username)s la configuración de la cuenta " #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Cambiar tu contraseña." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -782,7 +782,7 @@ msgstr "Imágenes para %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "Fichero PDF" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -938,15 +938,15 @@ msgstr "Añade un comentario " #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "hace %(formatted_time)s" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Agregado" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Creado" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1134,27 +1134,27 @@ msgstr "Parece que no hay ninguna página en esta dirección. ¡Lo siento!

\n" +"PO-Revision-Date: 2013-06-01 07:11+0000\n" +"Last-Translator: GenghisKhan \n" "Language-Team: Hebrew (http://www.transifex.com/projects/p/mediagoblin/language/he/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -36,7 +36,7 @@ msgstr "כתובת דוא״ל" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "שם משתמש או דוא״ל" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -263,7 +263,7 @@ msgstr "סיסמה שגויה" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "סיסמתך שונתה בהצלחה" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -280,17 +280,17 @@ msgstr "בכל אופן, קישור מדור symlink נמצא; הוסר.\n" #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "לא היתה אפשרות לקשר את \"%s\": %s קיים ואינו קישור סמלי (symlink)\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "מדלג על \"%s\"; כבר מוגדר.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "קישור ישן נמצא עבור \"%s\"; מסיר כעת.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -306,7 +306,7 @@ msgstr "צר לי, אינני תומך בטיפוס קובץ זה :(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "unoconv נכשל לפעול, בדוק קובץ יומן" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -617,7 +617,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "צור חשבון באתר זה\n או\n התקן את MediaGoblin על שרתך" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -664,11 +664,11 @@ msgstr "שמור שינויים" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "משנה כעת את הסיסמה של %(username)s'" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "שמור" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -699,7 +699,7 @@ msgstr "שינוי הגדרות חשבון עבור %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "שנה את סיסמתך." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -773,7 +773,7 @@ msgstr "תמונה עבור %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "קובץ PDF" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -929,15 +929,15 @@ msgstr "הוסף את תגובה זו" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "מלפני %(formatted_time)s" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "התווסף" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "נוצר" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1125,27 +1125,27 @@ msgstr "לא נראה שקיים עמוד בכתובת זו. צר לי!

א #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "שנה" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "חודש" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "שבוע" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "יום" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "שעה" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "דקה" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1184,7 +1184,7 @@ msgstr "הגיב/ה על פרסומך" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "מצטערים, תגובות מנוטרלות." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.mo index 2fb3a8ce..596ab843 100644 Binary files a/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.po index 81ca3838..77896b87 100644 --- a/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-06-05 22:51+0000\n" +"Last-Translator: tryggvib \n" "Language-Team: Icelandic (Iceland) (http://www.transifex.com/projects/p/mediagoblin/language/is_IS/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,7 +35,7 @@ msgstr "Netfang" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Notandanafn eða tölvupóstur" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -262,7 +262,7 @@ msgstr "Vitlaust lykilorð" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Það tókst að breyta lykilorðinu þínu" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -279,17 +279,17 @@ msgstr "Fann samt gamlan táknrænan tengil á möppu; fjarlægður.\n" #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "Gat ekki tengt \"%s\": %s er til og er ekki sýndartengill\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "Hoppa yfir \"%s\"; hefur nú þegar verið sett upp.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "Gamall tengill fannst fyrir \"%s\"; fjarlægi.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -305,7 +305,7 @@ msgstr "Ég styð því miður ekki þessa gerð af skrám :(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "tekst ekki að keyra unoconv, athugaðu annálsskrá" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -616,7 +616,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Búa til aðgang á þessari síðu\neða\nSettu upp þinn eigin margmiðlunarþjón" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -663,11 +663,11 @@ msgstr "Vista breytingar" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Breyti lykilorði fyrir notandann: %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Vista" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -698,7 +698,7 @@ msgstr "Breyti notandaaðgangsstillingum fyrir: %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Breyta lykilorðinu þínu." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -772,7 +772,7 @@ msgstr "Mynd fyrir %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF skrá" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -928,15 +928,15 @@ msgstr "Senda inn þessa athugasemd" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "Fyrir %(formatted_time)s" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Bætt við" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Skapað" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1124,27 +1124,27 @@ msgstr "Því miður! Það virðist ekki vera nein síða á þessari vefslóð #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "ár" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "mánuður" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "vika" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "dagur" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "klukkustund" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "mínúta" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1183,7 +1183,7 @@ msgstr "skrifaði athugasemd við færsluna þína" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Því miður, athugasemdir eru óvirkar." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.mo index f9666be3..9cbd03b2 100644 Binary files a/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.po index ea0d5d25..6a11d5da 100644 --- a/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-05-31 15:40+0000\n" +"Last-Translator: velmont \n" "Language-Team: Norwegian Nynorsk (Norway) (http://www.transifex.com/projects/p/mediagoblin/language/nn_NO/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,7 +35,7 @@ msgstr "Epost" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Brukarnamn eller epost" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -262,7 +262,7 @@ msgstr "Feil passord" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Endra passord" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -279,17 +279,17 @@ msgstr "However, old link directory symlink found; removed.\n" #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "Kunne ikkje lenkja «%s»: %s eksisterer og er ikkje ei symlenkje\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "Hopper over «%s»: allereie satt opp.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "Gamal lenkje funnen for «%s»; fjernar.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -305,7 +305,7 @@ msgstr "Orsak, stør ikkje den filtypen :(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "klarte ikkje køyra unoconv, sjekk logg-fil" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -616,7 +616,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Opprett ein konto på denne sida\n eller\n Set opp din eigen MediaGoblin-server" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -663,11 +663,11 @@ msgstr "Lagra" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Endrar passordet til %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Lagra" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -698,7 +698,7 @@ msgstr "Endrar kontoinnstellingane til %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Endra passordet ditt." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -772,7 +772,7 @@ msgstr "Bilete for %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF-fil" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -928,15 +928,15 @@ msgstr "Legg til dette innspelet" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "%(formatted_time)s sidan" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Lagt til" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Oppretta" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1124,27 +1124,27 @@ msgstr "Ser ikkje ut til å finnast noko her. Orsak.

\n

Dersom du er sikker #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "år" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "månad" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "veke" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "dag" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "time" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "minutt" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1183,7 +1183,7 @@ msgstr "kom med innspel på innlegget ditt" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Innspel er avslege" #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.mo index 5a234c82..8b318329 100644 Binary files a/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.po index 5a36889a..78ab219a 100644 --- a/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-05-28 13:51+0000\n" +"Last-Translator: Sergiusz Pawlowicz \n" "Language-Team: Polish (http://www.transifex.com/projects/p/mediagoblin/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,7 +35,7 @@ msgstr "Adres e-mail" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Nazwa konta lub adres poczty elektronicznej" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -262,7 +262,7 @@ msgstr "Nieprawidłowe hasło" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Twoje hasło zostało zmienione" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -279,17 +279,17 @@ msgstr "Znaleziono stary odnośnik symboliczny do katalogu; usunięto.\n" #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "Nie mogę zrobić odnośnika \"%s\": %s istnieje i nie jest odnośnikiem\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "Opuszczam \"%s\"; już jest gotowe.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "Znaleziono stary odnośnik dla \"%s\"; usuwam.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -305,7 +305,7 @@ msgstr "NIestety, nie obsługujemy tego typu plików :-(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "nie dało się uruchomić unoconv, sprawdź log" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -616,7 +616,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Załóż konto na tym serwerze\n albo\n Uruchom MediaGoblin na swoim własnym serwerze" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -663,11 +663,11 @@ msgstr "Zapisz zmiany" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Zmieniam hasło użytkownika %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Zachowaj" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -698,7 +698,7 @@ msgstr "Zmiana ustawień konta %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Zmień swoje hasło." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -772,7 +772,7 @@ msgstr "Grafika dla %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "Plik PDF" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -928,15 +928,15 @@ msgstr "Dodaj komentarz" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "%(formatted_time)s temu" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Dodano" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Utworzono" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1124,27 +1124,27 @@ msgstr "Wygląda na to, że nic tutaj nie ma!

Jeśli jesteś pewny, że ad #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "rok" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "miesiąc" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "tydzień" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "dzień" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "godzina" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "minuta" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1183,7 +1183,7 @@ msgstr "komentarze do twojego wpisu" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Komentowanie jest wyłączone." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.mo index 309855f5..8cfdf339 100644 Binary files a/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.po index 4a33f751..af2d94d6 100644 --- a/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-05-27 20:40+0000\n" +"Last-Translator: George Pop \n" "Language-Team: Romanian (http://www.transifex.com/projects/p/mediagoblin/language/ro/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,7 +35,7 @@ msgstr "Adresa de e-mail" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Numele de utilizator sau adresa de e-mail" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -262,7 +262,7 @@ msgstr "Parolă incorectă" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Parola a fost schimbată cu succes" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -279,17 +279,17 @@ msgstr "A fost însă găsit un symlink către vechiul folder; s-a șters.\n" #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "Nu s-a putut crea link pentru \"%s\": %s există deja și nu este symlink\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "S-a omis \"%s\"; configurat deja.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "Există deja un link pentru \"%s\"; va fi șters.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -305,7 +305,7 @@ msgstr "Scuze, nu recunosc acest tip de fișier :(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "unoconv nu poate fi executat; verificați log-ul" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -616,7 +616,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Creați un cont pe acest site\n sau\n Instalați MediaGoblin pe serverul dvs." #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -663,11 +663,11 @@ msgstr "Salvează modificările" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Se modifică parola pentru %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Salvează" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -698,7 +698,7 @@ msgstr "Se modifică setările contului pentru userul %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Modifică parolă." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -772,7 +772,7 @@ msgstr "Imagine pentru %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "Fișier PDF" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -928,15 +928,15 @@ msgstr "Trimite acest comentariu" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "în urmă cu %(formatted_time)s" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Adăugat" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Creat" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1124,27 +1124,27 @@ msgstr "Nu există nicio pagină la această adresă.

Dacă sunteți sigur #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "anul" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "luna" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "săptămâna" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "ziua" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "ora" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "minutul" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1183,7 +1183,7 @@ msgstr "a făcut un comentariu la postarea ta" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Comentariile sunt dezactivate." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.mo index 89836e48..ed28ff43 100644 Binary files a/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.po index 12c79b17..d0ff7bdd 100644 --- a/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-06-01 21:08+0000\n" +"Last-Translator: aleksejrs \n" "Language-Team: Russian (http://www.transifex.com/projects/p/mediagoblin/language/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,7 +35,7 @@ msgstr "Адрес электронной почты" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Имя пользователя или адрес электронной почты" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -262,7 +262,7 @@ msgstr "Неправильный пароль" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Ваш пароль сменён успешно" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -663,11 +663,11 @@ msgstr "Сохранить изменения" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Смена пароля %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Сохранить" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -698,7 +698,7 @@ msgstr "Настройка учётной записи %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Сменить пароль" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -772,7 +772,7 @@ msgstr "Изображение «%(media_title)s»" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF-файл" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -928,15 +928,15 @@ msgstr "Добавить этот комментарий" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "%(formatted_time)s назад" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Добавлен" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Создан" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1144,7 +1144,7 @@ msgstr "" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "мин" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1183,7 +1183,7 @@ msgstr "оставил комментарий к вашему файлу" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Сожалеем: возможность комментирования отключена." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.mo index a262aaee..fd48a37f 100644 Binary files a/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.po index ecb83098..e4d1bacc 100644 --- a/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.po @@ -14,8 +14,8 @@ msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-05-28 07:47+0000\n" +"Last-Translator: martin \n" "Language-Team: Slovak (http://www.transifex.com/projects/p/mediagoblin/language/sk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -39,7 +39,7 @@ msgstr "Email adresse" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "Použivateľské meno alebo e-mail" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -266,7 +266,7 @@ msgstr "Nesprávne heslo" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "Tvoje heslo bolo úspešne zmenené" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -283,17 +283,17 @@ msgstr "Odstránené; hoci bol pôvodný symbolický odkaz adresára nájdený.\ #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "Nemožno odkazovať na \"%s\": %s existuje a nie je symbolickým odkazom\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "Preskakujem \"%s\"; opakovane nastavené.\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "Nájdený starý odkaz pre \"%s\"; odstraňujem.\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" @@ -309,7 +309,7 @@ msgstr "Prepáč, nepodporujem tento typ súborov =(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "beh unoconv zlyhal, preskúmajte log záznam" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -620,7 +620,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "Vytvoriť účet na tejto stránke\n alebo\n Nastaviť MediaGoblin na vlastnom serveri" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -667,11 +667,11 @@ msgstr "Uložiť zmeny" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "Mením heslo používateľa %(username)s" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "Uložiť" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format @@ -702,7 +702,7 @@ msgstr "Mením nastavenia účtu používateľa %(username)s" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "Zmeniť svoje heslo." #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" @@ -776,7 +776,7 @@ msgstr "Obrázok pre %(media_title)s" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF súbor" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -932,15 +932,15 @@ msgstr "Pridať tento komentár" #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "pred %(formatted_time)s " #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "Pridané" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "Vytvorené" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 @@ -1128,27 +1128,27 @@ msgstr "Zdá sa, že na tejto adrese sa nič nenachádza. Prepáč!

Pokia #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "rok" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "mesiac" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "týždeň" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "deň" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "hodina" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "minúta" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1187,7 +1187,7 @@ msgstr "okmentoval tvoj príspevok" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Prepáč, komentovanie je vypnuté." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.mo new file mode 100644 index 00000000..4341870b Binary files /dev/null and b/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.po new file mode 100644 index 00000000..4155520f --- /dev/null +++ b/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.po @@ -0,0 +1,1252 @@ +# Translations template for PROJECT. +# Copyright (C) 2013 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# +# Translators: +# Caner BAŞARAN , 2013 +msgid "" +msgstr "" +"Project-Id-Version: GNU MediaGoblin\n" +"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" +"POT-Creation-Date: 2013-05-27 13:54-0500\n" +"PO-Revision-Date: 2013-06-06 15:44+0000\n" +"Last-Translator: Caner BAŞARAN \n" +"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/mediagoblin/language/tr_TR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" +"Language: tr_TR\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: mediagoblin/auth/forms.py:26 +msgid "Username" +msgstr "Kullanıcı adı" + +#: mediagoblin/auth/forms.py:30 mediagoblin/auth/forms.py:45 +#: mediagoblin/tests/test_util.py:110 +msgid "Password" +msgstr "Parola" + +#: mediagoblin/auth/forms.py:34 +msgid "Email address" +msgstr "E-posta adresi" + +#: mediagoblin/auth/forms.py:41 +msgid "Username or Email" +msgstr "Kullanıcı adı veya E-posta" + +#: mediagoblin/auth/forms.py:52 +msgid "Username or email" +msgstr "Kullanıcı adı ya da e-posta" + +#: mediagoblin/auth/tools.py:31 +msgid "Invalid User name or email address." +msgstr "" + +#: mediagoblin/auth/tools.py:32 +msgid "This field does not take email addresses." +msgstr "" + +#: mediagoblin/auth/tools.py:33 +msgid "This field requires an email address." +msgstr "" + +#: mediagoblin/auth/views.py:54 +msgid "Sorry, registration is disabled on this instance." +msgstr "Üzgünüz, bu durumda kayıt devre dışıdır." + +#: mediagoblin/auth/views.py:68 +msgid "Sorry, a user with that name already exists." +msgstr "Maalesef, bu isimde bir kullanıcı mevcut." + +#: mediagoblin/auth/views.py:72 +msgid "Sorry, a user with that email address already exists." +msgstr "Üzgünüz, bu e-posta adresine sahip bir kullanıcı zaten var." + +#: mediagoblin/auth/views.py:182 +msgid "" +"Your email address has been verified. You may now login, edit your profile, " +"and submit images!" +msgstr "E-posta adresiniz doğrulandı. Şimdi giriş yapabilir, profilinizi düzenleyip ve yeni görüntüleri gönderebilirsiniz!" + +#: mediagoblin/auth/views.py:188 +msgid "The verification key or user id is incorrect" +msgstr "Doğrulama anahtarı veya kullanıcı kimliği yanlış" + +#: mediagoblin/auth/views.py:206 +msgid "You must be logged in so we know who to send the email to!" +msgstr "" + +#: mediagoblin/auth/views.py:214 +msgid "You've already verified your email address!" +msgstr "Zaten e-posta adresinizi doğruladınız!" + +#: mediagoblin/auth/views.py:227 +msgid "Resent your verification email." +msgstr "Doğrulama e-postasını tekrar yolla." + +#: mediagoblin/auth/views.py:258 +msgid "" +"If that email address (case sensitive!) is registered an email has been sent" +" with instructions on how to change your password." +msgstr "" + +#: mediagoblin/auth/views.py:269 +msgid "Couldn't find someone with that username." +msgstr "" + +#: mediagoblin/auth/views.py:272 +msgid "" +"An email has been sent with instructions on how to change your password." +msgstr "Parolanızı nasıl değiştireceğinizle ilgili adımları anlatan bir e-posta gönderildi." + +#: mediagoblin/auth/views.py:279 +msgid "" +"Could not send password recovery email as your username is inactive or your " +"account's email address has not been verified." +msgstr "" + +#: mediagoblin/auth/views.py:336 +msgid "You can now log in using your new password." +msgstr "Şimdi yeni parolanızı giriş için kullanabilirsiniz." + +#: mediagoblin/edit/forms.py:25 mediagoblin/edit/forms.py:82 +#: mediagoblin/submit/forms.py:28 mediagoblin/submit/forms.py:47 +#: mediagoblin/user_pages/forms.py:45 +msgid "Title" +msgstr "Başlık" + +#: mediagoblin/edit/forms.py:28 mediagoblin/submit/forms.py:31 +msgid "Description of this work" +msgstr "" + +#: mediagoblin/edit/forms.py:29 mediagoblin/edit/forms.py:52 +#: mediagoblin/edit/forms.py:86 mediagoblin/submit/forms.py:32 +#: mediagoblin/submit/forms.py:51 mediagoblin/user_pages/forms.py:49 +msgid "" +"You can use\n" +" \n" +" Markdown for formatting." +msgstr "" + +#: mediagoblin/edit/forms.py:33 mediagoblin/submit/forms.py:36 +msgid "Tags" +msgstr "Etiketler" + +#: mediagoblin/edit/forms.py:35 mediagoblin/submit/forms.py:38 +msgid "Separate tags by commas." +msgstr "Etikerleri virgül ile ayırın." + +#: mediagoblin/edit/forms.py:38 mediagoblin/edit/forms.py:90 +msgid "Slug" +msgstr "" + +#: mediagoblin/edit/forms.py:39 mediagoblin/edit/forms.py:91 +msgid "The slug can't be empty" +msgstr "" + +#: mediagoblin/edit/forms.py:40 +msgid "" +"The title part of this media's address. You usually don't need to change " +"this." +msgstr "" + +#: mediagoblin/edit/forms.py:44 mediagoblin/submit/forms.py:41 +#: mediagoblin/templates/mediagoblin/utils/license.html:20 +msgid "License" +msgstr "" + +#: mediagoblin/edit/forms.py:50 +msgid "Bio" +msgstr "" + +#: mediagoblin/edit/forms.py:56 +msgid "Website" +msgstr "Web sitesi" + +#: mediagoblin/edit/forms.py:58 +msgid "This address contains errors" +msgstr "" + +#: mediagoblin/edit/forms.py:63 +msgid "License preference" +msgstr "" + +#: mediagoblin/edit/forms.py:69 +msgid "This will be your default license on upload forms." +msgstr "" + +#: mediagoblin/edit/forms.py:71 +msgid "Email me when others comment on my media" +msgstr "Medyama birisi yorum yazdığında bana e-posta at" + +#: mediagoblin/edit/forms.py:83 +msgid "The title can't be empty" +msgstr "" + +#: mediagoblin/edit/forms.py:85 mediagoblin/submit/forms.py:50 +#: mediagoblin/user_pages/forms.py:48 +msgid "Description of this collection" +msgstr "" + +#: mediagoblin/edit/forms.py:92 +msgid "" +"The title part of this collection's address. You usually don't need to " +"change this." +msgstr "" + +#: mediagoblin/edit/forms.py:99 +msgid "Old password" +msgstr "Eski parola" + +#: mediagoblin/edit/forms.py:101 +msgid "Enter your old password to prove you own this account." +msgstr "" + +#: mediagoblin/edit/forms.py:104 +msgid "New password" +msgstr "Yeni parola" + +#: mediagoblin/edit/views.py:67 +msgid "An entry with that slug already exists for this user." +msgstr "" + +#: mediagoblin/edit/views.py:85 +msgid "You are editing another user's media. Proceed with caution." +msgstr "Başka bir kullanıcının medyasını düzenlerken dikkatli davranın." + +#: mediagoblin/edit/views.py:155 +#, python-format +msgid "You added the attachment %s!" +msgstr "" + +#: mediagoblin/edit/views.py:182 +msgid "You can only edit your own profile." +msgstr "" + +#: mediagoblin/edit/views.py:188 +msgid "You are editing a user's profile. Proceed with caution." +msgstr "Başka bir kullanıcının profilini düzenlerken dikkatli davranın." + +#: mediagoblin/edit/views.py:204 +msgid "Profile changes saved" +msgstr "Profil değişiklikleri kaydedildi" + +#: mediagoblin/edit/views.py:240 +msgid "Account settings saved" +msgstr "Hesap ayarları kaydedildi" + +#: mediagoblin/edit/views.py:274 +msgid "You need to confirm the deletion of your account." +msgstr "" + +#: mediagoblin/edit/views.py:310 mediagoblin/submit/views.py:138 +#: mediagoblin/user_pages/views.py:222 +#, python-format +msgid "You already have a collection called \"%s\"!" +msgstr "" + +#: mediagoblin/edit/views.py:314 +msgid "A collection with that slug already exists for this user." +msgstr "" + +#: mediagoblin/edit/views.py:329 +msgid "You are editing another user's collection. Proceed with caution." +msgstr "" + +#: mediagoblin/edit/views.py:348 +msgid "Wrong password" +msgstr "Yanlış parola" + +#: mediagoblin/edit/views.py:363 +msgid "Your password was changed successfully" +msgstr "Parolanız başarılı bir şekilde değiştirildi" + +#: mediagoblin/gmg_commands/assetlink.py:60 +msgid "Cannot link theme... no theme set\n" +msgstr "" + +#: mediagoblin/gmg_commands/assetlink.py:73 +msgid "No asset directory for this theme\n" +msgstr "" + +#: mediagoblin/gmg_commands/assetlink.py:76 +msgid "However, old link directory symlink found; removed.\n" +msgstr "" + +#: mediagoblin/gmg_commands/assetlink.py:112 +#, python-format +msgid "Could not link \"%s\": %s exists and is not a symlink\n" +msgstr "" + +#: mediagoblin/gmg_commands/assetlink.py:119 +#, python-format +msgid "Skipping \"%s\"; already set up.\n" +msgstr "" + +#: mediagoblin/gmg_commands/assetlink.py:124 +#, python-format +msgid "Old link found for \"%s\"; removing.\n" +msgstr "" + +#: mediagoblin/meddleware/csrf.py:134 +msgid "" +"CSRF cookie not present. This is most likely the result of a cookie blocker " +"or somesuch.
Make sure to permit the settings of cookies for this " +"domain." +msgstr "" + +#: mediagoblin/media_types/__init__.py:111 +#: mediagoblin/media_types/__init__.py:155 +msgid "Sorry, I don't support that file type :(" +msgstr "Üzgünüz, bu tip dosyaları desteklemiyoruz :(" + +#: mediagoblin/media_types/pdf/processing.py:136 +msgid "unoconv failing to run, check log file" +msgstr "" + +#: mediagoblin/media_types/video/processing.py:37 +msgid "Video transcoding failed" +msgstr "" + +#: mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html:24 +msgid "Location" +msgstr "" + +#: mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html:52 +#, python-format +msgid "View on OpenStreetMap" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:29 +msgid "Allow" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:30 +msgid "Deny" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:34 +msgid "Name" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:35 +msgid "The name of the OAuth client" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:36 +msgid "Description" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:38 +msgid "" +"This will be visible to users allowing your\n" +" application to authenticate as them." +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:40 +msgid "Type" +msgstr "Tür" + +#: mediagoblin/plugins/oauth/forms.py:45 +msgid "" +"Confidential - The client can\n" +" make requests to the GNU MediaGoblin instance that can not be\n" +" intercepted by the user agent (e.g. server-side client).
\n" +" Public - The client can't make confidential\n" +" requests to the GNU MediaGoblin instance (e.g. client-side\n" +" JavaScript client)." +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:52 +msgid "Redirect URI" +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:54 +msgid "" +"The redirect URI for the applications, this field\n" +" is required for public clients." +msgstr "" + +#: mediagoblin/plugins/oauth/forms.py:66 +msgid "This field is required for public clients" +msgstr "" + +#: mediagoblin/plugins/oauth/views.py:56 +msgid "The client {0} has been registered!" +msgstr "" + +#: mediagoblin/plugins/oauth/templates/oauth/client/connections.html:22 +msgid "OAuth client connections" +msgstr "" + +#: mediagoblin/plugins/oauth/templates/oauth/client/list.html:22 +msgid "Your OAuth clients" +msgstr "" + +#: mediagoblin/plugins/oauth/templates/oauth/client/register.html:29 +#: mediagoblin/templates/mediagoblin/submit/collection.html:30 +#: mediagoblin/templates/mediagoblin/submit/start.html:34 +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:68 +msgid "Add" +msgstr "Ekle" + +#: mediagoblin/processing/__init__.py:193 +msgid "Invalid file given for media type." +msgstr "Bu medya türü için geçersiz dosya türü." + +#: mediagoblin/submit/forms.py:26 +msgid "File" +msgstr "Dosya" + +#: mediagoblin/submit/views.py:49 +msgid "You must provide a file." +msgstr "Bir dosya sağlamanız gerekir." + +#: mediagoblin/submit/views.py:93 +msgid "Woohoo! Submitted!" +msgstr "Hoooop! Gönderildi!" + +#: mediagoblin/submit/views.py:144 +#, python-format +msgid "Collection \"%s\" added!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/base.html:67 +msgid "Verify your email!" +msgstr "E-postanızı doğrulayın!" + +#: mediagoblin/templates/mediagoblin/base.html:68 +msgid "log out" +msgstr "çıkış" + +#: mediagoblin/templates/mediagoblin/base.html:73 +#: mediagoblin/templates/mediagoblin/auth/login.html:28 +#: mediagoblin/templates/mediagoblin/auth/login.html:36 +#: mediagoblin/templates/mediagoblin/auth/login.html:54 +msgid "Log in" +msgstr "Giriş" + +#: mediagoblin/templates/mediagoblin/base.html:82 +#, python-format +msgid "%(user_name)s's account" +msgstr "" + +#: mediagoblin/templates/mediagoblin/base.html:89 +msgid "Change account settings" +msgstr "Hesap ayarlarını değiştir" + +#: mediagoblin/templates/mediagoblin/base.html:93 +#: mediagoblin/templates/mediagoblin/base.html:108 +#: mediagoblin/templates/mediagoblin/admin/panel.html:21 +#: mediagoblin/templates/mediagoblin/admin/panel.html:26 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:21 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:26 +msgid "Media processing panel" +msgstr "Madya işlem paneli" + +#: mediagoblin/templates/mediagoblin/base.html:96 +msgid "Log out" +msgstr "Çıkış" + +#: mediagoblin/templates/mediagoblin/base.html:99 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:156 +msgid "Add media" +msgstr "Medya ekle" + +#: mediagoblin/templates/mediagoblin/base.html:102 +#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:41 +msgid "Create new collection" +msgstr "" + +#: mediagoblin/templates/mediagoblin/error.html:24 +msgid "Image of goblin stressing out" +msgstr "" + +#: mediagoblin/templates/mediagoblin/root.html:32 +msgid "Most recent media" +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:29 +msgid "" +"Here you can track the state of media being processed on this instance." +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:32 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:32 +msgid "Media in-processing" +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:58 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:56 +msgid "No media in-processing" +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:61 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:59 +msgid "These uploads failed to process:" +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:90 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:86 +msgid "No failed entries!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:92 +msgid "Last 10 successful uploads" +msgstr "" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:112 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:107 +msgid "No processed entries, yet!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/auth/change_fp.html:28 +#: mediagoblin/templates/mediagoblin/auth/change_fp.html:36 +msgid "Set your new password" +msgstr "" + +#: mediagoblin/templates/mediagoblin/auth/change_fp.html:39 +msgid "Set password" +msgstr "" + +#: mediagoblin/templates/mediagoblin/auth/forgot_password.html:23 +#: mediagoblin/templates/mediagoblin/auth/forgot_password.html:31 +msgid "Recover password" +msgstr "" + +#: mediagoblin/templates/mediagoblin/auth/forgot_password.html:34 +msgid "Send instructions" +msgstr "" + +#: mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt:19 +#, python-format +msgid "" +"Hi %(username)s,\n" +"\n" +"to change your GNU MediaGoblin password, open the following URL in \n" +"your web browser:\n" +"\n" +"%(verification_url)s\n" +"\n" +"If you think this is an error, just ignore this email and continue being\n" +"a happy goblin!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/auth/login.html:39 +msgid "Logging in failed!" +msgstr "Giriş başarısız!" + +#: mediagoblin/templates/mediagoblin/auth/login.html:44 +msgid "Don't have an account yet?" +msgstr "Hala hesabınız yok mu?" + +#: mediagoblin/templates/mediagoblin/auth/login.html:45 +msgid "Create one here!" +msgstr "Şimdi oluşturun!" + +#: mediagoblin/templates/mediagoblin/auth/login.html:51 +msgid "Forgot your password?" +msgstr "Parolanı mı unuttun?" + +#: mediagoblin/templates/mediagoblin/auth/register.html:28 +#: mediagoblin/templates/mediagoblin/auth/register.html:36 +msgid "Create an account!" +msgstr "Hesap oluştur!" + +#: mediagoblin/templates/mediagoblin/auth/register.html:40 +msgid "Create" +msgstr "Oluştur" + +#: mediagoblin/templates/mediagoblin/auth/verification_email.txt:19 +#, python-format +msgid "" +"Hi %(username)s,\n" +"\n" +"to activate your GNU MediaGoblin account, open the following URL in\n" +"your web browser:\n" +"\n" +"%(verification_url)s" +msgstr "Merhaba %(username)s,\n\nGNU MediaGoblin hesabınızı etkinleştirmek için, lütfen aşağıdaki\nURL(bağlantı)'yı Web tarayıcınızda açın:\n\n%(verification_url)s" + +#: mediagoblin/templates/mediagoblin/bits/base_footer.html:21 +#, python-format +msgid "" +"Powered by MediaGoblin, a GNU project." +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/base_footer.html:24 +#, python-format +msgid "" +"Released under the AGPL. Source code available." +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:20 +msgid "Explore" +msgstr "Keşfet" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:22 +msgid "Hi there, welcome to this MediaGoblin site!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:24 +msgid "" +"This site is running MediaGoblin, an " +"extraordinarily great piece of media hosting software." +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:25 +msgid "" +"To add your own media, place comments, and more, you can log in with your " +"MediaGoblin account." +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:27 +msgid "Don't have one yet? It's easy!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:28 +#, python-format +msgid "" +"Create an account at this site\n" +" or\n" +" Set up MediaGoblin on your own server" +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/logo.html:23 +#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 +msgid "MediaGoblin logo" +msgstr "MediaGoblin logo" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:23 +#: mediagoblin/templates/mediagoblin/edit/attachments.html:35 +#, python-format +msgid "Editing attachments for %(media_title)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:44 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:182 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:198 +msgid "Attachments" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:57 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:204 +msgid "Add attachment" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:61 +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:42 +#: mediagoblin/templates/mediagoblin/edit/edit.html:41 +#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:32 +#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:46 +#: mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html:52 +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:67 +#: mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html:48 +msgid "Cancel" +msgstr "İptal" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:63 +#: mediagoblin/templates/mediagoblin/edit/edit.html:42 +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:55 +#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:33 +#: mediagoblin/templates/mediagoblin/edit/edit_profile.html:40 +msgid "Save changes" +msgstr "Değişiklikleri kaydet" + +#: mediagoblin/templates/mediagoblin/edit/change_pass.html:28 +#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 +#, python-format +msgid "Changing %(username)s's password" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 +msgid "Save" +msgstr "Kaydet" + +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 +#, python-format +msgid "Really delete user '%(user_name)s' and all related media/comments?" +msgstr "Gerçekten '%(user_name)s' kullanıcısını ve ilgili tüm medya/yorumları silmek istiyor musun?" + +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:35 +msgid "Yes, really delete my account" +msgstr "Evet, gerçekten hesabımı silmek istiyorum" + +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:44 +#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:48 +#: mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html:49 +msgid "Delete permanently" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/edit.html:23 +#: mediagoblin/templates/mediagoblin/edit/edit.html:35 +#, python-format +msgid "Editing %(media_title)s" +msgstr "%(media_title)s düzenleme" + +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:28 +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:40 +#, python-format +msgid "Changing %(username)s's account settings" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 +msgid "Change your password." +msgstr "Parolanızı değiştirin." + +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 +msgid "Delete my account" +msgstr "Hesabımı sil" + +#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:29 +#, python-format +msgid "Editing %(collection_title)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/edit/edit_profile.html:23 +#: mediagoblin/templates/mediagoblin/edit/edit_profile.html:34 +#, python-format +msgid "Editing %(username)s's profile" +msgstr "%(username)s profilini düzenleme" + +#: mediagoblin/templates/mediagoblin/listings/collection.html:30 +#: mediagoblin/templates/mediagoblin/listings/collection.html:35 +#: mediagoblin/templates/mediagoblin/listings/tag.html:30 +#: mediagoblin/templates/mediagoblin/listings/tag.html:35 +#, python-format +msgid "Media tagged with: %(tag_name)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/ascii.html:34 +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:56 +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:65 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:136 +#: mediagoblin/templates/mediagoblin/media_displays/video.html:55 +msgid "Download" +msgstr "İndir" + +#: mediagoblin/templates/mediagoblin/media_displays/ascii.html:38 +msgid "Original" +msgstr "Özgün" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:44 +msgid "" +"Sorry, this audio will not work because \n" +"\tyour web browser does not support HTML5 \n" +"\taudio." +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:47 +msgid "" +"You can get a modern web browser that \n" +"\tcan play the audio at \n" +"\t http://getfirefox.com!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:60 +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:71 +#: mediagoblin/templates/mediagoblin/media_displays/video.html:61 +msgid "Original file" +msgstr "Özgün dosya" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:63 +msgid "WebM file (Vorbis codec)" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:59 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:87 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:93 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:99 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:105 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:59 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:65 +#, python-format +msgid "Image for %(media_title)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 +msgid "PDF file" +msgstr "PDF dosya" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 +msgid "Toggle Rotate" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:113 +msgid "Perspective" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:116 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:117 +msgid "Front" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:120 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:121 +msgid "Top" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:124 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:125 +msgid "Side" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:130 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:131 +msgid "WebGL" +msgstr "WebGL" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:138 +msgid "Download model" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:146 +msgid "File Format" +msgstr "Dosya Biçimi" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:148 +msgid "Object Height" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/video.html:44 +msgid "" +"Sorry, this video will not work because\n" +" your web browser does not support HTML5 \n" +" video." +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/video.html:47 +msgid "" +"You can get a modern web browser that \n" +" can play this video at \n" +" http://getfirefox.com!" +msgstr "" + +#: mediagoblin/templates/mediagoblin/media_displays/video.html:69 +msgid "WebM file (640p; VP8/Vorbis)" +msgstr "" + +#: mediagoblin/templates/mediagoblin/submit/collection.html:26 +msgid "Add a collection" +msgstr "" + +#: mediagoblin/templates/mediagoblin/submit/start.html:23 +#: mediagoblin/templates/mediagoblin/submit/start.html:30 +msgid "Add your media" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:30 +#, python-format +msgid "%(collection_title)s (%(username)s's collection)" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:39 +#, python-format +msgid "%(collection_title)s by %(username)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:52 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:79 +msgid "Edit" +msgstr "Düzenle" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:56 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:83 +msgid "Delete" +msgstr "Si" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:30 +#: mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html:30 +#, python-format +msgid "Really delete %(title)s?" +msgstr "Gerçekten %(title)s silmek istiyor musun?" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html:31 +#, python-format +msgid "Really remove %(media_title)s from %(collection_title)s?" +msgstr "Gerçekten %(collection_title)s %(media_title)s kaldırmak istiyor musun?" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html:54 +msgid "Remove" +msgstr "Kaldır" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:21 +#, python-format +msgid "%(username)s's collections" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:28 +#, python-format +msgid "%(username)s's collections" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/comment_email.txt:19 +#, python-format +msgid "" +"Hi %(username)s,\n" +"%(comment_author)s commented on your post (%(comment_url)s) at %(instance_name)s\n" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:30 +#, python-format +msgid "%(username)s's media" +msgstr "%(username)s medyası" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:38 +#, python-format +msgid "" +"%(username)s's media with tag %(tag)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48 +#, python-format +msgid "%(username)s's media" +msgstr "%(username)s medyası" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:38 +#, python-format +msgid "❖ Browsing media by %(username)s" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:95 +msgid "Add a comment" +msgstr "Bir yorum ekle" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:104 +msgid "Add this comment" +msgstr "Bu yorumu ekle" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:132 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:152 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:164 +#, python-format +msgid "%(formatted_time)s ago" +msgstr "%(formatted_time)s önce" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:150 +msgid "Added" +msgstr "Eklendi" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:161 +msgid "Created" +msgstr "Oluşturuldu" + +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 +#, python-format +msgid "Add “%(media_title)s” to a collection" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:54 +msgid "+" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:58 +msgid "Add a new collection" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:29 +msgid "" +"You can track the state of media being processed for your gallery here." +msgstr "Burada galerinizdeki işlenmekte olan medyanın durumunu takip edebilirsiniz." + +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:89 +msgid "Your last 10 successful uploads" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:31 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:89 +#, python-format +msgid "%(username)s's profile" +msgstr "%(username)s profili" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:43 +msgid "Sorry, no such user found." +msgstr "Üzgünüz, böyle bir kullanıcı bulunamadı." + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:50 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:70 +msgid "Email verification needed" +msgstr "E-posta doğrulaması gerekli" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:53 +msgid "Almost done! Your account still needs to be activated." +msgstr "Neredeyse bitti! Hesabınızı etkinleştirmeniz gerekiyor." + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:58 +msgid "" +"An email should arrive in a few moments with instructions on how to do so." +msgstr "Bunun nasıl yapılacağı ile ilgili talimatlar, birkaç dakika içinde size e-posta ulaşacak." + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:62 +msgid "In case it doesn't:" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:65 +msgid "Resend verification email" +msgstr "Doğrulama e-postası tekrar yolla" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:73 +msgid "" +"Someone has registered an account with this username, but it still has to be" +" activated." +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:79 +#, python-format +msgid "" +"If you are that person but you've lost your verification email, you can log in and resend it." +msgstr "Doğrulama e-postasını kaybettiyseniz, giriş yapabilir ve yeniden yollayabilirsiniz." + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:96 +msgid "Here's a spot to tell others about yourself." +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:100 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:117 +msgid "Edit profile" +msgstr "Profil düzenle" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:105 +msgid "This user hasn't filled in their profile (yet)." +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:124 +msgid "Browse collections" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:137 +#, python-format +msgid "View all of %(username)s's media" +msgstr "%(username)s tüm medyasını göster" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:150 +msgid "" +"This is where your media will appear, but you don't seem to have added " +"anything yet." +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:162 +#: mediagoblin/templates/mediagoblin/utils/collection_gallery.html:84 +#: mediagoblin/templates/mediagoblin/utils/object_gallery.html:70 +msgid "There doesn't seem to be any media here yet..." +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/collection_gallery.html:49 +msgid "(remove)" +msgstr "(kaldır)" + +#: mediagoblin/templates/mediagoblin/utils/collections.html:21 +msgid "Collected in" +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/collections.html:40 +msgid "Add to a collection" +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/feed_link.html:21 +#: mediagoblin/themes/airy/templates/mediagoblin/utils/feed_link.html:21 +msgid "feed icon" +msgstr "besleme simgesi" + +#: mediagoblin/templates/mediagoblin/utils/feed_link.html:23 +#: mediagoblin/themes/airy/templates/mediagoblin/utils/feed_link.html:23 +msgid "Atom feed" +msgstr "Atom besleme" + +#: mediagoblin/templates/mediagoblin/utils/license.html:25 +msgid "All rights reserved" +msgstr "Tüm hakları saklıdır" + +#: mediagoblin/templates/mediagoblin/utils/pagination.html:39 +msgid "← Newer" +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/pagination.html:45 +msgid "Older →" +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/pagination.html:48 +msgid "Go to page:" +msgstr "Sayfaya git:" + +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:28 +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:33 +msgid "newer" +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:39 +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:44 +msgid "older" +msgstr "" + +#: mediagoblin/templates/mediagoblin/utils/tags.html:20 +msgid "Tagged with" +msgstr "" + +#: mediagoblin/tools/exif.py:83 +msgid "Could not read the image file." +msgstr "" + +#: mediagoblin/tools/response.py:35 +msgid "Oops!" +msgstr "Amaninnn boo!" + +#: mediagoblin/tools/response.py:36 +msgid "An error occured" +msgstr "" + +#: mediagoblin/tools/response.py:51 +msgid "Operation not allowed" +msgstr "" + +#: mediagoblin/tools/response.py:52 +msgid "" +"Sorry Dave, I can't let you do that!

You have tried to perform a " +"function that you are not allowed to. Have you been trying to delete all " +"user accounts again?" +msgstr "" + +#: mediagoblin/tools/response.py:60 +msgid "" +"There doesn't seem to be a page at this address. Sorry!

If you're sure" +" the address is correct, maybe the page you're looking for has been moved or" +" deleted." +msgstr "" + +#: mediagoblin/tools/timesince.py:62 +msgid "year" +msgstr "yıl" + +#: mediagoblin/tools/timesince.py:63 +msgid "month" +msgstr "ay" + +#: mediagoblin/tools/timesince.py:64 +msgid "week" +msgstr "hafta" + +#: mediagoblin/tools/timesince.py:65 +msgid "day" +msgstr "gün" + +#: mediagoblin/tools/timesince.py:66 +msgid "hour" +msgstr "saat" + +#: mediagoblin/tools/timesince.py:67 +msgid "minute" +msgstr "dakika" + +#: mediagoblin/user_pages/forms.py:23 +msgid "Comment" +msgstr "" + +#: mediagoblin/user_pages/forms.py:25 +msgid "" +"You can use Markdown for" +" formatting." +msgstr "" + +#: mediagoblin/user_pages/forms.py:31 +msgid "I am sure I want to delete this" +msgstr "Bunu silmek için eminim" + +#: mediagoblin/user_pages/forms.py:35 +msgid "I am sure I want to remove this item from the collection" +msgstr "" + +#: mediagoblin/user_pages/forms.py:39 +msgid "Collection" +msgstr "" + +#: mediagoblin/user_pages/forms.py:40 +msgid "-- Select --" +msgstr "" + +#: mediagoblin/user_pages/forms.py:42 +msgid "Include a note" +msgstr "" + +#: mediagoblin/user_pages/lib.py:58 +msgid "commented on your post" +msgstr "" + +#: mediagoblin/user_pages/views.py:169 +msgid "Sorry, comments are disabled." +msgstr "Maalesef, yorum devre dışı." + +#: mediagoblin/user_pages/views.py:174 +msgid "Oops, your comment was empty." +msgstr "Amaninnn boo, yorumunuz boştu." + +#: mediagoblin/user_pages/views.py:180 +msgid "Your comment has been posted!" +msgstr "Yorumunuz gönderildi!" + +#: mediagoblin/user_pages/views.py:205 +msgid "Please check your entries and try again." +msgstr "" + +#: mediagoblin/user_pages/views.py:245 +msgid "You have to select or add a collection" +msgstr "" + +#: mediagoblin/user_pages/views.py:256 +#, python-format +msgid "\"%s\" already in collection \"%s\"" +msgstr "" + +#: mediagoblin/user_pages/views.py:262 +#, python-format +msgid "\"%s\" added to collection \"%s\"" +msgstr "" + +#: mediagoblin/user_pages/views.py:282 +msgid "You deleted the media." +msgstr "Medyayı sildiniz." + +#: mediagoblin/user_pages/views.py:289 +msgid "The media was not deleted because you didn't check that you were sure." +msgstr "Medya silinmedi çünkü emin olduğunuzu onaylamadınız." + +#: mediagoblin/user_pages/views.py:296 +msgid "You are about to delete another user's media. Proceed with caution." +msgstr "Başka bir kullanıcının medyasını silerken dikkatli davranın." + +#: mediagoblin/user_pages/views.py:370 +msgid "You deleted the item from the collection." +msgstr "" + +#: mediagoblin/user_pages/views.py:374 +msgid "The item was not removed because you didn't check that you were sure." +msgstr "" + +#: mediagoblin/user_pages/views.py:382 +msgid "" +"You are about to delete an item from another user's collection. Proceed with" +" caution." +msgstr "" + +#: mediagoblin/user_pages/views.py:415 +#, python-format +msgid "You deleted the collection \"%s\"" +msgstr "" + +#: mediagoblin/user_pages/views.py:422 +msgid "" +"The collection was not deleted because you didn't check that you were sure." +msgstr "" + +#: mediagoblin/user_pages/views.py:430 +msgid "" +"You are about to delete another user's collection. Proceed with caution." +msgstr "" diff --git a/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.mo new file mode 100644 index 00000000..1ed5a4f1 Binary files /dev/null and b/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.po new file mode 100644 index 00000000..4bb714fe --- /dev/null +++ b/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.po @@ -0,0 +1,1256 @@ +# Translations template for PROJECT. +# Copyright (C) 2013 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# +# Translators: +# , 2011 +# cwebber , 2013 +# m13253 , 2013 +# medicalwei , 2012 +# m13253 , 2013 +msgid "" +msgstr "" +"Project-Id-Version: GNU MediaGoblin\n" +"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" +"POT-Creation-Date: 2013-05-27 13:54-0500\n" +"PO-Revision-Date: 2013-06-16 11:06+0000\n" +"Last-Translator: m13253 \n" +"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/mediagoblin/language/zh_CN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: mediagoblin/auth/forms.py:26 +msgid "Username" +msgstr "用户名" + +#: mediagoblin/auth/forms.py:30 mediagoblin/auth/forms.py:45 +#: mediagoblin/tests/test_util.py:110 +msgid "Password" +msgstr "密码" + +#: mediagoblin/auth/forms.py:34 +msgid "Email address" +msgstr "电子邮件地址" + +#: mediagoblin/auth/forms.py:41 +msgid "Username or Email" +msgstr "用户名或电子邮件" + +#: mediagoblin/auth/forms.py:52 +msgid "Username or email" +msgstr "用户名或电子邮件" + +#: mediagoblin/auth/tools.py:31 +msgid "Invalid User name or email address." +msgstr "无效用户名或电子邮件地址。" + +#: mediagoblin/auth/tools.py:32 +msgid "This field does not take email addresses." +msgstr "此字段不能填写电子邮件地址。" + +#: mediagoblin/auth/tools.py:33 +msgid "This field requires an email address." +msgstr "此字段需填写电子邮件地址。" + +#: mediagoblin/auth/views.py:54 +msgid "Sorry, registration is disabled on this instance." +msgstr "抱歉,本站已暂停注册。" + +#: mediagoblin/auth/views.py:68 +msgid "Sorry, a user with that name already exists." +msgstr "抱歉,该用户名已存在。" + +#: mediagoblin/auth/views.py:72 +msgid "Sorry, a user with that email address already exists." +msgstr "抱歉,已有用户用该电子邮件注册。" + +#: mediagoblin/auth/views.py:182 +msgid "" +"Your email address has been verified. You may now login, edit your profile, " +"and submit images!" +msgstr "您的电子邮件地址已认证。您现在可以登录、修改个人资料并上传图片了!" + +#: mediagoblin/auth/views.py:188 +msgid "The verification key or user id is incorrect" +msgstr "验证码错误或用户 ID 错误" + +#: mediagoblin/auth/views.py:206 +msgid "You must be logged in so we know who to send the email to!" +msgstr "您必须登录以便让我们知道将电子邮件发给谁" + +#: mediagoblin/auth/views.py:214 +msgid "You've already verified your email address!" +msgstr "您已经认证过电子邮件地址了!" + +#: mediagoblin/auth/views.py:227 +msgid "Resent your verification email." +msgstr "重发认证邮件。" + +#: mediagoblin/auth/views.py:258 +msgid "" +"If that email address (case sensitive!) is registered an email has been sent" +" with instructions on how to change your password." +msgstr "若该邮件地址(区分大小写)已被注册,则密码修改说明已通过电子邮件送达。" + +#: mediagoblin/auth/views.py:269 +msgid "Couldn't find someone with that username." +msgstr "找不到有该用户名的人。" + +#: mediagoblin/auth/views.py:272 +msgid "" +"An email has been sent with instructions on how to change your password." +msgstr "密码修改说明已通过电子邮件送达。" + +#: mediagoblin/auth/views.py:279 +msgid "" +"Could not send password recovery email as your username is inactive or your " +"account's email address has not been verified." +msgstr "无法发送密码找回邮件,因为您的用户名未激活或者您账户的电子邮件地址未认证。" + +#: mediagoblin/auth/views.py:336 +msgid "You can now log in using your new password." +msgstr "您现在可以用新的密码来登录了!" + +#: mediagoblin/edit/forms.py:25 mediagoblin/edit/forms.py:82 +#: mediagoblin/submit/forms.py:28 mediagoblin/submit/forms.py:47 +#: mediagoblin/user_pages/forms.py:45 +msgid "Title" +msgstr "标题" + +#: mediagoblin/edit/forms.py:28 mediagoblin/submit/forms.py:31 +msgid "Description of this work" +msgstr "该作品的描述" + +#: mediagoblin/edit/forms.py:29 mediagoblin/edit/forms.py:52 +#: mediagoblin/edit/forms.py:86 mediagoblin/submit/forms.py:32 +#: mediagoblin/submit/forms.py:51 mediagoblin/user_pages/forms.py:49 +msgid "" +"You can use\n" +" \n" +" Markdown for formatting." +msgstr "您可以用 Markdown 来排版。" + +#: mediagoblin/edit/forms.py:33 mediagoblin/submit/forms.py:36 +msgid "Tags" +msgstr "标签" + +#: mediagoblin/edit/forms.py:35 mediagoblin/submit/forms.py:38 +msgid "Separate tags by commas." +msgstr "用逗号分隔标签。" + +#: mediagoblin/edit/forms.py:38 mediagoblin/edit/forms.py:90 +msgid "Slug" +msgstr "简称" + +#: mediagoblin/edit/forms.py:39 mediagoblin/edit/forms.py:91 +msgid "The slug can't be empty" +msgstr "简称不能为空" + +#: mediagoblin/edit/forms.py:40 +msgid "" +"The title part of this media's address. You usually don't need to change " +"this." +msgstr "该媒体网址的标题部份。通常不需要修改。" + +#: mediagoblin/edit/forms.py:44 mediagoblin/submit/forms.py:41 +#: mediagoblin/templates/mediagoblin/utils/license.html:20 +msgid "License" +msgstr "许可证" + +#: mediagoblin/edit/forms.py:50 +msgid "Bio" +msgstr "个性签名" + +#: mediagoblin/edit/forms.py:56 +msgid "Website" +msgstr "网站" + +#: mediagoblin/edit/forms.py:58 +msgid "This address contains errors" +msgstr "本网址出错了" + +#: mediagoblin/edit/forms.py:63 +msgid "License preference" +msgstr "许可证偏好" + +#: mediagoblin/edit/forms.py:69 +msgid "This will be your default license on upload forms." +msgstr "这将是您上传界面的默认许可证。" + +#: mediagoblin/edit/forms.py:71 +msgid "Email me when others comment on my media" +msgstr "当有人对我的媒体评论时给我电子邮件" + +#: mediagoblin/edit/forms.py:83 +msgid "The title can't be empty" +msgstr "标题不能是空的" + +#: mediagoblin/edit/forms.py:85 mediagoblin/submit/forms.py:50 +#: mediagoblin/user_pages/forms.py:48 +msgid "Description of this collection" +msgstr "这个合集的描述" + +#: mediagoblin/edit/forms.py:92 +msgid "" +"The title part of this collection's address. You usually don't need to " +"change this." +msgstr "此合集网址的标题部份,通常不需要修改。" + +#: mediagoblin/edit/forms.py:99 +msgid "Old password" +msgstr "旧的密码" + +#: mediagoblin/edit/forms.py:101 +msgid "Enter your old password to prove you own this account." +msgstr "输入您的旧密码来证明您拥有这个账户。" + +#: mediagoblin/edit/forms.py:104 +msgid "New password" +msgstr "新密码" + +#: mediagoblin/edit/views.py:67 +msgid "An entry with that slug already exists for this user." +msgstr "这个简称已经被别人用了" + +#: mediagoblin/edit/views.py:85 +msgid "You are editing another user's media. Proceed with caution." +msgstr "您正在修改别人的媒体,请小心操作。" + +#: mediagoblin/edit/views.py:155 +#, python-format +msgid "You added the attachment %s!" +msgstr "您加上了附件“%s”!" + +#: mediagoblin/edit/views.py:182 +msgid "You can only edit your own profile." +msgstr "您只能修改自己的个人资料" + +#: mediagoblin/edit/views.py:188 +msgid "You are editing a user's profile. Proceed with caution." +msgstr "您正在修改别人的个人资料,请小心操作。" + +#: mediagoblin/edit/views.py:204 +msgid "Profile changes saved" +msgstr "个人资料已修改" + +#: mediagoblin/edit/views.py:240 +msgid "Account settings saved" +msgstr "账户设置已保存" + +#: mediagoblin/edit/views.py:274 +msgid "You need to confirm the deletion of your account." +msgstr "您需要确认删除您的账户。" + +#: mediagoblin/edit/views.py:310 mediagoblin/submit/views.py:138 +#: mediagoblin/user_pages/views.py:222 +#, python-format +msgid "You already have a collection called \"%s\"!" +msgstr "您已经有一个称做“%s”的合集了!" + +#: mediagoblin/edit/views.py:314 +msgid "A collection with that slug already exists for this user." +msgstr "该用户已经有使用该简称的合集了。" + +#: mediagoblin/edit/views.py:329 +msgid "You are editing another user's collection. Proceed with caution." +msgstr "您正在修改别人的合集,请小心操作。" + +#: mediagoblin/edit/views.py:348 +msgid "Wrong password" +msgstr "密码错误" + +#: mediagoblin/edit/views.py:363 +msgid "Your password was changed successfully" +msgstr "您的密码已成功修改" + +#: mediagoblin/gmg_commands/assetlink.py:60 +msgid "Cannot link theme... no theme set\n" +msgstr "无法链接到主题……未设置主题\n" + +#: mediagoblin/gmg_commands/assetlink.py:73 +msgid "No asset directory for this theme\n" +msgstr "此主题没有素材目录\n" + +#: mediagoblin/gmg_commands/assetlink.py:76 +msgid "However, old link directory symlink found; removed.\n" +msgstr "但是旧的目录链接已经找到并移除。\n" + +#: mediagoblin/gmg_commands/assetlink.py:112 +#, python-format +msgid "Could not link \"%s\": %s exists and is not a symlink\n" +msgstr "无法链接到“%s”:“%s”已存在且不是链接\n" + +#: mediagoblin/gmg_commands/assetlink.py:119 +#, python-format +msgid "Skipping \"%s\"; already set up.\n" +msgstr "跳过“%s”;已设置过了。\n" + +#: mediagoblin/gmg_commands/assetlink.py:124 +#, python-format +msgid "Old link found for \"%s\"; removing.\n" +msgstr "“%s”的旧链接已经找到并移除。\n" + +#: mediagoblin/meddleware/csrf.py:134 +msgid "" +"CSRF cookie not present. This is most likely the result of a cookie blocker " +"or somesuch.
Make sure to permit the settings of cookies for this " +"domain." +msgstr "CSRF cookie 不存在。很可能是由类似 cookie 屏蔽器造成的。
请允许本域名的 cookie 设定。" + +#: mediagoblin/media_types/__init__.py:111 +#: mediagoblin/media_types/__init__.py:155 +msgid "Sorry, I don't support that file type :(" +msgstr "抱歉,我不支持这样的文件格式 :(" + +#: mediagoblin/media_types/pdf/processing.py:136 +msgid "unoconv failing to run, check log file" +msgstr "无法运行 unoconv,请检查日志" + +#: mediagoblin/media_types/video/processing.py:37 +msgid "Video transcoding failed" +msgstr "视频转码失败" + +#: mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html:24 +msgid "Location" +msgstr "位置" + +#: mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html:52 +#, python-format +msgid "View on OpenStreetMap" +msgstr "在 OpenStreetMap 上观看" + +#: mediagoblin/plugins/oauth/forms.py:29 +msgid "Allow" +msgstr "允许" + +#: mediagoblin/plugins/oauth/forms.py:30 +msgid "Deny" +msgstr "拒绝" + +#: mediagoblin/plugins/oauth/forms.py:34 +msgid "Name" +msgstr "名称" + +#: mediagoblin/plugins/oauth/forms.py:35 +msgid "The name of the OAuth client" +msgstr "OAuth client 的名称" + +#: mediagoblin/plugins/oauth/forms.py:36 +msgid "Description" +msgstr "描述" + +#: mediagoblin/plugins/oauth/forms.py:38 +msgid "" +"This will be visible to users allowing your\n" +" application to authenticate as them." +msgstr "本描述将会被进行应用程序认证的用户看到。" + +#: mediagoblin/plugins/oauth/forms.py:40 +msgid "Type" +msgstr "类型" + +#: mediagoblin/plugins/oauth/forms.py:45 +msgid "" +"Confidential - The client can\n" +" make requests to the GNU MediaGoblin instance that can not be\n" +" intercepted by the user agent (e.g. server-side client).
\n" +" Public - The client can't make confidential\n" +" requests to the GNU MediaGoblin instance (e.g. client-side\n" +" JavaScript client)." +msgstr "秘密 — OAuth client 可以对 GNU MediaGoblin 站点发送不被用户代理拦截的请求(例如服务端上的 client)。\n公开 — OAuth client 无法对 GNU MediaGoblin 站点发送秘密的请求(例如客户端的 JavaScript client)。" + +#: mediagoblin/plugins/oauth/forms.py:52 +msgid "Redirect URI" +msgstr "重定向 URI" + +#: mediagoblin/plugins/oauth/forms.py:54 +msgid "" +"The redirect URI for the applications, this field\n" +" is required for public clients." +msgstr "此应用程序的重定向 URI,本字段在公开类型的 OAuth client 为必填。" + +#: mediagoblin/plugins/oauth/forms.py:66 +msgid "This field is required for public clients" +msgstr "本字段在公开类型的 OAuth client 为必填" + +#: mediagoblin/plugins/oauth/views.py:56 +msgid "The client {0} has been registered!" +msgstr "OAuth client {0} 注册完成!" + +#: mediagoblin/plugins/oauth/templates/oauth/client/connections.html:22 +msgid "OAuth client connections" +msgstr "OAuth client 连接" + +#: mediagoblin/plugins/oauth/templates/oauth/client/list.html:22 +msgid "Your OAuth clients" +msgstr "您的 OAuth client" + +#: mediagoblin/plugins/oauth/templates/oauth/client/register.html:29 +#: mediagoblin/templates/mediagoblin/submit/collection.html:30 +#: mediagoblin/templates/mediagoblin/submit/start.html:34 +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:68 +msgid "Add" +msgstr "增加" + +#: mediagoblin/processing/__init__.py:193 +msgid "Invalid file given for media type." +msgstr "提供文件的媒体类型错误。" + +#: mediagoblin/submit/forms.py:26 +msgid "File" +msgstr "文件" + +#: mediagoblin/submit/views.py:49 +msgid "You must provide a file." +msgstr "您必须提供一个文件" + +#: mediagoblin/submit/views.py:93 +msgid "Woohoo! Submitted!" +msgstr "啊哈!已提交!" + +#: mediagoblin/submit/views.py:144 +#, python-format +msgid "Collection \"%s\" added!" +msgstr "合集“%s”已新增!" + +#: mediagoblin/templates/mediagoblin/base.html:67 +msgid "Verify your email!" +msgstr "确认您的电子邮件!" + +#: mediagoblin/templates/mediagoblin/base.html:68 +msgid "log out" +msgstr "登出" + +#: mediagoblin/templates/mediagoblin/base.html:73 +#: mediagoblin/templates/mediagoblin/auth/login.html:28 +#: mediagoblin/templates/mediagoblin/auth/login.html:36 +#: mediagoblin/templates/mediagoblin/auth/login.html:54 +msgid "Log in" +msgstr "登录" + +#: mediagoblin/templates/mediagoblin/base.html:82 +#, python-format +msgid "%(user_name)s's account" +msgstr "%(user_name)s 的账户" + +#: mediagoblin/templates/mediagoblin/base.html:89 +msgid "Change account settings" +msgstr "更改账户设置" + +#: mediagoblin/templates/mediagoblin/base.html:93 +#: mediagoblin/templates/mediagoblin/base.html:108 +#: mediagoblin/templates/mediagoblin/admin/panel.html:21 +#: mediagoblin/templates/mediagoblin/admin/panel.html:26 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:21 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:26 +msgid "Media processing panel" +msgstr "媒体处理面板" + +#: mediagoblin/templates/mediagoblin/base.html:96 +msgid "Log out" +msgstr "登出" + +#: mediagoblin/templates/mediagoblin/base.html:99 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:156 +msgid "Add media" +msgstr "新增媒体" + +#: mediagoblin/templates/mediagoblin/base.html:102 +#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:41 +msgid "Create new collection" +msgstr "新增合集" + +#: mediagoblin/templates/mediagoblin/error.html:24 +msgid "Image of goblin stressing out" +msgstr "满脸问号的哥布林" + +#: mediagoblin/templates/mediagoblin/root.html:32 +msgid "Most recent media" +msgstr "最新的媒体" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:29 +msgid "" +"Here you can track the state of media being processed on this instance." +msgstr "此处您可以追踪本站点处理媒体的状态。" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:32 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:32 +msgid "Media in-processing" +msgstr "媒体处理中" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:58 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:56 +msgid "No media in-processing" +msgstr "没有正在处理中的媒体" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:61 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:59 +msgid "These uploads failed to process:" +msgstr "无法处理这些上传内容:" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:90 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:86 +msgid "No failed entries!" +msgstr "没有失败的纪录!" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:92 +msgid "Last 10 successful uploads" +msgstr "最近 10 次成功上传的纪录" + +#: mediagoblin/templates/mediagoblin/admin/panel.html:112 +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:107 +msgid "No processed entries, yet!" +msgstr "现在还没有处理的纪录!" + +#: mediagoblin/templates/mediagoblin/auth/change_fp.html:28 +#: mediagoblin/templates/mediagoblin/auth/change_fp.html:36 +msgid "Set your new password" +msgstr "设置您的新密码" + +#: mediagoblin/templates/mediagoblin/auth/change_fp.html:39 +msgid "Set password" +msgstr "设置新密码" + +#: mediagoblin/templates/mediagoblin/auth/forgot_password.html:23 +#: mediagoblin/templates/mediagoblin/auth/forgot_password.html:31 +msgid "Recover password" +msgstr "找回密码" + +#: mediagoblin/templates/mediagoblin/auth/forgot_password.html:34 +msgid "Send instructions" +msgstr "发送找回密码说明" + +#: mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt:19 +#, python-format +msgid "" +"Hi %(username)s,\n" +"\n" +"to change your GNU MediaGoblin password, open the following URL in \n" +"your web browser:\n" +"\n" +"%(verification_url)s\n" +"\n" +"If you think this is an error, just ignore this email and continue being\n" +"a happy goblin!" +msgstr "%(username)s 您好:\n\n要修改 GNU MediaGoblin 的密码,请在您的浏览器中打开下面的网址:\n\n%(verification_url)s\n\n如果您认为这个是个误会,请忽略此封信件,继续当个快乐的哥布林!" + +#: mediagoblin/templates/mediagoblin/auth/login.html:39 +msgid "Logging in failed!" +msgstr "登录失败!" + +#: mediagoblin/templates/mediagoblin/auth/login.html:44 +msgid "Don't have an account yet?" +msgstr "还没有账户吗?" + +#: mediagoblin/templates/mediagoblin/auth/login.html:45 +msgid "Create one here!" +msgstr "在这里建立一个吧!" + +#: mediagoblin/templates/mediagoblin/auth/login.html:51 +msgid "Forgot your password?" +msgstr "忘了密码吗?" + +#: mediagoblin/templates/mediagoblin/auth/register.html:28 +#: mediagoblin/templates/mediagoblin/auth/register.html:36 +msgid "Create an account!" +msgstr "建立一个账户!" + +#: mediagoblin/templates/mediagoblin/auth/register.html:40 +msgid "Create" +msgstr "建立" + +#: mediagoblin/templates/mediagoblin/auth/verification_email.txt:19 +#, python-format +msgid "" +"Hi %(username)s,\n" +"\n" +"to activate your GNU MediaGoblin account, open the following URL in\n" +"your web browser:\n" +"\n" +"%(verification_url)s" +msgstr "%(username)s 您好:\n\n要启动 GNU MediaGoblin 账户,请在您的浏览器中打开下面的网址:\n\n%(verification_url)s" + +#: mediagoblin/templates/mediagoblin/bits/base_footer.html:21 +#, python-format +msgid "" +"Powered by MediaGoblin, a GNU project." +msgstr "Powered by MediaGoblin,一个 GNU 项目。" + +#: mediagoblin/templates/mediagoblin/bits/base_footer.html:24 +#, python-format +msgid "" +"Released under the AGPL. Source code available." +msgstr "以 AGPL 授权发布。备有源代码。" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:20 +msgid "Explore" +msgstr "探索" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:22 +msgid "Hi there, welcome to this MediaGoblin site!" +msgstr "嘿!欢迎来到 MediaGoblin 站! " + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:24 +msgid "" +"This site is running MediaGoblin, an " +"extraordinarily great piece of media hosting software." +msgstr "本站使用 MediaGoblin——与众不同的媒体分享网站。" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:25 +msgid "" +"To add your own media, place comments, and more, you can log in with your " +"MediaGoblin account." +msgstr "您可以登录您的 MediaGoblin 账户以上传媒体、张贴评论等等。" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:27 +msgid "Don't have one yet? It's easy!" +msgstr "没有账户吗?开账户很简单!" + +#: mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html:28 +#, python-format +msgid "" +"Create an account at this site\n" +" or\n" +" Set up MediaGoblin on your own server" +msgstr "在本站创建帐户\n 或者\n 在您自己的服务器上搭建 MediaGoblin" + +#: mediagoblin/templates/mediagoblin/bits/logo.html:23 +#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 +msgid "MediaGoblin logo" +msgstr "MediaGoblin 标志" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:23 +#: mediagoblin/templates/mediagoblin/edit/attachments.html:35 +#, python-format +msgid "Editing attachments for %(media_title)s" +msgstr "编辑 %(media_title)s 的附件" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:44 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:182 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:198 +msgid "Attachments" +msgstr "附件" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:57 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:204 +msgid "Add attachment" +msgstr "新增附件" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:61 +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:42 +#: mediagoblin/templates/mediagoblin/edit/edit.html:41 +#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:32 +#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:46 +#: mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html:52 +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:67 +#: mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html:48 +msgid "Cancel" +msgstr "取消" + +#: mediagoblin/templates/mediagoblin/edit/attachments.html:63 +#: mediagoblin/templates/mediagoblin/edit/edit.html:42 +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:55 +#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:33 +#: mediagoblin/templates/mediagoblin/edit/edit_profile.html:40 +msgid "Save changes" +msgstr "保存更改" + +#: mediagoblin/templates/mediagoblin/edit/change_pass.html:28 +#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 +#, python-format +msgid "Changing %(username)s's password" +msgstr "修改 %(username)s 的密码" + +#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 +msgid "Save" +msgstr "保存" + +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 +#, python-format +msgid "Really delete user '%(user_name)s' and all related media/comments?" +msgstr "真的要删除用户 %(user_name)s 及所有相关媒体和评论吗?" + +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:35 +msgid "Yes, really delete my account" +msgstr "是的,真的删除我的账户" + +#: mediagoblin/templates/mediagoblin/edit/delete_account.html:44 +#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:48 +#: mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html:49 +msgid "Delete permanently" +msgstr "永久删除" + +#: mediagoblin/templates/mediagoblin/edit/edit.html:23 +#: mediagoblin/templates/mediagoblin/edit/edit.html:35 +#, python-format +msgid "Editing %(media_title)s" +msgstr "编辑 %(media_title)s" + +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:28 +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:40 +#, python-format +msgid "Changing %(username)s's account settings" +msgstr "正在改变 %(username)s 的账户设置" + +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 +msgid "Change your password." +msgstr "修改您的密码。" + +#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 +msgid "Delete my account" +msgstr "删除我的帐户" + +#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:29 +#, python-format +msgid "Editing %(collection_title)s" +msgstr "编辑 %(collection_title)s" + +#: mediagoblin/templates/mediagoblin/edit/edit_profile.html:23 +#: mediagoblin/templates/mediagoblin/edit/edit_profile.html:34 +#, python-format +msgid "Editing %(username)s's profile" +msgstr "编辑 %(username)s 的个人资料" + +#: mediagoblin/templates/mediagoblin/listings/collection.html:30 +#: mediagoblin/templates/mediagoblin/listings/collection.html:35 +#: mediagoblin/templates/mediagoblin/listings/tag.html:30 +#: mediagoblin/templates/mediagoblin/listings/tag.html:35 +#, python-format +msgid "Media tagged with: %(tag_name)s" +msgstr "此媒体被标记为:%(tag_name)s" + +#: mediagoblin/templates/mediagoblin/media_displays/ascii.html:34 +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:56 +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:65 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:136 +#: mediagoblin/templates/mediagoblin/media_displays/video.html:55 +msgid "Download" +msgstr "下载" + +#: mediagoblin/templates/mediagoblin/media_displays/ascii.html:38 +msgid "Original" +msgstr "源文件" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:44 +msgid "" +"Sorry, this audio will not work because \n" +"\tyour web browser does not support HTML5 \n" +"\taudio." +msgstr "抱歉,此声音无法播放,因为您的浏览器不支持 HTML5 音频。" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:47 +msgid "" +"You can get a modern web browser that \n" +"\tcan play the audio at \n" +"\t http://getfirefox.com!" +msgstr "您可以在 http://getfirefox.com 取得可以播放此声音的浏览器!" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:60 +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:71 +#: mediagoblin/templates/mediagoblin/media_displays/video.html:61 +msgid "Original file" +msgstr "源文件" + +#: mediagoblin/templates/mediagoblin/media_displays/audio.html:63 +msgid "WebM file (Vorbis codec)" +msgstr "WebM 文件(Vorbis 编码)" + +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:59 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:87 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:93 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:99 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:105 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:59 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:65 +#, python-format +msgid "Image for %(media_title)s" +msgstr "%(media_title)s 的照片" + +#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 +msgid "PDF file" +msgstr "PDF 文件" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 +msgid "Toggle Rotate" +msgstr "切换旋转" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:113 +msgid "Perspective" +msgstr "透视" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:116 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:117 +msgid "Front" +msgstr "正面" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:120 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:121 +msgid "Top" +msgstr "顶面" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:124 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:125 +msgid "Side" +msgstr "侧面" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:130 +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:131 +msgid "WebGL" +msgstr "WebGL" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:138 +msgid "Download model" +msgstr "下载模型" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:146 +msgid "File Format" +msgstr "文件格式" + +#: mediagoblin/templates/mediagoblin/media_displays/stl.html:148 +msgid "Object Height" +msgstr "对象高度" + +#: mediagoblin/templates/mediagoblin/media_displays/video.html:44 +msgid "" +"Sorry, this video will not work because\n" +" your web browser does not support HTML5 \n" +" video." +msgstr "抱歉,此视频无法播放,因为您的浏览器不支持 HTML5 视频。" + +#: mediagoblin/templates/mediagoblin/media_displays/video.html:47 +msgid "" +"You can get a modern web browser that \n" +" can play this video at \n" +" http://getfirefox.com!" +msgstr "您可以在 http://getfirefox.com 取得可以播放此视频的浏览器!" + +#: mediagoblin/templates/mediagoblin/media_displays/video.html:69 +msgid "WebM file (640p; VP8/Vorbis)" +msgstr "WebM 文件(640p;VP8/Vorbis)" + +#: mediagoblin/templates/mediagoblin/submit/collection.html:26 +msgid "Add a collection" +msgstr "新增合集" + +#: mediagoblin/templates/mediagoblin/submit/start.html:23 +#: mediagoblin/templates/mediagoblin/submit/start.html:30 +msgid "Add your media" +msgstr "加入您的媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:30 +#, python-format +msgid "%(collection_title)s (%(username)s's collection)" +msgstr "%(collection_title)s (%(username)s 的合集)" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:39 +#, python-format +msgid "%(collection_title)s by %(username)s" +msgstr "%(collection_title)s by %(username)s" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:52 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:79 +msgid "Edit" +msgstr "编辑" + +#: mediagoblin/templates/mediagoblin/user_pages/collection.html:56 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:83 +msgid "Delete" +msgstr "删除" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:30 +#: mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html:30 +#, python-format +msgid "Really delete %(title)s?" +msgstr "真的要删除 %(title)s 吗?" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html:31 +#, python-format +msgid "Really remove %(media_title)s from %(collection_title)s?" +msgstr "确定要从 %(collection_title)s 移除 %(media_title)s 吗?" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html:54 +msgid "Remove" +msgstr "移除" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:21 +#, python-format +msgid "%(username)s's collections" +msgstr "%(username)s 的合集" + +#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:28 +#, python-format +msgid "%(username)s's collections" +msgstr "%(username)s 的合集" + +#: mediagoblin/templates/mediagoblin/user_pages/comment_email.txt:19 +#, python-format +msgid "" +"Hi %(username)s,\n" +"%(comment_author)s commented on your post (%(comment_url)s) at %(instance_name)s\n" +msgstr "%(username)s 您好:\n%(comment_author)s 在 %(instance_name)s 对您的内容 (%(comment_url)s) 张贴评论\n" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:30 +#, python-format +msgid "%(username)s's media" +msgstr "%(username)s的媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:38 +#, python-format +msgid "" +"%(username)s's media with tag %(tag)s" +msgstr "%(username)s 的有 %(tag)s 标签的媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48 +#, python-format +msgid "%(username)s's media" +msgstr "%(username)s 的媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:38 +#, python-format +msgid "❖ Browsing media by %(username)s" +msgstr "❖ 浏览 %(username)s 的媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:95 +msgid "Add a comment" +msgstr "新增评论" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:104 +msgid "Add this comment" +msgstr "增加评论" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:132 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:152 +#: mediagoblin/templates/mediagoblin/user_pages/media.html:164 +#, python-format +msgid "%(formatted_time)s ago" +msgstr "%(formatted_time)s前" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:150 +msgid "Added" +msgstr "已增加" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:161 +msgid "Created" +msgstr "已创建" + +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 +#, python-format +msgid "Add “%(media_title)s” to a collection" +msgstr "把“%(media_title)s”加入合集" + +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:54 +msgid "+" +msgstr "+" + +#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:58 +msgid "Add a new collection" +msgstr "新增新的合集" + +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:29 +msgid "" +"You can track the state of media being processed for your gallery here." +msgstr "您可以在这里追踪您的艺廊中媒体处理的状态。" + +#: mediagoblin/templates/mediagoblin/user_pages/processing_panel.html:89 +msgid "Your last 10 successful uploads" +msgstr "您的最近 10 次成功上传的纪录" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:31 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:89 +#, python-format +msgid "%(username)s's profile" +msgstr "%(username)s 的个人资料" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:43 +msgid "Sorry, no such user found." +msgstr "抱歉,找不到该用户。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:50 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:70 +msgid "Email verification needed" +msgstr "需要认证电子邮件地址" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:53 +msgid "Almost done! Your account still needs to be activated." +msgstr "快完成了!但您需要激活您的账户。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:58 +msgid "" +"An email should arrive in a few moments with instructions on how to do so." +msgstr "账户激活说明将在稍后送达。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:62 +msgid "In case it doesn't:" +msgstr "如果仍然无法认证,您可以:" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:65 +msgid "Resend verification email" +msgstr "重发认证邮件" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:73 +msgid "" +"Someone has registered an account with this username, but it still has to be" +" activated." +msgstr "有人用注册了该账户,但是该账户需要被启用。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:79 +#, python-format +msgid "" +"If you are that person but you've lost your verification email, you can log in and resend it." +msgstr "如果您就是本人但是未收到认证信,您可以登录然后重发一次。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:96 +msgid "Here's a spot to tell others about yourself." +msgstr "这个地方能让您向他人介绍自己。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:100 +#: mediagoblin/templates/mediagoblin/user_pages/user.html:117 +msgid "Edit profile" +msgstr "编辑个人资料" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:105 +msgid "This user hasn't filled in their profile (yet)." +msgstr "这个用户(还)没有填写个人资料。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:124 +msgid "Browse collections" +msgstr "浏览合集" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:137 +#, python-format +msgid "View all of %(username)s's media" +msgstr "查看 %(username)s 的全部媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:150 +msgid "" +"This is where your media will appear, but you don't seem to have added " +"anything yet." +msgstr "此处是您的媒体会出现的地方,但是似乎还没有加入任何东西。" + +#: mediagoblin/templates/mediagoblin/user_pages/user.html:162 +#: mediagoblin/templates/mediagoblin/utils/collection_gallery.html:84 +#: mediagoblin/templates/mediagoblin/utils/object_gallery.html:70 +msgid "There doesn't seem to be any media here yet..." +msgstr "那里好像还没有任何的媒体……" + +#: mediagoblin/templates/mediagoblin/utils/collection_gallery.html:49 +msgid "(remove)" +msgstr "(移除)" + +#: mediagoblin/templates/mediagoblin/utils/collections.html:21 +msgid "Collected in" +msgstr "合集于" + +#: mediagoblin/templates/mediagoblin/utils/collections.html:40 +msgid "Add to a collection" +msgstr "添加到合集" + +#: mediagoblin/templates/mediagoblin/utils/feed_link.html:21 +#: mediagoblin/themes/airy/templates/mediagoblin/utils/feed_link.html:21 +msgid "feed icon" +msgstr "feed 图标" + +#: mediagoblin/templates/mediagoblin/utils/feed_link.html:23 +#: mediagoblin/themes/airy/templates/mediagoblin/utils/feed_link.html:23 +msgid "Atom feed" +msgstr "Atom feed" + +#: mediagoblin/templates/mediagoblin/utils/license.html:25 +msgid "All rights reserved" +msgstr "版权所有" + +#: mediagoblin/templates/mediagoblin/utils/pagination.html:39 +msgid "← Newer" +msgstr "← 更新的" + +#: mediagoblin/templates/mediagoblin/utils/pagination.html:45 +msgid "Older →" +msgstr "更旧的 →" + +#: mediagoblin/templates/mediagoblin/utils/pagination.html:48 +msgid "Go to page:" +msgstr "跳到页数:" + +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:28 +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:33 +msgid "newer" +msgstr "更新的" + +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:39 +#: mediagoblin/templates/mediagoblin/utils/prev_next.html:44 +msgid "older" +msgstr "更旧的" + +#: mediagoblin/templates/mediagoblin/utils/tags.html:20 +msgid "Tagged with" +msgstr "标签" + +#: mediagoblin/tools/exif.py:83 +msgid "Could not read the image file." +msgstr "无法读取图片文件。" + +#: mediagoblin/tools/response.py:35 +msgid "Oops!" +msgstr "糟糕!" + +#: mediagoblin/tools/response.py:36 +msgid "An error occured" +msgstr "发生错误" + +#: mediagoblin/tools/response.py:51 +msgid "Operation not allowed" +msgstr "操作不允许" + +#: mediagoblin/tools/response.py:52 +msgid "" +"Sorry Dave, I can't let you do that!

You have tried to perform a " +"function that you are not allowed to. Have you been trying to delete all " +"user accounts again?" +msgstr "对不起老兄,我不能让你这样做!

您正在试着操作不允许您使用的功能。您难道想打算删除所有用户账户吗?" + +#: mediagoblin/tools/response.py:60 +msgid "" +"There doesn't seem to be a page at this address. Sorry!

If you're sure" +" the address is correct, maybe the page you're looking for has been moved or" +" deleted." +msgstr "不好意思,看起来这个网址上没有网页。

如果您确定这个网址是正确的,您在寻找的页面可能已经移动或是被删除了。" + +#: mediagoblin/tools/timesince.py:62 +msgid "year" +msgstr "年" + +#: mediagoblin/tools/timesince.py:63 +msgid "month" +msgstr "月" + +#: mediagoblin/tools/timesince.py:64 +msgid "week" +msgstr "周" + +#: mediagoblin/tools/timesince.py:65 +msgid "day" +msgstr "日" + +#: mediagoblin/tools/timesince.py:66 +msgid "hour" +msgstr "小时" + +#: mediagoblin/tools/timesince.py:67 +msgid "minute" +msgstr "分钟" + +#: mediagoblin/user_pages/forms.py:23 +msgid "Comment" +msgstr "评论" + +#: mediagoblin/user_pages/forms.py:25 +msgid "" +"You can use Markdown for" +" formatting." +msgstr "您可以用 Markdown 来排版。" + +#: mediagoblin/user_pages/forms.py:31 +msgid "I am sure I want to delete this" +msgstr "我确定我要删除这个媒体" + +#: mediagoblin/user_pages/forms.py:35 +msgid "I am sure I want to remove this item from the collection" +msgstr "我确定我要从合集中移除此项目" + +#: mediagoblin/user_pages/forms.py:39 +msgid "Collection" +msgstr "合集" + +#: mediagoblin/user_pages/forms.py:40 +msgid "-- Select --" +msgstr "— 请选择 —" + +#: mediagoblin/user_pages/forms.py:42 +msgid "Include a note" +msgstr "加注" + +#: mediagoblin/user_pages/lib.py:58 +msgid "commented on your post" +msgstr "在您的内容张贴评论" + +#: mediagoblin/user_pages/views.py:169 +msgid "Sorry, comments are disabled." +msgstr "抱歉,不开放评论。" + +#: mediagoblin/user_pages/views.py:174 +msgid "Oops, your comment was empty." +msgstr "啊,您的评论是空的。" + +#: mediagoblin/user_pages/views.py:180 +msgid "Your comment has been posted!" +msgstr "您的评论已经张贴完成!" + +#: mediagoblin/user_pages/views.py:205 +msgid "Please check your entries and try again." +msgstr "请检查项目并重试。" + +#: mediagoblin/user_pages/views.py:245 +msgid "You have to select or add a collection" +msgstr "您需要选择或是新增一个合集" + +#: mediagoblin/user_pages/views.py:256 +#, python-format +msgid "\"%s\" already in collection \"%s\"" +msgstr "“%s”已经在“%s”合集" + +#: mediagoblin/user_pages/views.py:262 +#, python-format +msgid "\"%s\" added to collection \"%s\"" +msgstr "“%s”加入“%s”合集" + +#: mediagoblin/user_pages/views.py:282 +msgid "You deleted the media." +msgstr "您已经删除此媒体。" + +#: mediagoblin/user_pages/views.py:289 +msgid "The media was not deleted because you didn't check that you were sure." +msgstr "由于您没有勾选确认,该媒体没有被移除。" + +#: mediagoblin/user_pages/views.py:296 +msgid "You are about to delete another user's media. Proceed with caution." +msgstr "您正在删除别人的媒体,请小心操作。" + +#: mediagoblin/user_pages/views.py:370 +msgid "You deleted the item from the collection." +msgstr "您已经从该合集中删除该项目。" + +#: mediagoblin/user_pages/views.py:374 +msgid "The item was not removed because you didn't check that you were sure." +msgstr "由于您没有勾选确认,该项目没有被移除。" + +#: mediagoblin/user_pages/views.py:382 +msgid "" +"You are about to delete an item from another user's collection. Proceed with" +" caution." +msgstr "您正在从别人的合集中删除项目,请小心操作。" + +#: mediagoblin/user_pages/views.py:415 +#, python-format +msgid "You deleted the collection \"%s\"" +msgstr "您已经删除“%s”合集。" + +#: mediagoblin/user_pages/views.py:422 +msgid "" +"The collection was not deleted because you didn't check that you were sure." +msgstr "由于您没有勾选确认,该合集没有被移除。" + +#: mediagoblin/user_pages/views.py:430 +msgid "" +"You are about to delete another user's collection. Proceed with caution." +msgstr "您正在删除别人的合集,请小心操作。" diff --git a/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.mo index 3fd4911a..4b7a2398 100644 Binary files a/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.mo and b/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.mo differ diff --git a/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.po index e2a8e680..05ecd4b5 100644 --- a/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.po @@ -5,14 +5,16 @@ # Translators: # , 2011 # Harry Chen , 2011-2012 +# medicalwei , 2013 # medicalwei , 2012 +# m13253 , 2013 msgid "" msgstr "" "Project-Id-Version: GNU MediaGoblin\n" "Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n" "POT-Creation-Date: 2013-05-27 13:54-0500\n" -"PO-Revision-Date: 2013-05-27 18:54+0000\n" -"Last-Translator: cwebber \n" +"PO-Revision-Date: 2013-06-16 01:40+0000\n" +"Last-Translator: m13253 \n" "Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/mediagoblin/language/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -36,7 +38,7 @@ msgstr "Email 位址" #: mediagoblin/auth/forms.py:41 msgid "Username or Email" -msgstr "" +msgstr "使用者名稱或 email" #: mediagoblin/auth/forms.py:52 msgid "Username or email" @@ -44,15 +46,15 @@ msgstr "使用者名稱或 email" #: mediagoblin/auth/tools.py:31 msgid "Invalid User name or email address." -msgstr "" +msgstr "無效的使用者名稱或 email 位置。" #: mediagoblin/auth/tools.py:32 msgid "This field does not take email addresses." -msgstr "" +msgstr "本欄位不接受 email 位置。" #: mediagoblin/auth/tools.py:33 msgid "This field requires an email address." -msgstr "" +msgstr "本欄位需要 email 位置。" #: mediagoblin/auth/views.py:54 msgid "Sorry, registration is disabled on this instance." @@ -92,11 +94,11 @@ msgstr "重送認證信。" msgid "" "If that email address (case sensitive!) is registered an email has been sent" " with instructions on how to change your password." -msgstr "" +msgstr "如果那 email 位置 (請注意大小寫) 已經註冊,寫有修改密碼步驟的 email 已經送出。" #: mediagoblin/auth/views.py:269 msgid "Couldn't find someone with that username." -msgstr "" +msgstr "找不到相關的使用者名稱。" #: mediagoblin/auth/views.py:272 msgid "" @@ -173,15 +175,15 @@ msgstr "本網址出錯了" #: mediagoblin/edit/forms.py:63 msgid "License preference" -msgstr "" +msgstr "授權偏好" #: mediagoblin/edit/forms.py:69 msgid "This will be your default license on upload forms." -msgstr "" +msgstr "在上傳頁面,這將會是您預設的授權模式。" #: mediagoblin/edit/forms.py:71 msgid "Email me when others comment on my media" -msgstr "當有人對我的媒體評論時寄信給我" +msgstr "當有人對我的媒體留言時寄信給我" #: mediagoblin/edit/forms.py:83 msgid "The title can't be empty" @@ -225,7 +227,7 @@ msgstr "您加上了附件「%s」!" #: mediagoblin/edit/views.py:182 msgid "You can only edit your own profile." -msgstr "" +msgstr "您只能修改您自己的個人檔案。" #: mediagoblin/edit/views.py:188 msgid "You are editing a user's profile. Proceed with caution." @@ -241,7 +243,7 @@ msgstr "帳號設定已儲存" #: mediagoblin/edit/views.py:274 msgid "You need to confirm the deletion of your account." -msgstr "" +msgstr "您必須要確認是否刪除您的帳號。" #: mediagoblin/edit/views.py:310 mediagoblin/submit/views.py:138 #: mediagoblin/user_pages/views.py:222 @@ -263,7 +265,7 @@ msgstr "密碼錯誤" #: mediagoblin/edit/views.py:363 msgid "Your password was changed successfully" -msgstr "" +msgstr "您的密碼已經成功修改" #: mediagoblin/gmg_commands/assetlink.py:60 msgid "Cannot link theme... no theme set\n" @@ -280,24 +282,24 @@ msgstr "但是舊的目錄連結已經找到並移除。\n" #: mediagoblin/gmg_commands/assetlink.py:112 #, python-format msgid "Could not link \"%s\": %s exists and is not a symlink\n" -msgstr "" +msgstr "無法連結「%s」:%s 存在,且不是符號連結\n" #: mediagoblin/gmg_commands/assetlink.py:119 #, python-format msgid "Skipping \"%s\"; already set up.\n" -msgstr "" +msgstr "跳過「%s」,已經建置完成。\n" #: mediagoblin/gmg_commands/assetlink.py:124 #, python-format msgid "Old link found for \"%s\"; removing.\n" -msgstr "" +msgstr "找到「%s」舊的連結,刪除中。\n" #: mediagoblin/meddleware/csrf.py:134 msgid "" "CSRF cookie not present. This is most likely the result of a cookie blocker " "or somesuch.
Make sure to permit the settings of cookies for this " "domain." -msgstr "" +msgstr "跨網站存取 (CSRF) 的 cookie 不存在,有可能是 cookie 阻擋程式之類的程式導致的。
請允許此網域的 cookie 設定。" #: mediagoblin/media_types/__init__.py:111 #: mediagoblin/media_types/__init__.py:155 @@ -306,7 +308,7 @@ msgstr "抱歉,我不支援這樣的檔案格式 :(" #: mediagoblin/media_types/pdf/processing.py:136 msgid "unoconv failing to run, check log file" -msgstr "" +msgstr "unoconv 無法執行,請檢查紀錄檔" #: mediagoblin/media_types/video/processing.py:37 msgid "Video transcoding failed" @@ -335,7 +337,7 @@ msgstr "名稱" #: mediagoblin/plugins/oauth/forms.py:35 msgid "The name of the OAuth client" -msgstr "OAuth client 的名稱" +msgstr "OAuth 用戶程式的名稱" #: mediagoblin/plugins/oauth/forms.py:36 msgid "Description" @@ -359,7 +361,7 @@ msgid "" " Public - The client can't make confidential\n" " requests to the GNU MediaGoblin instance (e.g. client-side\n" " JavaScript client)." -msgstr "秘密 — OAuth client 可以對 GNU MediaGoblin 站台發送不被使用者代理攔截的請求 (例如伺服端的 client)。\n公開 — OAuth client 無法對 GNU MediaGoblin 站台發送秘密的請求 (例如客戶端的 JavaScript client)。" +msgstr "秘密 — OAuth 用戶程式可以對 GNU MediaGoblin 站台發送不被使用者代理攔截的請求 (例如伺服端的用戶程式)。\n公開 — OAuth 用戶程式無法對 GNU MediaGoblin 站台發送秘密的請求 (例如客戶端的 JavaScript 用戶程式)。" #: mediagoblin/plugins/oauth/forms.py:52 msgid "Redirect URI" @@ -369,23 +371,23 @@ msgstr "重定向 URI" msgid "" "The redirect URI for the applications, this field\n" " is required for public clients." -msgstr "此應用程式的重定向 URI,本欄位在公開類型的 OAuth client 為必填。" +msgstr "此應用程式的重定向 URI,本欄位在公開類型的 OAuth 用戶程式為必填。" #: mediagoblin/plugins/oauth/forms.py:66 msgid "This field is required for public clients" -msgstr "本欄位在公開類型的 OAuth client 為必填" +msgstr "本欄位在公開類型的用戶程式為必填" #: mediagoblin/plugins/oauth/views.py:56 msgid "The client {0} has been registered!" -msgstr "OAuth client {0} 註冊完成!" +msgstr "OAuth 用戶程式 {0} 註冊完成!" #: mediagoblin/plugins/oauth/templates/oauth/client/connections.html:22 msgid "OAuth client connections" -msgstr "" +msgstr "OAuth 用戶程式連線" #: mediagoblin/plugins/oauth/templates/oauth/client/list.html:22 msgid "Your OAuth clients" -msgstr "" +msgstr "您的 OAuth 用戶程式" #: mediagoblin/plugins/oauth/templates/oauth/client/register.html:29 #: mediagoblin/templates/mediagoblin/submit/collection.html:30 @@ -450,7 +452,7 @@ msgstr "媒體處理面板" #: mediagoblin/templates/mediagoblin/base.html:96 msgid "Log out" -msgstr "" +msgstr "登出" #: mediagoblin/templates/mediagoblin/base.html:99 #: mediagoblin/templates/mediagoblin/user_pages/user.html:156 @@ -577,7 +579,7 @@ msgstr "%(username)s 您好:\n\n要啟動 GNU MediaGoblin 帳號,請在您 msgid "" "Powered by MediaGoblin, a GNU project." -msgstr "" +msgstr "本站使用 MediaGoblin,這是一個 GNU 專案。" #: mediagoblin/templates/mediagoblin/bits/base_footer.html:24 #, python-format @@ -617,7 +619,7 @@ msgid "" "Create an account at this site\n" " or\n" " Set up MediaGoblin on your own server" -msgstr "" +msgstr "在本站建立您的帳號\n 或是\n 在您自己的伺服器上安裝 MediaGoblin" #: mediagoblin/templates/mediagoblin/bits/logo.html:23 #: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23 @@ -664,20 +666,20 @@ msgstr "儲存變更" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:38 #, python-format msgid "Changing %(username)s's password" -msgstr "" +msgstr "更改 %(username)s 的密碼" #: mediagoblin/templates/mediagoblin/edit/change_pass.html:45 msgid "Save" -msgstr "" +msgstr "儲存" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:28 #, python-format msgid "Really delete user '%(user_name)s' and all related media/comments?" -msgstr "" +msgstr "真的要刪除使用者「%(user_name)s」以及相關的媒體與留言?" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:35 msgid "Yes, really delete my account" -msgstr "" +msgstr "是的,我真的要把我的帳號刪除" #: mediagoblin/templates/mediagoblin/edit/delete_account.html:44 #: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:48 @@ -699,11 +701,11 @@ msgstr "正在改變 %(username)s 的帳號設定" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:46 msgid "Change your password." -msgstr "" +msgstr "更改您的密碼。" #: mediagoblin/templates/mediagoblin/edit/edit_account.html:62 msgid "Delete my account" -msgstr "" +msgstr "刪除我的帳號" #: mediagoblin/templates/mediagoblin/edit/edit_collection.html:29 #, python-format @@ -722,7 +724,7 @@ msgstr "編輯 %(username)s 的個人檔案" #: mediagoblin/templates/mediagoblin/listings/tag.html:35 #, python-format msgid "Media tagged with: %(tag_name)s" -msgstr "此媒體被 tag 成:%(tag_name)s" +msgstr "這個媒體具有以下標籤:%(tag_name)s" #: mediagoblin/templates/mediagoblin/media_displays/ascii.html:34 #: mediagoblin/templates/mediagoblin/media_displays/audio.html:56 @@ -773,7 +775,7 @@ msgstr " %(media_title)s 的照片" #: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79 msgid "PDF file" -msgstr "" +msgstr "PDF 檔" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:112 msgid "Toggle Rotate" @@ -781,7 +783,7 @@ msgstr "切換旋轉" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:113 msgid "Perspective" -msgstr "視角" +msgstr "透視" #: mediagoblin/templates/mediagoblin/media_displays/stl.html:116 #: mediagoblin/templates/mediagoblin/media_displays/stl.html:117 @@ -820,14 +822,14 @@ msgid "" "Sorry, this video will not work because\n" " your web browser does not support HTML5 \n" " video." -msgstr "" +msgstr "抱歉,由於您的瀏覽器不支援 HTML5 影片,本影片無法播放" #: mediagoblin/templates/mediagoblin/media_displays/video.html:47 msgid "" "You can get a modern web browser that \n" " can play this video at \n" " http://getfirefox.com!" -msgstr "" +msgstr "您可以在 http://getfirefox.com 取得可以播放此影片的先進瀏覽器。" #: mediagoblin/templates/mediagoblin/media_displays/video.html:69 msgid "WebM file (640p; VP8/Vorbis)" @@ -880,19 +882,19 @@ msgstr "移除" #: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:21 #, python-format msgid "%(username)s's collections" -msgstr "" +msgstr "%(username)s 的蒐藏" #: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:28 #, python-format msgid "%(username)s's collections" -msgstr "" +msgstr "%(username)s 的蒐藏" #: mediagoblin/templates/mediagoblin/user_pages/comment_email.txt:19 #, python-format msgid "" "Hi %(username)s,\n" "%(comment_author)s commented on your post (%(comment_url)s) at %(instance_name)s\n" -msgstr "%(username)s 您好:\n%(comment_author)s 在 %(instance_name)s 對您的內容 (%(comment_url)s) 張貼評論\n" +msgstr "%(username)s 您好:\n%(comment_author)s 在 %(instance_name)s 對您的內容 (%(comment_url)s) 張貼留言\n" #: mediagoblin/templates/mediagoblin/user_pages/gallery.html:30 #, python-format @@ -904,7 +906,7 @@ msgstr "%(username)s的媒體" msgid "" "%(username)s's media with tag %(tag)s" -msgstr "" +msgstr "標籤為 %(tag)s%(username)s 的媒體" #: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48 #, python-format @@ -918,32 +920,32 @@ msgstr "❖ 瀏覽 %(username)s 的媒體" #: mediagoblin/templates/mediagoblin/user_pages/media.html:95 msgid "Add a comment" -msgstr "新增評論" +msgstr "新增留言" #: mediagoblin/templates/mediagoblin/user_pages/media.html:104 msgid "Add this comment" -msgstr "增加評論" +msgstr "增加留言" #: mediagoblin/templates/mediagoblin/user_pages/media.html:132 #: mediagoblin/templates/mediagoblin/user_pages/media.html:152 #: mediagoblin/templates/mediagoblin/user_pages/media.html:164 #, python-format msgid "%(formatted_time)s ago" -msgstr "" +msgstr "%(formatted_time)s 前" #: mediagoblin/templates/mediagoblin/user_pages/media.html:150 msgid "Added" -msgstr "" +msgstr "新增於" #: mediagoblin/templates/mediagoblin/user_pages/media.html:161 msgid "Created" -msgstr "" +msgstr "建立於" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28 #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40 #, python-format msgid "Add “%(media_title)s” to a collection" -msgstr "" +msgstr "加入 “%(media_title)s” 至蒐藏" #: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:54 msgid "+" @@ -1022,7 +1024,7 @@ msgstr "這個使用者(還)沒有填寫個人檔案。" #: mediagoblin/templates/mediagoblin/user_pages/user.html:124 msgid "Browse collections" -msgstr "" +msgstr "瀏覽蒐藏" #: mediagoblin/templates/mediagoblin/user_pages/user.html:137 #, python-format @@ -1047,11 +1049,11 @@ msgstr " (移除)" #: mediagoblin/templates/mediagoblin/utils/collections.html:21 msgid "Collected in" -msgstr "" +msgstr "蒐集了" #: mediagoblin/templates/mediagoblin/utils/collections.html:40 msgid "Add to a collection" -msgstr "" +msgstr "加入至蒐藏" #: mediagoblin/templates/mediagoblin/utils/feed_link.html:21 #: mediagoblin/themes/airy/templates/mediagoblin/utils/feed_link.html:21 @@ -1125,31 +1127,31 @@ msgstr "不好意思,看起來這個網址上沒有網頁。

如果您 #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "年" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "月" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "週" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "日" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "小時" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "分" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" -msgstr "" +msgstr "留言" #: mediagoblin/user_pages/forms.py:25 msgid "" @@ -1168,7 +1170,7 @@ msgstr "我確定我要從蒐藏中移除此項目" #: mediagoblin/user_pages/forms.py:39 msgid "Collection" -msgstr "" +msgstr "蒐藏" #: mediagoblin/user_pages/forms.py:40 msgid "-- Select --" @@ -1180,11 +1182,11 @@ msgstr "加註" #: mediagoblin/user_pages/lib.py:58 msgid "commented on your post" -msgstr "在您的內容張貼評論" +msgstr "在您的內容張貼留言" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "抱歉,留言被關閉。" #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py index 444c624f..e0711416 100644 --- a/mediagoblin/init/__init__.py +++ b/mediagoblin/init/__init__.py @@ -58,16 +58,20 @@ def setup_global_and_app_config(config_path): return global_config, app_config -def setup_database(): +def setup_database(run_migrations=False): app_config = mg_globals.app_config + global_config = mg_globals.global_config # Load all models for media types (plugins, ...) load_models(app_config) - # Set up the database - db = setup_connection_and_db_from_config(app_config) - - check_db_migrations_current(db) + db = setup_connection_and_db_from_config(app_config, run_migrations) + if run_migrations: + #Run the migrations to initialize/update the database. + from mediagoblin.gmg_commands.dbupdate import run_all_migrations + run_all_migrations(db, app_config, global_config) + else: + check_db_migrations_current(db) setup_globals(database=db) diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py index 169cc935..57242bf6 100644 --- a/mediagoblin/init/celery/__init__.py +++ b/mediagoblin/init/celery/__init__.py @@ -16,12 +16,18 @@ import os import sys +import logging from celery import Celery from mediagoblin.tools.pluginapi import hook_runall -MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task'] +_log = logging.getLogger(__name__) + + +MANDATORY_CELERY_IMPORTS = [ + 'mediagoblin.processing.task', + 'mediagoblin.notifications.task'] DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module' @@ -97,3 +103,13 @@ def setup_celery_from_config(app_config, global_config, if set_environ: os.environ['CELERY_CONFIG_MODULE'] = settings_module + + # Replace the default celery.current_app.conf if celery has already been + # initiated + from celery import current_app + + _log.info('Setting celery configuration from object "{0}"'.format( + settings_module)) + current_app.config_from_object(this_module) + + _log.debug('Celery broker host: {0}'.format(current_app.conf['BROKER_HOST'])) diff --git a/mediagoblin/listings/views.py b/mediagoblin/listings/views.py index 35af7148..07dbb3d5 100644 --- a/mediagoblin/listings/views.py +++ b/mediagoblin/listings/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 . +from mediagoblin import mg_globals from mediagoblin.db.models import MediaEntry from mediagoblin.db.util import media_entries_for_tag_slug from mediagoblin.tools.pagination import Pagination @@ -80,6 +81,17 @@ def atom_feed(request): link = request.urlgen('index', qualified=True) feed_title += "for all recent items" + atomlinks = [ + {'href': link, + 'rel': 'alternate', + 'type': 'text/html'}] + + if mg_globals.app_config["push_urls"]: + for push_url in mg_globals.app_config["push_urls"]: + atomlinks.append({ + 'rel': 'hub', + 'href': push_url}) + cursor = cursor.order_by(MediaEntry.created.desc()) cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) @@ -87,9 +99,8 @@ def atom_feed(request): feed_title, feed_url=request.url, id=link, - links=[{'href': link, - 'rel': 'alternate', - 'type': 'text/html'}]) + links=atomlinks) + for entry in cursor: feed.add(entry.get('title'), entry.description_html, diff --git a/mediagoblin/meddleware/csrf.py b/mediagoblin/meddleware/csrf.py index 661f0ba2..44d42d75 100644 --- a/mediagoblin/meddleware/csrf.py +++ b/mediagoblin/meddleware/csrf.py @@ -111,7 +111,7 @@ class CsrfMeddleware(BaseMeddleware): httponly=True) # update the Vary header - response.vary = (getattr(response, 'vary', None) or []) + ['Cookie'] + response.vary = list(getattr(response, 'vary', None) or []) + ['Cookie'] def _make_token(self, request): """Generate a new token to use for CSRF protection.""" diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py index 20e1918e..134157dc 100644 --- a/mediagoblin/media_types/__init__.py +++ b/mediagoblin/media_types/__init__.py @@ -15,12 +15,10 @@ # along with this program. If not, see . import os -import sys import logging import tempfile -from mediagoblin import mg_globals -from mediagoblin.tools.common import import_component +from mediagoblin.tools.pluginapi import hook_handle from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ _log = logging.getLogger(__name__) @@ -52,36 +50,6 @@ class MediaManagerBase(object): return hasattr(self, i) -class CompatMediaManager(object): - def __init__(self, mm_dict, entry=None): - self.mm_dict = mm_dict - self.entry = entry - - def __call__(self, entry): - "So this object can look like a class too, somehow" - assert self.entry is None - return self.__class__(self.mm_dict, entry) - - def __getitem__(self, i): - return self.mm_dict[i] - - def __contains__(self, i): - return (i in self.mm_dict) - - @property - def media_fetch_order(self): - return self.mm_dict.get('media_fetch_order') - - def sniff_handler(self, *args, **kwargs): - func = self.mm_dict.get("sniff_handler", None) - if func is not None: - return func(*args, **kwargs) - return False - - def __getattr__(self, i): - return self.mm_dict[i] - - def sniff_media(media): ''' Iterate through the enabled media types and find those suited @@ -98,40 +66,18 @@ def sniff_media(media): media_file.write(media.stream.read()) media.stream.seek(0) - for media_type, manager in get_media_managers(): - _log.info('Sniffing {0}'.format(media_type)) - if manager.sniff_handler(media_file, media=media): - _log.info('{0} accepts the file'.format(media_type)) - return media_type, manager - else: - _log.debug('{0} did not accept the file'.format(media_type)) + media_type = hook_handle('sniff_handler', media_file, media=media) + if media_type: + _log.info('{0} accepts the file'.format(media_type)) + return media_type, hook_handle(('media_manager', media_type)) + else: + _log.debug('{0} did not accept the file'.format(media_type)) raise FileTypeNotSupported( # TODO: Provide information on which file types are supported _(u'Sorry, I don\'t support that file type :(')) -def get_media_types(): - """ - Generator, yields the available media types - """ - for media_type in mg_globals.app_config['media_types']: - yield media_type - - -def get_media_managers(): - ''' - Generator, yields all enabled media managers - ''' - for media_type in get_media_types(): - mm = import_component(media_type + ":MEDIA_MANAGER") - - if isinstance(mm, dict): - mm = CompatMediaManager(mm) - - yield media_type, mm - - def get_media_type_and_manager(filename): ''' Try to find the media type based on the file name, extension @@ -142,11 +88,10 @@ def get_media_type_and_manager(filename): # Get the file extension ext = os.path.splitext(filename)[1].lower() - for media_type, manager in get_media_managers(): - # Omit the dot from the extension and match it against - # the media manager - if ext[1:] in manager.accepted_extensions: - return media_type, manager + # Omit the dot from the extension and match it against + # the media manager + if hook_handle('get_media_type_and_manager', ext[1:]): + return hook_handle('get_media_type_and_manager', ext[1:]) else: _log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format( filename)) diff --git a/mediagoblin/media_types/ascii/__init__.py b/mediagoblin/media_types/ascii/__init__.py index 0931e83a..4baf8dd3 100644 --- a/mediagoblin/media_types/ascii/__init__.py +++ b/mediagoblin/media_types/ascii/__init__.py @@ -17,15 +17,31 @@ from mediagoblin.media_types import MediaManagerBase from mediagoblin.media_types.ascii.processing import process_ascii, \ sniff_handler +from mediagoblin.tools import pluginapi + +ACCEPTED_EXTENSIONS = ["txt", "asc", "nfo"] +MEDIA_TYPE = 'mediagoblin.media_types.ascii' + + +def setup_plugin(): + config = pluginapi.get_config(MEDIA_TYPE) class ASCIIMediaManager(MediaManagerBase): human_readable = "ASCII" processor = staticmethod(process_ascii) - sniff_handler = staticmethod(sniff_handler) display_template = "mediagoblin/media_displays/ascii.html" default_thumb = "images/media_thumbs/ascii.jpg" - accepted_extensions = ["txt", "asc", "nfo"] - -MEDIA_MANAGER = ASCIIMediaManager + +def get_media_type_and_manager(ext): + if ext in ACCEPTED_EXTENSIONS: + return MEDIA_TYPE, ASCIIMediaManager + + +hooks = { + 'setup': setup_plugin, + 'get_media_type_and_manager': get_media_type_and_manager, + ('media_manager', MEDIA_TYPE): lambda: ASCIIMediaManager, + 'sniff_handler': sniff_handler, +} diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py index 2f6079be..aca784e8 100644 --- a/mediagoblin/media_types/ascii/processing.py +++ b/mediagoblin/media_types/ascii/processing.py @@ -28,17 +28,19 @@ from mediagoblin.media_types.ascii import asciitoimage _log = logging.getLogger(__name__) SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo'] +MEDIA_TYPE = 'mediagoblin.media_types.ascii' def sniff_handler(media_file, **kw): + _log.info('Sniffing {0}'.format(MEDIA_TYPE)) if kw.get('media') is not None: name, ext = os.path.splitext(kw['media'].filename) clean_ext = ext[1:].lower() if clean_ext in SUPPORTED_EXTENSIONS: - return True + return MEDIA_TYPE - return False + return None def process_ascii(proc_state): diff --git a/mediagoblin/media_types/audio/__init__.py b/mediagoblin/media_types/audio/__init__.py index 2eb7300e..90f842ba 100644 --- a/mediagoblin/media_types/audio/__init__.py +++ b/mediagoblin/media_types/audio/__init__.py @@ -17,14 +17,29 @@ from mediagoblin.media_types import MediaManagerBase from mediagoblin.media_types.audio.processing import process_audio, \ sniff_handler +from mediagoblin.tools import pluginapi + +ACCEPTED_EXTENSIONS = ["mp3", "flac", "wav", "m4a"] +MEDIA_TYPE = 'mediagoblin.media_types.audio' + + +def setup_plugin(): + config = pluginapi.get_config(MEDIA_TYPE) class AudioMediaManager(MediaManagerBase): human_readable = "Audio" processor = staticmethod(process_audio) - sniff_handler = staticmethod(sniff_handler) display_template = "mediagoblin/media_displays/audio.html" - accepted_extensions = ["mp3", "flac", "wav", "m4a"] -MEDIA_MANAGER = AudioMediaManager +def get_media_type_and_manager(ext): + if ext in ACCEPTED_EXTENSIONS: + return MEDIA_TYPE, AudioMediaManager + +hooks = { + 'setup': setup_plugin, + 'get_media_type_and_manager': get_media_type_and_manager, + 'sniff_handler': sniff_handler, + ('media_manager', MEDIA_TYPE): lambda: AudioMediaManager, +} diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py index 101b83e5..22383bc1 100644 --- a/mediagoblin/media_types/audio/processing.py +++ b/mediagoblin/media_types/audio/processing.py @@ -27,19 +27,22 @@ from mediagoblin.media_types.audio.transcoders import (AudioTranscoder, _log = logging.getLogger(__name__) +MEDIA_TYPE = 'mediagoblin.media_types.audio' + def sniff_handler(media_file, **kw): + _log.info('Sniffing {0}'.format(MEDIA_TYPE)) try: transcoder = AudioTranscoder() data = transcoder.discover(media_file.name) except BadMediaFail: _log.debug('Audio discovery raised BadMediaFail') - return False + return None if data.is_audio == True and data.is_video == False: - return True + return MEDIA_TYPE - return False + return None def process_audio(proc_state): diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py index 5130ef48..1bb9c6f3 100644 --- a/mediagoblin/media_types/image/__init__.py +++ b/mediagoblin/media_types/image/__init__.py @@ -13,23 +13,30 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - import datetime from mediagoblin.media_types import MediaManagerBase from mediagoblin.media_types.image.processing import process_image, \ sniff_handler +from mediagoblin.tools import pluginapi + + +ACCEPTED_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "tiff"] +MEDIA_TYPE = 'mediagoblin.media_types.image' + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.media_types.image') class ImageMediaManager(MediaManagerBase): human_readable = "Image" processor = staticmethod(process_image) - sniff_handler = staticmethod(sniff_handler) display_template = "mediagoblin/media_displays/image.html" default_thumb = "images/media_thumbs/image.png" - accepted_extensions = ["jpg", "jpeg", "png", "gif", "tiff"] + media_fetch_order = [u'medium', u'original', u'thumb'] - + def get_original_date(self): """ Get the original date and time from the EXIF information. Returns @@ -52,4 +59,14 @@ class ImageMediaManager(MediaManagerBase): return None -MEDIA_MANAGER = ImageMediaManager +def get_media_type_and_manager(ext): + if ext in ACCEPTED_EXTENSIONS: + return MEDIA_TYPE, ImageMediaManager + + +hooks = { + 'setup': setup_plugin, + 'get_media_type_and_manager': get_media_type_and_manager, + 'sniff_handler': sniff_handler, + ('media_manager', MEDIA_TYPE): lambda: ImageMediaManager, +} diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py index bc0ce3f8..baf2ac7e 100644 --- a/mediagoblin/media_types/image/processing.py +++ b/mediagoblin/media_types/image/processing.py @@ -35,6 +35,8 @@ PIL_FILTERS = { 'BICUBIC': Image.BICUBIC, 'ANTIALIAS': Image.ANTIALIAS} +MEDIA_TYPE = 'mediagoblin.media_types.image' + def resize_image(proc_state, resized, keyname, target_name, new_size, exif_tags, workdir): @@ -95,17 +97,18 @@ def resize_tool(proc_state, force, keyname, target_name, exif_tags, conversions_subdir) -SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg'] +SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg', 'tiff'] def sniff_handler(media_file, **kw): + _log.info('Sniffing {0}'.format(MEDIA_TYPE)) if kw.get('media') is not None: # That's a double negative! name, ext = os.path.splitext(kw['media'].filename) clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase if clean_ext in SUPPORTED_FILETYPES: _log.info('Found file extension in supported filetypes') - return True + return MEDIA_TYPE else: _log.debug('Media present, extension not found in {0}'.format( SUPPORTED_FILETYPES)) @@ -113,7 +116,7 @@ def sniff_handler(media_file, **kw): _log.warning('Need additional information (keyword argument \'media\')' ' to be able to handle sniffing') - return False + return None def process_image(proc_state): diff --git a/mediagoblin/media_types/pdf/__init__.py b/mediagoblin/media_types/pdf/__init__.py index f0ba7867..67509ddc 100644 --- a/mediagoblin/media_types/pdf/__init__.py +++ b/mediagoblin/media_types/pdf/__init__.py @@ -17,15 +17,31 @@ from mediagoblin.media_types import MediaManagerBase from mediagoblin.media_types.pdf.processing import process_pdf, \ sniff_handler +from mediagoblin.tools import pluginapi + +ACCEPTED_EXTENSIONS = ['pdf'] +MEDIA_TYPE = 'mediagoblin.media_types.pdf' + + +def setup_plugin(): + config = pluginapi.get_config(MEDIA_TYPE) class PDFMediaManager(MediaManagerBase): human_readable = "PDF" processor = staticmethod(process_pdf) - sniff_handler = staticmethod(sniff_handler) display_template = "mediagoblin/media_displays/pdf.html" default_thumb = "images/media_thumbs/pdf.jpg" - accepted_extensions = ["pdf"] -MEDIA_MANAGER = PDFMediaManager +def get_media_type_and_manager(ext): + if ext in ACCEPTED_EXTENSIONS: + return MEDIA_TYPE, PDFMediaManager + + +hooks = { + 'setup': setup_plugin, + 'get_media_type_and_manager': get_media_type_and_manager, + 'sniff_handler': sniff_handler, + ('media_manager', MEDIA_TYPE): lambda: PDFMediaManager, +} diff --git a/mediagoblin/media_types/pdf/processing.py b/mediagoblin/media_types/pdf/processing.py index 49742fd7..f35b4376 100644 --- a/mediagoblin/media_types/pdf/processing.py +++ b/mediagoblin/media_types/pdf/processing.py @@ -25,6 +25,8 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _ _log = logging.getLogger(__name__) +MEDIA_TYPE = 'mediagoblin.media_types.pdf' + # TODO - cache (memoize) util # This is a list created via uniconv --show and hand removing some types that @@ -163,16 +165,17 @@ def check_prerequisites(): return True def sniff_handler(media_file, **kw): + _log.info('Sniffing {0}'.format(MEDIA_TYPE)) if not check_prerequisites(): - return False + return None if kw.get('media') is not None: name, ext = os.path.splitext(kw['media'].filename) clean_ext = ext[1:].lower() if clean_ext in supported_extensions(): - return True + return MEDIA_TYPE - return False + return None def create_pdf_thumb(original, thumb_filename, width, height): # Note: pdftocairo adds '.png', remove it @@ -250,8 +253,8 @@ def process_pdf(proc_state): else: pdf_filename = queued_filename.rsplit('.', 1)[0] + '.pdf' unoconv = where('unoconv') - call(executable=unoconv, - args=[unoconv, '-v', '-f', 'pdf', queued_filename]) + Popen(executable=unoconv, + args=[unoconv, '-v', '-f', 'pdf', queued_filename]).wait() if not os.path.exists(pdf_filename): _log.debug('unoconv failed to convert file to pdf') raise BadMediaFail() diff --git a/mediagoblin/media_types/stl/__init__.py b/mediagoblin/media_types/stl/__init__.py index 6ae8a8b9..1d2a8478 100644 --- a/mediagoblin/media_types/stl/__init__.py +++ b/mediagoblin/media_types/stl/__init__.py @@ -17,15 +17,30 @@ from mediagoblin.media_types import MediaManagerBase from mediagoblin.media_types.stl.processing import process_stl, \ sniff_handler +from mediagoblin.tools import pluginapi + +MEDIA_TYPE = 'mediagoblin.media_types.stl' +ACCEPTED_EXTENSIONS = ["obj", "stl"] + + +def setup_plugin(): + config = pluginapi.get_config(MEDIA_TYPE) class STLMediaManager(MediaManagerBase): human_readable = "stereo lithographics" processor = staticmethod(process_stl) - sniff_handler = staticmethod(sniff_handler) display_template = "mediagoblin/media_displays/stl.html" default_thumb = "images/media_thumbs/video.jpg" - accepted_extensions = ["obj", "stl"] -MEDIA_MANAGER = STLMediaManager +def get_media_type_and_manager(ext): + if ext in ACCEPTED_EXTENSIONS: + return MEDIA_TYPE, STLMediaManager + +hooks = { + 'setup': setup_plugin, + 'get_media_type_and_manager': get_media_type_and_manager, + 'sniff_handler': sniff_handler, + ('media_manager', MEDIA_TYPE): lambda: STLMediaManager, +} diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py index 49382495..53751416 100644 --- a/mediagoblin/media_types/stl/processing.py +++ b/mediagoblin/media_types/stl/processing.py @@ -29,6 +29,7 @@ from mediagoblin.media_types.stl import model_loader _log = logging.getLogger(__name__) SUPPORTED_FILETYPES = ['stl', 'obj'] +MEDIA_TYPE = 'mediagoblin.media_types.stl' BLEND_FILE = pkg_resources.resource_filename( 'mediagoblin.media_types.stl', @@ -43,13 +44,14 @@ BLEND_SCRIPT = pkg_resources.resource_filename( def sniff_handler(media_file, **kw): + _log.info('Sniffing {0}'.format(MEDIA_TYPE)) if kw.get('media') is not None: name, ext = os.path.splitext(kw['media'].filename) clean_ext = ext[1:].lower() - + if clean_ext in SUPPORTED_FILETYPES: _log.info('Found file extension in supported filetypes') - return True + return MEDIA_TYPE else: _log.debug('Media present, extension not found in {0}'.format( SUPPORTED_FILETYPES)) @@ -57,7 +59,7 @@ def sniff_handler(media_file, **kw): _log.warning('Need additional information (keyword argument \'media\')' ' to be able to handle sniffing') - return False + return None def blender_render(config): diff --git a/mediagoblin/media_types/tools.py b/mediagoblin/media_types/tools.py new file mode 100644 index 00000000..fe7b3772 --- /dev/null +++ b/mediagoblin/media_types/tools.py @@ -0,0 +1,27 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +import logging + +from mediagoblin import mg_globals + +_log = logging.getLogger(__name__) + + +def media_type_warning(): + if mg_globals.app_config.get('media_types'): + _log.warning('Media_types have been converted to plugins. Old' + ' media_types will no longer work. Please convert them' + ' to plugins to continue using them.') diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py index 569cf11a..e8a4308b 100644 --- a/mediagoblin/media_types/video/__init__.py +++ b/mediagoblin/media_types/video/__init__.py @@ -17,20 +17,35 @@ from mediagoblin.media_types import MediaManagerBase from mediagoblin.media_types.video.processing import process_video, \ sniff_handler +from mediagoblin.tools import pluginapi + +MEDIA_TYPE = 'mediagoblin.media_types.video' +ACCEPTED_EXTENSIONS = [ + "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"] + + +def setup_plugin(): + config = pluginapi.get_config(MEDIA_TYPE) class VideoMediaManager(MediaManagerBase): human_readable = "Video" processor = staticmethod(process_video) - sniff_handler = staticmethod(sniff_handler) display_template = "mediagoblin/media_displays/video.html" default_thumb = "images/media_thumbs/video.jpg" - accepted_extensions = [ - "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"] - + # Used by the media_entry.get_display_media method media_fetch_order = [u'webm_640', u'original'] default_webm_type = 'video/webm; codecs="vp8, vorbis"' -MEDIA_MANAGER = VideoMediaManager +def get_media_type_and_manager(ext): + if ext in ACCEPTED_EXTENSIONS: + return MEDIA_TYPE, VideoMediaManager + +hooks = { + 'setup': setup_plugin, + 'get_media_type_and_manager': get_media_type_and_manager, + 'sniff_handler': sniff_handler, + ('media_manager', MEDIA_TYPE): lambda: VideoMediaManager, +} diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index ff2c94a0..5386ba60 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -29,6 +29,8 @@ from .util import skip_transcode _log = logging.getLogger(__name__) _log.setLevel(logging.DEBUG) +MEDIA_TYPE = 'mediagoblin.media_types.video' + class VideoTranscodingFail(BaseProcessingFail): ''' @@ -41,17 +43,18 @@ def sniff_handler(media_file, **kw): transcoder = transcoders.VideoTranscoder() data = transcoder.discover(media_file.name) + _log.info('Sniffing {0}'.format(MEDIA_TYPE)) _log.debug('Discovered: {0}'.format(data)) if not data: _log.error('Could not discover {0}'.format( kw.get('media'))) - return False + return None if data['is_video'] == True: - return True + return MEDIA_TYPE - return False + return None def process_video(proc_state): @@ -186,7 +189,7 @@ def store_metadata(media_entry, metadata): [(key, tags_metadata[key]) for key in [ "application-name", "artist", "audio-codec", "bitrate", - "container-format", "copyright", "encoder", + "container-format", "copyright", "encoder", "encoder-version", "license", "nominal-bitrate", "title", "video-codec"] if key in tags_metadata]) @@ -203,7 +206,7 @@ def store_metadata(media_entry, metadata): dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(), dt.get_minute(), dt.get_second(), dt.get_microsecond()).isoformat() - + metadata['tags'] = tags # Only save this field if there's something to save diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index 90a767dd..9d6b7655 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -22,9 +22,15 @@ import logging import urllib import multiprocessing import gobject + +old_argv = sys.argv +sys.argv = [] + import pygst pygst.require('0.10') import gst + +sys.argv = old_argv import struct try: from PIL import Image diff --git a/mediagoblin/notifications/__init__.py b/mediagoblin/notifications/__init__.py new file mode 100644 index 00000000..4b7fbb8c --- /dev/null +++ b/mediagoblin/notifications/__init__.py @@ -0,0 +1,141 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +import logging + +from mediagoblin.db.models import Notification, \ + CommentNotification, CommentSubscription +from mediagoblin.notifications.task import email_notification_task +from mediagoblin.notifications.tools import generate_comment_message + +_log = logging.getLogger(__name__) + +def trigger_notification(comment, media_entry, request): + ''' + Send out notifications about a new comment. + ''' + subscriptions = CommentSubscription.query.filter_by( + media_entry_id=media_entry.id).all() + + for subscription in subscriptions: + if not subscription.notify: + continue + + if comment.get_author == subscription.user: + continue + + cn = CommentNotification( + user_id=subscription.user_id, + subject_id=comment.id) + + cn.save() + + if subscription.send_email: + message = generate_comment_message( + subscription.user, + comment, + media_entry, + request) + + email_notification_task.apply_async([cn.id, message]) + + +def mark_notification_seen(notification): + if notification: + notification.seen = True + notification.save() + + +def mark_comment_notification_seen(comment_id, user): + notification = CommentNotification.query.filter_by( + user_id=user.id, + subject_id=comment_id).first() + + _log.debug('Marking {0} as seen.'.format(notification)) + + mark_notification_seen(notification) + + +def get_comment_subscription(user_id, media_entry_id): + return CommentSubscription.query.filter_by( + user_id=user_id, + media_entry_id=media_entry_id).first() + +def add_comment_subscription(user, media_entry): + ''' + Create a comment subscription for a User on a MediaEntry. + + Uses the User's wants_comment_notification to set email notifications for + the subscription to enabled/disabled. + ''' + cn = get_comment_subscription(user.id, media_entry.id) + + if not cn: + cn = CommentSubscription( + user_id=user.id, + media_entry_id=media_entry.id) + + cn.notify = True + + if not user.wants_comment_notification: + cn.send_email = False + + cn.save() + + +def silence_comment_subscription(user, media_entry): + ''' + Silence a subscription so that the user is never notified in any way about + new comments on an entry + ''' + cn = get_comment_subscription(user.id, media_entry.id) + + if cn: + cn.notify = False + cn.send_email = False + cn.save() + + +def remove_comment_subscription(user, media_entry): + cn = get_comment_subscription(user.id, media_entry.id) + + if cn: + cn.delete() + + +NOTIFICATION_FETCH_LIMIT = 100 + + +def get_notifications(user_id, only_unseen=True): + query = Notification.query.filter_by(user_id=user_id) + + if only_unseen: + query = query.filter_by(seen=False) + + notifications = query.limit( + NOTIFICATION_FETCH_LIMIT).all() + + return notifications + +def get_notification_count(user_id, only_unseen=True): + query = Notification.query.filter_by(user_id=user_id) + + if only_unseen: + query = query.filter_by(seen=False) + + count = query.count() + + return count diff --git a/mediagoblin/notifications/routing.py b/mediagoblin/notifications/routing.py new file mode 100644 index 00000000..e57956d3 --- /dev/null +++ b/mediagoblin/notifications/routing.py @@ -0,0 +1,25 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +from mediagoblin.tools.routing import add_route + +add_route('mediagoblin.notifications.subscribe_comments', + '/u//m//notifications/subscribe/comments/', + 'mediagoblin.notifications.views:subscribe_comments') + +add_route('mediagoblin.notifications.silence_comments', + '/u//m//notifications/silence/', + 'mediagoblin.notifications.views:silence_comments') diff --git a/mediagoblin/notifications/task.py b/mediagoblin/notifications/task.py new file mode 100644 index 00000000..52573b57 --- /dev/null +++ b/mediagoblin/notifications/task.py @@ -0,0 +1,46 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +import logging + +from celery import registry +from celery.task import Task + +from mediagoblin.tools.mail import send_email +from mediagoblin.db.models import CommentNotification + + +_log = logging.getLogger(__name__) + + +class EmailNotificationTask(Task): + ''' + Celery notification task. + + This task is executed by celeryd to offload long-running operations from + the web server. + ''' + def run(self, notification_id, message): + cn = CommentNotification.query.filter_by(id=notification_id).first() + _log.info('Sending notification email about {0}'.format(cn)) + + return send_email( + message['from'], + [message['to']], + message['subject'], + message['body']) + +email_notification_task = registry.tasks[EmailNotificationTask.name] diff --git a/mediagoblin/notifications/tools.py b/mediagoblin/notifications/tools.py new file mode 100644 index 00000000..25432780 --- /dev/null +++ b/mediagoblin/notifications/tools.py @@ -0,0 +1,55 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +from mediagoblin.tools.template import render_template +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin import mg_globals + +def generate_comment_message(user, comment, media, request): + """ + Sends comment email to user when a comment is made on their media. + + Args: + - user: the user object to whom the email is sent + - comment: the comment object referencing user's media + - media: the media object the comment is about + - request: the request + """ + + comment_url = request.urlgen( + 'mediagoblin.user_pages.media_home.view_comment', + comment=comment.id, + user=media.get_uploader.username, + media=media.slug_or_id, + qualified=True) + '#comment' + + comment_author = comment.get_author.username + + rendered_email = render_template( + request, 'mediagoblin/user_pages/comment_email.txt', + {'username': user.username, + 'comment_author': comment_author, + 'comment_content': comment.content, + 'comment_url': comment_url}) + + return { + 'from': mg_globals.app_config['email_sender_address'], + 'to': user.email, + 'subject': '{instance_title} - {comment_author} '.format( + comment_author=comment_author, + instance_title=mg_globals.app_config['html_title']) \ + + _('commented on your post'), + 'body': rendered_email} diff --git a/mediagoblin/notifications/views.py b/mediagoblin/notifications/views.py new file mode 100644 index 00000000..d275bc92 --- /dev/null +++ b/mediagoblin/notifications/views.py @@ -0,0 +1,54 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +from mediagoblin.tools.response import render_to_response, render_404, redirect +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.decorators import (uses_pagination, get_user_media_entry, + get_media_entry_by_id, + require_active_login, user_may_delete_media, user_may_alter_collection, + get_user_collection, get_user_collection_item, active_user_from_url) + +from mediagoblin import messages + +from mediagoblin.notifications import add_comment_subscription, \ + silence_comment_subscription + +from werkzeug.exceptions import BadRequest + +@get_user_media_entry +@require_active_login +def subscribe_comments(request, media): + + add_comment_subscription(request.user, media) + + messages.add_message(request, + messages.SUCCESS, + _('Subscribed to comments on %s!') + % media.title) + + return redirect(request, location=media.url_for_self(request.urlgen)) + +@get_user_media_entry +@require_active_login +def silence_comments(request, media): + silence_comment_subscription(request.user, media) + + messages.add_message(request, + messages.SUCCESS, + _('You will not receive notifications for comments on' + ' %s.') % media.title) + + return redirect(request, location=media.url_for_self(request.urlgen)) diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py new file mode 100644 index 00000000..33a554b0 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/__init__.py @@ -0,0 +1,88 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +from mediagoblin.plugins.basic_auth import forms as auth_forms +from mediagoblin.plugins.basic_auth import tools as auth_tools +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.db.models import User +from mediagoblin.tools import pluginapi +from sqlalchemy import or_ + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.basic_auth') + + +def get_user(**kwargs): + username = kwargs.pop('username', None) + if username: + user = User.query.filter( + or_( + User.username == username, + User.email == username, + )).first() + return user + + +def create_user(registration_form): + user = get_user(username=registration_form.username.data) + if not user and 'password' in registration_form: + user = create_basic_user(registration_form) + user.pw_hash = gen_password_hash( + registration_form.password.data) + user.save() + return user + + +def get_login_form(request): + return auth_forms.LoginForm(request.form) + + +def get_registration_form(request): + return auth_forms.RegistrationForm(request.form) + + +def gen_password_hash(raw_pass, extra_salt=None): + return auth_tools.bcrypt_gen_password_hash(raw_pass, extra_salt) + + +def check_password(raw_pass, stored_hash, extra_salt=None): + if stored_hash: + return auth_tools.bcrypt_check_password(raw_pass, + stored_hash, extra_salt) + return None + + +def auth(): + return True + + +def append_to_global_context(context): + context['pass_auth'] = True + return context + + +hooks = { + 'setup': setup_plugin, + 'authentication': auth, + 'auth_get_user': get_user, + 'auth_create_user': create_user, + 'auth_get_login_form': get_login_form, + 'auth_get_registration_form': get_registration_form, + 'auth_gen_password_hash': gen_password_hash, + 'auth_check_password': check_password, + 'auth_fake_login_attempt': auth_tools.fake_login_attempt, + 'template_global_context': append_to_global_context, +} diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py new file mode 100644 index 00000000..6cf01b38 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/forms.py @@ -0,0 +1,46 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +import wtforms + +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ +from mediagoblin.auth.tools import normalize_user_or_email_field + + +class RegistrationForm(wtforms.Form): + username = wtforms.TextField( + _('Username'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_email=False)]) + password = wtforms.PasswordField( + _('Password'), + [wtforms.validators.Required(), + wtforms.validators.Length(min=5, max=1024)]) + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) + + +class LoginForm(wtforms.Form): + username = wtforms.TextField( + _('Username or Email'), + [wtforms.validators.Required(), + normalize_user_or_email_field()]) + password = wtforms.PasswordField( + _('Password')) + stay_logged_in = wtforms.BooleanField( + label='', + description=_('Stay logged in')) diff --git a/mediagoblin/auth/lib.py b/mediagoblin/plugins/basic_auth/tools.py similarity index 72% rename from mediagoblin/auth/lib.py rename to mediagoblin/plugins/basic_auth/tools.py index bfc36b28..1300bb9a 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/plugins/basic_auth/tools.py @@ -13,14 +13,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -import random - import bcrypt - -from mediagoblin.tools.mail import send_email -from mediagoblin.tools.template import render_template -from mediagoblin import mg_globals +import random def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None): @@ -88,33 +82,3 @@ def fake_login_attempt(): randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) randplus_stored_hash == randplus_hashed_pass - - -EMAIL_FP_VERIFICATION_TEMPLATE = ( - u"http://{host}{uri}?" - u"userid={userid}&token={fp_verification_key}") - - -def send_fp_verification_email(user, request): - """ - Send the verification email to users to change their password. - - Args: - - user: a user object - - request: the request - """ - rendered_email = render_template( - request, 'mediagoblin/auth/fp_verification_email.txt', - {'username': user.username, - 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( - host=request.host, - uri=request.urlgen('mediagoblin.auth.verify_forgot_password'), - userid=unicode(user.id), - fp_verification_key=user.fp_verification_key)}) - - # TODO: There is no error handling in place - send_email( - mg_globals.app_config['email_sender_address'], - [user.email], - 'GNU MediaGoblin - Change forgotten password!', - rendered_email) diff --git a/mediagoblin/plugins/openid/__init__.py b/mediagoblin/plugins/openid/__init__.py new file mode 100644 index 00000000..ee88808c --- /dev/null +++ b/mediagoblin/plugins/openid/__init__.py @@ -0,0 +1,123 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +import os +import uuid + +from sqlalchemy import or_ + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.db.models import User +from mediagoblin.plugins.openid.models import OpenIDUserURL +from mediagoblin.tools import pluginapi +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.openid') + + routes = [ + ('mediagoblin.plugins.openid.register', + '/auth/openid/register/', + 'mediagoblin.plugins.openid.views:register'), + ('mediagoblin.plugins.openid.login', + '/auth/openid/login/', + 'mediagoblin.plugins.openid.views:login'), + ('mediagoblin.plugins.openid.finish_login', + '/auth/openid/login/finish/', + 'mediagoblin.plugins.openid.views:finish_login'), + ('mediagoblin.plugins.openid.edit', + '/edit/openid/', + 'mediagoblin.plugins.openid.views:start_edit'), + ('mediagoblin.plugins.openid.finish_edit', + '/edit/openid/finish/', + 'mediagoblin.plugins.openid.views:finish_edit'), + ('mediagoblin.plugins.openid.delete', + '/edit/openid/delete/', + 'mediagoblin.plugins.openid.views:delete_openid'), + ('mediagoblin.plugins.openid.finish_delete', + '/edit/openid/delete/finish/', + 'mediagoblin.plugins.openid.views:finish_delete')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'register_link': 'mediagoblin/plugins/openid/register_link.html', + 'login_link': 'mediagoblin/plugins/openid/login_link.html', + 'edit_link': 'mediagoblin/plugins/openid/edit_link.html'}) + + +def create_user(register_form): + if 'openid' in register_form: + username = register_form.username.data + user = User.query.filter( + or_( + User.username == username, + User.email == username, + )).first() + + if not user: + user = create_basic_user(register_form) + + new_entry = OpenIDUserURL() + new_entry.openid_url = register_form.openid.data + new_entry.user_id = user.id + new_entry.save() + + return user + + +def extra_validation(register_form): + openid = register_form.openid.data if 'openid' in \ + register_form else None + if openid: + openid_url_exists = OpenIDUserURL.query.filter_by( + openid_url=openid + ).count() + + extra_validation_passes = True + + if openid_url_exists: + register_form.openid.errors.append( + _('Sorry, an account is already registered to that OpenID.')) + extra_validation_passes = False + + return extra_validation_passes + + +def no_pass_redirect(): + return 'openid' + + +def add_to_form_context(context): + context['openid_link'] = True + return context + + +def Auth(): + return True + +hooks = { + 'setup': setup_plugin, + 'authentication': Auth, + 'auth_extra_validation': extra_validation, + 'auth_create_user': create_user, + 'auth_no_pass_redirect': no_pass_redirect, + ('mediagoblin.auth.register', + 'mediagoblin/auth/register.html'): add_to_form_context, +} diff --git a/mediagoblin/plugins/openid/forms.py b/mediagoblin/plugins/openid/forms.py new file mode 100644 index 00000000..f26024bd --- /dev/null +++ b/mediagoblin/plugins/openid/forms.py @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +import wtforms + +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ +from mediagoblin.auth.tools import normalize_user_or_email_field + + +class RegistrationForm(wtforms.Form): + openid = wtforms.HiddenField( + '', + [wtforms.validators.Required()]) + username = wtforms.TextField( + _('Username'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_email=False)]) + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) + + +class LoginForm(wtforms.Form): + openid = wtforms.TextField( + _('OpenID'), + [wtforms.validators.Required(), + # Can openid's only be urls? + wtforms.validators.URL(message='Please enter a valid url.')]) diff --git a/mediagoblin/plugins/openid/models.py b/mediagoblin/plugins/openid/models.py new file mode 100644 index 00000000..6773f0ad --- /dev/null +++ b/mediagoblin/plugins/openid/models.py @@ -0,0 +1,65 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +from sqlalchemy import Column, Integer, Unicode, ForeignKey +from sqlalchemy.orm import relationship, backref + +from mediagoblin.db.models import User +from mediagoblin.db.base import Base + + +class OpenIDUserURL(Base): + __tablename__ = "openid__user_urls" + + id = Column(Integer, primary_key=True) + openid_url = Column(Unicode, nullable=False) + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + # OpenID's are owned by their user, so do the full thing. + user = relationship(User, backref=backref('openid_urls', + cascade='all, delete-orphan')) + + +# OpenID Store Models +class Nonce(Base): + __tablename__ = "openid__nonce" + + server_url = Column(Unicode, primary_key=True) + timestamp = Column(Integer, primary_key=True) + salt = Column(Unicode, primary_key=True) + + def __unicode__(self): + return u'Nonce: %r, %r' % (self.server_url, self.salt) + + +class Association(Base): + __tablename__ = "openid__association" + + server_url = Column(Unicode, primary_key=True) + handle = Column(Unicode, primary_key=True) + secret = Column(Unicode) + issued = Column(Integer) + lifetime = Column(Integer) + assoc_type = Column(Unicode) + + def __unicode__(self): + return u'Association: %r, %r' % (self.server_url, self.handle) + + +MODELS = [ + OpenIDUserURL, + Nonce, + Association +] diff --git a/mediagoblin/plugins/openid/store.py b/mediagoblin/plugins/openid/store.py new file mode 100644 index 00000000..8f9a7012 --- /dev/null +++ b/mediagoblin/plugins/openid/store.py @@ -0,0 +1,127 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +import base64 +import time + +from openid.association import Association as OIDAssociation +from openid.store.interface import OpenIDStore +from openid.store import nonce + +from mediagoblin.plugins.openid.models import Association, Nonce + + +class SQLAlchemyOpenIDStore(OpenIDStore): + def __init__(self): + self.max_nonce_age = 6 * 60 * 60 + + def storeAssociation(self, server_url, association): + assoc = Association.query.filter_by( + server_url=server_url, handle=association.handle + ).first() + + if not assoc: + assoc = Association() + assoc.server_url = unicode(server_url) + assoc.handle = association.handle + + # django uses base64 encoding, python-openid uses a blob field for + # secret + assoc.secret = unicode(base64.encodestring(association.secret)) + assoc.issued = association.issued + assoc.lifetime = association.lifetime + assoc.assoc_type = association.assoc_type + assoc.save() + + def getAssociation(self, server_url, handle=None): + assocs = [] + if handle is not None: + assocs = Association.query.filter_by( + server_url=server_url, handle=handle + ) + else: + assocs = Association.query.filter_by( + server_url=server_url + ) + + if assocs.count() == 0: + return None + else: + associations = [] + for assoc in assocs: + association = OIDAssociation( + assoc.handle, base64.decodestring(assoc.secret), + assoc.issued, assoc.lifetime, assoc.assoc_type + ) + if association.getExpiresIn() == 0: + assoc.delete() + else: + associations.append((association.issued, association)) + + if not associations: + return None + associations.sort() + return associations[-1][1] + + def removeAssociation(self, server_url, handle): + assocs = Association.query.filter_by( + server_url=server_url, handle=handle + ).first() + + assoc_exists = True if assocs else False + for assoc in assocs: + assoc.delete() + return assoc_exists + + def useNonce(self, server_url, timestamp, salt): + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + ononce = Nonce.query.filter_by( + server_url=server_url, + timestamp=timestamp, + salt=salt + ).first() + + if ononce: + return False + else: + ononce = Nonce() + ononce.server_url = server_url + ononce.timestamp = timestamp + ononce.salt = salt + ononce.save() + return True + + def cleanupNonces(self, _now=None): + if _now is None: + _now = int(time.time()) + expired = Nonce.query.filter( + Nonce.timestamp < (_now - nonce.SKEW) + ) + count = expired.count() + for each in expired: + each.delete() + return count + + def cleanupAssociations(self): + now = int(time.time()) + assoc = Association.query.all() + count = 0 + for each in assoc: + if (each.lifetime + each.issued) <= now: + each.delete() + count = count + 1 + return count diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html new file mode 100644 index 00000000..8d308c81 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html @@ -0,0 +1,44 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Add an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} +

+ {{ csrf_token }} +
+

{% trans %}Add an OpenID{% endtrans %}

+

+ + {% trans %}Delete an OpenID{% endtrans %} + +

+ {{ wtforms_util.render_divs(form, True) }} +
+ +
+
+
+{% endblock %} + diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html new file mode 100644 index 00000000..84301b9e --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html @@ -0,0 +1,43 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Delete an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} +
+ {{ csrf_token }} +
+

{% trans %}Delete an OpenID{% endtrans %}

+

+ + {% trans %}Add an OpenID{% endtrans %} + +

+ {{ wtforms_util.render_divs(form, True) }} +
+ +
+
+
+{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html new file mode 100644 index 00000000..2e63e1f8 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} + +{% block openid_edit_link %} +

+ + {% trans %}Edit your OpenID's{% endtrans %} + +

+{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html new file mode 100644 index 00000000..33df7200 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html @@ -0,0 +1,65 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_head %} + +{% endblock %} + +{% block title -%} + {% trans %}Log in{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} +
+ {{ csrf_token }} +
+

{% trans %}Log in{% endtrans %}

+ {% if login_failed %} +
+ {% trans %}Logging in failed!{% endtrans %} +
+ {% endif %} + {% if allow_registration %} +

+ {% trans %}Log in to create an account!{% endtrans %} +

+ {% endif %} + {% if pass_auth is defined %} +

+ + {%- trans %}Or login with a password!{% endtrans %} + +

+ {% endif %} + {{ wtforms_util.render_divs(login_form, True) }} +
+ +
+ {% if next %} + + {% endif %} +
+
+{% endblock %} + diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html new file mode 100644 index 00000000..e5e77d01 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} + +{% block openid_login_link %} +

+ + {%- trans %}Or login with OpenID!{% endtrans %} + +

+{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html new file mode 100644 index 00000000..9bccb4d8 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html @@ -0,0 +1,27 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} + +{% block openid_register_link %} + {% if openid_link is defined %} +

+ + {%- trans %}Or register with OpenID!{% endtrans %} + +

+ {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html new file mode 100644 index 00000000..68d028d0 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html @@ -0,0 +1,24 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +#} +{% extends "mediagoblin/base.html" %} + +{% block mediagoblin_content %} +
+ {{ html|safe }} +
+{% endblock %} diff --git a/mediagoblin/plugins/openid/views.py b/mediagoblin/plugins/openid/views.py new file mode 100644 index 00000000..b639a4cb --- /dev/null +++ b/mediagoblin/plugins/openid/views.py @@ -0,0 +1,404 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +from openid.consumer import consumer +from openid.consumer.discover import DiscoveryFailure +from openid.extensions.sreg import SRegRequest, SRegResponse + +from mediagoblin import mg_globals, messages +from mediagoblin.db.models import User +from mediagoblin.decorators import (auth_enabled, allow_registration, + require_active_login) +from mediagoblin.tools.response import redirect, render_to_response +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.plugins.openid import forms as auth_forms +from mediagoblin.plugins.openid.models import OpenIDUserURL +from mediagoblin.plugins.openid.store import SQLAlchemyOpenIDStore +from mediagoblin.auth.tools import register_user + + +def _start_verification(request, form, return_to, sreg=True): + """ + Start OpenID Verification. + + Returns False if verification fails, otherwise, will return either a + redirect or render_to_response object + """ + openid_url = form.openid.data + c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore()) + + # Try to discover provider + try: + auth_request = c.begin(openid_url) + except DiscoveryFailure: + # Discovery failed, return to login page + form.openid.errors.append( + _('Sorry, the OpenID server could not be found')) + + return False + + host = 'http://' + request.host + + if sreg: + # Ask provider for email and nickname + auth_request.addExtension(SRegRequest(required=['email', 'nickname'])) + + # Do we even need this? + if auth_request is None: + form.openid.errors.append( + _('No OpenID service was found for %s' % openid_url)) + + elif auth_request.shouldSendRedirect(): + # Begin the authentication process as a HTTP redirect + redirect_url = auth_request.redirectURL( + host, return_to) + + return redirect( + request, location=redirect_url) + + else: + # Send request as POST + form_html = auth_request.htmlMarkup( + host, host + return_to, + # Is this necessary? + form_tag_attrs={'id': 'openid_message'}) + + # Beware: this renders a template whose content is a form + # and some javascript to submit it upon page load. Non-JS + # users will have to click the form submit button to + # initiate OpenID authentication. + return render_to_response( + request, + 'mediagoblin/plugins/openid/request_form.html', + {'html': form_html}) + + return False + + +def _finish_verification(request): + """ + Complete OpenID Verification Process. + + If the verification failed, will return false, otherwise, will return + the response + """ + c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore()) + + # Check the response from the provider + response = c.complete(request.args, request.base_url) + if response.status == consumer.FAILURE: + messages.add_message( + request, + messages.WARNING, + _('Verification of %s failed: %s' % + (response.getDisplayIdentifier(), response.message))) + + elif response.status == consumer.SUCCESS: + # Verification was successfull + return response + + elif response.status == consumer.CANCEL: + # Verification canceled + messages.add_message( + request, + messages.WARNING, + _('Verification cancelled')) + + return False + + +def _response_email(response): + """ Gets the email from the OpenID providers response""" + sreg_response = SRegResponse.fromSuccessResponse(response) + if sreg_response and 'email' in sreg_response: + return sreg_response.data['email'] + return None + + +def _response_nickname(response): + """ Gets the nickname from the OpenID providers response""" + sreg_response = SRegResponse.fromSuccessResponse(response) + if sreg_response and 'nickname' in sreg_response: + return sreg_response.data['nickname'] + return None + + +@auth_enabled +def login(request): + """OpenID Login View""" + login_form = auth_forms.LoginForm(request.form) + allow_registration = mg_globals.app_config["allow_registration"] + + # Can't store next in request.GET because of redirects to OpenID provider + # Store it in the session + next = request.GET.get('next') + request.session['next'] = next + + login_failed = False + + if request.method == 'POST' and login_form.validate(): + return_to = request.urlgen( + 'mediagoblin.plugins.openid.finish_login') + + success = _start_verification(request, login_form, return_to) + + if success: + return success + + login_failed = True + + return render_to_response( + request, + 'mediagoblin/plugins/openid/login.html', + {'login_form': login_form, + 'next': request.session.get('next'), + 'login_failed': login_failed, + 'post_url': request.urlgen('mediagoblin.plugins.openid.login'), + 'allow_registration': allow_registration}) + + +@auth_enabled +def finish_login(request): + """Complete OpenID Login Process""" + response = _finish_verification(request) + + if not response: + # Verification failed, redirect to login page. + return redirect(request, 'mediagoblin.plugins.openid.login') + + # Verification was successfull + query = OpenIDUserURL.query.filter_by( + openid_url=response.identity_url, + ).first() + user = query.user if query else None + + if user: + # Set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() + + if request.session.get('next'): + return redirect(request, location=request.session.pop('next')) + else: + return redirect(request, "index") + else: + # No user, need to register + if not mg_globals.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this instance.')) + return redirect(request, 'index') + + # Get email and nickname from response + email = _response_email(response) + username = _response_nickname(response) + + register_form = auth_forms.RegistrationForm(request.form, + openid=response.identity_url, + email=email, + username=username) + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.register')}) + + +@allow_registration +@auth_enabled +def register(request): + """OpenID Registration View""" + if request.method == 'GET': + # Need to connect to openid provider before registering a user to + # get the users openid url. If method is 'GET', then this page was + # acessed without logging in first. + return redirect(request, 'mediagoblin.plugins.openid.login') + + register_form = auth_forms.RegistrationForm(request.form) + + if register_form.validate(): + user = register_user(request, register_form) + + if user: + # 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, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.register')}) + + +@require_active_login +def start_edit(request): + """Starts the process of adding an openid url to a users account""" + form = auth_forms.LoginForm(request.form) + + if request.method == 'POST' and form.validate(): + query = OpenIDUserURL.query.filter_by( + openid_url=form.openid.data + ).first() + user = query.user if query else None + + if not user: + return_to = request.urlgen('mediagoblin.plugins.openid.finish_edit') + success = _start_verification(request, form, return_to, False) + + if success: + return success + else: + form.openid.errors.append( + _('Sorry, an account is already registered to that OpenID.')) + + return render_to_response( + request, + 'mediagoblin/plugins/openid/add.html', + {'form': form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.edit')}) + + +@require_active_login +def finish_edit(request): + """Finishes the process of adding an openid url to a user""" + response = _finish_verification(request) + + if not response: + # Verification failed, redirect to add openid page. + return redirect(request, 'mediagoblin.plugins.openid.edit') + + # Verification was successfull + query = OpenIDUserURL.query.filter_by( + openid_url=response.identity_url, + ).first() + user_exists = query.user if query else None + + if user_exists: + # user exists with that openid url, redirect back to edit page + messages.add_message( + request, + messages.WARNING, + _('Sorry, an account is already registered to that OpenID.')) + return redirect(request, 'mediagoblin.plugins.openid.edit') + + else: + # Save openid to user + user = User.query.filter_by( + id=request.session['user_id'] + ).first() + + new_entry = OpenIDUserURL() + new_entry.openid_url = response.identity_url + new_entry.user_id = user.id + new_entry.save() + + messages.add_message( + request, + messages.SUCCESS, + _('Your OpenID url was saved successfully.')) + + return redirect(request, 'mediagoblin.edit.account') + + +@require_active_login +def delete_openid(request): + """View to remove an openid from a users account""" + form = auth_forms.LoginForm(request.form) + + if request.method == 'POST' and form.validate(): + # Check if a user has this openid + query = OpenIDUserURL.query.filter_by( + openid_url=form.openid.data + ) + user = query.first().user if query.first() else None + + if user and user.id == int(request.session['user_id']): + count = len(user.openid_urls) + if not count > 1 and not user.pw_hash: + # Make sure the user has a pw or another OpenID + messages.add_message( + request, + messages.WARNING, + _("You can't delete your only OpenID URL unless you" + " have a password set")) + elif user: + # There is a user, but not the same user who is logged in + form.openid.errors.append( + _('That OpenID is not registered to this account.')) + + if not form.errors and not request.session.get('messages'): + # Okay to continue with deleting openid + return_to = request.urlgen( + 'mediagoblin.plugins.openid.finish_delete') + success = _start_verification(request, form, return_to, False) + + if success: + return success + + return render_to_response( + request, + 'mediagoblin/plugins/openid/delete.html', + {'form': form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.delete')}) + + +@require_active_login +def finish_delete(request): + """Finishes the deletion of an OpenID from an user's account""" + response = _finish_verification(request) + + if not response: + # Verification failed, redirect to delete openid page. + return redirect(request, 'mediagoblin.plugins.openid.delete') + + query = OpenIDUserURL.query.filter_by( + openid_url=response.identity_url + ) + user = query.first().user if query.first() else None + + # Need to check this again because of generic openid urls such as google's + if user and user.id == int(request.session['user_id']): + count = len(user.openid_urls) + if count > 1 or user.pw_hash: + # User has more then one openid or also has a password. + query.first().delete() + + messages.add_message( + request, + messages.SUCCESS, + _('OpenID was successfully removed.')) + + return redirect(request, 'mediagoblin.edit.account') + + elif not count > 1: + messages.add_message( + request, + messages.WARNING, + _("You can't delete your only OpenID URL unless you have a " + "password set")) + + return redirect(request, 'mediagoblin.plugins.openid.delete') + + else: + messages.add_message( + request, + messages.WARNING, + _('That OpenID is not registered to this account.')) + + return redirect(request, 'mediagoblin.plugins.openid.delete') diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py index 0642ad8e..c9377ad4 100644 --- a/mediagoblin/routing.py +++ b/mediagoblin/routing.py @@ -35,6 +35,7 @@ def get_url_map(): import mediagoblin.edit.routing import mediagoblin.webfinger.routing import mediagoblin.listings.routing + import mediagoblin.notifications.routing for route in PluginManager().get_routes(): add_route(*route) diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index 0378350e..338828d2 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -129,6 +129,7 @@ header { .header_dropdown { margin-bottom: 20px; + padding: 0px 10px 0px 10px; } .header_dropdown li { @@ -333,6 +334,10 @@ text-align: center; width: 20px; } +.boolean { + margin-bottom: 8px; + } + textarea#description, textarea#bio { resize: vertical; height: 100px; @@ -384,6 +389,12 @@ a.comment_whenlink:hover, a.report_whenlink:hover { margin-top: 8px; } +.comment_active { + box-shadow: 0px 0px 15px 15px #378566; + background: #378566; + color: #f7f7f7; +} + textarea#comment_content { resize: vertical; width: 100%; diff --git a/mediagoblin/static/css/pdf_viewer.css b/mediagoblin/static/css/pdf_viewer.css deleted file mode 100644 index c04c8981..00000000 --- a/mediagoblin/static/css/pdf_viewer.css +++ /dev/null @@ -1,1448 +0,0 @@ -/* Copyright 2012 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -* { - padding: 0; - margin: 0; -} - -html { - height: 100%; -} - -body { - height: 100%; - background-color: #404040; - background-image: url(../extlib/pdf.js/web/images/texture.png); -} - -body, -input, -button, -select { - font: message-box; -} - -.hidden { - display: none; -} -[hidden] { - display: none !important; -} - -#viewerContainer:-webkit-full-screen { - top: 0px; - border-top: 2px solid transparent; - background-color: #404040; - background-image: url(../extlib/pdf.js/web/images/texture.png); - width: 100%; - height: 100%; - overflow: hidden; - cursor: none; -} - -#viewerContainer:-moz-full-screen { - top: 0px; - border-top: 2px solid transparent; - background-color: #404040; - background-image: url(../extlib/pdf.js/web/images/texture.png); - width: 100%; - height: 100%; - overflow: hidden; - cursor: none; -} - -#viewerContainer:fullscreen { - top: 0px; - border-top: 2px solid transparent; - background-color: #404040; - background-image: url(../extlib/pdf.js/web/images/texture.png); - width: 100%; - height: 100%; - overflow: hidden; - cursor: none; -} - - -:-webkit-full-screen .page { - margin-bottom: 100%; -} - -:-moz-full-screen .page { - margin-bottom: 100%; -} - -:fullscreen .page { - margin-bottom: 100%; -} - -#viewerContainer.presentationControls { - cursor: default; -} - -/* outer/inner center provides horizontal center */ -html[dir='ltr'] .outerCenter { - float: right; - position: relative; - right: 50%; -} -html[dir='rtl'] .outerCenter { - float: left; - position: relative; - left: 50%; -} -html[dir='ltr'] .innerCenter { - float: right; - position: relative; - right: -50%; -} -html[dir='rtl'] .innerCenter { - float: left; - position: relative; - left: -50%; -} - -#outerContainer { - width: 100%; - height: 100%; -} - -#sidebarContainer { - left: 0; - right: 0; - height: 200px; - visibility: hidden; - -webkit-transition-duration: 200ms; - -webkit-transition-timing-function: ease; - -moz-transition-duration: 200ms; - -moz-transition-timing-function: ease; - -ms-transition-duration: 200ms; - -ms-transition-timing-function: ease; - -o-transition-duration: 200ms; - -o-transition-timing-function: ease; - transition-duration: 200ms; - transition-timing-function: ease; - -} -html[dir='ltr'] #sidebarContainer { - -webkit-transition-property: top; - -moz-transition-property: top; - -ms-transition-property: top; - -o-transition-property: top; - transition-property: top; - top: -200px; -} -html[dir='rtl'] #sidebarContainer { - -webkit-transition-property: top; - -ms-transition-property: top; - -o-transition-property: top; - transition-property: top; - top: -200px; -} - -#outerContainer.sidebarMoving > #sidebarContainer, -#outerContainer.sidebarOpen > #sidebarContainer { - visibility: visible; -} -html[dir='ltr'] #outerContainer.sidebarOpen > #sidebarContainer { - left: 0px; -} -html[dir='rtl'] #outerContainer.sidebarOpen > #sidebarContainer { - right: 0px; -} - -#mainContainer { - top: 0; - right: 0; - bottom: 0; - left: 0; - min-width: 320px; - -webkit-transition-duration: 200ms; - -webkit-transition-timing-function: ease; - -moz-transition-duration: 200ms; - -moz-transition-timing-function: ease; - -ms-transition-duration: 200ms; - -ms-transition-timing-function: ease; - -o-transition-duration: 200ms; - -o-transition-timing-function: ease; - transition-duration: 200ms; - transition-timing-function: ease; -} -html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer { - -webkit-transition-property: left; - -moz-transition-property: left; - -ms-transition-property: left; - -o-transition-property: left; - transition-property: left; - left: 200px; -} -html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer { - -webkit-transition-property: right; - -moz-transition-property: right; - -ms-transition-property: right; - -o-transition-property: right; - transition-property: right; - right: 200px; -} - -#sidebarContent { - top: 32px; - bottom: 0; - overflow: auto; - height: 200px; - - background-color: hsla(0,0%,0%,.1); - box-shadow: inset -1px 0 0 hsla(0,0%,0%,.25); -} -html[dir='ltr'] #sidebarContent { - left: 0; -} -html[dir='rtl'] #sidebarContent { - right: 0; -} - -#viewerContainer { - overflow: auto; - box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05); - top: 32px; - right: 0; - bottom: 0; - left: 0; - height: 480px; - width: 640px; -} - -.toolbar { - left: 0; - right: 0; - height: 32px; - z-index: 9999; - cursor: default; -} - -#toolbarContainer { - width: 100%; -} - -#toolbarSidebar { - width: 200px; - height: 32px; - background-image: url(../extlib/pdf.js/web/images/texture.png), - -webkit-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - -moz-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - -ms-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - -o-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); - box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25), - - inset 0 -1px 0 hsla(0,0%,100%,.05), - 0 1px 0 hsla(0,0%,0%,.15), - 0 0 1px hsla(0,0%,0%,.1); -} - -#toolbarViewer, .findbar { - position: relative; - height: 32px; - background-color: #474747; /* IE9 */ - background-image: url(../extlib/pdf.js/web/images/texture.png), - -webkit-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - -moz-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - -ms-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - -o-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); - background-image: url(../extlib/pdf.js/web/images/texture.png), - linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); - box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08), - inset 0 1px 1px hsla(0,0%,0%,.15), - inset 0 -1px 0 hsla(0,0%,100%,.05), - 0 1px 0 hsla(0,0%,0%,.15), - 0 1px 1px hsla(0,0%,0%,.1); -} - -.findbar { - top: 64px; - z-index: 10000; - height: 32px; - - min-width: 16px; - padding: 0px 6px 0px 6px; - margin: 4px 2px 4px 2px; - color: hsl(0,0%,85%); - font-size: 12px; - line-height: 14px; - text-align: left; - cursor: default; -} - -html[dir='ltr'] .findbar { - left: 68px; -} - -html[dir='rtl'] .findbar { - right: 68px; -} - -.findbar label { - -webkit-user-select: none; - -moz-user-select: none; -} - -#findInput[data-status="pending"] { - background-image: url(../extlib/pdf.js/web/images/loading-small.png); - background-repeat: no-repeat; - background-position: right; -} - -.doorHanger { - border: 1px solid hsla(0,0%,0%,.5); - border-radius: 2px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} -.doorHanger:after, .doorHanger:before { - bottom: 100%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - pointer-events: none; -} -.doorHanger:after { - border-bottom-color: hsla(0,0%,32%,.99); - border-width: 8px; -} -.doorHanger:before { - border-bottom-color: hsla(0,0%,0%,.5); - border-width: 9px; -} - -html[dir='ltr'] .doorHanger:after { - left: 13px; - margin-left: -8px; -} - -html[dir='ltr'] .doorHanger:before { - left: 13px; - margin-left: -9px; -} - -html[dir='rtl'] .doorHanger:after { - right: 13px; - margin-right: -8px; -} - -html[dir='rtl'] .doorHanger:before { - right: 13px; - margin-right: -9px; -} - -#findMsg { - font-style: italic; - color: #A6B7D0; -} - -.notFound { - background-color: rgb(255, 137, 153); -} - -html[dir='ltr'] #toolbarViewerLeft { - margin-left: -1px; -} -html[dir='rtl'] #toolbarViewerRight { - margin-left: -1px; -} - - -html[dir='ltr'] #toolbarViewerLeft, -html[dir='rtl'] #toolbarViewerRight { - position: absolute; - top: 0; - left: 0; -} -html[dir='ltr'] #toolbarViewerRight, -html[dir='rtl'] #toolbarViewerLeft { - position: absolute; - top: 0; - right: 0; -} -html[dir='ltr'] #toolbarViewerLeft > *, -html[dir='ltr'] #toolbarViewerMiddle > *, -html[dir='ltr'] #toolbarViewerRight > *, -html[dir='ltr'] .findbar > * { - float: left; -} -html[dir='rtl'] #toolbarViewerLeft > *, -html[dir='rtl'] #toolbarViewerMiddle > *, -html[dir='rtl'] #toolbarViewerRight > *, -html[dir='rtl'] .findbar > * { - float: right; -} - -html[dir='ltr'] .splitToolbarButton { - margin: 3px 2px 4px 0; - display: inline-block; -} -html[dir='rtl'] .splitToolbarButton { - margin: 3px 0 4px 2px; - display: inline-block; -} -html[dir='ltr'] .splitToolbarButton > .toolbarButton { - border-radius: 0; - float: left; -} -html[dir='rtl'] .splitToolbarButton > .toolbarButton { - border-radius: 0; - float: right; -} - -.toolbarButton { - border: 0 none; - background-color: rgba(0, 0, 0, 0); - width: 32px; - height: 25px; -} - -.toolbarButton > span { - display: inline-block; - width: 0; - height: 0; - overflow: hidden; -} - -.toolbarButton[disabled] { - opacity: .5; -} - -.toolbarButton.group { - margin-right: 0; -} - -.splitToolbarButton.toggled .toolbarButton { - margin: 0; -} - -.splitToolbarButton:hover > .toolbarButton, -.splitToolbarButton:focus > .toolbarButton, -.splitToolbarButton.toggled > .toolbarButton, -.toolbarButton.textButton { - background-color: hsla(0,0%,0%,.12); - background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - border: 1px solid hsla(0,0%,0%,.35); - border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42); - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.15) inset, - 0 1px 0 hsla(0,0%,100%,.05); - -webkit-transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 150ms; - -webkit-transition-timing-function: ease; - -moz-transition-property: background-color, border-color, box-shadow; - -moz-transition-duration: 150ms; - -moz-transition-timing-function: ease; - -ms-transition-property: background-color, border-color, box-shadow; - -ms-transition-duration: 150ms; - -ms-transition-timing-function: ease; - -o-transition-property: background-color, border-color, box-shadow; - -o-transition-duration: 150ms; - -o-transition-timing-function: ease; - transition-property: background-color, border-color, box-shadow; - transition-duration: 150ms; - transition-timing-function: ease; - -} -.splitToolbarButton > .toolbarButton:hover, -.splitToolbarButton > .toolbarButton:focus, -.dropdownToolbarButton:hover, -.toolbarButton.textButton:hover, -.toolbarButton.textButton:focus { - background-color: hsla(0,0%,0%,.2); - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.15) inset, - 0 0 1px hsla(0,0%,0%,.05); - z-index: 199; -} -html[dir='ltr'] .splitToolbarButton > .toolbarButton:first-child, -html[dir='rtl'] .splitToolbarButton > .toolbarButton:last-child { - position: relative; - margin: 0; - margin-right: -1px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - border-right-color: transparent; -} -html[dir='ltr'] .splitToolbarButton > .toolbarButton:last-child, -html[dir='rtl'] .splitToolbarButton > .toolbarButton:first-child { - position: relative; - margin: 0; - margin-left: -1px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - border-left-color: transparent; -} -.splitToolbarButtonSeparator { - padding: 8px 0; - width: 1px; - background-color: hsla(0,0%,00%,.5); - z-index: 99; - box-shadow: 0 0 0 1px hsla(0,0%,100%,.08); - display: inline-block; - margin: 5px 0; -} -html[dir='ltr'] .splitToolbarButtonSeparator { - float: left; -} -html[dir='rtl'] .splitToolbarButtonSeparator { - float: right; -} -.splitToolbarButton:hover > .splitToolbarButtonSeparator, -.splitToolbarButton.toggled > .splitToolbarButtonSeparator { - padding: 12px 0; - margin: 1px 0; - box-shadow: 0 0 0 1px hsla(0,0%,100%,.03); - -webkit-transition-property: padding; - -webkit-transition-duration: 10ms; - -webkit-transition-timing-function: ease; - -moz-transition-property: padding; - -moz-transition-duration: 10ms; - -moz-transition-timing-function: ease; - -ms-transition-property: padding; - -ms-transition-duration: 10ms; - -ms-transition-timing-function: ease; - -o-transition-property: padding; - -o-transition-duration: 10ms; - -o-transition-timing-function: ease; - transition-property: padding; - transition-duration: 10ms; - transition-timing-function: ease; -} - -.toolbarButton, -.dropdownToolbarButton { - min-width: 16px; - padding: 2px 6px 0; - border: 1px solid transparent; - border-radius: 2px; - color: hsl(0,0%,95%); - font-size: 12px; - line-height: 14px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - /* Opera does not support user-select, use <... unselectable="on"> instead */ - cursor: default; - -webkit-transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 150ms; - -webkit-transition-timing-function: ease; - -moz-transition-property: background-color, border-color, box-shadow; - -moz-transition-duration: 150ms; - -moz-transition-timing-function: ease; - -ms-transition-property: background-color, border-color, box-shadow; - -ms-transition-duration: 150ms; - -ms-transition-timing-function: ease; - -o-transition-property: background-color, border-color, box-shadow; - -o-transition-duration: 150ms; - -o-transition-timing-function: ease; - transition-property: background-color, border-color, box-shadow; - transition-duration: 150ms; - transition-timing-function: ease; -} - -html[dir='ltr'] .toolbarButton, -html[dir='ltr'] .dropdownToolbarButton { - margin: 3px 2px 4px 0; -} -html[dir='rtl'] .toolbarButton, -html[dir='rtl'] .dropdownToolbarButton { - margin: 3px 0 4px 2px; -} - -.toolbarButton:hover, -.toolbarButton:focus, -.dropdownToolbarButton { - background-color: hsla(0,0%,0%,.12); - background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - border: 1px solid hsla(0,0%,0%,.35); - border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42); - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.15) inset, - 0 1px 0 hsla(0,0%,100%,.05); -} - -.toolbarButton:hover:active, -.dropdownToolbarButton:hover:active { - background-color: hsla(0,0%,0%,.2); - background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45); - box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2) inset, - 0 1px 0 hsla(0,0%,100%,.05); - -webkit-transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 10ms; - -webkit-transition-timing-function: linear; - -moz-transition-property: background-color, border-color, box-shadow; - -moz-transition-duration: 10ms; - -moz-transition-timing-function: linear; - -ms-transition-property: background-color, border-color, box-shadow; - -ms-transition-duration: 10ms; - -ms-transition-timing-function: linear; - -o-transition-property: background-color, border-color, box-shadow; - -o-transition-duration: 10ms; - -o-transition-timing-function: linear; - transition-property: background-color, border-color, box-shadow; - transition-duration: 10ms; - transition-timing-function: linear; -} - -.toolbarButton.toggled, -.splitToolbarButton.toggled > .toolbarButton.toggled { - background-color: hsla(0,0%,0%,.3); - background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5); - box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2) inset, - 0 1px 0 hsla(0,0%,100%,.05); - -webkit-transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 10ms; - -webkit-transition-timing-function: linear; - -moz-transition-property: background-color, border-color, box-shadow; - -moz-transition-duration: 10ms; - -moz-transition-timing-function: linear; - -ms-transition-property: background-color, border-color, box-shadow; - -ms-transition-duration: 10ms; - -ms-transition-timing-function: linear; - -o-transition-property: background-color, border-color, box-shadow; - -o-transition-duration: 10ms; - -o-transition-timing-function: linear; - transition-property: background-color, border-color, box-shadow; - transition-duration: 10ms; - transition-timing-function: linear; -} - -.toolbarButton.toggled:hover:active, -.splitToolbarButton.toggled > .toolbarButton.toggled:hover:active { - background-color: hsla(0,0%,0%,.4); - border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.5) hsla(0,0%,0%,.55); - box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset, - 0 0 1px hsla(0,0%,0%,.3) inset, - 0 1px 0 hsla(0,0%,100%,.05); -} - -.dropdownToolbarButton { - width: 120px; - max-width: 120px; - padding: 3px 2px 2px; - overflow: hidden; - background: url(../extlib/pdf.js/web/images/toolbarButton-menuArrows.png) no-repeat; -} -html[dir='ltr'] .dropdownToolbarButton { - background-position: 95%; -} -html[dir='rtl'] .dropdownToolbarButton { - background-position: 5%; -} - -.dropdownToolbarButton > select { - -webkit-appearance: none; - -moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */ - min-width: 140px; - font-size: 12px; - color: hsl(0,0%,95%); - margin: 0; - padding: 0; - border: none; - background: rgba(0,0,0,0); /* Opera does not support 'transparent' - {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/auth/forgot_password.html b/mediagoblin/templates/mediagoblin/auth/forgot_password.html index 46aeddef..6cfd2c85 100644 --- a/mediagoblin/templates/mediagoblin/auth/forgot_password.html +++ b/mediagoblin/templates/mediagoblin/auth/forgot_password.html @@ -29,7 +29,7 @@ {{ csrf_token }}

{% trans %}Recover password{% endtrans %}

- {{ wtforms_util.render_divs(fp_form) }} + {{ wtforms_util.render_divs(fp_form, True) }}
diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 4a39059d..3329b5d0 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -29,7 +29,7 @@ {%- endblock %} {% block mediagoblin_content %} -
{{ csrf_token }}
@@ -41,15 +41,19 @@ {% endif %} {% if allow_registration %}

- {% trans %}Don't have an account yet?{% endtrans %} + {% trans %}Don't have an account yet?{% endtrans %} + {%- trans %}Create one here!{% endtrans %}

{% endif %} - {{ wtforms_util.render_divs(login_form) }} -

- - {% trans %}Forgot your password?{% endtrans %} -

+ {% template_hook("login_link") %} + {{ wtforms_util.render_divs(login_form, True) }} + {% if pass_auth %} +

+ + {% trans %}Forgot your password?{% endtrans %} +

+ {% endif %}
diff --git a/mediagoblin/templates/mediagoblin/auth/register.html b/mediagoblin/templates/mediagoblin/auth/register.html index 6dff0207..a7b8033f 100644 --- a/mediagoblin/templates/mediagoblin/auth/register.html +++ b/mediagoblin/templates/mediagoblin/auth/register.html @@ -30,11 +30,12 @@ {% block mediagoblin_content %} -

{% trans %}Create an account!{% endtrans %}

- {{ wtforms_util.render_divs(register_form) }} + {% template_hook("register_link") %} + {{ wtforms_util.render_divs(register_form, True) }} {{ csrf_token }}
- - {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index b52b65e7..575ddf42 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -34,6 +34,8 @@ src="{{ request.staticdirect('/js/extlib/jquery.js') }}"> + {# For clarification, the difference between the extra_head.html template # and the head template hook is that the former should be used by @@ -57,6 +59,12 @@
{%- if request.user %} {% if request.user and request.user.status == 'active' %} + + {% set notification_count = request.notifications.get_notification_count(request.user.id) %} + {% if notification_count %} + + {{ notification_count }} + {% endif %}
{% elif request.user and request.user.status == "needs_email_verification" %} @@ -67,7 +75,7 @@ {% trans %}Verify your email!{% endtrans %} or {% trans %}log out{% endtrans %} {% endif %} - {%- else %} + {%- elif auth %} {%- trans %}Log in{% endtrans -%} @@ -112,6 +120,7 @@

{% endif %} + {% include 'mediagoblin/fragments/header_notifications.html' %}
{% endif %} diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html index 544ee146..9ef28a4d 100644 --- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html +++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html @@ -17,19 +17,25 @@ #} {% if request.user %} -

{% trans %}Explore{% endtrans %}

-{% else %} -

{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}

- -

{% trans %}This site is running MediaGoblin, an extraordinarily great piece of media hosting software.{% endtrans %}

-

{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}

- {% if allow_registration %} -

{% trans %}Don't have one yet? It's easy!{% endtrans %}

- {% trans register_url=request.urlgen('mediagoblin.auth.register') -%} - Create an account at this site - or - Set up MediaGoblin on your own server - {%- endtrans %} +

{% trans %}Explore{% endtrans %}

+ {% else %} +

{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}

+ +

{% trans %}This site is running MediaGoblin, an extraordinarily great piece of media hosting software.{% endtrans %}

+ {% if auth %} +

{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}

+ {% if allow_registration %} +

{% trans %}Don't have one yet? It's easy!{% endtrans %}

+ {% trans register_url=request.urlgen('mediagoblin.auth.register') -%} + Create an account at this site + or + {%- endtrans %} + {% endif %} + {% endif %} + {% trans %} + Set up MediaGoblin on your own server + {%- endtrans %} + +
{% endif %} -
-{% endif %} + diff --git a/mediagoblin/templates/mediagoblin/edit/change_pass.html b/mediagoblin/templates/mediagoblin/edit/change_pass.html index ff909b07..2a1ffee0 100644 --- a/mediagoblin/templates/mediagoblin/edit/change_pass.html +++ b/mediagoblin/templates/mediagoblin/edit/change_pass.html @@ -39,7 +39,7 @@ Changing {{ username }}'s password {%- endtrans -%} - {{ wtforms_util.render_divs(form) }} + {{ wtforms_util.render_divs(form, True) }} {{ csrf_token }}
+ {% if pass_auth is defined %}

{% trans %}Change your password.{% endtrans %}

-
-

{{ form.wants_comment_notification }} - {{ wtforms_util.render_label(form.wants_comment_notification) }}

-
- {{- wtforms_util.render_field_div(form.license_preference) }} -
+ {% endif %} + {% template_hook("edit_link") %} + {{ wtforms_util.render_divs(form, True) }} +
{{ csrf_token }}
diff --git a/mediagoblin/templates/mediagoblin/edit/verification.txt b/mediagoblin/templates/mediagoblin/edit/verification.txt new file mode 100644 index 00000000..d53cd5e8 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/edit/verification.txt @@ -0,0 +1,29 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +-#} + +{% trans username=username, verification_url=verification_url|safe -%} +Hi, + +We wanted to verify that you are {{ username }}. If this is the case, then +please follow the link below to verify your new email address. + +{{ verification_url }} + +If you are not {{ username }} or didn't request an email change, you can ignore +this email. +{%- endtrans %} diff --git a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html new file mode 100644 index 00000000..613100aa --- /dev/null +++ b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html @@ -0,0 +1,40 @@ +{% set notifications = request.notifications.get_notifications(request.user.id) %} +{% if notifications %} +
+

{% trans %}New comments{% endtrans %}

+ +
+{% endif %} diff --git a/mediagoblin/templates/mediagoblin/media_displays/pdf.html b/mediagoblin/templates/mediagoblin/media_displays/pdf.html index e946f3ab..9319e87c 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/pdf.html +++ b/mediagoblin/templates/mediagoblin/media_displays/pdf.html @@ -46,19 +46,21 @@ {%- endblock %} {% block mediagoblin_media %} -{% if pdf_js %} - - -{% else %} - - {% trans media_title=media.title -%} Image for {{ media_title}}{% endtrans %} - -{% endif %} + {% if pdf_js %} + + {% else %} + + 
+           {%- trans media_title=media.title -%}
+             Image for {{ media_title}}
+           {%- endtrans %} + + {% endif %} {% endblock %} {% block mediagoblin_sidebar %} diff --git a/mediagoblin/templates/mediagoblin/media_displays/stl.html b/mediagoblin/templates/mediagoblin/media_displays/stl.html index a89e0b4f..bc12ce4e 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/stl.html +++ b/mediagoblin/templates/mediagoblin/media_displays/stl.html @@ -108,32 +108,26 @@ window.show_things = function () {
- + {%- trans %}Perspective{% endtrans -%} - + {%- trans %}Front{% endtrans -%} - + {%- trans %}Top{% endtrans -%} - + {%- trans %}Side{% endtrans -%} {% if media.media_data.file_type == "stl" %} + onclick="show_things();"> {%- trans %}WebGL{% endtrans -%} {% endif %} {%- trans %}Download model{% endtrans -%} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 1e64ae07..b10ef3be 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -81,6 +81,7 @@ user= media.get_uploader.username, media_id=media.id) %} {% trans %}Delete{% endtrans %} + {% endif %} {% autoescape False %}

{{ media.description_html }}

@@ -94,6 +95,8 @@ class="button_action" id="button_addcomment" title="Add a comment"> {% trans %}Add a comment{% endtrans %} + {% include "mediagoblin/utils/comment-subscription.html" %} + {% endif %} {% if request.user %}
Subscribe to comments + + {% else %} + Silence comments + + {% endif %} +{%- endif %} diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html index be6976c2..e079274e 100644 --- a/mediagoblin/templates/mediagoblin/utils/wtforms.html +++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html @@ -33,25 +33,37 @@ {%- endmacro %} {# Generically render a field #} -{% macro render_field_div(field) %} - {{- render_label_p(field) }} -
- {{ field }} - {%- if field.errors -%} - {% for error in field.errors %} -

{{ error }}

- {% endfor %} - {%- endif %} - {%- if field.description %} -

{{ field.description|safe }}

- {%- endif %} -
+{% macro render_field_div(field, autofocus_first=False) %} + {% if field.type == 'BooleanField' %} + {{ render_bool(field) }} + {% else %} + {{- render_label_p(field) }} +
+ {% if autofocus_first %} + {{ field(autofocus=True) }} + {% else %} + {{ field }} + {% endif %} + {%- if field.errors -%} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {%- endif %} + {%- if field.description %} +

{{ field.description|safe }}

+ {%- endif %} +
+ {% endif %} {%- endmacro %} {# Auto-render a form as a series of divs #} -{% macro render_divs(form) -%} +{% macro render_divs(form, autofocus_first=False) -%} {% for field in form %} - {{ render_field_div(field) }} + {% if autofocus_first and loop.first %} + {{ render_field_div(field, True) }} + {% else %} + {{ render_field_div(field) }} + {% endif %} {% endfor %} {%- endmacro %} @@ -74,3 +86,19 @@ {% endfor %} {%- endmacro %} + +{# Render a boolean field #} +{% macro render_bool(field) %} +
+ + {%- if field.errors -%} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endif %} +
+{% endmacro %} + diff --git a/mediagoblin/tests/appconfig_context_modified.ini b/mediagoblin/tests/appconfig_context_modified.ini index 80ca69b1..cc6721f5 100644 --- a/mediagoblin/tests/appconfig_context_modified.ini +++ b/mediagoblin/tests/appconfig_context_modified.ini @@ -3,8 +3,9 @@ direct_remote_path = /test_static/ email_sender_address = "notice@mediagoblin.example.org" email_debug_mode = true -# TODO: Switch to using an in-memory database -sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" +#Runs with an in-memory sqlite db for speed. +sql_engine = "sqlite://" +run_migrations = true # Celery shouldn't be set up by the application as it's setup via # mediagoblin.init.celery.from_celery diff --git a/mediagoblin/tests/appconfig_static_plugin.ini b/mediagoblin/tests/appconfig_static_plugin.ini index dc251171..5ce5c5bd 100644 --- a/mediagoblin/tests/appconfig_static_plugin.ini +++ b/mediagoblin/tests/appconfig_static_plugin.ini @@ -3,8 +3,9 @@ direct_remote_path = /test_static/ email_sender_address = "notice@mediagoblin.example.org" email_debug_mode = true -# TODO: Switch to using an in-memory database -sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" +#Runs with an in-memory sqlite db for speed. +sql_engine = "sqlite://" +run_migrations = true # Celery shouldn't be set up by the application as it's setup via # mediagoblin.init.celery.from_celery diff --git a/mediagoblin/tests/auth_configs/__init__.py b/mediagoblin/tests/auth_configs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini b/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini new file mode 100644 index 00000000..a64e9e40 --- /dev/null +++ b/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini @@ -0,0 +1,25 @@ +[mediagoblin] +direct_remote_path = /test_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true + +# TODO: Switch to using an in-memory database +sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" + +# Celery shouldn't be set up by the application as it's setup via +# mediagoblin.init.celery.from_celery +celery_setup_elsewhere = true + +[storage:publicstore] +base_dir = %(here)s/user_dev/media/public +base_url = /mgoblin_media/ + +[storage:queuestore] +base_dir = %(here)s/user_dev/media/queue + +[celery] +CELERY_ALWAYS_EAGER = true +CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db" +BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db" + +[plugins] diff --git a/mediagoblin/tests/auth_configs/openid_appconfig.ini b/mediagoblin/tests/auth_configs/openid_appconfig.ini new file mode 100644 index 00000000..c2bd82fd --- /dev/null +++ b/mediagoblin/tests/auth_configs/openid_appconfig.ini @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +[mediagoblin] +direct_remote_path = /test_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true + +# TODO: Switch to using an in-memory database +sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" + +# Celery shouldn't be set up by the application as it's setup via +# mediagoblin.init.celery.from_celery +celery_setup_elsewhere = true + +[storage:publicstore] +base_dir = %(here)s/user_dev/media/public +base_url = /mgoblin_media/ + +[storage:queuestore] +base_dir = %(here)s/user_dev/media/queue + +[celery] +CELERY_ALWAYS_EAGER = true +CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db" +BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db" + +[plugins] +[[mediagoblin.plugins.openid]] diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index 755727f9..61503d32 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -13,54 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - import urlparse -import datetime +import pkg_resources +import pytest from mediagoblin import mg_globals -from mediagoblin.auth import lib as auth_lib from mediagoblin.db.models import User -from mediagoblin.tests.tools import fixture_add_user +from mediagoblin.tests.tools import get_app, fixture_add_user from mediagoblin.tools import template, mail - - -######################## -# Test bcrypt auth funcs -######################## - -def test_bcrypt_check_password(): - # Check known 'lollerskates' password against check function - assert auth_lib.bcrypt_check_password( - 'lollerskates', - '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO') - - assert not auth_lib.bcrypt_check_password( - 'notthepassword', - '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO') - - # Same thing, but with extra fake salt. - assert not auth_lib.bcrypt_check_password( - 'notthepassword', - '$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6', - '3><7R45417') - - -def test_bcrypt_gen_password_hash(): - pw = 'youwillneverguessthis' - - # Normal password hash generation, and check on that hash - hashed_pw = auth_lib.bcrypt_gen_password_hash(pw) - assert auth_lib.bcrypt_check_password( - pw, hashed_pw) - assert not auth_lib.bcrypt_check_password( - 'notthepassword', hashed_pw) - - # Same thing, extra salt. - hashed_pw = auth_lib.bcrypt_gen_password_hash(pw, '3><7R45417') - assert auth_lib.bcrypt_check_password( - pw, hashed_pw, '3><7R45417') - assert not auth_lib.bcrypt_check_password( - 'notthepassword', hashed_pw, '3><7R45417') +from mediagoblin.auth import tools as auth_tools def test_register_views(test_app): @@ -132,8 +93,8 @@ def test_register_views(test_app): assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT ## Make sure user is in place - new_user = mg_globals.database.User.find_one( - {'username': u'happygirl'}) + new_user = mg_globals.database.User.query.filter_by( + username=u'happygirl').first() assert new_user assert new_user.status == u'needs_email_verification' assert new_user.email_verified == False @@ -156,24 +117,19 @@ def test_register_views(test_app): assert path == u'/auth/verify_email/' parsed_get_params = urlparse.parse_qs(get_params) - ### user should have these same parameters - assert parsed_get_params['userid'] == [ - unicode(new_user.id)] - assert parsed_get_params['token'] == [ - new_user.verification_key] - ## Try verifying with bs verification key, shouldn't work template.clear_test_template_context() response = test_app.get( - "/auth/verify_email/?userid=%s&token=total_bs" % unicode( - new_user.id)) + "/auth/verify_email/?token=total_bs") response.follow() - context = template.TEMPLATE_TEST_CONTEXT[ - 'mediagoblin/user_pages/user.html'] + + # Correct redirect? + assert urlparse.urlsplit(response.location)[2] == '/' + # assert context['verification_successful'] == True # TODO: Would be good to test messages here when we can do so... - new_user = mg_globals.database.User.find_one( - {'username': u'happygirl'}) + new_user = mg_globals.database.User.query.filter_by( + username=u'happygirl').first() assert new_user assert new_user.status == u'needs_email_verification' assert new_user.email_verified == False @@ -186,8 +142,8 @@ def test_register_views(test_app): 'mediagoblin/user_pages/user.html'] # assert context['verification_successful'] == True # TODO: Would be good to test messages here when we can do so... - new_user = mg_globals.database.User.find_one( - {'username': u'happygirl'}) + new_user = mg_globals.database.User.query.filter_by( + username=u'happygirl').first() assert new_user assert new_user.status == u'active' assert new_user.email_verified == True @@ -233,35 +189,17 @@ def test_register_views(test_app): path = urlparse.urlsplit(email_context['verification_url'])[2] get_params = urlparse.urlsplit(email_context['verification_url'])[3] - assert path == u'/auth/forgot_password/verify/' parsed_get_params = urlparse.parse_qs(get_params) - - # user should have matching parameters - new_user = mg_globals.database.User.find_one({'username': u'happygirl'}) - assert parsed_get_params['userid'] == [unicode(new_user.id)] - assert parsed_get_params['token'] == [new_user.fp_verification_key] - - ### The forgotten password token should be set to expire in ~ 10 days - # A few ticks have expired so there are only 9 full days left... - assert (new_user.fp_token_expire - datetime.datetime.now()).days == 9 + assert path == u'/auth/forgot_password/verify/' ## Try using a bs password-changing verification key, shouldn't work template.clear_test_template_context() response = test_app.get( - "/auth/forgot_password/verify/?userid=%s&token=total_bs" % unicode( - new_user.id), status=404) - assert response.status.split()[0] == u'404' # status="404 NOT FOUND" + "/auth/forgot_password/verify/?token=total_bs") + response.follow() - ## Try using an expired token to change password, shouldn't work - template.clear_test_template_context() - new_user = mg_globals.database.User.find_one({'username': u'happygirl'}) - real_token_expiration = new_user.fp_token_expire - new_user.fp_token_expire = datetime.datetime.now() - new_user.save() - response = test_app.get("%s?%s" % (path, get_params), status=404) - assert response.status.split()[0] == u'404' # status="404 NOT FOUND" - new_user.fp_token_expire = real_token_expiration - new_user.save() + # Correct redirect? + assert urlparse.urlsplit(response.location)[2] == '/' ## Verify step 1 of password-change works -- can see form to change password template.clear_test_template_context() @@ -272,7 +210,6 @@ def test_register_views(test_app): template.clear_test_template_context() response = test_app.post( '/auth/forgot_password/verify/', { - 'userid': parsed_get_params['userid'], 'password': 'iamveryveryhappy', 'token': parsed_get_params['token']}) response.follow() @@ -298,6 +235,7 @@ def test_authentication_views(test_app): # Make a new user test_user = fixture_add_user(active_user=False) + # Get login # --------- test_app.get('/auth/login/') @@ -310,7 +248,6 @@ def test_authentication_views(test_app): context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] form = context['login_form'] assert form.username.errors == [u'This field is required.'] - assert form.password.errors == [u'This field is required.'] # Failed login - blank user # ------------------------- @@ -328,9 +265,7 @@ def test_authentication_views(test_app): response = test_app.post( '/auth/login/', { 'username': u'chris'}) - context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] - form = context['login_form'] - assert form.password.errors == [u'This field is required.'] + assert 'mediagoblin/auth/login.html' in template.TEMPLATE_TEST_CONTEXT # Failed login - bad user # ----------------------- @@ -394,3 +329,47 @@ def test_authentication_views(test_app): 'password': 'toast', 'next' : '/u/chris/'}) assert urlparse.urlsplit(response.location)[2] == '/u/chris/' + + +@pytest.fixture() +def authentication_disabled_app(request): + return get_app( + request, + mgoblin_config=pkg_resources.resource_filename( + 'mediagoblin.tests.auth_configs', + 'authentication_disabled_appconfig.ini')) + + +def test_authentication_disabled_app(authentication_disabled_app): + # app.auth should = false + assert mg_globals.app.auth is False + + # Try to visit register page + template.clear_test_template_context() + response = authentication_disabled_app.get('/auth/register/') + response.follow() + + # Correct redirect? + assert urlparse.urlsplit(response.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + # Try to vist login page + template.clear_test_template_context() + response = authentication_disabled_app.get('/auth/login/') + response.follow() + + # Correct redirect? + assert urlparse.urlsplit(response.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + ## Test check_login_simple should return None + assert auth_tools.check_login_simple('test', 'simple') is None + + # Try to visit the forgot password page + template.clear_test_template_context() + response = authentication_disabled_app.get('/auth/register/') + response.follow() + + # Correct redirect? + assert urlparse.urlsplit(response.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT diff --git a/mediagoblin/tests/test_basic_auth.py b/mediagoblin/tests/test_basic_auth.py new file mode 100644 index 00000000..cdd80fca --- /dev/null +++ b/mediagoblin/tests/test_basic_auth.py @@ -0,0 +1,59 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . +from mediagoblin.plugins.basic_auth import tools as auth_tools +from mediagoblin.tools.testing import _activate_testing + +_activate_testing() + + +######################## +# Test bcrypt auth funcs +######################## + + +def test_bcrypt_check_password(): + # Check known 'lollerskates' password against check function + assert auth_tools.bcrypt_check_password( + 'lollerskates', + '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO') + + assert not auth_tools.bcrypt_check_password( + 'notthepassword', + '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO') + + # Same thing, but with extra fake salt. + assert not auth_tools.bcrypt_check_password( + 'notthepassword', + '$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6', + '3><7R45417') + + +def test_bcrypt_gen_password_hash(): + pw = 'youwillneverguessthis' + + # Normal password hash generation, and check on that hash + hashed_pw = auth_tools.bcrypt_gen_password_hash(pw) + assert auth_tools.bcrypt_check_password( + pw, hashed_pw) + assert not auth_tools.bcrypt_check_password( + 'notthepassword', hashed_pw) + + # Same thing, extra salt. + hashed_pw = auth_tools.bcrypt_gen_password_hash(pw, '3><7R45417') + assert auth_tools.bcrypt_check_password( + pw, hashed_pw, '3><7R45417') + assert not auth_tools.bcrypt_check_password( + 'notthepassword', hashed_pw, '3><7R45417') diff --git a/mediagoblin/tests/test_celery_setup.py b/mediagoblin/tests/test_celery_setup.py index 5530c6f2..0184436a 100644 --- a/mediagoblin/tests/test_celery_setup.py +++ b/mediagoblin/tests/test_celery_setup.py @@ -48,7 +48,7 @@ def test_setup_celery_from_config(): assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float) assert fake_celery_module.CELERY_RESULT_PERSISTENT is True assert fake_celery_module.CELERY_IMPORTS == [ - 'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task'] + 'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task', 'mediagoblin.notifications.task'] assert fake_celery_module.CELERY_RESULT_BACKEND == 'database' assert fake_celery_module.CELERY_RESULT_DBURI == ( 'sqlite:///' + diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index 08b4f8cf..d70d0478 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -15,13 +15,13 @@ # along with this program. If not, see . import urlparse -import pytest from mediagoblin import mg_globals from mediagoblin.db.models import User from mediagoblin.tests.tools import fixture_add_user -from mediagoblin.tools import template -from mediagoblin.auth.lib import bcrypt_check_password +from mediagoblin import auth +from mediagoblin.tools import template, mail + class TestUserEdit(object): def setup(self): @@ -74,7 +74,7 @@ class TestUserEdit(object): # test_user has to be fetched again in order to have the current values test_user = User.query.filter_by(username=u'chris').first() - assert bcrypt_check_password('123456', test_user.pw_hash) + assert auth.check_password('123456', test_user.pw_hash) # Update current user passwd self.user_password = '123456' @@ -88,7 +88,7 @@ class TestUserEdit(object): }) test_user = User.query.filter_by(username=u'chris').first() - assert not bcrypt_check_password('098765', test_user.pw_hash) + assert not auth.check_password('098765', test_user.pw_hash) def test_change_bio_url(self, test_app): @@ -141,4 +141,68 @@ class TestUserEdit(object): assert form.url.errors == [ u'This address contains errors'] + def test_email_change(self, test_app): + self.login(test_app) + + # Test email already in db + template.clear_test_template_context() + test_app.post( + '/edit/account/', { + 'new_email': 'chris@example.com', + 'password': 'toast'}) + + # Check form errors + context = template.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/edit/edit_account.html'] + assert context['form'].new_email.errors == [ + u'Sorry, a user with that email address already exists.'] + + # Test successful email change + template.clear_test_template_context() + res = test_app.post( + '/edit/account/', { + 'new_email': 'new@example.com', + 'password': 'toast'}) + res.follow() + + # Correct redirect? + assert urlparse.urlsplit(res.location)[2] == '/u/chris/' + + # Make sure we get email verification and try verifying + assert len(mail.EMAIL_TEST_INBOX) == 1 + message = mail.EMAIL_TEST_INBOX.pop() + assert message['To'] == 'new@example.com' + email_context = template.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/edit/verification.txt'] + assert email_context['verification_url'] in \ + message.get_payload(decode=True) + + path = urlparse.urlsplit(email_context['verification_url'])[2] + assert path == u'/edit/verify_email/' + + ## Try verifying with bs verification key, shouldn't work + template.clear_test_template_context() + res = test_app.get( + "/edit/verify_email/?token=total_bs") + res.follow() + + # Correct redirect? + assert urlparse.urlsplit(res.location)[2] == '/' + + # Email shouldn't be saved + email_in_db = mg_globals.database.User.query.filter_by( + email='new@example.com').first() + email = User.query.filter_by(username='chris').first().email + assert email_in_db is None + assert email == 'chris@example.com' + + # Verify email activation works + template.clear_test_template_context() + get_params = urlparse.urlsplit(email_context['verification_url'])[3] + res = test_app.get('%s?%s' % (path, get_params)) + res.follow() + + # New email saved? + email = User.query.filter_by(username='chris').first().email + assert email == 'new@example.com' # test changing the url inproperly diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini index 0466b53b..535cf1c1 100644 --- a/mediagoblin/tests/test_mgoblin_app.ini +++ b/mediagoblin/tests/test_mgoblin_app.ini @@ -3,8 +3,9 @@ direct_remote_path = /test_static/ email_sender_address = "notice@mediagoblin.example.org" email_debug_mode = true -# TODO: Switch to using an in-memory database -sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" +#Runs with an in-memory sqlite db for speed. +sql_engine = "sqlite://" +run_migrations = true # tag parsing tags_max_length = 50 @@ -12,8 +13,6 @@ tags_max_length = 50 # So we can start to test attachments: allow_attachments = True -media_types = mediagoblin.media_types.image, mediagoblin.media_types.pdf - [storage:publicstore] base_dir = %(here)s/user_dev/media/public base_url = /mgoblin_media/ @@ -31,3 +30,7 @@ BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db" [[mediagoblin.plugins.oauth]] [[mediagoblin.plugins.httpapiauth]] [[mediagoblin.plugins.piwigo]] +[[mediagoblin.plugins.basic_auth]] +[[mediagoblin.plugins.openid]] +[[mediagoblin.media_types.image]] +[[mediagoblin.media_types.pdf]] diff --git a/mediagoblin/tests/test_misc.py b/mediagoblin/tests/test_misc.py index 755d863f..43ad0b6d 100644 --- a/mediagoblin/tests/test_misc.py +++ b/mediagoblin/tests/test_misc.py @@ -28,8 +28,10 @@ def test_user_deletes_other_comments(test_app): user_a = fixture_add_user(u"chris_a") user_b = fixture_add_user(u"chris_b") - media_a = fixture_media_entry(uploader=user_a.id, save=False) - media_b = fixture_media_entry(uploader=user_b.id, save=False) + media_a = fixture_media_entry(uploader=user_a.id, save=False, + expunge=False, fake_upload=False) + media_b = fixture_media_entry(uploader=user_b.id, save=False, + expunge=False, fake_upload=False) Session.add(media_a) Session.add(media_b) Session.flush() @@ -79,7 +81,7 @@ def test_user_deletes_other_comments(test_app): def test_media_deletes_broken_attachment(test_app): user_a = fixture_add_user(u"chris_a") - media = fixture_media_entry(uploader=user_a.id, save=False) + media = fixture_media_entry(uploader=user_a.id, save=False, expunge=False) media.attachment_files.append(dict( name=u"some name", filepath=[u"does", u"not", u"exist"], diff --git a/mediagoblin/tests/test_notifications.py b/mediagoblin/tests/test_notifications.py new file mode 100644 index 00000000..d52b8d5a --- /dev/null +++ b/mediagoblin/tests/test_notifications.py @@ -0,0 +1,151 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +import pytest + +import urlparse + +from mediagoblin.tools import template, mail + +from mediagoblin.db.models import Notification, CommentNotification, \ + CommentSubscription +from mediagoblin.db.base import Session + +from mediagoblin.notifications import mark_comment_notification_seen + +from mediagoblin.tests.tools import fixture_add_comment, \ + fixture_media_entry, fixture_add_user, \ + fixture_comment_subscription + + +class TestNotifications: + @pytest.fixture(autouse=True) + def setup(self, test_app): + self.test_app = test_app + + # TODO: Possibly abstract into a decorator like: + # @as_authenticated_user('chris') + self.test_user = fixture_add_user() + + self.current_user = None + + self.login() + + def login(self, username=u'chris', password=u'toast'): + response = self.test_app.post( + '/auth/login/', { + 'username': username, + 'password': password}) + + response.follow() + + assert urlparse.urlsplit(response.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + ctx = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] + + assert Session.merge(ctx['request'].user).username == username + + self.current_user = ctx['request'].user + + def logout(self): + self.test_app.get('/auth/logout/') + self.current_user = None + + @pytest.mark.parametrize('wants_email', [True, False]) + def test_comment_notification(self, wants_email): + ''' + Test + - if a notification is created when posting a comment on + another users media entry. + - that the comment data is consistent and exists. + + ''' + user = fixture_add_user('otherperson', password='nosreprehto', + wants_comment_notification=wants_email) + + user_id = user.id + + media_entry = fixture_media_entry(uploader=user.id, state=u'processed') + + media_entry_id = media_entry.id + + subscription = fixture_comment_subscription(media_entry) + + subscription_id = subscription.id + + media_uri_id = '/u/{0}/m/{1}/'.format(user.username, + media_entry.id) + media_uri_slug = '/u/{0}/m/{1}/'.format(user.username, + media_entry.slug) + + self.test_app.post( + media_uri_id + 'comment/add/', + { + 'comment_content': u'Test comment #42' + } + ) + + notifications = Notification.query.filter_by( + user_id=user.id).all() + + assert len(notifications) == 1 + + notification = notifications[0] + + assert type(notification) == CommentNotification + assert notification.seen == False + assert notification.user_id == user.id + assert notification.subject.get_author.id == self.test_user.id + assert notification.subject.content == u'Test comment #42' + + if wants_email == True: + assert mail.EMAIL_TEST_MBOX_INBOX == [ + {'from': 'notice@mediagoblin.example.org', + 'message': 'Content-Type: text/plain; \ +charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: \ +base64\nSubject: GNU MediaGoblin - chris commented on your \ +post\nFrom: notice@mediagoblin.example.org\nTo: \ +otherperson@example.com\n\nSGkgb3RoZXJwZXJzb24sCmNocmlzIGNvbW1lbnRlZCBvbiB5b3VyIHBvc3QgKGh0dHA6Ly9sb2Nh\nbGhvc3Q6ODAvdS9vdGhlcnBlcnNvbi9tL3NvbWUtdGl0bGUvYy8xLyNjb21tZW50KSBhdCBHTlUg\nTWVkaWFHb2JsaW4KClRlc3QgY29tbWVudCAjNDIKCkdOVSBNZWRpYUdvYmxpbg==\n', + 'to': [u'otherperson@example.com']}] + else: + assert mail.EMAIL_TEST_MBOX_INBOX == [] + + # Save the ids temporarily because of DetachedInstanceError + notification_id = notification.id + comment_id = notification.subject.id + + self.logout() + self.login('otherperson', 'nosreprehto') + + self.test_app.get(media_uri_slug + '/c/{0}/'.format(comment_id)) + + notification = Notification.query.filter_by(id=notification_id).first() + + assert notification.seen == True + + self.test_app.get(media_uri_slug + '/notifications/silence/') + + subscription = CommentSubscription.query.filter_by(id=subscription_id)\ + .first() + + assert subscription.notify == False + + notifications = Notification.query.filter_by( + user_id=user_id).all() + + # User should not have been notified + assert len(notifications) == 1 diff --git a/mediagoblin/tests/test_openid.py b/mediagoblin/tests/test_openid.py new file mode 100644 index 00000000..23a2290e --- /dev/null +++ b/mediagoblin/tests/test_openid.py @@ -0,0 +1,373 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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 . + +import urlparse +import pkg_resources +import pytest +import mock + +openid_consumer = pytest.importorskip( + "openid.consumer.consumer") + +from mediagoblin import mg_globals +from mediagoblin.db.base import Session +from mediagoblin.db.models import User +from mediagoblin.plugins.openid.models import OpenIDUserURL +from mediagoblin.tests.tools import get_app, fixture_add_user +from mediagoblin.tools import template + +# App with plugin enabled +@pytest.fixture() +def openid_plugin_app(request): + return get_app( + request, + mgoblin_config=pkg_resources.resource_filename( + 'mediagoblin.tests.auth_configs', + 'openid_appconfig.ini')) + + +class TestOpenIDPlugin(object): + def _setup(self, openid_plugin_app, value=True, edit=False, delete=False): + if value: + response = openid_consumer.SuccessResponse(mock.Mock(), mock.Mock()) + if edit or delete: + response.identity_url = u'http://add.myopenid.com' + else: + response.identity_url = u'http://real.myopenid.com' + self._finish_verification = mock.Mock(return_value=response) + else: + self._finish_verification = mock.Mock(return_value=False) + + @mock.patch('mediagoblin.plugins.openid.views._response_email', mock.Mock(return_value=None)) + @mock.patch('mediagoblin.plugins.openid.views._response_nickname', mock.Mock(return_value=None)) + @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification) + def _setup_start(self, openid_plugin_app, edit, delete): + if edit: + self._start_verification = mock.Mock(return_value=openid_plugin_app.post( + '/edit/openid/finish/')) + elif delete: + self._start_verification = mock.Mock(return_value=openid_plugin_app.post( + '/edit/openid/delete/finish/')) + else: + self._start_verification = mock.Mock(return_value=openid_plugin_app.post( + '/auth/openid/login/finish/')) + _setup_start(self, openid_plugin_app, edit, delete) + + def test_bad_login(self, openid_plugin_app): + """ Test that attempts to login with invalid paramaters""" + + # Test GET request for auth/register page + res = openid_plugin_app.get('/auth/register/').follow() + + # Make sure it redirected to the correct place + assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/' + + # Test GET request for auth/login page + res = openid_plugin_app.get('/auth/login/') + res.follow() + + # Correct redirect? + assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/' + + # Test GET request for auth/openid/register page + res = openid_plugin_app.get('/auth/openid/register/') + res.follow() + + # Correct redirect? + assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/' + + # Test GET request for auth/openid/login/finish page + res = openid_plugin_app.get('/auth/openid/login/finish/') + res.follow() + + # Correct redirect? + assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/' + + # Test GET request for auth/openid/login page + res = openid_plugin_app.get('/auth/openid/login/') + + # Correct place? + assert 'mediagoblin/plugins/openid/login.html' in template.TEMPLATE_TEST_CONTEXT + + # Try to login with an empty form + template.clear_test_template_context() + openid_plugin_app.post( + '/auth/openid/login/', {}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/login.html'] + form = context['login_form'] + assert form.openid.errors == [u'This field is required.'] + + # Try to login with wrong form values + template.clear_test_template_context() + openid_plugin_app.post( + '/auth/openid/login/', { + 'openid': 'not_a_url.com'}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/login.html'] + form = context['login_form'] + assert form.openid.errors == [u'Please enter a valid url.'] + + # Should be no users in the db + assert User.query.count() == 0 + + # Phony OpenID URl + template.clear_test_template_context() + openid_plugin_app.post( + '/auth/openid/login/', { + 'openid': 'http://phoney.myopenid.com/'}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/login.html'] + form = context['login_form'] + assert form.openid.errors == [u'Sorry, the OpenID server could not be found'] + + def test_login(self, openid_plugin_app): + """Tests that test login and registion with openid""" + # Test finish_login redirects correctly when response = False + self._setup(openid_plugin_app, False) + + @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification) + @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification) + def _test_non_response(): + template.clear_test_template_context() + res = openid_plugin_app.post( + '/auth/openid/login/', { + 'openid': 'http://phoney.myopenid.com/'}) + res.follow() + + # Correct Place? + assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/' + assert 'mediagoblin/plugins/openid/login.html' in template.TEMPLATE_TEST_CONTEXT + _test_non_response() + + # Test login with new openid + # Need to clear_test_template_context before calling _setup + template.clear_test_template_context() + self._setup(openid_plugin_app) + + @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification) + @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification) + def _test_new_user(): + openid_plugin_app.post( + '/auth/openid/login/', { + 'openid': u'http://real.myopenid.com'}) + + # Right place? + assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + # Register User + res = openid_plugin_app.post( + '/auth/openid/register/', { + 'openid': register_form.openid.data, + 'username': u'chris', + 'email': u'chris@example.com'}) + res.follow() + + # Correct place? + assert urlparse.urlsplit(res.location)[2] == '/u/chris/' + assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT + + # No need to test if user is in logged in and verification email + # awaits, since openid uses the register_user function which is + # tested in test_auth + + # Logout User + openid_plugin_app.get('/auth/logout') + + # Get user and detach from session + test_user = mg_globals.database.User.query.filter_by( + username=u'chris').first() + Session.expunge(test_user) + + # Log back in + # Could not get it to work by 'POST'ing to /auth/openid/login/ + template.clear_test_template_context() + res = openid_plugin_app.post( + '/auth/openid/login/finish/', { + 'openid': u'http://real.myopenid.com'}) + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + # Make sure user is in the session + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] + session = context['request'].session + assert session['user_id'] == unicode(test_user.id) + + _test_new_user() + + # Test register with empty form + template.clear_test_template_context() + openid_plugin_app.post( + '/auth/openid/register/', {}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.openid.errors == [u'This field is required.'] + assert register_form.email.errors == [u'This field is required.'] + assert register_form.username.errors == [u'This field is required.'] + + # Try to register with existing username and email + template.clear_test_template_context() + openid_plugin_app.post( + '/auth/openid/register/', { + 'openid': 'http://real.myopenid.com', + 'email': 'chris@example.com', + 'username': 'chris'}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.username.errors == [u'Sorry, a user with that name already exists.'] + assert register_form.email.errors == [u'Sorry, a user with that email address already exists.'] + assert register_form.openid.errors == [u'Sorry, an account is already registered to that OpenID.'] + + def test_add_delete(self, openid_plugin_app): + """Test adding and deleting openids""" + # Add user + test_user = fixture_add_user(password='') + openid = OpenIDUserURL() + openid.openid_url = 'http://real.myopenid.com' + openid.user_id = test_user.id + openid.save() + + # Log user in + template.clear_test_template_context() + self._setup(openid_plugin_app) + + @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification) + @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification) + def _login_user(): + openid_plugin_app.post( + '/auth/openid/login/finish/', { + 'openid': u'http://real.myopenid.com'}) + + _login_user() + + # Try and delete only OpenID url + template.clear_test_template_context() + res = openid_plugin_app.post( + '/edit/openid/delete/', { + 'openid': 'http://real.myopenid.com'}) + assert 'mediagoblin/plugins/openid/delete.html' in template.TEMPLATE_TEST_CONTEXT + + # Add OpenID to user + # Empty form + template.clear_test_template_context() + res = openid_plugin_app.post( + '/edit/openid/', {}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/add.html'] + form = context['form'] + assert form.openid.errors == [u'This field is required.'] + + # Try with a bad url + template.clear_test_template_context() + openid_plugin_app.post( + '/edit/openid/', { + 'openid': u'not_a_url.com'}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/add.html'] + form = context['form'] + assert form.openid.errors == [u'Please enter a valid url.'] + + # Try with a url that's already registered + template.clear_test_template_context() + openid_plugin_app.post( + '/edit/openid/', { + 'openid': 'http://real.myopenid.com'}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/add.html'] + form = context['form'] + assert form.openid.errors == [u'Sorry, an account is already registered to that OpenID.'] + + # Test adding openid to account + # Need to clear_test_template_context before calling _setup + template.clear_test_template_context() + self._setup(openid_plugin_app, edit=True) + + # Need to remove openid_url from db because it was added at setup + openid = OpenIDUserURL.query.filter_by( + openid_url=u'http://add.myopenid.com') + openid.delete() + + @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification) + @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification) + def _test_add(): + # Successful add + template.clear_test_template_context() + res = openid_plugin_app.post( + '/edit/openid/', { + 'openid': u'http://add.myopenid.com'}) + res.follow() + + # Correct place? + assert urlparse.urlsplit(res.location)[2] == '/edit/account/' + assert 'mediagoblin/edit/edit_account.html' in template.TEMPLATE_TEST_CONTEXT + + # OpenID Added? + new_openid = mg_globals.database.OpenIDUserURL.query.filter_by( + openid_url=u'http://add.myopenid.com').first() + assert new_openid + + _test_add() + + # Test deleting openid from account + # Need to clear_test_template_context before calling _setup + template.clear_test_template_context() + self._setup(openid_plugin_app, delete=True) + + # Need to add OpenID back to user because it was deleted during + # patch + openid = OpenIDUserURL() + openid.openid_url = 'http://add.myopenid.com' + openid.user_id = test_user.id + openid.save() + + @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification) + @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification) + def _test_delete(self, test_user): + # Delete openid from user + # Create another user to test deleting OpenID that doesn't belong to them + new_user = fixture_add_user(username='newman') + openid = OpenIDUserURL() + openid.openid_url = 'http://realfake.myopenid.com/' + openid.user_id = new_user.id + openid.save() + + # Try and delete OpenID url that isn't the users + template.clear_test_template_context() + res = openid_plugin_app.post( + '/edit/openid/delete/', { + 'openid': 'http://realfake.myopenid.com/'}) + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/delete.html'] + form = context['form'] + assert form.openid.errors == [u'That OpenID is not registered to this account.'] + + # Delete OpenID + # Kind of weird to POST to delete/finish + template.clear_test_template_context() + res = openid_plugin_app.post( + '/edit/openid/delete/finish/', { + 'openid': u'http://add.myopenid.com'}) + res.follow() + + # Correct place? + assert urlparse.urlsplit(res.location)[2] == '/edit/account/' + assert 'mediagoblin/edit/edit_account.html' in template.TEMPLATE_TEST_CONTEXT + + # OpenID deleted? + new_openid = mg_globals.database.OpenIDUserURL.query.filter_by( + openid_url=u'http://add.myopenid.com').first() + assert not new_openid + + _test_delete(self, test_user) diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index 162b2d19..ac941063 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -26,7 +26,7 @@ from mediagoblin.tests.tools import fixture_add_user from mediagoblin import mg_globals from mediagoblin.db.models import MediaEntry from mediagoblin.tools import template -from mediagoblin.media_types.image import MEDIA_MANAGER as img_MEDIA_MANAGER +from mediagoblin.media_types.image import ImageMediaManager from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \ @@ -77,7 +77,7 @@ class TestSubmission: return {'upload_files': [('file', filename)]} def check_comments(self, request, media_id, count): - comments = request.db.MediaComment.find({'media_entry': media_id}) + comments = request.db.MediaComment.query.filter_by(media_entry=media_id) assert count == len(list(comments)) def test_missing_fields(self): @@ -122,7 +122,7 @@ class TestSubmission: assert 'mediagoblin/user_pages/user.html' in context def check_media(self, request, find_data, count=None): - media = MediaEntry.find(find_data) + media = MediaEntry.query.filter_by(**find_data) if count is not None: assert media.count() == count if count == 0: @@ -219,7 +219,7 @@ class TestSubmission: media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) assert media.media_type == u'mediagoblin.media_types.image' - assert isinstance(media.media_manager, img_MEDIA_MANAGER) + assert isinstance(media.media_manager, ImageMediaManager) assert media.media_manager.entry == media @@ -240,8 +240,8 @@ class TestSubmission: request = context['request'] - media = request.db.MediaEntry.find_one({ - u'title': u'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE'}) + media = request.db.MediaEntry.query.filter_by( + title=u'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE').first() assert media.media_type == 'mediagoblin.media_types.image' @@ -252,7 +252,7 @@ class TestSubmission: response, context = self.do_post({'title': title}, do_follow=True, **self.upload_data(filename)) self.check_url(response, '/u/{0}/'.format(self.test_user.username)) - entry = mg_globals.database.MediaEntry.find_one({'title': title}) + entry = mg_globals.database.MediaEntry.query.filter_by(title=title).first() assert entry.state == 'failed' assert entry.fail_error == u'mediagoblin.processing:BadMediaFail' diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index 2ee39e89..98361adc 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -15,23 +15,22 @@ # along with this program. If not, see . -import sys import os import pkg_resources import shutil -from functools import wraps from paste.deploy import loadapp from webtest import TestApp from mediagoblin import mg_globals -from mediagoblin.db.models import User, MediaEntry, Collection +from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \ + CommentSubscription, CommentNotification from mediagoblin.tools import testing from mediagoblin.init.config import read_mediagoblin_config from mediagoblin.db.base import Session from mediagoblin.meddleware import BaseMeddleware -from mediagoblin.auth.lib import bcrypt_gen_password_hash +from mediagoblin.auth import gen_password_hash from mediagoblin.gmg_commands.dbupdate import run_dbupdate @@ -165,13 +164,13 @@ def assert_db_meets_expected(db, expected): 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']}) + document = collection.query.filter_by(id=expected_document['id']).first() assert document is not None # make sure it exists assert document == expected_document # make sure it matches def fixture_add_user(username=u'chris', password=u'toast', - active_user=True): + active_user=True, wants_comment_notification=True): # Reuse existing user or create a new one test_user = User.query.filter_by(username=username).first() if test_user is None: @@ -179,11 +178,13 @@ def fixture_add_user(username=u'chris', password=u'toast', test_user.username = username test_user.email = username + u'@example.com' if password is not None: - test_user.pw_hash = bcrypt_gen_password_hash(password) + test_user.pw_hash = gen_password_hash(password) if active_user: test_user.email_verified = True test_user.status = u'active' + test_user.wants_comment_notification = wants_comment_notification + test_user.save() # Reload @@ -195,19 +196,79 @@ def fixture_add_user(username=u'chris', password=u'toast', return test_user +def fixture_comment_subscription(entry, notify=True, send_email=None): + if send_email is None: + uploader = User.query.filter_by(id=entry.uploader).first() + send_email = uploader.wants_comment_notification + + cs = CommentSubscription( + media_entry_id=entry.id, + user_id=entry.uploader, + notify=notify, + send_email=send_email) + + cs.save() + + cs = CommentSubscription.query.filter_by(id=cs.id).first() + + Session.expunge(cs) + + return cs + + +def fixture_add_comment_notification(entry_id, subject_id, user_id, + seen=False): + cn = CommentNotification(user_id=user_id, + seen=seen, + subject_id=subject_id) + cn.save() + + cn = CommentNotification.query.filter_by(id=cn.id).first() + + Session.expunge(cn) + + return cn + + def fixture_media_entry(title=u"Some title", slug=None, - uploader=None, save=True, gen_slug=True): + uploader=None, save=True, gen_slug=True, + state=u'unprocessed', fake_upload=True, + expunge=True): + """ + Add a media entry for testing purposes. + + Caution: if you're adding multiple entries with fake_upload=True, + make sure you save between them... otherwise you'll hit an + IntegrityError from multiple newly-added-MediaEntries adding + FileKeynames at once. :) + """ + if uploader is None: + uploader = fixture_add_user().id + entry = MediaEntry() entry.title = title entry.slug = slug - entry.uploader = uploader or fixture_add_user().id + entry.uploader = uploader entry.media_type = u'image' + entry.state = state + + if fake_upload: + entry.media_files = {'thumb': ['a', 'b', 'c.jpg'], + 'medium': ['d', 'e', 'f.png'], + 'original': ['g', 'h', 'i.png']} + entry.media_type = u'mediagoblin.media_types.image' if gen_slug: entry.generate_slug() + if save: entry.save() + if expunge: + entry = MediaEntry.query.filter_by(id=entry.id).first() + + Session.expunge(entry) + return entry @@ -231,3 +292,25 @@ def fixture_add_collection(name=u"My first Collection", user=None): return coll +def fixture_add_comment(author=None, media_entry=None, comment=None): + if author is None: + author = fixture_add_user().id + + if media_entry is None: + media_entry = fixture_media_entry().id + + if comment is None: + comment = \ + 'Auto-generated test comment by user #{0} on media #{0}'.format( + author, media_entry) + + comment = MediaComment(author=author, + media_entry=media_entry, + content=comment) + + comment.save() + + Session.expunge(comment) + + return comment + diff --git a/mediagoblin/tools/mail.py b/mediagoblin/tools/mail.py index 6886c859..0fabc5a9 100644 --- a/mediagoblin/tools/mail.py +++ b/mediagoblin/tools/mail.py @@ -90,7 +90,12 @@ def send_email(from_addr, to_addrs, subject, message_body): if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']: mhost = FakeMhost() elif not mg_globals.app_config['email_debug_mode']: - mhost = smtplib.SMTP( + if mg_globals.app_config['email_smtp_use_ssl']: + smtp_init = smtplib.SMTP_SSL + else: + smtp_init = smtplib.SMTP + + mhost = smtp_init( mg_globals.app_config['email_smtp_host'], mg_globals.app_config['email_smtp_port']) diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py index 85558300..8d9c02d4 100644 --- a/mediagoblin/tools/response.py +++ b/mediagoblin/tools/response.py @@ -92,7 +92,7 @@ def render_http_exception(request, exc, description): elif stock_desc and exc.code == 404: return render_404(request) - return render_error(request, title=exc.args[0], + return render_error(request, title='{0} {1}'.format(exc.code, exc.name), err_msg=description, status=exc.code) diff --git a/mediagoblin/tools/session.py b/mediagoblin/tools/session.py index fdc32523..a57f69cc 100644 --- a/mediagoblin/tools/session.py +++ b/mediagoblin/tools/session.py @@ -17,10 +17,12 @@ import itsdangerous import logging -import crypto +from mediagoblin.tools import crypto _log = logging.getLogger(__name__) +MAX_AGE = 30 * 24 * 60 * 60 + class Session(dict): def __init__(self, *args, **kwargs): self.send_new_cookie = False @@ -64,5 +66,10 @@ class SessionManager(object): elif not session: response.delete_cookie(self.cookie_name) else: + if session.get('stay_logged_in', False): + max_age = MAX_AGE + else: + max_age = None + response.set_cookie(self.cookie_name, self.signer.dumps(session), - httponly=True) + max_age=max_age, httponly=True) diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py index 3d651a6e..615ce129 100644 --- a/mediagoblin/tools/template.py +++ b/mediagoblin/tools/template.py @@ -71,6 +71,7 @@ def get_jinja_env(template_loader, locale): template_env.globals['app_config'] = mg_globals.app_config template_env.globals['global_config'] = mg_globals.global_config template_env.globals['version'] = _version.__version__ + template_env.globals['auth'] = mg_globals.app.auth template_env.filters['urlencode'] = url_quote_plus diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 1a78bcc7..06ea0ab0 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -26,8 +26,10 @@ from mediagoblin.tools.response import render_to_response, render_404, \ from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.pagination import Pagination from mediagoblin.user_pages import forms as user_forms -from mediagoblin.user_pages.lib import (send_comment_email, build_report_object, - add_media_to_collection) +from mediagoblin.user_pages.lib import (send_comment_email, + add_media_to_collection) +from mediagoblin.notifications import trigger_notification, \ + add_comment_subscription, mark_comment_notification_seen from mediagoblin.decorators import (uses_pagination, get_user_media_entry, get_media_entry_by_id, user_has_privilege, @@ -36,6 +38,7 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry, get_media_comment_by_id, user_not_banned) from werkzeug.contrib.atom import AtomFeed +from werkzeug.exceptions import MethodNotAllowed _log = logging.getLogger(__name__) @@ -112,6 +115,7 @@ def user_gallery(request, page, url_user=None): 'media_entries': media_entries, 'pagination': pagination}) + MEDIA_COMMENTS_PER_PAGE = 50 @user_not_banned @@ -123,6 +127,9 @@ def media_home(request, media, page, **kwargs): """ comment_id = request.matchdict.get('comment', None) if comment_id: + if request.user: + mark_comment_notification_seen(comment_id, request.user) + pagination = Pagination( page, media.get_comments( mg_globals.app_config['comments_ascending']), @@ -138,7 +145,7 @@ def media_home(request, media, page, **kwargs): comment_form = user_forms.MediaCommentForm(request.form) - media_template_name = media.media_manager['display_template'] + media_template_name = media.media_manager.display_template return render_to_response( request, @@ -157,7 +164,8 @@ def media_post_comment(request, media): """ recieves POST from a MediaEntry() comment form, saves the comment. """ - assert request.method == 'POST' + if not request.method == 'POST': + raise MethodNotAllowed() comment = request.db.MediaComment() comment.media_entry = media.id @@ -182,11 +190,9 @@ def media_post_comment(request, media): request, messages.SUCCESS, _('Your comment has been posted!')) - media_uploader = media.get_uploader - #don't send email if you comment on your own post - if (comment.author != media_uploader and - media_uploader.wants_comment_notification): - send_comment_email(media_uploader, comment, media, request) + trigger_notification(comment, media, request) + + add_comment_subscription(request.user, media) return redirect_obj(request, media) diff --git a/setup.py b/setup.py index f320e92c..14a9a24f 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from setuptools import setup +from setuptools import setup, find_packages import os import re @@ -36,7 +36,7 @@ def get_version(): setup( name="mediagoblin", version=get_version(), - packages=['mediagoblin'], + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), zip_safe=False, include_package_data = True, # scripts and dependencies @@ -45,19 +45,19 @@ setup( 'PasteScript', 'wtforms', 'py-bcrypt', - 'pytest>=2.3', + 'pytest>=2.3.1', 'pytest-xdist', 'werkzeug>=0.7', 'celery==2.5.3', 'kombu==2.1.7', 'jinja2', 'sphinx', - 'Babel', + 'Babel<1.0', 'argparse', 'webtest<2', 'ConfigObj', 'Markdown', - 'sqlalchemy>=0.7.0', + 'sqlalchemy>=0.8.0', 'sqlalchemy-migrate', 'mock', 'itsdangerous',