diff options
author | tilly-Q <nattilypigeonfowl@gmail.com> | 2013-07-29 18:40:19 -0400 |
---|---|---|
committer | tilly-Q <nattilypigeonfowl@gmail.com> | 2013-07-29 18:40:19 -0400 |
commit | 52a355b27541597fc155dab5e4885207b12a0a7b (patch) | |
tree | 7d00edf4d9e39a6b7e4c838ca6f7cb07a4d9a51c | |
parent | f26c21cd5b7998c903fa67aaf164c07743fee651 (diff) | |
parent | 08cd10d84fd89df3f0cad3835ba8ab8b8000d4b2 (diff) | |
download | mediagoblin-52a355b27541597fc155dab5e4885207b12a0a7b.tar.lz mediagoblin-52a355b27541597fc155dab5e4885207b12a0a7b.tar.xz mediagoblin-52a355b27541597fc155dab5e4885207b12a0a7b.zip |
Merge branch 'ticket-679' into OPW-Moderation-Update
Conflicts:
mediagoblin/auth/tools.py
mediagoblin/auth/views.py
mediagoblin/db/migration_tools.py
mediagoblin/db/migrations.py
mediagoblin/db/models.py
mediagoblin/decorators.py
mediagoblin/user_pages/views.py
140 files changed, 6689 insertions, 6033 deletions
@@ -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! @@ -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 + <http://creativecommons.org/publicdomain/zero/1.0/>. + +========== +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 + <http://code.google.com/p/sqlalchemy-migrate/>`_. + See `their docs <https://sqlalchemy-migrate.readthedocs.org/>`_. +- `Alembic <https://bitbucket.org/zzzeek/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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. -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 <http://www.gnu.org/licenses/>. -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.')) + + return redirect( + request, + 'index') + + user = User.query.filter_by(id=int(token)).first() - if user and user.verification_key == unicode(request.GET['token']): + 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'] + # 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.')) + + return redirect( + request, + 'index') + # check if it's a valid user id - user = User.query.filter_by(id=formdata_userid).first() + user = User.query.filter_by(id=int(token)).first() + + # no user in db if not user: - return render_404(request) + messages.add_message( + request, messages.ERROR, + _('The user id is incorrect.')) + return redirect( + request, 'index') - # 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')): + # 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,14 +118,25 @@ def run_dbupdate(app_config, global_config): in the future, plugins) """ + # 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( - app_config['media_types'], global_config.get('plugins', {}).keys()) - # Set up the database - db = setup_connection_and_db_from_config(app_config, migrations=True) - Session = sessionmaker(bind=db.engine) # Setup media managers for all dbdata, run init/migrate and print info 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 <http://www.gnu.org/licenses/>. from mediagoblin.gmg_commands import util as commands_util -from mediagoblin.auth import lib as auth_lib +from mediagoblin import 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 Binary files differindex a07400ae..e2fcf85d 100644 --- a/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/de/LC_MESSAGES/mediagoblin.mo 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 <elrond+mediagoblin.org@samba-tng.org>, 2011 # Art O. Pal <artopal@fastmail.fm>, 2011 # spaetz <sebastian@sspaeth.de>, 2012 -# Vinzenz Vietzke <vietzke@b1-systems.de>, 2012 -# Vinzenz Vietzke <vietzke@b1-systems.de>, 2011 +# Vinzenz Vietzke <vinz@vinzv.de>, 2012 +# Vinzenz Vietzke <vinz@vinzv.de>, 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-05-28 10:43+0000\n" +"Last-Translator: Elrond <elrond+mediagoblin.org@samba-tng.org>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Registriere dich auf dieser Seite</a> oder <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Installiere MediaGoblin auf deinem eigenen Server</a>" #: 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!</p #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "Jahr" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "Monat" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "Woche" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "Tag" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "Stunde" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "Minute" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" diff --git a/mediagoblin/i18n/en/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/en/LC_MESSAGES/mediagoblin.po index 913c2ec1..1b22b786 100644 --- a/mediagoblin/i18n/en/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/en/LC_MESSAGES/mediagoblin.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2013-05-27 13:54-0500\n" +"POT-Creation-Date: 2013-06-16 20:06-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\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 Binary files differindex 406b269f..645af16b 100644 --- a/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/eo/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-06-01 21:16+0000\n" +"Last-Translator: aleksejrs <deletesoftware@yandex.ru>\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 Binary files differindex ececfb2e..c5e50f53 100644 --- a/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/es/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-06-02 21:23+0000\n" +"Last-Translator: larjona <larjona99@gmail.com>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Crear una cuenta en este sitio</a>\n o\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Instalar MediaGoblin en tu propio servidor</a>" #: 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!</p><p #: mediagoblin/tools/timesince.py:62 msgid "year" -msgstr "" +msgstr "año" #: mediagoblin/tools/timesince.py:63 msgid "month" -msgstr "" +msgstr "mes" #: mediagoblin/tools/timesince.py:64 msgid "week" -msgstr "" +msgstr "semana" #: mediagoblin/tools/timesince.py:65 msgid "day" -msgstr "" +msgstr "dÃa" #: mediagoblin/tools/timesince.py:66 msgid "hour" -msgstr "" +msgstr "hora" #: mediagoblin/tools/timesince.py:67 msgid "minute" -msgstr "" +msgstr "minuto" #: mediagoblin/user_pages/forms.py:23 msgid "Comment" @@ -1193,7 +1193,7 @@ msgstr "comentó tu publicación" #: mediagoblin/user_pages/views.py:169 msgid "Sorry, comments are disabled." -msgstr "" +msgstr "Lo siento, los comentarios están desactivados." #: mediagoblin/user_pages/views.py:174 msgid "Oops, your comment was empty." diff --git a/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.mo b/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.mo Binary files differindex e2c22c79..09412b0a 100644 --- a/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.mo diff --git a/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.po b/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.po index 0cef1736..4a5c2b52 100644 --- a/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.po +++ b/mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.po @@ -11,8 +11,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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-06-01 07:11+0000\n" +"Last-Translator: GenghisKhan <genghiskhan@gmx.ca>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">צור חשבון ב×תר ×–×”</a>\n ×ו\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">התקן ×ת MediaGoblin על שרתך</a>" #: 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 "×œ× × ×¨××” ×©×§×™×™× ×¢×ž×•×“ בכתובת זו. צר לי!</p><p>× #: 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 Binary files differindex 2fb3a8ce..596ab843 100644 --- a/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/is_IS/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-06-05 22:51+0000\n" +"Last-Translator: tryggvib <tryggvib@fsfi.is>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Búa til aðgang á þessari sÃðu</a>\neða\n<a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Settu upp þinn eigin margmiðlunarþjón</a>" #: 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 Binary files differindex f9666be3..9cbd03b2 100644 --- a/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/nn_NO/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-05-31 15:40+0000\n" +"Last-Translator: velmont <odin.omdal@gmail.com>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Opprett ein konto pÃ¥ denne sida</a>\n eller\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set opp din eigen MediaGoblin-server</a>" #: 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.</p>\n<p>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 Binary files differindex 5a234c82..8b318329 100644 --- a/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/pl/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-05-28 13:51+0000\n" +"Last-Translator: Sergiusz Pawlowicz <transifex@pawlowicz.name>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Załóż konto na tym serwerze</a>\n albo\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Uruchom MediaGoblin na swoim wÅ‚asnym serwerze</a>" #: 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!</p><p>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 Binary files differindex 309855f5..8cfdf339 100644 --- a/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/ro/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-05-27 20:40+0000\n" +"Last-Translator: George Pop <gapop@hotmail.com>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">CreaÈ›i un cont pe acest site</a>\n sau\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">InstalaÈ›i MediaGoblin pe serverul dvs.</a>" #: 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ă.</p><p>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 Binary files differindex 89836e48..ed28ff43 100644 --- a/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/ru/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-06-01 21:08+0000\n" +"Last-Translator: aleksejrs <deletesoftware@yandex.ru>\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 Binary files differindex a262aaee..fd48a37f 100644 --- a/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/sk/LC_MESSAGES/mediagoblin.mo 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-05-28 07:47+0000\n" +"Last-Translator: martin <zatroch.martin@gmail.com>\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 "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">VytvoriÅ¥ úÄet na tejto stránke</a>\n alebo\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">NastaviÅ¥ MediaGoblin na vlastnom serveri</a>" #: 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áÄ!</p><p>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 Binary files differnew file mode 100644 index 00000000..4341870b --- /dev/null +++ b/mediagoblin/i18n/tr_TR/LC_MESSAGES/mediagoblin.mo 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 <basaran.caner@gmail.com>, 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 <basaran.caner@gmail.com>\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" +" <a href=\"http://daringfireball.net/projects/markdown/basics\">\n" +" Markdown</a> 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.<br/>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 <a href=\"%(osm_url)s\">OpenStreetMap</a>" +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 "" +"<strong>Confidential</strong> - 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).<br />\n" +" <strong>Public</strong> - 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 <strong>required</strong> 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 "<a href=\"%(user_url)s\">%(user_name)s</a>'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 <a href=\"http://mediagoblin.org/\" title='Version " +"%(version)s'>MediaGoblin</a>, a <a href=\"http://gnu.org/\">GNU</a> project." +msgstr "" + +#: mediagoblin/templates/mediagoblin/bits/base_footer.html:24 +#, python-format +msgid "" +"Released under the <a " +"href=\"http://www.fsf.org/licensing/licenses/agpl-3.0.html\">AGPL</a>. <a " +"href=\"%(source_link)s\">Source code</a> 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 <a href=\"http://mediagoblin.org\">MediaGoblin</a>, 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 "" +"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" +" or\n" +" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" +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 <a href=\"http://getfirefox.com\">\n" +"\t http://getfirefox.com</a>!" +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 <a href=\"http://getfirefox.com\">\n" +" http://getfirefox.com</a>!" +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 <a href=\"%(user_url)s\">%(username)s</a>" +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 "<a href=\"%(user_url)s\">%(username)s</a>'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 "" +"<a href=\"%(user_url)s\">%(username)s</a>'s media with tag <a " +"href=\"%(tag_url)s\">%(tag)s</a>" +msgstr "" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48 +#, python-format +msgid "<a href=\"%(user_url)s\">%(username)s</a>'s media" +msgstr "<a href=\"%(user_url)s\">%(username)s</a> medyası" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:38 +#, python-format +msgid "â– Browsing media by <a href=\"%(user_url)s\">%(username)s</a>" +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 <a " +"href=\"%(login_url)s\">log in</a> and resend it." +msgstr "DoÄŸrulama e-postasını kaybettiyseniz, <a href=\"%(login_url)s\">giriÅŸ</a> 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!</p><p>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!</p><p>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 <a " +"href=\"http://daringfireball.net/projects/markdown/basics\">Markdown</a> 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 Binary files differnew file mode 100644 index 00000000..1ed5a4f1 --- /dev/null +++ b/mediagoblin/i18n/zh_CN/LC_MESSAGES/mediagoblin.mo 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: +# <chc@citi.sinica.edu.tw>, 2011 +# cwebber <cwebber@dustycloud.org>, 2013 +# m13253 <m13253@hotmail.com>, 2013 +# medicalwei <medicalwei@gmail.com>, 2012 +# m13253 <m13253@hotmail.com>, 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 <m13253@hotmail.com>\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" +" <a href=\"http://daringfireball.net/projects/markdown/basics\">\n" +" Markdown</a> for formatting." +msgstr "您å¯ä»¥ç”¨ <a href=\"http://wowubuntu.com/markdown/\">Markdown</a> æ¥æŽ’ç‰ˆã€‚" + +#: 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.<br/>Make sure to permit the settings of cookies for this " +"domain." +msgstr "CSRF cookie ä¸å˜åœ¨ã€‚很å¯èƒ½æ˜¯ç”±ç±»ä¼¼ cookie å±è”½å™¨é€ æˆçš„。<br />请å…许本域åçš„ 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 <a href=\"%(osm_url)s\">OpenStreetMap</a>" +msgstr "在 <a href=\"%(osm_url)s\">OpenStreetMap</a> 上观看" + +#: 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 "" +"<strong>Confidential</strong> - 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).<br />\n" +" <strong>Public</strong> - The client can't make confidential\n" +" requests to the GNU MediaGoblin instance (e.g. client-side\n" +" JavaScript client)." +msgstr "<strong>秘密</strong> — OAuth client å¯ä»¥å¯¹ GNU MediaGoblin 站点å‘é€ä¸è¢«ç”¨æˆ·ä»£ç†æ‹¦æˆªçš„请求(例如æœåŠ¡ç«¯ä¸Šçš„ client)。\n<strong>公开</strong> — 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 <strong>required</strong> for public clients." +msgstr "æ¤åº”用程åºçš„é‡å®šå‘ URIï¼Œæœ¬å—æ®µåœ¨å…¬å¼€ç±»åž‹çš„ OAuth client 为<strong>å¿…å¡«</strong>。" + +#: 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 "<a href=\"%(user_url)s\">%(user_name)s</a>'s account" +msgstr "<a href=\"%(user_url)s\">%(user_name)s</a> 的账户" + +#: 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 <a href=\"http://mediagoblin.org/\" title='Version " +"%(version)s'>MediaGoblin</a>, a <a href=\"http://gnu.org/\">GNU</a> project." +msgstr "Powered by <a href=\"http://mediagoblin.org/\" title='Version %(version)s'>MediaGoblin</a>,一个 <a href=\"http://gnu.org/\">GNU</a> 项目。" + +#: mediagoblin/templates/mediagoblin/bits/base_footer.html:24 +#, python-format +msgid "" +"Released under the <a " +"href=\"http://www.fsf.org/licensing/licenses/agpl-3.0.html\">AGPL</a>. <a " +"href=\"%(source_link)s\">Source code</a> available." +msgstr "以 <a href=\"http://www.fsf.org/licensing/licenses/agpl-3.0.html\">AGPL</a> 授æƒå‘布。备有<a href=\"%(source_link)s\">æºä»£ç </a>。" + +#: 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 <a href=\"http://mediagoblin.org\">MediaGoblin</a>, an " +"extraordinarily great piece of media hosting software." +msgstr "本站使用 <a href=\"http://mediagoblin.org\">MediaGoblin</a>——与众ä¸åŒçš„媒体分享网站。" + +#: 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 "" +"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" +" or\n" +" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">åœ¨æœ¬ç«™åˆ›å»ºå¸æˆ·</a>\n 或者\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">在您自己的æœåŠ¡å™¨ä¸Šæå»º MediaGoblin</a>" + +#: 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 <a href=\"http://getfirefox.com\">\n" +"\t http://getfirefox.com</a>!" +msgstr "您å¯ä»¥åœ¨ <a href=\"http://getfirefox.com\">http://getfirefox.com</a> å–å¾—å¯ä»¥æ’放æ¤å£°éŸ³çš„æµè§ˆå™¨ï¼" + +#: 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 <a href=\"http://getfirefox.com\">\n" +" http://getfirefox.com</a>!" +msgstr "您å¯ä»¥åœ¨ <a href=\"http://getfirefox.com\">http://getfirefox.com</a> å–å¾—å¯ä»¥æ’放æ¤è§†é¢‘çš„æµè§ˆå™¨ï¼" + +#: 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 <a href=\"%(user_url)s\">%(username)s</a>" +msgstr "%(collection_title)s by <a href=\"%(user_url)s\">%(username)s</a>" + +#: 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 "<a href=\"%(user_url)s\">%(username)s</a>'s collections" +msgstr "<a href=\"%(user_url)s\">%(username)s</a> çš„åˆé›†" + +#: 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 "" +"<a href=\"%(user_url)s\">%(username)s</a>'s media with tag <a " +"href=\"%(tag_url)s\">%(tag)s</a>" +msgstr "<a href=\"%(user_url)s\">%(username)s</a> 的有 <a href=\"%(tag_url)s\">%(tag)s</a> æ ‡ç¾çš„媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48 +#, python-format +msgid "<a href=\"%(user_url)s\">%(username)s</a>'s media" +msgstr "<a href=\"%(user_url)s\">%(username)s</a> 的媒体" + +#: mediagoblin/templates/mediagoblin/user_pages/media.html:38 +#, python-format +msgid "â– Browsing media by <a href=\"%(user_url)s\">%(username)s</a>" +msgstr "â– æµè§ˆ <a href=\"%(user_url)s\">%(username)s</a> 的媒体" + +#: 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 <a " +"href=\"%(login_url)s\">log in</a> and resend it." +msgstr "如果您就是本人但是未收到认è¯ä¿¡ï¼Œæ‚¨å¯ä»¥<a href=\"%(login_url)s\">登录</a>ç„¶åŽé‡å‘一次。" + +#: 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!</p><p>You have tried to perform a " +"function that you are not allowed to. Have you been trying to delete all " +"user accounts again?" +msgstr "对ä¸èµ·è€å…„,我ä¸èƒ½è®©ä½ è¿™æ ·åšï¼</p><p>您æ£åœ¨è¯•ç€æ“作ä¸å…è®¸æ‚¨ä½¿ç”¨çš„åŠŸèƒ½ã€‚æ‚¨éš¾é“æƒ³æ‰“ç®—åˆ é™¤æ‰€æœ‰ç”¨æˆ·è´¦æˆ·å—?" + +#: mediagoblin/tools/response.py:60 +msgid "" +"There doesn't seem to be a page at this address. Sorry!</p><p>If you're sure" +" the address is correct, maybe the page you're looking for has been moved or" +" deleted." +msgstr "ä¸å¥½æ„æ€ï¼Œçœ‹èµ·æ¥è¿™ä¸ªç½‘å€ä¸Šæ²¡æœ‰ç½‘页。</p><p>å¦‚æžœæ‚¨ç¡®å®šè¿™ä¸ªç½‘å€æ˜¯æ£ç¡®çš„,您在寻找的页é¢å¯èƒ½å·²ç»ç§»åŠ¨æˆ–æ˜¯è¢«åˆ é™¤äº†ã€‚" + +#: 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 <a " +"href=\"http://daringfireball.net/projects/markdown/basics\">Markdown</a> for" +" formatting." +msgstr "您å¯ä»¥ç”¨ <a href=\"http://wowubuntu.com/markdown/\">Markdown</a> æ¥æŽ’ç‰ˆã€‚" + +#: 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 Binary files differindex 3fd4911a..4b7a2398 100644 --- a/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.mo +++ b/mediagoblin/i18n/zh_TW/LC_MESSAGES/mediagoblin.mo 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: # <chc@citi.sinica.edu.tw>, 2011 # Harry Chen <harryhow@gmail.com>, 2011-2012 +# medicalwei <medicalwei@gmail.com>, 2013 # medicalwei <medicalwei@gmail.com>, 2012 +# m13253 <m13253@hotmail.com>, 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 <cwebber@dustycloud.org>\n" +"PO-Revision-Date: 2013-06-16 01:40+0000\n" +"Last-Translator: m13253 <m13253@hotmail.com>\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.<br/>Make sure to permit the settings of cookies for this " "domain." -msgstr "" +msgstr "跨網站å˜å– (CSRF) çš„ cookie ä¸å˜åœ¨ï¼Œæœ‰å¯èƒ½æ˜¯ cookie 阻擋程å¼ä¹‹é¡žçš„程å¼å°Žè‡´çš„。<br/>è«‹å…許æ¤ç¶²åŸŸçš„ 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 "" " <strong>Public</strong> - The client can't make confidential\n" " requests to the GNU MediaGoblin instance (e.g. client-side\n" " JavaScript client)." -msgstr "<strong>秘密</strong> — OAuth client å¯ä»¥å° GNU MediaGoblin ç«™å°ç™¼é€ä¸è¢«ä½¿ç”¨è€…ä»£ç†æ””截的請求 (例如伺æœç«¯çš„ client)。\n<strong>公開</strong> — OAuth client ç„¡æ³•å° GNU MediaGoblin ç«™å°ç™¼é€ç§˜å¯†çš„請求 (例如客戶端的 JavaScript client)。" +msgstr "<strong>秘密</strong> — OAuth 用戶程å¼å¯ä»¥å° GNU MediaGoblin ç«™å°ç™¼é€ä¸è¢«ä½¿ç”¨è€…ä»£ç†æ””截的請求 (例如伺æœç«¯çš„用戶程å¼)。\n<strong>公開</strong> — 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 <strong>required</strong> 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 <a href=\"http://mediagoblin.org/\" title='Version " "%(version)s'>MediaGoblin</a>, a <a href=\"http://gnu.org/\">GNU</a> project." -msgstr "" +msgstr "本站使用 <a href=\"http://mediagoblin.org/\" title='Version %(version)s'>MediaGoblin</a>,這是一個 <a href=\"http://gnu.org/\">GNU</a> 專案。" #: mediagoblin/templates/mediagoblin/bits/base_footer.html:24 #, python-format @@ -617,7 +619,7 @@ msgid "" "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n" " or\n" " <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>" -msgstr "" +msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">在本站建立您的帳號</a>\n 或是\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">在您自己的伺æœå™¨ä¸Šå®‰è£ MediaGoblin</a>" #: 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 <a href=\"http://getfirefox.com\">\n" " http://getfirefox.com</a>!" -msgstr "" +msgstr "您å¯ä»¥åœ¨ <a href=\"http://getfirefox.com\">http://getfirefox.com</a> å–å¾—å¯ä»¥æ’放æ¤å½±ç‰‡çš„先進ç€è¦½å™¨ã€‚" #: 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 "<a href=\"%(user_url)s\">%(username)s</a>'s collections" -msgstr "" +msgstr "<a href=\"%(user_url)s\">%(username)s</a> çš„è’è—" #: 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 "" "<a href=\"%(user_url)s\">%(username)s</a>'s media with tag <a " "href=\"%(tag_url)s\">%(tag)s</a>" -msgstr "" +msgstr "標籤為 <a href=\"%(tag_url)s\">%(tag)s</a> çš„ <a href=\"%(user_url)s\">%(username)s</a> 的媒體" #: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48 #, python-format @@ -918,32 +920,32 @@ msgstr "â– ç€è¦½ <a href=\"%(user_url)s\">%(username)s</a> 的媒體" #: 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 "ä¸å¥½æ„æ€ï¼Œçœ‹èµ·ä¾†é€™å€‹ç¶²å€ä¸Šæ²’有網é 。</p><p>å¦‚æžœæ‚¨ç¢ #: 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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. 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 <http://www.gnu.org/licenses/>. - 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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +from mediagoblin.tools.routing import add_route + +add_route('mediagoblin.notifications.subscribe_comments', + '/u/<string:user>/m/<string:media>/notifications/subscribe/comments/', + 'mediagoblin.notifications.views:subscribe_comments') + +add_route('mediagoblin.notifications.silence_comments', + '/u/<string:user>/m/<string:media>/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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. +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 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 <http://www.gnu.org/licenses/>. - -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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Add an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Add an OpenID{% endtrans %}</h1> + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}"> + {% trans %}Delete an OpenID{% endtrans %} + </a> + </p> + {{ wtforms_util.render_divs(form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Add{% endtrans %}" class="button_form"/> + </div> + </div> + </form> +{% 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 <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Delete an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Delete an OpenID{% endtrans %}</h1> + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}"> + {% trans %}Add an OpenID{% endtrans %} + </a> + </p> + {{ wtforms_util.render_divs(form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/> + </div> + </div> + </form> +{% 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 <http://www.gnu.org/licenses/>. +#} + +{% block openid_edit_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}"> + {% trans %}Edit your OpenID's{% endtrans %} + </a> + </p> +{% 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 <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_head %} + <script type="text/javascript" + src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script> +{% endblock %} + +{% block title -%} + {% trans %}Log in{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ post_url }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Log in{% endtrans %}</h1> + {% if login_failed %} + <div class="form_field_error"> + {% trans %}Logging in failed!{% endtrans %} + </div> + {% endif %} + {% if allow_registration %} + <p> + {% trans %}Log in to create an account!{% endtrans %} + </p> + {% endif %} + {% if pass_auth is defined %} + <p> + <a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}"> + {%- trans %}Or login with a password!{% endtrans %} + </a> + </p> + {% endif %} + {{ wtforms_util.render_divs(login_form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/> + </div> + {% if next %} + <input type="hidden" name="next" value="{{ next }}" class="button_form" + style="display: none;"/> + {% endif %} + </div> + </form> +{% 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 <http://www.gnu.org/licenses/>. +#} + +{% block openid_login_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}"> + {%- trans %}Or login with OpenID!{% endtrans %} + </a> + </p> +{% 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 <http://www.gnu.org/licenses/>. +#} + +{% block openid_register_link %} + {% if openid_link is defined %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}"> + {%- trans %}Or register with OpenID!{% endtrans %} + </a> + </p> + {% 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 <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% block mediagoblin_content %} + <div onload="document.getElementById('openid_message').submit()"> + {{ html|safe }} + </div> +{% 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 <http://www.gnu.org/licenses/>. +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' <select> background */ -} - -.dropdownToolbarButton > select > option { - background: hsl(0,0%,24%); -} - -#customScaleOption { - display: none; -} - -#pageWidthOption { - border-bottom: 1px rgba(255, 255, 255, .5) solid; -} - -html[dir='ltr'] .splitToolbarButton:first-child, -html[dir='ltr'] .toolbarButton:first-child, -html[dir='rtl'] .splitToolbarButton:last-child, -html[dir='rtl'] .toolbarButton:last-child { - margin-left: 4px; -} -html[dir='ltr'] .splitToolbarButton:last-child, -html[dir='ltr'] .toolbarButton:last-child, -html[dir='rtl'] .splitToolbarButton:first-child, -html[dir='rtl'] .toolbarButton:first-child { - margin-right: 4px; -} - -.toolbarButtonSpacer { - width: 30px; - display: inline-block; - height: 1px; -} - -.toolbarButtonFlexibleSpacer { - -webkit-box-flex: 1; - -moz-box-flex: 1; - min-width: 30px; -} - -.toolbarButton#sidebarToggle::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-sidebarToggle.png); -} - -html[dir='ltr'] #findPrevious { - margin-left: 3px; -} -html[dir='ltr'] #findNext { - margin-right: 3px; -} - -html[dir='rtl'] #findPrevious { - margin-right: 3px; -} -html[dir='rtl'] #findNext { - margin-left: 3px; -} - -html[dir='ltr'] .toolbarButton.findPrevious::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/findbarButton-previous.png); -} - -html[dir='rtl'] .toolbarButton.findPrevious::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/findbarButton-previous-rtl.png); -} - -html[dir='ltr'] .toolbarButton.findNext::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/findbarButton-next.png); -} - -html[dir='rtl'] .toolbarButton.findNext::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/findbarButton-next-rtl.png); -} - -html[dir='ltr'] .toolbarButton.pageUp::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-pageUp.png); -} - -html[dir='rtl'] .toolbarButton.pageUp::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-pageUp-rtl.png); -} - -html[dir='ltr'] .toolbarButton.pageDown::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-pageDown.png); -} - -html[dir='rtl'] .toolbarButton.pageDown::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-pageDown-rtl.png); -} - -.toolbarButton.zoomOut::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-zoomOut.png); -} - -.toolbarButton.zoomIn::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-zoomIn.png); -} - -.toolbarButton.fullscreen::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-fullscreen.png); -} - -.toolbarButton.print::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-print.png); -} - -.toolbarButton.openFile::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-openFile.png); -} - -.toolbarButton.download::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-download.png); -} - -.toolbarButton.bookmark { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - margin-top: 3px; - padding-top: 4px; -} - -#viewBookmark[href='#'] { - opacity: .5; - pointer-events: none; -} - -.toolbarButton.bookmark::before { - content: url(../extlib/pdf.js/web/images/toolbarButton-bookmark.png); -} - -#viewThumbnail.toolbarButton::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-viewThumbnail.png); -} - -#viewOutline.toolbarButton::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-viewOutline.png); -} - -#viewFind.toolbarButton::before { - display: inline-block; - content: url(../extlib/pdf.js/web/images/toolbarButton-search.png); -} - - -.toolbarField { - padding: 3px 6px; - margin: 4px 0 4px 0; - border: 1px solid transparent; - border-radius: 2px; - background-color: hsla(0,0%,100%,.09); - background-image: -moz-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%,0%,.05) inset, - 0 1px 0 hsla(0,0%,100%,.05); - color: hsl(0,0%,95%); - font-size: 12px; - line-height: 14px; - outline-style: none; - -moz-transition-property: background-color, border-color, box-shadow; - -moz-transition-duration: 150ms; - -moz-transition-timing-function: ease; -} - -.toolbarField[type=checkbox] { - display: inline-block; - margin: 8px 0px; -} - -.toolbarField.pageNumber { - min-width: 16px; - text-align: right; - width: 40px; -} - -.toolbarField.pageNumber::-webkit-inner-spin-button, -.toolbarField.pageNumber::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.toolbarField:hover { - background-color: hsla(0,0%,100%,.11); - border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.43) hsla(0,0%,0%,.45); -} - -.toolbarField:focus { - background-color: hsla(0,0%,100%,.15); - border-color: hsla(204,100%,65%,.8) hsla(204,100%,65%,.85) hsla(204,100%,65%,.9); -} - -.toolbarLabel { - min-width: 16px; - padding: 3px 6px 3px 2px; - margin: 4px 2px 4px 0; - border: 1px solid transparent; - border-radius: 2px; - color: hsl(0,0%,85%); - font-size: 12px; - line-height: 14px; - text-align: left; - -webkit-user-select: none; - -moz-user-select: none; - cursor: default; -} - -#thumbnailView { - top: 0; - bottom: 0; - padding: 10px 10px 0; - overflow: auto; -} - -.thumbnail { - float: left; -} - -.thumbnail:not([data-loaded]) { - border: 1px dashed rgba(255, 255, 255, 0.5); - margin-bottom: 10px; -} - -.thumbnailImage { - -moz-transition-duration: 150ms; - border: 1px solid transparent; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3); - opacity: 0.8; - z-index: 99; -} - -.thumbnailSelectionRing { - border-radius: 2px; - padding: 7px; - -moz-transition-duration: 150ms; -} - -a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage, -.thumbnail:hover > .thumbnailSelectionRing > .thumbnailImage { - opacity: .9; -} - -a:focus > .thumbnail > .thumbnailSelectionRing, -.thumbnail:hover > .thumbnailSelectionRing { - background-color: hsla(0,0%,100%,.15); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.2) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,.9); -} - -.thumbnail.selected > .thumbnailSelectionRing > .thumbnailImage { - box-shadow: 0 0 0 1px hsla(0,0%,0%,.5); - opacity: 1; -} - -.thumbnail.selected > .thumbnailSelectionRing { - background-color: hsla(0,0%,100%,.3); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,1); -} - -#outlineView { - width: 192px; - top: 0; - bottom: 0; - padding: 4px 4px 0; - overflow: auto; - -webkit-user-select: none; - -moz-user-select: none; -} - -html[dir='ltr'] .outlineItem > .outlineItems { - margin-left: 20px; -} - -html[dir='rtl'] .outlineItem > .outlineItems { - margin-right: 20px; -} - -.outlineItem > a { - text-decoration: none; - display: inline-block; - min-width: 95%; - height: auto; - margin-bottom: 1px; - border-radius: 2px; - color: hsla(0,0%,100%,.8); - font-size: 13px; - line-height: 15px; - -moz-user-select: none; - white-space: normal; -} - -html[dir='ltr'] .outlineItem > a { - padding: 2px 0 5px 10px; -} - -html[dir='rtl'] .outlineItem > a { - padding: 2px 10px 5px 0; -} - -.outlineItem > a:hover { - background-color: hsla(0,0%,100%,.02); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.2) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,.9); -} - -.outlineItem.selected { - background-color: hsla(0,0%,100%,.08); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,1); -} - -.noOutline, -.noResults { - font-size: 12px; - color: hsla(0,0%,100%,.8); - font-style: italic; - cursor: default; -} - -#findScrollView { - position: absolute; - top: 10px; - bottom: 10px; - left: 10px; - width: 280px; -} - -#sidebarControls { - position:absolute; - width: 180px; - height: 32px; - left: 15px; - bottom: 35px; -} - -canvas { - margin: auto; - display: block; -} - -.page { - direction: ltr; - width: 816px; - height: 1056px; - margin: 1px auto -8px auto; - position: relative; - overflow: visible; - border: 9px solid transparent; - background-clip: content-box; - border-image: url(../extlib/pdf.js/web/images/shadow.png) 9 9 repeat; - background-color: white; -} - -.page > a { - display: block; - position: absolute; -} - -.page > a:hover { - opacity: 0.2; - background: #ff0; - box-shadow: 0px 2px 10px #ff0; -} - -.loadingIcon { - position: absolute; - display: block; - left: 0; - top: 0; - right: 0; - bottom: 0; - background: url('../extlib/pdf.js/web/images/loading-icon.gif') center no-repeat; -} - -#loadingBox { - position: absolute; - top: 50%; - margin-top: -25px; - left: 0; - right: 0; - text-align: center; - color: #ddd; - font-size: 14px; -} - -#loadingBar { - display: inline-block; - clear: both; - margin: 0px; - margin-top: 5px; - line-height: 0; - border-radius: 2px; - width: 200px; - height: 25px; - - background-color: hsla(0,0%,0%,.3); - background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - border: 1px solid #000; - box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2) inset, - 0 0 1px 1px rgba(255, 255, 255, 0.1); -} - -#loadingBar .progress { - display: inline-block; - float: left; - - background: #666; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2b2b2), color-stop(100%,#898989)); - background: -webkit-linear-gradient(top, #b2b2b2 0%,#898989 100%); - background: -moz-linear-gradient(top, #b2b2b2 0%,#898989 100%); - background: -ms-linear-gradient(top, #b2b2b2 0%,#898989 100%); - background: -o-linear-gradient(top, #b2b2b2 0%,#898989 100%); - background: linear-gradient(top, #b2b2b2 0%,#898989 100%); - - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - - width: 0%; - height: 100%; -} - -#loadingBar .progress.full { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; -} - -#loadingBar .progress.indeterminate { - width: 100%; - height: 25px; - background-image: -moz-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040); - background-image: -webkit-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040); - background-image: -ms-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040); - background-image: -o-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040); - background-size: 75px 25px; - -moz-animation: progressIndeterminate 1s linear infinite; - -webkit-animation: progressIndeterminate 1s linear infinite; -} - -@-moz-keyframes progressIndeterminate { - from { background-position: 0px 0px; } - to { background-position: 75px 0px; } -} - -@-webkit-keyframes progressIndeterminate { - from { background-position: 0px 0px; } - to { background-position: 75px 0px; } -} - -.textLayer { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - color: #000; - font-family: sans-serif; - overflow: hidden; -} - -.textLayer > div { - color: transparent; - position: absolute; - line-height: 1; - white-space: pre; - cursor: text; -} - -.textLayer .highlight { - margin: -1px; - padding: 1px; - - background-color: rgba(180, 0, 170, 0.2); - border-radius: 4px; -} - -.textLayer .highlight.begin { - border-radius: 4px 0px 0px 4px; -} - -.textLayer .highlight.end { - border-radius: 0px 4px 4px 0px; -} - -.textLayer .highlight.middle { - border-radius: 0px; -} - -.textLayer .highlight.selected { - background-color: rgba(0, 100, 0, 0.2); -} - -/* TODO: file FF bug to support ::-moz-selection:window-inactive - so we can override the opaque grey background when the window is inactive; - see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */ -::selection { background:rgba(0,0,255,0.3); } -::-moz-selection { background:rgba(0,0,255,0.3); } - -.annotText > div { - z-index: 200; - position: absolute; - padding: 0.6em; - max-width: 20em; - background-color: #FFFF99; - box-shadow: 0px 2px 10px #333; - border-radius: 7px; -} - -.annotText > img { - position: absolute; - opacity: 0.6; -} - -.annotText > img:hover { - cursor: pointer; - opacity: 1; -} - -.annotText > div > h1 { - font-size: 1.2em; - border-bottom: 1px solid #000000; - margin: 0px; -} - -#errorWrapper { - background: none repeat scroll 0 0 #FF5555; - color: white; - left: 0; - position: absolute; - right: 0; - top: 32px; - z-index: 1000; - padding: 3px; - font-size: 0.8em; -} - -#errorMessageLeft { - float: left; -} - -#errorMessageRight { - float: right; -} - -#errorMoreInfo { - background-color: #FFFFFF; - color: black; - padding: 3px; - margin: 3px; - width: 98%; -} - -.clearBoth { - clear: both; -} - -.fileInput { - background: white; - color: black; - margin-top: 5px; -} - -#PDFBug { - background: none repeat scroll 0 0 white; - border: 1px solid #666666; - position: fixed; - top: 32px; - right: 0; - bottom: 0; - font-size: 10px; - padding: 0; - width: 300px; -} -#PDFBug .controls { - background:#EEEEEE; - border-bottom: 1px solid #666666; - padding: 3px; -} -#PDFBug .panels { - bottom: 0; - left: 0; - overflow: auto; - position: absolute; - right: 0; - top: 27px; -} -#PDFBug button.active { - font-weight: bold; -} -.debuggerShowText { - background: none repeat scroll 0 0 yellow; - color: blue; - opacity: 0.3; -} -.debuggerHideText:hover { - background: none repeat scroll 0 0 yellow; - opacity: 0.3; -} -#PDFBug .stats { - font-family: courier; - font-size: 10px; - white-space: pre; -} -#PDFBug .stats .title { - font-weight: bold; -} -#PDFBug table { - font-size: 10px; -} - -#viewer.textLayer-visible .textLayer > div, -#viewer.textLayer-hover .textLayer > div:hover { - background-color: white; - color: black; -} - -#viewer.textLayer-shadow .textLayer > div { - background-color: rgba(255,255,255, .6); - color: black; -} - -@page { - margin: 0; -} - -#printContainer { - display: none; -} - -@media print { - /* Rules for browsers that don't support mozPrintCallback. */ - #sidebarContainer, .toolbar, #loadingBox, #errorWrapper, .textLayer { - display: none; - } - - #mainContainer, #viewerContainer, .page, .page canvas { - position: static; - padding: 0; - margin: 0; - } - - .page { - float: left; - display: none; - box-shadow: none; - } - - .page[data-loaded] { - display: block; - } - - /* Rules for browsers that support mozPrintCallback */ - body[data-mozPrintCallback] #outerContainer { - display: none; - } - body[data-mozPrintCallback] #printContainer { - display: block; - } - #printContainer canvas { - position: relative; - top: 0; - left: 0; - } -} - -@media all and (max-width: 950px) { - html[dir='ltr'] #outerContainer.sidebarMoving .outerCenter, - html[dir='ltr'] #outerContainer.sidebarOpen .outerCenter { - float: left; - left: 180px; - } - html[dir='rtl'] #outerContainer.sidebarMoving .outerCenter, - html[dir='rtl'] #outerContainer.sidebarOpen .outerCenter { - float: right; - right: 180px; - } -} - -@media all and (max-width: 770px) { - #sidebarContainer { - top: 33px; - z-index: 100; - } - #sidebarContent { - top: 32px; - background-color: hsla(0,0%,0%,.7); - } - - html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer { - left: 0px; - } - html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer { - right: 0px; - } - - html[dir='ltr'] .outerCenter { - float: left; - left: 180px; - } - html[dir='rtl'] .outerCenter { - float: right; - right: 180px; - } -} - -@media all and (max-width: 600px) { - .hiddenSmallView { - display: none; - } - html[dir='ltr'] .outerCenter { - left: 156px; - } - html[dir='rtr'] .outerCenter { - right: 156px; - } - .toolbarButtonSpacer { - width: 0; - } -} - -@media all and (max-width: 500px) { - #scaleSelectContainer, #pageNumberLabel { - display: none; - } -} - diff --git a/mediagoblin/static/js/notifications.js b/mediagoblin/static/js/notifications.js new file mode 100644 index 00000000..0153463a --- /dev/null +++ b/mediagoblin/static/js/notifications.js @@ -0,0 +1,36 @@ +'use strict'; +/** + * 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 <http://www.gnu.org/licenses/>. + */ + +var notifications = {}; + +(function (n) { + n._base = '/'; + n._endpoint = 'notifications/json'; + + n.init = function () { + $('.notification-gem').on('click', function () { + $('.header_dropdown_down:visible').click(); + }); + } + +})(notifications) + +$(document).ready(function () { + notifications.init(); +}); diff --git a/mediagoblin/static/js/pdf_viewer.js b/mediagoblin/static/js/pdf_viewer.js deleted file mode 100644 index 79c1e708..00000000 --- a/mediagoblin/static/js/pdf_viewer.js +++ /dev/null @@ -1,3615 +0,0 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ -/* 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. - */ -/* globals PDFJS, PDFBug, FirefoxCom, Stats */ - -'use strict'; - -var DEFAULT_SCALE = 'auto'; -var DEFAULT_SCALE_DELTA = 1.1; -var UNKNOWN_SCALE = 0; -var CACHE_SIZE = 20; -var CSS_UNITS = 96.0 / 72.0; -var SCROLLBAR_PADDING = 40; -var VERTICAL_PADDING = 5; -var MIN_SCALE = 0.25; -var MAX_SCALE = 4.0; -var IMAGE_DIR = './images/'; -var SETTINGS_MEMORY = 20; -var ANNOT_MIN_SIZE = 10; -var RenderingStates = { - INITIAL: 0, - RUNNING: 1, - PAUSED: 2, - FINISHED: 3 -}; -var FindStates = { - FIND_FOUND: 0, - FIND_NOTFOUND: 1, - FIND_WRAPPED: 2, - FIND_PENDING: 3 -}; - -//#if (FIREFOX || MOZCENTRAL || B2G || GENERIC || CHROME) -//PDFJS.workerSrc = '../build/pdf.js'; -//#endif - -var mozL10n = document.mozL10n || document.webL10n; - -function getFileName(url) { - var anchor = url.indexOf('#'); - var query = url.indexOf('?'); - var end = Math.min( - anchor > 0 ? anchor : url.length, - query > 0 ? query : url.length); - return url.substring(url.lastIndexOf('/', end) + 1, end); -} - -function scrollIntoView(element, spot) { - // Assuming offsetParent is available (it's not available when viewer is in - // hidden iframe or object). We have to scroll: if the offsetParent is not set - // producing the error. See also animationStartedClosure. - var parent = element.offsetParent; - var offsetY = element.offsetTop + element.clientTop; - if (!parent) { - console.error('offsetParent is not set -- cannot scroll'); - return; - } - while (parent.clientHeight == parent.scrollHeight) { - offsetY += parent.offsetTop; - parent = parent.offsetParent; - if (!parent) - return; // no need to scroll - } - if (spot) - offsetY += spot.top; - parent.scrollTop = offsetY; -} - -var Cache = function cacheCache(size) { - var data = []; - this.push = function cachePush(view) { - var i = data.indexOf(view); - if (i >= 0) - data.splice(i); - data.push(view); - if (data.length > size) - data.shift().destroy(); - }; -}; - -var ProgressBar = (function ProgressBarClosure() { - - function clamp(v, min, max) { - return Math.min(Math.max(v, min), max); - } - - function ProgressBar(id, opts) { - - // Fetch the sub-elements for later - this.div = document.querySelector(id + ' .progress'); - - // Get options, with sensible defaults - this.height = opts.height || 100; - this.width = opts.width || 100; - this.units = opts.units || '%'; - - // Initialize heights - this.div.style.height = this.height + this.units; - } - - ProgressBar.prototype = { - - updateBar: function ProgressBar_updateBar() { - if (this._indeterminate) { - this.div.classList.add('indeterminate'); - return; - } - - var progressSize = this.width * this._percent / 100; - - if (this._percent > 95) - this.div.classList.add('full'); - else - this.div.classList.remove('full'); - this.div.classList.remove('indeterminate'); - - this.div.style.width = progressSize + this.units; - }, - - get percent() { - return this._percent; - }, - - set percent(val) { - this._indeterminate = isNaN(val); - this._percent = clamp(val, 0, 100); - this.updateBar(); - } - }; - - return ProgressBar; -})(); - -//#if FIREFOX || MOZCENTRAL -//#include firefoxcom.js -//#endif - -// Settings Manager - This is a utility for saving settings -// First we see if localStorage is available -// If not, we use FUEL in FF -// Use asyncStorage for B2G -var Settings = (function SettingsClosure() { -//#if !(FIREFOX || MOZCENTRAL || B2G) - var isLocalStorageEnabled = (function localStorageEnabledTest() { - // Feature test as per http://diveintohtml5.info/storage.html - // The additional localStorage call is to get around a FF quirk, see - // bug #495747 in bugzilla - try { - return 'localStorage' in window && window['localStorage'] !== null && - localStorage; - } catch (e) { - return false; - } - })(); -//#endif - - function Settings(fingerprint) { - this.fingerprint = fingerprint; - this.initializedPromise = new PDFJS.Promise(); - - var resolvePromise = (function settingsResolvePromise(db) { - this.initialize(db || '{}'); - this.initializedPromise.resolve(); - }).bind(this); - -//#if B2G -// asyncStorage.getItem('database', resolvePromise); -//#endif - -//#if FIREFOX || MOZCENTRAL -// resolvePromise(FirefoxCom.requestSync('getDatabase', null)); -//#endif - -//#if !(FIREFOX || MOZCENTRAL || B2G) - if (isLocalStorageEnabled) - resolvePromise(localStorage.getItem('database')); -//#endif - } - - Settings.prototype = { - initialize: function settingsInitialize(database) { - database = JSON.parse(database); - if (!('files' in database)) - database.files = []; - if (database.files.length >= SETTINGS_MEMORY) - database.files.shift(); - var index; - for (var i = 0, length = database.files.length; i < length; i++) { - var branch = database.files[i]; - if (branch.fingerprint == this.fingerprint) { - index = i; - break; - } - } - if (typeof index != 'number') - index = database.files.push({fingerprint: this.fingerprint}) - 1; - this.file = database.files[index]; - this.database = database; - }, - - set: function settingsSet(name, val) { - if (!this.initializedPromise.isResolved) - return; - - var file = this.file; - file[name] = val; - var database = JSON.stringify(this.database); - -//#if B2G -// asyncStorage.setItem('database', database); -//#endif - -//#if FIREFOX || MOZCENTRAL -// FirefoxCom.requestSync('setDatabase', database); -//#endif - -//#if !(FIREFOX || MOZCENTRAL || B2G) - if (isLocalStorageEnabled) - localStorage.setItem('database', database); -//#endif - }, - - get: function settingsGet(name, defaultValue) { - if (!this.initializedPromise.isResolved) - return defaultValue; - - return this.file[name] || defaultValue; - } - }; - - return Settings; -})(); - -var cache = new Cache(CACHE_SIZE); -var currentPageNumber = 1; - -var PDFFindController = { - startedTextExtraction: false, - - extractTextPromises: [], - - // If active, find results will be highlighted. - active: false, - - // Stores the text for each page. - pageContents: [], - - pageMatches: [], - - // Currently selected match. - selected: { - pageIdx: -1, - matchIdx: -1 - }, - - // Where find algorithm currently is in the document. - offset: { - pageIdx: null, - matchIdx: null - }, - - resumePageIdx: null, - - resumeCallback: null, - - state: null, - - dirtyMatch: false, - - findTimeout: null, - - initialize: function() { - var events = [ - 'find', - 'findagain', - 'findhighlightallchange', - 'findcasesensitivitychange' - ]; - - this.handleEvent = this.handleEvent.bind(this); - - for (var i = 0; i < events.length; i++) { - window.addEventListener(events[i], this.handleEvent); - } - }, - - calcFindMatch: function(pageIndex) { - var pageContent = this.pageContents[pageIndex]; - var query = this.state.query; - var caseSensitive = this.state.caseSensitive; - var queryLen = query.length; - - if (queryLen === 0) { - // Do nothing the matches should be wiped out already. - return; - } - - if (!caseSensitive) { - pageContent = pageContent.toLowerCase(); - query = query.toLowerCase(); - } - - var matches = []; - - var matchIdx = -queryLen; - while (true) { - matchIdx = pageContent.indexOf(query, matchIdx + queryLen); - if (matchIdx === -1) { - break; - } - - matches.push(matchIdx); - } - this.pageMatches[pageIndex] = matches; - this.updatePage(pageIndex); - if (this.resumePageIdx === pageIndex) { - var callback = this.resumeCallback; - this.resumePageIdx = null; - this.resumeCallback = null; - callback(); - } - }, - - extractText: function() { - if (this.startedTextExtraction) { - return; - } - this.startedTextExtraction = true; - - this.pageContents = []; - for (var i = 0, ii = PDFView.pdfDocument.numPages; i < ii; i++) { - this.extractTextPromises.push(new PDFJS.Promise()); - } - - var self = this; - function extractPageText(pageIndex) { - PDFView.pages[pageIndex].getTextContent().then( - function textContentResolved(data) { - // Build the find string. - var bidiTexts = data.bidiTexts; - var str = ''; - - for (var i = 0; i < bidiTexts.length; i++) { - str += bidiTexts[i].str; - } - - // Store the pageContent as a string. - self.pageContents.push(str); - - self.extractTextPromises[pageIndex].resolve(pageIndex); - if ((pageIndex + 1) < PDFView.pages.length) - extractPageText(pageIndex + 1); - } - ); - } - extractPageText(0); - return this.extractTextPromise; - }, - - handleEvent: function(e) { - if (this.state === null || e.type !== 'findagain') { - this.dirtyMatch = true; - } - this.state = e.detail; - this.updateUIState(FindStates.FIND_PENDING); - - this.extractText(); - - clearTimeout(this.findTimeout); - if (e.type === 'find') { - // Only trigger the find action after 250ms of silence. - this.findTimeout = setTimeout(this.nextMatch.bind(this), 250); - } else { - this.nextMatch(); - } - }, - - updatePage: function(idx) { - var page = PDFView.pages[idx]; - - if (this.selected.pageIdx === idx) { - // If the page is selected, scroll the page into view, which triggers - // rendering the page, which adds the textLayer. Once the textLayer is - // build, it will scroll onto the selected match. - page.scrollIntoView(); - } - - if (page.textLayer) { - page.textLayer.updateMatches(); - } - }, - - nextMatch: function() { - var pages = PDFView.pages; - var previous = this.state.findPrevious; - var numPages = PDFView.pages.length; - - this.active = true; - - if (this.dirtyMatch) { - // Need to recalculate the matches, reset everything. - this.dirtyMatch = false; - this.selected.pageIdx = this.selected.matchIdx = -1; - this.offset.pageIdx = previous ? numPages - 1 : 0; - this.offset.matchIdx = null; - this.hadMatch = false; - this.resumeCallback = null; - this.resumePageIdx = null; - this.pageMatches = []; - var self = this; - - for (var i = 0; i < numPages; i++) { - // Wipe out any previous highlighted matches. - this.updatePage(i); - - // As soon as the text is extracted start finding the matches. - this.extractTextPromises[i].onData(function(pageIdx) { - // Use a timeout since all the pages may already be extracted and we - // want to start highlighting before finding all the matches. - setTimeout(function() { - self.calcFindMatch(pageIdx); - }); - }); - } - } - - // If there's no query there's no point in searching. - if (this.state.query === '') { - this.updateUIState(FindStates.FIND_FOUND); - return; - } - - // If we're waiting on a page, we return since we can't do anything else. - if (this.resumeCallback) { - return; - } - - var offset = this.offset; - // If there's already a matchIdx that means we are iterating through a - // page's matches. - if (offset.matchIdx !== null) { - var numPageMatches = this.pageMatches[offset.pageIdx].length; - if ((!previous && offset.matchIdx + 1 < numPageMatches) || - (previous && offset.matchIdx > 0)) { - // The simple case, we just have advance the matchIdx to select the next - // match on the page. - this.hadMatch = true; - offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; - this.updateMatch(true); - return; - } - // We went beyond the current page's matches, so we advance to the next - // page. - this.advanceOffsetPage(previous); - } - // Start searching through the page. - this.nextPageMatch(); - }, - - nextPageMatch: function() { - if (this.resumePageIdx !== null) - console.error('There can only be one pending page.'); - - var matchesReady = function(matches) { - var offset = this.offset; - var numMatches = matches.length; - var previous = this.state.findPrevious; - if (numMatches) { - // There were matches for the page, so initialize the matchIdx. - this.hadMatch = true; - offset.matchIdx = previous ? numMatches - 1 : 0; - this.updateMatch(true); - } else { - // No matches attempt to search the next page. - this.advanceOffsetPage(previous); - if (offset.wrapped) { - offset.matchIdx = null; - if (!this.hadMatch) { - // No point in wrapping there were no matches. - this.updateMatch(false); - return; - } - } - // Search the next page. - this.nextPageMatch(); - } - }.bind(this); - - var pageIdx = this.offset.pageIdx; - var pageMatches = this.pageMatches; - if (!pageMatches[pageIdx]) { - // The matches aren't ready setup a callback so we can be notified, - // when they are ready. - this.resumeCallback = function() { - matchesReady(pageMatches[pageIdx]); - }; - this.resumePageIdx = pageIdx; - return; - } - // The matches are finished already. - matchesReady(pageMatches[pageIdx]); - }, - - advanceOffsetPage: function(previous) { - var offset = this.offset; - var numPages = this.extractTextPromises.length; - offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; - offset.matchIdx = null; - if (offset.pageIdx >= numPages || offset.pageIdx < 0) { - offset.pageIdx = previous ? numPages - 1 : 0; - offset.wrapped = true; - return; - } - }, - - updateMatch: function(found) { - var state = FindStates.FIND_NOTFOUND; - var wrapped = this.offset.wrapped; - this.offset.wrapped = false; - if (found) { - var previousPage = this.selected.pageIdx; - this.selected.pageIdx = this.offset.pageIdx; - this.selected.matchIdx = this.offset.matchIdx; - state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND; - // Update the currently selected page to wipe out any selected matches. - if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { - this.updatePage(previousPage); - } - } - this.updateUIState(state, this.state.findPrevious); - if (this.selected.pageIdx !== -1) { - this.updatePage(this.selected.pageIdx, true); - } - }, - - updateUIState: function(state, previous) { - if (PDFView.supportsIntegratedFind) { - FirefoxCom.request('updateFindControlState', - {result: state, findPrevious: previous}); - return; - } - PDFFindBar.updateUIState(state, previous); - } -}; - -var PDFFindBar = { - // TODO: Enable the FindBar *AFTER* the pagesPromise in the load function - // got resolved - - opened: false, - - initialize: function() { - this.bar = document.getElementById('findbar'); - this.toggleButton = document.getElementById('viewFind'); - this.findField = document.getElementById('findInput'); - this.highlightAll = document.getElementById('findHighlightAll'); - this.caseSensitive = document.getElementById('findMatchCase'); - this.findMsg = document.getElementById('findMsg'); - this.findStatusIcon = document.getElementById('findStatusIcon'); - - var self = this; - this.toggleButton.addEventListener('click', function() { - self.toggle(); - }); - - this.findField.addEventListener('input', function() { - self.dispatchEvent(''); - }); - - this.bar.addEventListener('keydown', function(evt) { - switch (evt.keyCode) { - case 13: // Enter - if (evt.target === self.findField) { - self.dispatchEvent('again', evt.shiftKey); - } - break; - case 27: // Escape - self.close(); - break; - } - }); - - document.getElementById('findPrevious').addEventListener('click', - function() { self.dispatchEvent('again', true); } - ); - - document.getElementById('findNext').addEventListener('click', function() { - self.dispatchEvent('again', false); - }); - - this.highlightAll.addEventListener('click', function() { - self.dispatchEvent('highlightallchange'); - }); - - this.caseSensitive.addEventListener('click', function() { - self.dispatchEvent('casesensitivitychange'); - }); - }, - - dispatchEvent: function(aType, aFindPrevious) { - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('find' + aType, true, true, { - query: this.findField.value, - caseSensitive: this.caseSensitive.checked, - highlightAll: this.highlightAll.checked, - findPrevious: aFindPrevious - }); - return window.dispatchEvent(event); - }, - - updateUIState: function(state, previous) { - var notFound = false; - var findMsg = ''; - var status = ''; - - switch (state) { - case FindStates.FIND_FOUND: - break; - - case FindStates.FIND_PENDING: - status = 'pending'; - break; - - case FindStates.FIND_NOTFOUND: - findMsg = mozL10n.get('find_not_found', null, 'Phrase not found'); - notFound = true; - break; - - case FindStates.FIND_WRAPPED: - if (previous) { - findMsg = mozL10n.get('find_reached_top', null, - 'Reached top of document, continued from bottom'); - } else { - findMsg = mozL10n.get('find_reached_bottom', null, - 'Reached end of document, continued from top'); - } - break; - } - - if (notFound) { - this.findField.classList.add('notFound'); - } else { - this.findField.classList.remove('notFound'); - } - - this.findField.setAttribute('data-status', status); - this.findMsg.textContent = findMsg; - }, - - open: function() { - if (this.opened) return; - - this.opened = true; - this.toggleButton.classList.add('toggled'); - this.bar.classList.remove('hidden'); - this.findField.select(); - this.findField.focus(); - }, - - close: function() { - if (!this.opened) return; - - this.opened = false; - this.toggleButton.classList.remove('toggled'); - this.bar.classList.add('hidden'); - - PDFFindController.active = false; - }, - - toggle: function() { - if (this.opened) { - this.close(); - } else { - this.open(); - } - } -}; - -var PDFView = { - pages: [], - thumbnails: [], - currentScale: UNKNOWN_SCALE, - currentScaleValue: null, - initialBookmark: document.location.hash.substring(1), - startedTextExtraction: false, - pageText: [], - container: null, - thumbnailContainer: null, - initialized: false, - fellback: false, - pdfDocument: null, - sidebarOpen: false, - pageViewScroll: null, - thumbnailViewScroll: null, - isFullscreen: false, - previousScale: null, - pageRotation: 0, - mouseScrollTimeStamp: 0, - mouseScrollDelta: 0, - lastScroll: 0, - previousPageNumber: 1, - - // called once when the document is loaded - initialize: function pdfViewInitialize() { - var self = this; - var container = this.container = document.getElementById('viewerContainer'); - this.pageViewScroll = {}; - this.watchScroll(container, this.pageViewScroll, updateViewarea); - - var thumbnailContainer = this.thumbnailContainer = - document.getElementById('thumbnailView'); - this.thumbnailViewScroll = {}; - this.watchScroll(thumbnailContainer, this.thumbnailViewScroll, - this.renderHighestPriority.bind(this)); - - PDFFindBar.initialize(); - PDFFindController.initialize(); - - this.initialized = true; - container.addEventListener('scroll', function() { - self.lastScroll = Date.now(); - }, false); - }, - - getPage: function pdfViewGetPage(n) { - return this.pdfDocument.getPage(n); - }, - - // Helper function to keep track whether a div was scrolled up or down and - // then call a callback. - watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) { - state.down = true; - state.lastY = viewAreaElement.scrollTop; - viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) { - var currentY = viewAreaElement.scrollTop; - var lastY = state.lastY; - if (currentY > lastY) - state.down = true; - else if (currentY < lastY) - state.down = false; - // else do nothing and use previous value - state.lastY = currentY; - callback(); - }, true); - }, - - setScale: function pdfViewSetScale(val, resetAutoSettings, noScroll) { - if (val == this.currentScale) - return; - - var pages = this.pages; - for (var i = 0; i < pages.length; i++) - pages[i].update(val * CSS_UNITS); - - if (!noScroll && this.currentScale != val) - this.pages[this.page - 1].scrollIntoView(); - this.currentScale = val; - - var event = document.createEvent('UIEvents'); - event.initUIEvent('scalechange', false, false, window, 0); - event.scale = val; - event.resetAutoSettings = resetAutoSettings; - window.dispatchEvent(event); - }, - - parseScale: function pdfViewParseScale(value, resetAutoSettings, noScroll) { - if ('custom' == value) - return; - - var scale = parseFloat(value); - this.currentScaleValue = value; - if (scale) { - this.setScale(scale, true, noScroll); - return; - } - - var container = this.container; - var currentPage = this.pages[this.page - 1]; - if (!currentPage) { - return; - } - - var pageWidthScale = (container.clientWidth - SCROLLBAR_PADDING) / - currentPage.width * currentPage.scale / CSS_UNITS; - var pageHeightScale = (container.clientHeight - VERTICAL_PADDING) / - currentPage.height * currentPage.scale / CSS_UNITS; - switch (value) { - case 'page-actual': - scale = 1; - break; - case 'page-width': - scale = pageWidthScale; - break; - case 'page-height': - scale = pageHeightScale; - break; - case 'page-fit': - scale = Math.min(pageWidthScale, pageHeightScale); - break; - case 'auto': - scale = Math.min(1.0, pageWidthScale); - break; - } - this.setScale(scale, resetAutoSettings, noScroll); - - selectScaleOption(value); - }, - - zoomIn: function pdfViewZoomIn() { - var newScale = (this.currentScale * DEFAULT_SCALE_DELTA).toFixed(2); - newScale = Math.ceil(newScale * 10) / 10; - newScale = Math.min(MAX_SCALE, newScale); - this.parseScale(newScale, true); - }, - - zoomOut: function pdfViewZoomOut() { - var newScale = (this.currentScale / DEFAULT_SCALE_DELTA).toFixed(2); - newScale = Math.floor(newScale * 10) / 10; - newScale = Math.max(MIN_SCALE, newScale); - this.parseScale(newScale, true); - }, - - set page(val) { - var pages = this.pages; - var input = document.getElementById('pageNumber'); - var event = document.createEvent('UIEvents'); - event.initUIEvent('pagechange', false, false, window, 0); - - if (!(0 < val && val <= pages.length)) { - this.previousPageNumber = val; - event.pageNumber = this.page; - window.dispatchEvent(event); - return; - } - - pages[val - 1].updateStats(); - this.previousPageNumber = currentPageNumber; - currentPageNumber = val; - event.pageNumber = val; - window.dispatchEvent(event); - - // checking if the this.page was called from the updateViewarea function: - // avoiding the creation of two "set page" method (internal and public) - if (updateViewarea.inProgress) - return; - - // Avoid scrolling the first page during loading - if (this.loading && val == 1) - return; - - pages[val - 1].scrollIntoView(); - }, - - get page() { - return currentPageNumber; - }, - - get supportsPrinting() { - var canvas = document.createElement('canvas'); - var value = 'mozPrintCallback' in canvas; - // shadow - Object.defineProperty(this, 'supportsPrinting', { value: value, - enumerable: true, - configurable: true, - writable: false }); - return value; - }, - - get supportsFullscreen() { - var doc = document.documentElement; - var support = doc.requestFullscreen || doc.mozRequestFullScreen || - doc.webkitRequestFullScreen; - - // Disable fullscreen button if we're in an iframe - if (!!window.frameElement) - support = false; - - Object.defineProperty(this, 'supportsFullScreen', { value: support, - enumerable: true, - configurable: true, - writable: false }); - return support; - }, - - get supportsIntegratedFind() { - var support = false; -//#if !(FIREFOX || MOZCENTRAL) -//#else -// support = FirefoxCom.requestSync('supportsIntegratedFind'); -//#endif - Object.defineProperty(this, 'supportsIntegratedFind', { value: support, - enumerable: true, - configurable: true, - writable: false }); - return support; - }, - - get supportsDocumentFonts() { - var support = true; -//#if !(FIREFOX || MOZCENTRAL) -//#else -// support = FirefoxCom.requestSync('supportsDocumentFonts'); -//#endif - Object.defineProperty(this, 'supportsDocumentFonts', { value: support, - enumerable: true, - configurable: true, - writable: false }); - return support; - }, - - get isHorizontalScrollbarEnabled() { - var div = document.getElementById('viewerContainer'); - return div.scrollWidth > div.clientWidth; - }, - - initPassiveLoading: function pdfViewInitPassiveLoading() { - if (!PDFView.loadingBar) { - PDFView.loadingBar = new ProgressBar('#loadingBar', {}); - } - - window.addEventListener('message', function window_message(e) { - var args = e.data; - - if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) - return; - switch (args.pdfjsLoadAction) { - case 'progress': - PDFView.progress(args.loaded / args.total); - break; - case 'complete': - if (!args.data) { - PDFView.error(mozL10n.get('loading_error', null, - 'An error occurred while loading the PDF.'), e); - break; - } - PDFView.open(args.data, 0); - break; - } - }); - FirefoxCom.requestSync('initPassiveLoading', null); - }, - - setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { - this.url = url; - try { - this.setTitle(decodeURIComponent(getFileName(url)) || url); - } catch (e) { - // decodeURIComponent may throw URIError, - // fall back to using the unprocessed url in that case - this.setTitle(url); - } - }, - - setTitle: function pdfViewSetTitle(title) { - document.title = title; -//#if B2G -// document.getElementById('activityTitle').textContent = title; -//#endif - }, - - open: function pdfViewOpen(url, scale, password) { - var parameters = {password: password}; - if (typeof url === 'string') { // URL - this.setTitleUsingUrl(url); - parameters.url = url; - } else if (url && 'byteLength' in url) { // ArrayBuffer - parameters.data = url; - } - - if (!PDFView.loadingBar) { - PDFView.loadingBar = new ProgressBar('#loadingBar', {}); - } - - this.pdfDocument = null; - var self = this; - self.loading = true; - PDFJS.getDocument(parameters).then( - function getDocumentCallback(pdfDocument) { - self.load(pdfDocument, scale); - self.loading = false; - }, - function getDocumentError(message, exception) { - if (exception && exception.name === 'PasswordException') { - if (exception.code === 'needpassword') { - var promptString = mozL10n.get('request_password', null, - 'PDF is protected by a password:'); - password = prompt(promptString); - if (password && password.length > 0) { - return PDFView.open(url, scale, password); - } - } - } - - var loadingErrorMessage = mozL10n.get('loading_error', null, - 'An error occurred while loading the PDF.'); - - if (exception && exception.name === 'InvalidPDFException') { - // change error message also for other builds - var loadingErrorMessage = mozL10n.get('invalid_file_error', null, - 'Invalid or corrupted PDF file.'); -//#if B2G -// window.alert(loadingErrorMessage); -// return window.close(); -//#endif - } - - if (exception && exception.name === 'MissingPDFException') { - // special message for missing PDF's - var loadingErrorMessage = mozL10n.get('missing_file_error', null, - 'Missing PDF file.'); - -//#if B2G -// window.alert(loadingErrorMessage); -// return window.close(); -//#endif - } - - var loadingIndicator = document.getElementById('loading'); - loadingIndicator.textContent = mozL10n.get('loading_error_indicator', - null, 'Error'); - var moreInfo = { - message: message - }; - self.error(loadingErrorMessage, moreInfo); - self.loading = false; - }, - function getDocumentProgress(progressData) { - self.progress(progressData.loaded / progressData.total); - } - ); - }, - - download: function pdfViewDownload() { - function noData() { - FirefoxCom.request('download', { originalUrl: url }); - } - var url = this.url.split('#')[0]; -//#if !(FIREFOX || MOZCENTRAL) - url += '#pdfjs.action=download'; - window.open(url, '_parent'); -//#else -// // Document isn't ready just try to download with the url. -// if (!this.pdfDocument) { -// noData(); -// return; -// } -// this.pdfDocument.getData().then( -// function getDataSuccess(data) { -// var blob = PDFJS.createBlob(data.buffer, 'application/pdf'); -// var blobUrl = window.URL.createObjectURL(blob); -// -// FirefoxCom.request('download', { blobUrl: blobUrl, originalUrl: url }, -// function response(err) { -// if (err) { -// // This error won't really be helpful because it's likely the -// // fallback won't work either (or is already open). -// PDFView.error('PDF failed to download.'); -// } -// window.URL.revokeObjectURL(blobUrl); -// } -// ); -// }, -// noData // Error occurred try downloading with just the url. -// ); -//#endif - }, - - fallback: function pdfViewFallback() { -//#if !(FIREFOX || MOZCENTRAL) -// return; -//#else -// // Only trigger the fallback once so we don't spam the user with messages -// // for one PDF. -// if (this.fellback) -// return; -// this.fellback = true; -// var url = this.url.split('#')[0]; -// FirefoxCom.request('fallback', url, function response(download) { -// if (!download) -// return; -// PDFView.download(); -// }); -//#endif - }, - - navigateTo: function pdfViewNavigateTo(dest) { - if (typeof dest === 'string') - dest = this.destinations[dest]; - if (!(dest instanceof Array)) - return; // invalid destination - // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..> - var destRef = dest[0]; - var pageNumber = destRef instanceof Object ? - this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1); - if (pageNumber > this.pages.length) - pageNumber = this.pages.length; - if (pageNumber) { - this.page = pageNumber; - var currentPage = this.pages[pageNumber - 1]; - if (!this.isFullscreen) { // Avoid breaking fullscreen mode. - currentPage.scrollIntoView(dest); - } - } - }, - - getDestinationHash: function pdfViewGetDestinationHash(dest) { - if (typeof dest === 'string') - return PDFView.getAnchorUrl('#' + escape(dest)); - if (dest instanceof Array) { - var destRef = dest[0]; // see navigateTo method for dest format - var pageNumber = destRef instanceof Object ? - this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : - (destRef + 1); - if (pageNumber) { - var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber); - var destKind = dest[1]; - if (typeof destKind === 'object' && 'name' in destKind && - destKind.name == 'XYZ') { - var scale = (dest[4] || this.currentScale); - pdfOpenParams += '&zoom=' + (scale * 100); - if (dest[2] || dest[3]) { - pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); - } - } - return pdfOpenParams; - } - } - return ''; - }, - - /** - * For the firefox extension we prefix the full url on anchor links so they - * don't come up as resource:// urls and so open in new tab/window works. - * @param {String} anchor The anchor hash include the #. - */ - getAnchorUrl: function getAnchorUrl(anchor) { -//#if !(FIREFOX || MOZCENTRAL) - return anchor; -//#else -// return this.url.split('#')[0] + anchor; -//#endif - }, - - /** - * Returns scale factor for the canvas. It makes sense for the HiDPI displays. - * @return {Object} The object with horizontal (sx) and vertical (sy) - scales. The scaled property is set to false if scaling is - not required, true otherwise. - */ - getOutputScale: function pdfViewGetOutputDPI() { - var pixelRatio = 'devicePixelRatio' in window ? window.devicePixelRatio : 1; - return { - sx: pixelRatio, - sy: pixelRatio, - scaled: pixelRatio != 1 - }; - }, - - /** - * Show the error box. - * @param {String} message A message that is human readable. - * @param {Object} moreInfo (optional) Further information about the error - * that is more technical. Should have a 'message' - * and optionally a 'stack' property. - */ - error: function pdfViewError(message, moreInfo) { - var moreInfoText = mozL10n.get('error_version_info', - {version: PDFJS.version || '?', build: PDFJS.build || '?'}, - 'PDF.js v{{version}} (build: {{build}})') + '\n'; - if (moreInfo) { - moreInfoText += - mozL10n.get('error_message', {message: moreInfo.message}, - 'Message: {{message}}'); - if (moreInfo.stack) { - moreInfoText += '\n' + - mozL10n.get('error_stack', {stack: moreInfo.stack}, - 'Stack: {{stack}}'); - } else { - if (moreInfo.filename) { - moreInfoText += '\n' + - mozL10n.get('error_file', {file: moreInfo.filename}, - 'File: {{file}}'); - } - if (moreInfo.lineNumber) { - moreInfoText += '\n' + - mozL10n.get('error_line', {line: moreInfo.lineNumber}, - 'Line: {{line}}'); - } - } - } - - var loadingBox = document.getElementById('loadingBox'); - loadingBox.setAttribute('hidden', 'true'); - -//#if !(FIREFOX || MOZCENTRAL) - var errorWrapper = document.getElementById('errorWrapper'); - errorWrapper.removeAttribute('hidden'); - - var errorMessage = document.getElementById('errorMessage'); - errorMessage.textContent = message; - - var closeButton = document.getElementById('errorClose'); - closeButton.onclick = function() { - errorWrapper.setAttribute('hidden', 'true'); - }; - - var errorMoreInfo = document.getElementById('errorMoreInfo'); - var moreInfoButton = document.getElementById('errorShowMore'); - var lessInfoButton = document.getElementById('errorShowLess'); - moreInfoButton.onclick = function() { - errorMoreInfo.removeAttribute('hidden'); - moreInfoButton.setAttribute('hidden', 'true'); - lessInfoButton.removeAttribute('hidden'); - }; - lessInfoButton.onclick = function() { - errorMoreInfo.setAttribute('hidden', 'true'); - moreInfoButton.removeAttribute('hidden'); - lessInfoButton.setAttribute('hidden', 'true'); - }; - moreInfoButton.removeAttribute('hidden'); - lessInfoButton.setAttribute('hidden', 'true'); - errorMoreInfo.value = moreInfoText; - - errorMoreInfo.rows = moreInfoText.split('\n').length - 1; -//#else -// console.error(message + '\n' + moreInfoText); -// this.fallback(); -//#endif - }, - - progress: function pdfViewProgress(level) { - var percent = Math.round(level * 100); - PDFView.loadingBar.percent = percent; - }, - - load: function pdfViewLoad(pdfDocument, scale) { - function bindOnAfterDraw(pageView, thumbnailView) { - // when page is painted, using the image as thumbnail base - pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { - thumbnailView.setImage(pageView.canvas); - }; - } - - this.pdfDocument = pdfDocument; - - var errorWrapper = document.getElementById('errorWrapper'); - errorWrapper.setAttribute('hidden', 'true'); - - var loadingBox = document.getElementById('loadingBox'); - loadingBox.setAttribute('hidden', 'true'); - var loadingIndicator = document.getElementById('loading'); - loadingIndicator.textContent = ''; - - var thumbsView = document.getElementById('thumbnailView'); - thumbsView.parentNode.scrollTop = 0; - - while (thumbsView.hasChildNodes()) - thumbsView.removeChild(thumbsView.lastChild); - - if ('_loadingInterval' in thumbsView) - clearInterval(thumbsView._loadingInterval); - - var container = document.getElementById('viewer'); - while (container.hasChildNodes()) - container.removeChild(container.lastChild); - - var pagesCount = pdfDocument.numPages; - var id = pdfDocument.fingerprint; - document.getElementById('numPages').textContent = - mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}'); - document.getElementById('pageNumber').max = pagesCount; - - PDFView.documentFingerprint = id; - var store = PDFView.store = new Settings(id); - - this.pageRotation = 0; - - var pages = this.pages = []; - this.pageText = []; - this.startedTextExtraction = false; - var pagesRefMap = this.pagesRefMap = {}; - var thumbnails = this.thumbnails = []; - - var pagesPromise = new PDFJS.Promise(); - var self = this; - - var firstPagePromise = pdfDocument.getPage(1); - - // Fetch a single page so we can get a viewport that will be the default - // viewport for all pages - firstPagePromise.then(function(pdfPage) { - var viewport = pdfPage.getViewport(scale || 1.0); - var pagePromises = []; - for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { - var viewportClone = viewport.clone(); - var pageView = new PageView(container, pageNum, scale, - self.navigateTo.bind(self), - viewportClone); - var thumbnailView = new ThumbnailView(thumbsView, pageNum, - viewportClone); - bindOnAfterDraw(pageView, thumbnailView); - pages.push(pageView); - thumbnails.push(thumbnailView); - } - - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('documentload', true, true, {}); - window.dispatchEvent(event); - - for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { - var pagePromise = pdfDocument.getPage(pageNum); - pagePromise.then(function(pdfPage) { - var pageNum = pdfPage.pageNumber; - var pageView = pages[pageNum - 1]; - if (!pageView.pdfPage) { - // The pdfPage might already be set if we've already entered - // pageView.draw() - pageView.setPdfPage(pdfPage); - } - var thumbnailView = thumbnails[pageNum - 1]; - if (!thumbnailView.pdfPage) { - thumbnailView.setPdfPage(pdfPage); - } - - var pageRef = pdfPage.ref; - var refStr = pageRef.num + ' ' + pageRef.gen + ' R'; - pagesRefMap[refStr] = pdfPage.pageNumber; - }); - pagePromises.push(pagePromise); - } - - PDFJS.Promise.all(pagePromises).then(function(pages) { - pagesPromise.resolve(pages); - }); - }); - - var storePromise = store.initializedPromise; - PDFJS.Promise.all([firstPagePromise, storePromise]).then(function() { - var storedHash = null; - if (store.get('exists', false)) { - var pageNum = store.get('page', '1'); - var zoom = store.get('zoom', PDFView.currentScale); - var left = store.get('scrollLeft', '0'); - var top = store.get('scrollTop', '0'); - - storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' + - left + ',' + top; - } - self.setInitialView(storedHash, scale); - }); - - pagesPromise.then(function() { - if (PDFView.supportsPrinting) { - pdfDocument.getJavaScript().then(function(javaScript) { - if (javaScript.length) { - console.warn('Warning: JavaScript is not supported'); - PDFView.fallback(); - } - // Hack to support auto printing. - var regex = /\bprint\s*\(/g; - for (var i = 0, ii = javaScript.length; i < ii; i++) { - var js = javaScript[i]; - if (js && regex.test(js)) { - setTimeout(function() { - window.print(); - }); - return; - } - } - }); - } - }); - - var destinationsPromise = pdfDocument.getDestinations(); - destinationsPromise.then(function(destinations) { - self.destinations = destinations; - }); - - // outline depends on destinations and pagesRefMap - var promises = [pagesPromise, destinationsPromise, - PDFView.animationStartedPromise]; - PDFJS.Promise.all(promises).then(function() { - pdfDocument.getOutline().then(function(outline) { - self.outline = new DocumentOutlineView(outline); - }); - - // Make all navigation keys work on document load, - // unless the viewer is embedded in another page. - if (window.parent.location === window.location) { - PDFView.container.focus(); - } - }); - - pdfDocument.getMetadata().then(function(data) { - var info = data.info, metadata = data.metadata; - self.documentInfo = info; - self.metadata = metadata; - - // Provides some basic debug information - console.log('PDF ' + pdfDocument.fingerprint + ' [' + - info.PDFFormatVersion + ' ' + (info.Producer || '-') + - ' / ' + (info.Creator || '-') + ']' + - (PDFJS.version ? ' (PDF.js: ' + PDFJS.version + ')' : '')); - - var pdfTitle; - if (metadata) { - if (metadata.has('dc:title')) - pdfTitle = metadata.get('dc:title'); - } - - if (!pdfTitle && info && info['Title']) - pdfTitle = info['Title']; - - if (pdfTitle) - self.setTitle(pdfTitle + ' - ' + document.title); - - if (info.IsAcroFormPresent) { - console.warn('Warning: AcroForm/XFA is not supported'); - PDFView.fallback(); - } - }); - }, - - setInitialView: function pdfViewSetInitialView(storedHash, scale) { - // Reset the current scale, as otherwise the page's scale might not get - // updated if the zoom level stayed the same. - this.currentScale = 0; - this.currentScaleValue = null; - if (this.initialBookmark) { - this.setHash(this.initialBookmark); - this.initialBookmark = null; - } - else if (storedHash) - this.setHash(storedHash); - else if (scale) { - this.parseScale(scale, true); - this.page = 1; - } - - if (PDFView.currentScale === UNKNOWN_SCALE) { - // Scale was not initialized: invalid bookmark or scale was not specified. - // Setting the default one. - this.parseScale(DEFAULT_SCALE, true); - } - }, - - renderHighestPriority: function pdfViewRenderHighestPriority() { - // Pages have a higher priority than thumbnails, so check them first. - var visiblePages = this.getVisiblePages(); - var pageView = this.getHighestPriority(visiblePages, this.pages, - this.pageViewScroll.down); - if (pageView) { - this.renderView(pageView, 'page'); - return; - } - // No pages needed rendering so check thumbnails. - if (this.sidebarOpen) { - var visibleThumbs = this.getVisibleThumbs(); - var thumbView = this.getHighestPriority(visibleThumbs, - this.thumbnails, - this.thumbnailViewScroll.down); - if (thumbView) - this.renderView(thumbView, 'thumbnail'); - } - }, - - getHighestPriority: function pdfViewGetHighestPriority(visible, views, - scrolledDown) { - // The state has changed figure out which page has the highest priority to - // render next (if any). - // Priority: - // 1 visible pages - // 2 if last scrolled down page after the visible pages - // 2 if last scrolled up page before the visible pages - var visibleViews = visible.views; - - var numVisible = visibleViews.length; - if (numVisible === 0) { - return false; - } - for (var i = 0; i < numVisible; ++i) { - var view = visibleViews[i].view; - if (!this.isViewFinished(view)) - return view; - } - - // All the visible views have rendered, try to render next/previous pages. - if (scrolledDown) { - var nextPageIndex = visible.last.id; - // ID's start at 1 so no need to add 1. - if (views[nextPageIndex] && !this.isViewFinished(views[nextPageIndex])) - return views[nextPageIndex]; - } else { - var previousPageIndex = visible.first.id - 2; - if (views[previousPageIndex] && - !this.isViewFinished(views[previousPageIndex])) - return views[previousPageIndex]; - } - // Everything that needs to be rendered has been. - return false; - }, - - isViewFinished: function pdfViewNeedsRendering(view) { - return view.renderingState === RenderingStates.FINISHED; - }, - - // Render a page or thumbnail view. This calls the appropriate function based - // on the views state. If the view is already rendered it will return false. - renderView: function pdfViewRender(view, type) { - var state = view.renderingState; - switch (state) { - case RenderingStates.FINISHED: - return false; - case RenderingStates.PAUSED: - PDFView.highestPriorityPage = type + view.id; - view.resume(); - break; - case RenderingStates.RUNNING: - PDFView.highestPriorityPage = type + view.id; - break; - case RenderingStates.INITIAL: - PDFView.highestPriorityPage = type + view.id; - view.draw(this.renderHighestPriority.bind(this)); - break; - } - return true; - }, - - setHash: function pdfViewSetHash(hash) { - if (!hash) - return; - - if (hash.indexOf('=') >= 0) { - var params = PDFView.parseQueryString(hash); - // borrowing syntax from "Parameters for Opening PDF Files" - if ('nameddest' in params) { - PDFView.navigateTo(params.nameddest); - return; - } - if ('page' in params) { - var pageNumber = (params.page | 0) || 1; - if ('zoom' in params) { - var zoomArgs = params.zoom.split(','); // scale,left,top - // building destination array - - // If the zoom value, it has to get divided by 100. If it is a string, - // it should stay as it is. - var zoomArg = zoomArgs[0]; - var zoomArgNumber = parseFloat(zoomArg); - if (zoomArgNumber) - zoomArg = zoomArgNumber / 100; - - var dest = [null, {name: 'XYZ'}, - zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null, - zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null, - zoomArg]; - var currentPage = this.pages[pageNumber - 1]; - currentPage.scrollIntoView(dest); - } else { - this.page = pageNumber; // simple page - } - } - if ('pagemode' in params) { - var toggle = document.getElementById('sidebarToggle'); - if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks') { - if (!this.sidebarOpen) { - toggle.click(); - } - this.switchSidebarView(params.pagemode === 'thumbs' ? - 'thumbs' : 'outline'); - } else if (params.pagemode === 'none' && this.sidebarOpen) { - toggle.click(); - } - } - } else if (/^\d+$/.test(hash)) // page number - this.page = hash; - else // named destination - PDFView.navigateTo(unescape(hash)); - }, - - switchSidebarView: function pdfViewSwitchSidebarView(view) { - var thumbsView = document.getElementById('thumbnailView'); - var outlineView = document.getElementById('outlineView'); - - var thumbsButton = document.getElementById('viewThumbnail'); - var outlineButton = document.getElementById('viewOutline'); - - switch (view) { - case 'thumbs': - var wasOutlineViewVisible = thumbsView.classList.contains('hidden'); - - thumbsButton.classList.add('toggled'); - outlineButton.classList.remove('toggled'); - thumbsView.classList.remove('hidden'); - outlineView.classList.add('hidden'); - - PDFView.renderHighestPriority(); - - if (wasOutlineViewVisible) { - // Ensure that the thumbnail of the current page is visible - // when switching from the outline view. - scrollIntoView(document.getElementById('thumbnailContainer' + - this.page)); - } - break; - - case 'outline': - thumbsButton.classList.remove('toggled'); - outlineButton.classList.add('toggled'); - thumbsView.classList.add('hidden'); - outlineView.classList.remove('hidden'); - - if (outlineButton.getAttribute('disabled')) - return; - break; - } - }, - - getVisiblePages: function pdfViewGetVisiblePages() { - if (!this.isFullscreen) { - return this.getVisibleElements(this.container, this.pages, true); - } else { - // The algorithm in getVisibleElements is broken in fullscreen mode. - var visible = [], page = this.page; - var currentPage = this.pages[page - 1]; - visible.push({ id: currentPage.id, view: currentPage }); - - return { first: currentPage, last: currentPage, views: visible}; - } - }, - - getVisibleThumbs: function pdfViewGetVisibleThumbs() { - return this.getVisibleElements(this.thumbnailContainer, this.thumbnails); - }, - - // Generic helper to find out what elements are visible within a scroll pane. - getVisibleElements: function pdfViewGetVisibleElements( - scrollEl, views, sortByVisibility) { - var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; - var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; - - var visible = [], view; - var currentHeight, viewHeight, hiddenHeight, percentHeight; - var currentWidth, viewWidth; - for (var i = 0, ii = views.length; i < ii; ++i) { - view = views[i]; - currentHeight = view.el.offsetTop + view.el.clientTop; - viewHeight = view.el.clientHeight; - if ((currentHeight + viewHeight) < top) { - continue; - } - if (currentHeight > bottom) { - break; - } - currentWidth = view.el.offsetLeft + view.el.clientLeft; - viewWidth = view.el.clientWidth; - if ((currentWidth + viewWidth) < left || currentWidth > right) { - continue; - } - hiddenHeight = Math.max(0, top - currentHeight) + - Math.max(0, currentHeight + viewHeight - bottom); - percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; - - visible.push({ id: view.id, y: currentHeight, - view: view, percent: percentHeight }); - } - - var first = visible[0]; - var last = visible[visible.length - 1]; - - if (sortByVisibility) { - visible.sort(function(a, b) { - var pc = a.percent - b.percent; - if (Math.abs(pc) > 0.001) { - return -pc; - } - return a.id - b.id; // ensure stability - }); - } - return {first: first, last: last, views: visible}; - }, - - // Helper function to parse query string (e.g. ?param1=value&parm2=...). - parseQueryString: function pdfViewParseQueryString(query) { - var parts = query.split('&'); - var params = {}; - for (var i = 0, ii = parts.length; i < parts.length; ++i) { - var param = parts[i].split('='); - var key = param[0]; - var value = param.length > 1 ? param[1] : null; - params[unescape(key)] = unescape(value); - } - return params; - }, - - beforePrint: function pdfViewSetupBeforePrint() { - if (!this.supportsPrinting) { - var printMessage = mozL10n.get('printing_not_supported', null, - 'Warning: Printing is not fully supported by this browser.'); - this.error(printMessage); - return; - } - - var alertNotReady = false; - if (!this.pages.length) { - alertNotReady = true; - } else { - for (var i = 0, ii = this.pages.length; i < ii; ++i) { - if (!this.pages[i].pdfPage) { - alertNotReady = true; - break; - } - } - } - if (alertNotReady) { - var notReadyMessage = mozL10n.get('printing_not_ready', null, - 'Warning: The PDF is not fully loaded for printing.'); - window.alert(notReadyMessage); - return; - } - - var body = document.querySelector('body'); - body.setAttribute('data-mozPrintCallback', true); - for (var i = 0, ii = this.pages.length; i < ii; ++i) { - this.pages[i].beforePrint(); - } - }, - - afterPrint: function pdfViewSetupAfterPrint() { - var div = document.getElementById('printContainer'); - while (div.hasChildNodes()) - div.removeChild(div.lastChild); - }, - - fullscreen: function pdfViewFullscreen() { - var isFullscreen = document.fullscreenElement || document.mozFullScreen || - document.webkitIsFullScreen; - - if (isFullscreen) { - return false; - } - - var wrapper = document.getElementById('viewerContainer'); - if (document.documentElement.requestFullscreen) { - wrapper.requestFullscreen(); - } else if (document.documentElement.mozRequestFullScreen) { - wrapper.mozRequestFullScreen(); - } else if (document.documentElement.webkitRequestFullScreen) { - wrapper.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); - } else { - return false; - } - - this.isFullscreen = true; - var currentPage = this.pages[this.page - 1]; - this.previousScale = this.currentScaleValue; - this.parseScale('page-fit', true); - - // Wait for fullscreen to take effect - setTimeout(function() { - currentPage.scrollIntoView(); - }, 0); - - this.showPresentationControls(); - return true; - }, - - exitFullscreen: function pdfViewExitFullscreen() { - this.isFullscreen = false; - this.parseScale(this.previousScale); - this.page = this.page; - this.clearMouseScrollState(); - this.hidePresentationControls(); - - // Ensure that the thumbnail of the current page is visible - // when exiting fullscreen mode. - scrollIntoView(document.getElementById('thumbnailContainer' + this.page)); - }, - - showPresentationControls: function pdfViewShowPresentationControls() { - var DELAY_BEFORE_HIDING_CONTROLS = 3000; - var wrapper = document.getElementById('viewerContainer'); - if (this.presentationControlsTimeout) { - clearTimeout(this.presentationControlsTimeout); - } else { - wrapper.classList.add('presentationControls'); - } - this.presentationControlsTimeout = setTimeout(function hideControls() { - wrapper.classList.remove('presentationControls'); - delete PDFView.presentationControlsTimeout; - }, DELAY_BEFORE_HIDING_CONTROLS); - }, - - hidePresentationControls: function pdfViewShowPresentationControls() { - if (!this.presentationControlsTimeout) { - return; - } - clearTimeout(this.presentationControlsTimeout); - delete this.presentationControlsTimeout; - - var wrapper = document.getElementById('viewerContainer'); - wrapper.classList.remove('presentationControls'); - }, - - rotatePages: function pdfViewPageRotation(delta) { - - this.pageRotation = (this.pageRotation + 360 + delta) % 360; - - for (var i = 0, l = this.pages.length; i < l; i++) { - var page = this.pages[i]; - page.update(page.scale, this.pageRotation); - } - - for (var i = 0, l = this.thumbnails.length; i < l; i++) { - var thumb = this.thumbnails[i]; - thumb.update(this.pageRotation); - } - - this.parseScale(this.currentScaleValue, true); - - this.renderHighestPriority(); - - var currentPage = this.pages[this.page - 1]; - if (!currentPage) { - return; - } - - // Wait for fullscreen to take effect - setTimeout(function() { - currentPage.scrollIntoView(); - }, 0); - }, - - /** - * This function flips the page in presentation mode if the user scrolls up - * or down with large enough motion and prevents page flipping too often. - * - * @this {PDFView} - * @param {number} mouseScrollDelta The delta value from the mouse event. - */ - mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) { - var MOUSE_SCROLL_COOLDOWN_TIME = 50; - - var currentTime = (new Date()).getTime(); - var storedTime = this.mouseScrollTimeStamp; - - // In case one page has already been flipped there is a cooldown time - // which has to expire before next page can be scrolled on to. - if (currentTime > storedTime && - currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) - return; - - // In case the user decides to scroll to the opposite direction than before - // clear the accumulated delta. - if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) || - (this.mouseScrollDelta < 0 && mouseScrollDelta > 0)) - this.clearMouseScrollState(); - - this.mouseScrollDelta += mouseScrollDelta; - - var PAGE_FLIP_THRESHOLD = 120; - if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) { - - var PageFlipDirection = { - UP: -1, - DOWN: 1 - }; - - // In fullscreen mode scroll one page at a time. - var pageFlipDirection = (this.mouseScrollDelta > 0) ? - PageFlipDirection.UP : - PageFlipDirection.DOWN; - this.clearMouseScrollState(); - var currentPage = this.page; - - // In case we are already on the first or the last page there is no need - // to do anything. - if ((currentPage == 1 && pageFlipDirection == PageFlipDirection.UP) || - (currentPage == this.pages.length && - pageFlipDirection == PageFlipDirection.DOWN)) - return; - - this.page += pageFlipDirection; - this.mouseScrollTimeStamp = currentTime; - } - }, - - /** - * This function clears the member attributes used with mouse scrolling in - * presentation mode. - * - * @this {PDFView} - */ - clearMouseScrollState: function pdfViewClearMouseScrollState() { - this.mouseScrollTimeStamp = 0; - this.mouseScrollDelta = 0; - } -}; - -var PageView = function pageView(container, id, scale, - navigateTo, defaultViewport) { - this.id = id; - - this.rotation = 0; - this.scale = scale || 1.0; - this.viewport = defaultViewport; - this.pdfPageRotate = defaultViewport.rotate; - - this.renderingState = RenderingStates.INITIAL; - this.resume = null; - - this.textContent = null; - this.textLayer = null; - - var anchor = document.createElement('a'); - anchor.name = '' + this.id; - - var div = this.el = document.createElement('div'); - div.id = 'pageContainer' + this.id; - div.className = 'page'; - div.style.width = Math.floor(this.viewport.width) + 'px'; - div.style.height = Math.floor(this.viewport.height) + 'px'; - - container.appendChild(anchor); - container.appendChild(div); - - this.setPdfPage = function pageViewSetPdfPage(pdfPage) { - this.pdfPage = pdfPage; - this.pdfPageRotate = pdfPage.rotate; - this.viewport = pdfPage.getViewport(this.scale); - this.stats = pdfPage.stats; - this.update(); - }; - - this.destroy = function pageViewDestroy() { - this.update(); - if (this.pdfPage) { - this.pdfPage.destroy(); - } - }; - - this.update = function pageViewUpdate(scale, rotation) { - this.renderingState = RenderingStates.INITIAL; - this.resume = null; - - if (typeof rotation !== 'undefined') { - this.rotation = rotation; - } - - this.scale = scale || this.scale; - - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = this.viewport.clone({ - scale: this.scale, - rotation: totalRotation - }); - - div.style.width = Math.floor(this.viewport.width) + 'px'; - div.style.height = Math.floor(this.viewport.height) + 'px'; - - while (div.hasChildNodes()) - div.removeChild(div.lastChild); - div.removeAttribute('data-loaded'); - - delete this.canvas; - - this.loadingIconDiv = document.createElement('div'); - this.loadingIconDiv.className = 'loadingIcon'; - div.appendChild(this.loadingIconDiv); - }; - - Object.defineProperty(this, 'width', { - get: function PageView_getWidth() { - return this.viewport.width; - }, - enumerable: true - }); - - Object.defineProperty(this, 'height', { - get: function PageView_getHeight() { - return this.viewport.height; - }, - enumerable: true - }); - - function setupAnnotations(pdfPage, viewport) { - function bindLink(link, dest) { - link.href = PDFView.getDestinationHash(dest); - link.onclick = function pageViewSetupLinksOnclick() { - if (dest) - PDFView.navigateTo(dest); - return false; - }; - } - function createElementWithStyle(tagName, item, rect) { - if (!rect) { - rect = viewport.convertToViewportRectangle(item.rect); - rect = PDFJS.Util.normalizeRect(rect); - } - var element = document.createElement(tagName); - element.style.left = Math.floor(rect[0]) + 'px'; - element.style.top = Math.floor(rect[1]) + 'px'; - element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'; - element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'; - return element; - } - function createTextAnnotation(item) { - var container = document.createElement('section'); - container.className = 'annotText'; - - var rect = viewport.convertToViewportRectangle(item.rect); - rect = PDFJS.Util.normalizeRect(rect); - // sanity check because of OOo-generated PDFs - if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) { - rect[3] = rect[1] + ANNOT_MIN_SIZE; - } - if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) { - rect[2] = rect[0] + (rect[3] - rect[1]); // make it square - } - var image = createElementWithStyle('img', item, rect); - var iconName = item.name; - image.src = IMAGE_DIR + 'annotation-' + - iconName.toLowerCase() + '.svg'; - image.alt = mozL10n.get('text_annotation_type', {type: iconName}, - '[{{type}} Annotation]'); - var content = document.createElement('div'); - content.setAttribute('hidden', true); - var title = document.createElement('h1'); - var text = document.createElement('p'); - content.style.left = Math.floor(rect[2]) + 'px'; - content.style.top = Math.floor(rect[1]) + 'px'; - title.textContent = item.title; - - if (!item.content && !item.title) { - content.setAttribute('hidden', true); - } else { - var e = document.createElement('span'); - var lines = item.content.split(/(?:\r\n?|\n)/); - for (var i = 0, ii = lines.length; i < ii; ++i) { - var line = lines[i]; - e.appendChild(document.createTextNode(line)); - if (i < (ii - 1)) - e.appendChild(document.createElement('br')); - } - text.appendChild(e); - image.addEventListener('mouseover', function annotationImageOver() { - content.removeAttribute('hidden'); - }, false); - - image.addEventListener('mouseout', function annotationImageOut() { - content.setAttribute('hidden', true); - }, false); - } - - content.appendChild(title); - content.appendChild(text); - container.appendChild(image); - container.appendChild(content); - - return container; - } - - pdfPage.getAnnotations().then(function(items) { - for (var i = 0; i < items.length; i++) { - var item = items[i]; - switch (item.type) { - case 'Link': - var link = createElementWithStyle('a', item); - link.href = item.url || ''; - if (!item.url) - bindLink(link, ('dest' in item) ? item.dest : null); - div.appendChild(link); - break; - case 'Text': - var textAnnotation = createTextAnnotation(item); - if (textAnnotation) - div.appendChild(textAnnotation); - break; - } - } - }); - } - - this.getPagePoint = function pageViewGetPagePoint(x, y) { - return this.viewport.convertToPdfPoint(x, y); - }; - - this.scrollIntoView = function pageViewScrollIntoView(dest) { - if (!dest) { - scrollIntoView(div); - return; - } - - var x = 0, y = 0; - var width = 0, height = 0, widthScale, heightScale; - var scale = 0; - switch (dest[1].name) { - case 'XYZ': - x = dest[2]; - y = dest[3]; - scale = dest[4]; - // If x and/or y coordinates are not supplied, default to - // _top_ left of the page (not the obvious bottom left, - // since aligning the bottom of the intended page with the - // top of the window is rarely helpful). - x = x !== null ? x : 0; - y = y !== null ? y : this.height / this.scale; - break; - case 'Fit': - case 'FitB': - scale = 'page-fit'; - break; - case 'FitH': - case 'FitBH': - y = dest[2]; - scale = 'page-width'; - break; - case 'FitV': - case 'FitBV': - x = dest[2]; - scale = 'page-height'; - break; - case 'FitR': - x = dest[2]; - y = dest[3]; - width = dest[4] - x; - height = dest[5] - y; - widthScale = (this.container.clientWidth - SCROLLBAR_PADDING) / - width / CSS_UNITS; - heightScale = (this.container.clientHeight - SCROLLBAR_PADDING) / - height / CSS_UNITS; - scale = Math.min(widthScale, heightScale); - break; - default: - return; - } - - if (scale && scale !== PDFView.currentScale) - PDFView.parseScale(scale, true, true); - else if (PDFView.currentScale === UNKNOWN_SCALE) - PDFView.parseScale(DEFAULT_SCALE, true, true); - - var boundingRect = [ - this.viewport.convertToViewportPoint(x, y), - this.viewport.convertToViewportPoint(x + width, y + height) - ]; - setTimeout(function pageViewScrollIntoViewRelayout() { - // letting page to re-layout before scrolling - var scale = PDFView.currentScale; - var x = Math.min(boundingRect[0][0], boundingRect[1][0]); - var y = Math.min(boundingRect[0][1], boundingRect[1][1]); - var width = Math.abs(boundingRect[0][0] - boundingRect[1][0]); - var height = Math.abs(boundingRect[0][1] - boundingRect[1][1]); - - scrollIntoView(div, {left: x, top: y, width: width, height: height}); - }, 0); - }; - - this.getTextContent = function pageviewGetTextContent() { - if (!this.textContent) { - this.textContent = this.pdfPage.getTextContent(); - } - return this.textContent; - }; - - this.draw = function pageviewDraw(callback) { - var pdfPage = this.pdfPage; - - if (!pdfPage) { - var promise = PDFView.getPage(this.id); - promise.then(function(pdfPage) { - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - return; - } - - if (this.renderingState !== RenderingStates.INITIAL) { - console.error('Must be in new state before drawing'); - } - - this.renderingState = RenderingStates.RUNNING; - - var canvas = document.createElement('canvas'); - canvas.id = 'page' + this.id; - div.appendChild(canvas); - this.canvas = canvas; - - var scale = this.scale, viewport = this.viewport; - var outputScale = PDFView.getOutputScale(); - canvas.width = Math.floor(viewport.width) * outputScale.sx; - canvas.height = Math.floor(viewport.height) * outputScale.sy; - - var textLayerDiv = null; - if (!PDFJS.disableTextLayer) { - textLayerDiv = document.createElement('div'); - textLayerDiv.className = 'textLayer'; - textLayerDiv.style.width = canvas.width + 'px'; - textLayerDiv.style.height = canvas.height + 'px'; - div.appendChild(textLayerDiv); - } - var textLayer = this.textLayer = - textLayerDiv ? new TextLayerBuilder(textLayerDiv, this.id - 1) : null; - - if (outputScale.scaled) { - var cssScale = 'scale(' + (1 / outputScale.sx) + ', ' + - (1 / outputScale.sy) + ')'; - CustomStyle.setProp('transform' , canvas, cssScale); - CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); - if (textLayerDiv) { - CustomStyle.setProp('transform' , textLayerDiv, cssScale); - CustomStyle.setProp('transformOrigin' , textLayerDiv, '0% 0%'); - } - } - - var ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, canvas.width, canvas.height); - // TODO(mack): use data attributes to store these - ctx._scaleX = outputScale.sx; - ctx._scaleY = outputScale.sy; - if (outputScale.scaled) { - ctx.scale(outputScale.sx, outputScale.sy); - } -//#if (FIREFOX || MOZCENTRAL) -// // Checking if document fonts are used only once -// var checkIfDocumentFontsUsed = !PDFView.pdfDocument.embeddedFontsUsed; -//#endif - - // Rendering area - - var self = this; - var renderingWasReset = false; - function pageViewDrawCallback(error) { - if (renderingWasReset) { - return; - } - - self.renderingState = RenderingStates.FINISHED; - - if (self.loadingIconDiv) { - div.removeChild(self.loadingIconDiv); - delete self.loadingIconDiv; - } - -//#if (FIREFOX || MOZCENTRAL) -// if (checkIfDocumentFontsUsed && PDFView.pdfDocument.embeddedFontsUsed && -// !PDFView.supportsDocumentFonts) { -// console.error(mozL10n.get('web_fonts_disabled', null, -// 'Web fonts are disabled: unable to use embedded PDF fonts.')); -// PDFView.fallback(); -// } -//#endif - if (error) { - PDFView.error(mozL10n.get('rendering_error', null, - 'An error occurred while rendering the page.'), error); - } - - self.stats = pdfPage.stats; - self.updateStats(); - if (self.onAfterDraw) - self.onAfterDraw(); - - cache.push(self); - - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagerender', true, true, { - pageNumber: pdfPage.pageNumber - }); - div.dispatchEvent(event); - - callback(); - } - - var renderContext = { - canvasContext: ctx, - viewport: this.viewport, - textLayer: textLayer, - continueCallback: function pdfViewcContinueCallback(cont) { - if (self.renderingState === RenderingStates.INITIAL) { - // The page update() was called, we just need to abort any rendering. - renderingWasReset = true; - return; - } - - if (PDFView.highestPriorityPage !== 'page' + self.id) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function resumeCallback() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; - return; - } - cont(); - } - }; - this.pdfPage.render(renderContext).then( - function pdfPageRenderCallback() { - pageViewDrawCallback(null); - }, - function pdfPageRenderError(error) { - pageViewDrawCallback(error); - } - ); - - if (textLayer) { - this.getTextContent().then( - function textContentResolved(textContent) { - textLayer.setTextContent(textContent); - } - ); - } - - setupAnnotations(this.pdfPage, this.viewport); - div.setAttribute('data-loaded', true); - }; - - this.beforePrint = function pageViewBeforePrint() { - var pdfPage = this.pdfPage; - - var viewport = pdfPage.getViewport(1); - // Use the same hack we use for high dpi displays for printing to get better - // output until bug 811002 is fixed in FF. - var PRINT_OUTPUT_SCALE = 2; - var canvas = this.canvas = document.createElement('canvas'); - canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; - canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; - canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; - canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; - var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + - (1 / PRINT_OUTPUT_SCALE) + ')'; - CustomStyle.setProp('transform' , canvas, cssScale); - CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); - - var printContainer = document.getElementById('printContainer'); - printContainer.appendChild(canvas); - - var self = this; - canvas.mozPrintCallback = function(obj) { - var ctx = obj.context; - - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); - - var renderContext = { - canvasContext: ctx, - viewport: viewport - }; - - pdfPage.render(renderContext).then(function() { - // Tell the printEngine that rendering this canvas/page has finished. - obj.done(); - self.pdfPage.destroy(); - }, function(error) { - console.error(error); - // Tell the printEngine that rendering this canvas/page has failed. - // This will make the print proces stop. - if ('abort' in obj) - obj.abort(); - else - obj.done(); - self.pdfPage.destroy(); - }); - }; - }; - - this.updateStats = function pageViewUpdateStats() { - if (!this.stats) { - return; - } - - if (PDFJS.pdfBug && Stats.enabled) { - var stats = this.stats; - Stats.add(this.id, stats); - } - }; -}; - -var ThumbnailView = function thumbnailView(container, id, defaultViewport) { - var anchor = document.createElement('a'); - anchor.href = PDFView.getAnchorUrl('#page=' + id); - anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); - anchor.onclick = function stopNavigation() { - PDFView.page = id; - return false; - }; - - - this.pdfPage = undefined; - this.viewport = defaultViewport; - this.pdfPageRotate = defaultViewport.rotate; - - this.rotation = 0; - this.pageWidth = this.viewport.width; - this.pageHeight = this.viewport.height; - this.pageRatio = this.pageWidth / this.pageHeight; - this.id = id; - - this.canvasWidth = 98; - this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; - this.scale = (this.canvasWidth / this.pageWidth); - - var div = this.el = document.createElement('div'); - div.id = 'thumbnailContainer' + id; - div.className = 'thumbnail'; - - if (id === 1) { - // Highlight the thumbnail of the first page when no page number is - // specified (or exists in cache) when the document is loaded. - div.classList.add('selected'); - } - - var ring = document.createElement('div'); - ring.className = 'thumbnailSelectionRing'; - ring.style.width = this.canvasWidth + 'px'; - ring.style.height = this.canvasHeight + 'px'; - - div.appendChild(ring); - anchor.appendChild(div); - container.appendChild(anchor); - - this.hasImage = false; - this.renderingState = RenderingStates.INITIAL; - - this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) { - this.pdfPage = pdfPage; - this.pdfPageRotate = pdfPage.rotate; - this.viewport = pdfPage.getViewport(1); - this.update(); - }; - - this.update = function thumbnailViewUpdate(rot) { - if (!this.pdfPage) { - return; - } - - if (rot !== undefined) { - this.rotation = rot; - } - - var totalRotation = (this.rotation + this.pdfPage.rotate) % 360; - this.viewport = this.viewport.clone({ - scale: 1, - rotation: totalRotation - }); - this.pageWidth = this.viewport.width; - this.pageHeight = this.viewport.height; - this.pageRatio = this.pageWidth / this.pageHeight; - - this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; - this.scale = (this.canvasWidth / this.pageWidth); - - div.removeAttribute('data-loaded'); - ring.textContent = ''; - ring.style.width = this.canvasWidth + 'px'; - ring.style.height = this.canvasHeight + 'px'; - - this.hasImage = false; - this.renderingState = RenderingStates.INITIAL; - this.resume = null; - }; - - this.getPageDrawContext = function thumbnailViewGetPageDrawContext() { - var canvas = document.createElement('canvas'); - canvas.id = 'thumbnail' + id; - - canvas.width = this.canvasWidth; - canvas.height = this.canvasHeight; - canvas.className = 'thumbnailImage'; - canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', - {page: id}, 'Thumbnail of Page {{page}}')); - - div.setAttribute('data-loaded', true); - - ring.appendChild(canvas); - - var ctx = canvas.getContext('2d'); - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); - ctx.restore(); - return ctx; - }; - - this.drawingRequired = function thumbnailViewDrawingRequired() { - return !this.hasImage; - }; - - this.draw = function thumbnailViewDraw(callback) { - if (!this.pdfPage) { - var promise = PDFView.getPage(this.id); - promise.then(function(pdfPage) { - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - return; - } - - if (this.renderingState !== RenderingStates.INITIAL) { - console.error('Must be in new state before drawing'); - } - - this.renderingState = RenderingStates.RUNNING; - if (this.hasImage) { - callback(); - return; - } - - var self = this; - var ctx = this.getPageDrawContext(); - var drawViewport = this.viewport.clone({ scale: this.scale }); - var renderContext = { - canvasContext: ctx, - viewport: drawViewport, - continueCallback: function(cont) { - if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; - return; - } - cont(); - } - }; - this.pdfPage.render(renderContext).then( - function pdfPageRenderCallback() { - self.renderingState = RenderingStates.FINISHED; - callback(); - }, - function pdfPageRenderError(error) { - self.renderingState = RenderingStates.FINISHED; - callback(); - } - ); - this.hasImage = true; - }; - - this.setImage = function thumbnailViewSetImage(img) { - if (this.hasImage || !img) - return; - this.renderingState = RenderingStates.FINISHED; - var ctx = this.getPageDrawContext(); - ctx.drawImage(img, 0, 0, img.width, img.height, - 0, 0, ctx.canvas.width, ctx.canvas.height); - - this.hasImage = true; - }; -}; - -var DocumentOutlineView = function documentOutlineView(outline) { - var outlineView = document.getElementById('outlineView'); - while (outlineView.firstChild) - outlineView.removeChild(outlineView.firstChild); - - function bindItemLink(domObj, item) { - domObj.href = PDFView.getDestinationHash(item.dest); - domObj.onclick = function documentOutlineViewOnclick(e) { - PDFView.navigateTo(item.dest); - return false; - }; - } - - if (!outline) { - var noOutline = document.createElement('div'); - noOutline.classList.add('noOutline'); - noOutline.textContent = mozL10n.get('no_outline', null, - 'No Outline Available'); - outlineView.appendChild(noOutline); - return; - } - - var queue = [{parent: outlineView, items: outline}]; - while (queue.length > 0) { - var levelData = queue.shift(); - var i, n = levelData.items.length; - for (i = 0; i < n; i++) { - var item = levelData.items[i]; - var div = document.createElement('div'); - div.className = 'outlineItem'; - var a = document.createElement('a'); - bindItemLink(a, item); - a.textContent = item.title; - div.appendChild(a); - - if (item.items.length > 0) { - var itemsDiv = document.createElement('div'); - itemsDiv.className = 'outlineItems'; - div.appendChild(itemsDiv); - queue.push({parent: itemsDiv, items: item.items}); - } - - levelData.parent.appendChild(div); - } - } -}; - -// optimised CSS custom property getter/setter -var CustomStyle = (function CustomStyleClosure() { - - // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ - // animate-css-transforms-firefox-webkit.html - // in some versions of IE9 it is critical that ms appear in this list - // before Moz - var prefixes = ['ms', 'Moz', 'Webkit', 'O']; - var _cache = { }; - - function CustomStyle() { - } - - CustomStyle.getProp = function get(propName, element) { - // check cache only when no element is given - if (arguments.length == 1 && typeof _cache[propName] == 'string') { - return _cache[propName]; - } - - element = element || document.documentElement; - var style = element.style, prefixed, uPropName; - - // test standard property first - if (typeof style[propName] == 'string') { - return (_cache[propName] = propName); - } - - // capitalize - uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); - - // test vendor specific properties - for (var i = 0, l = prefixes.length; i < l; i++) { - prefixed = prefixes[i] + uPropName; - if (typeof style[prefixed] == 'string') { - return (_cache[propName] = prefixed); - } - } - - //if all fails then set to undefined - return (_cache[propName] = 'undefined'); - }; - - CustomStyle.setProp = function set(propName, element, str) { - var prop = this.getProp(propName); - if (prop != 'undefined') - element.style[prop] = str; - }; - - return CustomStyle; -})(); - -var TextLayerBuilder = function textLayerBuilder(textLayerDiv, pageIdx) { - var textLayerFrag = document.createDocumentFragment(); - - this.textLayerDiv = textLayerDiv; - this.layoutDone = false; - this.divContentDone = false; - this.pageIdx = pageIdx; - this.matches = []; - - this.beginLayout = function textLayerBuilderBeginLayout() { - this.textDivs = []; - this.textLayerQueue = []; - this.renderingDone = false; - }; - - this.endLayout = function textLayerBuilderEndLayout() { - this.layoutDone = true; - this.insertDivContent(); - }; - - this.renderLayer = function textLayerBuilderRenderLayer() { - var self = this; - var textDivs = this.textDivs; - var bidiTexts = this.textContent.bidiTexts; - var textLayerDiv = this.textLayerDiv; - var canvas = document.createElement('canvas'); - var ctx = canvas.getContext('2d'); - - // No point in rendering so many divs as it'd make the browser unusable - // even after the divs are rendered - var MAX_TEXT_DIVS_TO_RENDER = 100000; - if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER) - return; - - for (var i = 0, ii = textDivs.length; i < ii; i++) { - var textDiv = textDivs[i]; - if ('isWhitespace' in textDiv.dataset) { - continue; - } - textLayerFrag.appendChild(textDiv); - - ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily; - var width = ctx.measureText(textDiv.textContent).width; - - if (width > 0) { - var textScale = textDiv.dataset.canvasWidth / width; - - var transform = 'scale(' + textScale + ', 1)'; - if (bidiTexts[i].dir === 'ttb') { - transform = 'rotate(90deg) ' + transform; - } - CustomStyle.setProp('transform' , textDiv, transform); - CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%'); - - textLayerDiv.appendChild(textDiv); - } - } - - this.renderingDone = true; - this.updateMatches(); - - textLayerDiv.appendChild(textLayerFrag); - }; - - this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() { - // Schedule renderLayout() if user has been scrolling, otherwise - // run it right away - var RENDER_DELAY = 200; // in ms - var self = this; - if (Date.now() - PDFView.lastScroll > RENDER_DELAY) { - // Render right away - this.renderLayer(); - } else { - // Schedule - if (this.renderTimer) - clearTimeout(this.renderTimer); - this.renderTimer = setTimeout(function() { - self.setupRenderLayoutTimer(); - }, RENDER_DELAY); - } - }; - - this.appendText = function textLayerBuilderAppendText(geom) { - var textDiv = document.createElement('div'); - - // vScale and hScale already contain the scaling to pixel units - var fontHeight = geom.fontSize * Math.abs(geom.vScale); - textDiv.dataset.canvasWidth = geom.canvasWidth * geom.hScale; - textDiv.dataset.fontName = geom.fontName; - - textDiv.style.fontSize = fontHeight + 'px'; - textDiv.style.fontFamily = geom.fontFamily; - textDiv.style.left = geom.x + 'px'; - textDiv.style.top = (geom.y - fontHeight) + 'px'; - - // The content of the div is set in the `setTextContent` function. - - this.textDivs.push(textDiv); - }; - - this.insertDivContent = function textLayerUpdateTextContent() { - // Only set the content of the divs once layout has finished, the content - // for the divs is available and content is not yet set on the divs. - if (!this.layoutDone || this.divContentDone || !this.textContent) - return; - - this.divContentDone = true; - - var textDivs = this.textDivs; - var bidiTexts = this.textContent.bidiTexts; - - for (var i = 0; i < bidiTexts.length; i++) { - var bidiText = bidiTexts[i]; - var textDiv = textDivs[i]; - if (!/\S/.test(bidiText.str)) { - textDiv.dataset.isWhitespace = true; - continue; - } - - textDiv.textContent = bidiText.str; - // bidiText.dir may be 'ttb' for vertical texts. - textDiv.dir = bidiText.dir === 'rtl' ? 'rtl' : 'ltr'; - } - - this.setupRenderLayoutTimer(); - }; - - this.setTextContent = function textLayerBuilderSetTextContent(textContent) { - this.textContent = textContent; - this.insertDivContent(); - }; - - this.convertMatches = function textLayerBuilderConvertMatches(matches) { - var i = 0; - var iIndex = 0; - var bidiTexts = this.textContent.bidiTexts; - var end = bidiTexts.length - 1; - var queryLen = PDFFindController.state.query.length; - - var lastDivIdx = -1; - var pos; - - var ret = []; - - // Loop over all the matches. - for (var m = 0; m < matches.length; m++) { - var matchIdx = matches[m]; - // # Calculate the begin position. - - // Loop over the divIdxs. - while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { - iIndex += bidiTexts[i].str.length; - i++; - } - - // TODO: Do proper handling here if something goes wrong. - if (i == bidiTexts.length) { - console.error('Could not find matching mapping'); - } - - var match = { - begin: { - divIdx: i, - offset: matchIdx - iIndex - } - }; - - // # Calculate the end position. - matchIdx += queryLen; - - // Somewhat same array as above, but use a > instead of >= to get the end - // position right. - while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { - iIndex += bidiTexts[i].str.length; - i++; - } - - match.end = { - divIdx: i, - offset: matchIdx - iIndex - }; - ret.push(match); - } - - return ret; - }; - - this.renderMatches = function textLayerBuilder_renderMatches(matches) { - // Early exit if there is nothing to render. - if (matches.length === 0) { - return; - } - - var bidiTexts = this.textContent.bidiTexts; - var textDivs = this.textDivs; - var prevEnd = null; - var isSelectedPage = this.pageIdx === PDFFindController.selected.pageIdx; - var selectedMatchIdx = PDFFindController.selected.matchIdx; - var highlightAll = PDFFindController.state.highlightAll; - - var infty = { - divIdx: -1, - offset: undefined - }; - - function beginText(begin, className) { - var divIdx = begin.divIdx; - var div = textDivs[divIdx]; - div.textContent = ''; - - var content = bidiTexts[divIdx].str.substring(0, begin.offset); - var node = document.createTextNode(content); - if (className) { - var isSelected = isSelectedPage && - divIdx === selectedMatchIdx; - var span = document.createElement('span'); - span.className = className + (isSelected ? ' selected' : ''); - span.appendChild(node); - div.appendChild(span); - return; - } - div.appendChild(node); - } - - function appendText(from, to, className) { - var divIdx = from.divIdx; - var div = textDivs[divIdx]; - - var content = bidiTexts[divIdx].str.substring(from.offset, to.offset); - var node = document.createTextNode(content); - if (className) { - var span = document.createElement('span'); - span.className = className; - span.appendChild(node); - div.appendChild(span); - return; - } - div.appendChild(node); - } - - function highlightDiv(divIdx, className) { - textDivs[divIdx].className = className; - } - - var i0 = selectedMatchIdx, i1 = i0 + 1, i; - - if (highlightAll) { - i0 = 0; - i1 = matches.length; - } else if (!isSelectedPage) { - // Not highlighting all and this isn't the selected page, so do nothing. - return; - } - - for (i = i0; i < i1; i++) { - var match = matches[i]; - var begin = match.begin; - var end = match.end; - - var isSelected = isSelectedPage && i === selectedMatchIdx; - var highlightSuffix = (isSelected ? ' selected' : ''); - if (isSelected) - scrollIntoView(textDivs[begin.divIdx], {top: -50}); - - // Match inside new div. - if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { - // If there was a previous div, then add the text at the end - if (prevEnd !== null) { - appendText(prevEnd, infty); - } - // clears the divs and set the content until the begin point. - beginText(begin); - } else { - appendText(prevEnd, begin); - } - - if (begin.divIdx === end.divIdx) { - appendText(begin, end, 'highlight' + highlightSuffix); - } else { - appendText(begin, infty, 'highlight begin' + highlightSuffix); - for (var n = begin.divIdx + 1; n < end.divIdx; n++) { - highlightDiv(n, 'highlight middle' + highlightSuffix); - } - beginText(end, 'highlight end' + highlightSuffix); - } - prevEnd = end; - } - - if (prevEnd) { - appendText(prevEnd, infty); - } - }; - - this.updateMatches = function textLayerUpdateMatches() { - // Only show matches, once all rendering is done. - if (!this.renderingDone) - return; - - // Clear out all matches. - var matches = this.matches; - var textDivs = this.textDivs; - var bidiTexts = this.textContent.bidiTexts; - var clearedUntilDivIdx = -1; - - // Clear out all current matches. - for (var i = 0; i < matches.length; i++) { - var match = matches[i]; - var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); - for (var n = begin; n <= match.end.divIdx; n++) { - var div = textDivs[n]; - div.textContent = bidiTexts[n].str; - div.className = ''; - } - clearedUntilDivIdx = match.end.divIdx + 1; - } - - if (!PDFFindController.active) - return; - - // Convert the matches on the page controller into the match format used - // for the textLayer. - this.matches = matches = - this.convertMatches(PDFFindController.pageMatches[this.pageIdx] || []); - - this.renderMatches(this.matches); - }; -}; - -document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) { - PDFView.initialize(); - var params = PDFView.parseQueryString(document.location.search.substring(1)); - -//#if !(FIREFOX || MOZCENTRAL) - var file = params.file || DEFAULT_URL; -//#else -//var file = window.location.toString() -//#endif - -//#if !(FIREFOX || MOZCENTRAL) - if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { - document.getElementById('openFile').setAttribute('hidden', 'true'); - } else { - document.getElementById('fileInput').value = null; - } -//#else -//document.getElementById('openFile').setAttribute('hidden', 'true'); -//#endif - - // Special debugging flags in the hash section of the URL. - var hash = document.location.hash.substring(1); - var hashParams = PDFView.parseQueryString(hash); - - if ('disableWorker' in hashParams) - PDFJS.disableWorker = (hashParams['disableWorker'] === 'true'); - -//#if !(FIREFOX || MOZCENTRAL) - var locale = navigator.language; - if ('locale' in hashParams) - locale = hashParams['locale']; - mozL10n.setLanguage(locale); -//#endif - - if ('textLayer' in hashParams) { - switch (hashParams['textLayer']) { - case 'off': - PDFJS.disableTextLayer = true; - break; - case 'visible': - case 'shadow': - case 'hover': - var viewer = document.getElementById('viewer'); - viewer.classList.add('textLayer-' + hashParams['textLayer']); - break; - } - } - -//#if !(FIREFOX || MOZCENTRAL) - if ('pdfBug' in hashParams) { -//#else -//if ('pdfBug' in hashParams && FirefoxCom.requestSync('pdfBugEnabled')) { -//#endif - PDFJS.pdfBug = true; - var pdfBug = hashParams['pdfBug']; - var enabled = pdfBug.split(','); - PDFBug.enable(enabled); - PDFBug.init(); - } - - if (!PDFView.supportsPrinting) { - document.getElementById('print').classList.add('hidden'); - } - - if (!PDFView.supportsFullscreen) { - document.getElementById('fullscreen').classList.add('hidden'); - } - - if (PDFView.supportsIntegratedFind) { - document.querySelector('#viewFind').classList.add('hidden'); - } - - // Listen for warnings to trigger the fallback UI. Errors should be caught - // and call PDFView.error() so we don't need to listen for those. - PDFJS.LogManager.addLogger({ - warn: function() { - PDFView.fallback(); - } - }); - - var mainContainer = document.getElementById('mainContainer'); - var outerContainer = document.getElementById('outerContainer'); - mainContainer.addEventListener('transitionend', function(e) { - if (e.target == mainContainer) { - var event = document.createEvent('UIEvents'); - event.initUIEvent('resize', false, false, window, 0); - window.dispatchEvent(event); - outerContainer.classList.remove('sidebarMoving'); - } - }, true); - - document.getElementById('sidebarToggle').addEventListener('click', - function() { - this.classList.toggle('toggled'); - outerContainer.classList.add('sidebarMoving'); - outerContainer.classList.toggle('sidebarOpen'); - PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen'); - PDFView.renderHighestPriority(); - }); - - document.getElementById('viewThumbnail').addEventListener('click', - function() { - PDFView.switchSidebarView('thumbs'); - }); - - document.getElementById('viewOutline').addEventListener('click', - function() { - PDFView.switchSidebarView('outline'); - }); - - document.getElementById('previous').addEventListener('click', - function() { - PDFView.page--; - }); - - document.getElementById('next').addEventListener('click', - function() { - PDFView.page++; - }); - - document.querySelector('.zoomIn').addEventListener('click', - function() { - PDFView.zoomIn(); - }); - - document.querySelector('.zoomOut').addEventListener('click', - function() { - PDFView.zoomOut(); - }); - - document.getElementById('fullscreen').addEventListener('click', - function() { - PDFView.fullscreen(); - }); - - document.getElementById('openFile').addEventListener('click', - function() { - document.getElementById('fileInput').click(); - }); - - document.getElementById('print').addEventListener('click', - function() { - window.print(); - }); - - document.getElementById('download').addEventListener('click', - function() { - PDFView.download(); - }); - - document.getElementById('pageNumber').addEventListener('click', - function() { - this.select(); - }); - - document.getElementById('pageNumber').addEventListener('change', - function() { - // Handle the user inputting a floating point number. - PDFView.page = (this.value | 0); - - if (this.value !== (this.value | 0).toString()) { - this.value = PDFView.page; - } - }); - - document.getElementById('scaleSelect').addEventListener('change', - function() { - PDFView.parseScale(this.value); - }); - - document.getElementById('first_page').addEventListener('click', - function() { - PDFView.page = 1; - }); - - document.getElementById('last_page').addEventListener('click', - function() { - PDFView.page = PDFView.pdfDocument.numPages; - }); - - document.getElementById('page_rotate_ccw').addEventListener('click', - function() { - PDFView.rotatePages(-90); - }); - - document.getElementById('page_rotate_cw').addEventListener('click', - function() { - PDFView.rotatePages(90); - }); - -//#if (FIREFOX || MOZCENTRAL) -//if (FirefoxCom.requestSync('getLoadingType') == 'passive') { -// PDFView.setTitleUsingUrl(file); -// PDFView.initPassiveLoading(); -// return; -//} -//#endif - -//#if !B2G - PDFView.open(file, 0); -//#endif -}, true); - -function updateViewarea() { - - if (!PDFView.initialized) - return; - var visible = PDFView.getVisiblePages(); - var visiblePages = visible.views; - if (visiblePages.length === 0) { - return; - } - - PDFView.renderHighestPriority(); - - var currentId = PDFView.page; - var firstPage = visible.first; - - for (var i = 0, ii = visiblePages.length, stillFullyVisible = false; - i < ii; ++i) { - var page = visiblePages[i]; - - if (page.percent < 100) - break; - - if (page.id === PDFView.page) { - stillFullyVisible = true; - break; - } - } - - if (!stillFullyVisible) { - currentId = visiblePages[0].id; - } - - if (!PDFView.isFullscreen) { - updateViewarea.inProgress = true; // used in "set page" - PDFView.page = currentId; - updateViewarea.inProgress = false; - } - - var currentScale = PDFView.currentScale; - var currentScaleValue = PDFView.currentScaleValue; - var normalizedScaleValue = currentScaleValue == currentScale ? - currentScale * 100 : currentScaleValue; - - var pageNumber = firstPage.id; - var pdfOpenParams = '#page=' + pageNumber; - pdfOpenParams += '&zoom=' + normalizedScaleValue; - var currentPage = PDFView.pages[pageNumber - 1]; - var topLeft = currentPage.getPagePoint(PDFView.container.scrollLeft, - (PDFView.container.scrollTop - firstPage.y)); - pdfOpenParams += ',' + Math.round(topLeft[0]) + ',' + Math.round(topLeft[1]); - - var store = PDFView.store; - store.initializedPromise.then(function() { - store.set('exists', true); - store.set('page', pageNumber); - store.set('zoom', normalizedScaleValue); - store.set('scrollLeft', Math.round(topLeft[0])); - store.set('scrollTop', Math.round(topLeft[1])); - }); - var href = PDFView.getAnchorUrl(pdfOpenParams); - document.getElementById('viewBookmark').href = href; -} - -window.addEventListener('resize', function webViewerResize(evt) { - if (PDFView.initialized && - (document.getElementById('pageWidthOption').selected || - document.getElementById('pageFitOption').selected || - document.getElementById('pageAutoOption').selected)) - PDFView.parseScale(document.getElementById('scaleSelect').value); - updateViewarea(); -}); - -window.addEventListener('hashchange', function webViewerHashchange(evt) { - PDFView.setHash(document.location.hash.substring(1)); -}); - -window.addEventListener('change', function webViewerChange(evt) { - var files = evt.target.files; - if (!files || files.length === 0) - return; - - // Read the local file into a Uint8Array. - var fileReader = new FileReader(); - fileReader.onload = function webViewerChangeFileReaderOnload(evt) { - var buffer = evt.target.result; - var uint8Array = new Uint8Array(buffer); - PDFView.open(uint8Array, 0); - }; - - var file = files[0]; - fileReader.readAsArrayBuffer(file); - PDFView.setTitleUsingUrl(file.name); - - // URL does not reflect proper document location - hiding some icons. - document.getElementById('viewBookmark').setAttribute('hidden', 'true'); - document.getElementById('download').setAttribute('hidden', 'true'); -}, true); - -function selectScaleOption(value) { - var options = document.getElementById('scaleSelect').options; - var predefinedValueFound = false; - for (var i = 0; i < options.length; i++) { - var option = options[i]; - if (option.value != value) { - option.selected = false; - continue; - } - option.selected = true; - predefinedValueFound = true; - } - return predefinedValueFound; -} - -window.addEventListener('localized', function localized(evt) { - document.getElementsByTagName('html')[0].dir = mozL10n.getDirection(); - - // Adjust the width of the zoom box to fit the content. - PDFView.animationStartedPromise.then( - function() { - var container = document.getElementById('scaleSelectContainer'); - var select = document.getElementById('scaleSelect'); - select.setAttribute('style', 'min-width: inherit;'); - var width = select.clientWidth + 8; - select.setAttribute('style', 'min-width: ' + (width + 20) + 'px;'); - container.setAttribute('style', 'min-width: ' + width + 'px; ' + - 'max-width: ' + width + 'px;'); - }); -}, true); - -window.addEventListener('scalechange', function scalechange(evt) { - var customScaleOption = document.getElementById('customScaleOption'); - customScaleOption.selected = false; - - if (!evt.resetAutoSettings && - (document.getElementById('pageWidthOption').selected || - document.getElementById('pageFitOption').selected || - document.getElementById('pageAutoOption').selected)) { - updateViewarea(); - return; - } - - var predefinedValueFound = selectScaleOption('' + evt.scale); - if (!predefinedValueFound) { - customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%'; - customScaleOption.selected = true; - } - - document.getElementById('zoom_out').disabled = (evt.scale === MIN_SCALE); - document.getElementById('zoom_in').disabled = (evt.scale === MAX_SCALE); - - updateViewarea(); -}, true); - -window.addEventListener('pagechange', function pagechange(evt) { - var page = evt.pageNumber; - if (PDFView.previousPageNumber !== page) { - document.getElementById('pageNumber').value = page; - var selected = document.querySelector('.thumbnail.selected'); - if (selected) - selected.classList.remove('selected'); - var thumbnail = document.getElementById('thumbnailContainer' + page); - thumbnail.classList.add('selected'); - var visibleThumbs = PDFView.getVisibleThumbs(); - var numVisibleThumbs = visibleThumbs.views.length; - // If the thumbnail isn't currently visible scroll it into view. - if (numVisibleThumbs > 0) { - var first = visibleThumbs.first.id; - // Account for only one thumbnail being visible. - var last = numVisibleThumbs > 1 ? - visibleThumbs.last.id : first; - if (page <= first || page >= last) - scrollIntoView(thumbnail); - } - - } - document.getElementById('previous').disabled = (page <= 1); - document.getElementById('next').disabled = (page >= PDFView.pages.length); -}, true); - -// Firefox specific event, so that we can prevent browser from zooming -window.addEventListener('DOMMouseScroll', function(evt) { - if (evt.ctrlKey) { - evt.preventDefault(); - - var ticks = evt.detail; - var direction = (ticks > 0) ? 'zoomOut' : 'zoomIn'; - for (var i = 0, length = Math.abs(ticks); i < length; i++) - PDFView[direction](); - } else if (PDFView.isFullscreen) { - var FIREFOX_DELTA_FACTOR = -40; - PDFView.mouseScroll(evt.detail * FIREFOX_DELTA_FACTOR); - } -}, false); - -window.addEventListener('mousemove', function mousemove(evt) { - if (PDFView.isFullscreen) { - PDFView.showPresentationControls(); - } -}, false); - -window.addEventListener('mousedown', function mousedown(evt) { - if (PDFView.isFullscreen && evt.button === 0) { - // Enable clicking of links in fullscreen mode. - // Note: Only links that point to the currently loaded PDF document works. - var targetHref = evt.target.href; - var internalLink = targetHref && (targetHref.replace(/#.*$/, '') === - window.location.href.replace(/#.*$/, '')); - if (!internalLink) { - // Unless an internal link was clicked, advance a page in fullscreen mode. - evt.preventDefault(); - PDFView.page++; - } - } -}, false); - -window.addEventListener('click', function click(evt) { - if (PDFView.isFullscreen && evt.button === 0) { - // Necessary since preventDefault() in 'mousedown' won't stop - // the event propagation in all circumstances. - evt.preventDefault(); - } -}, false); - -window.addEventListener('keydown', function keydown(evt) { - var handled = false; - var cmd = (evt.ctrlKey ? 1 : 0) | - (evt.altKey ? 2 : 0) | - (evt.shiftKey ? 4 : 0) | - (evt.metaKey ? 8 : 0); - - // First, handle the key bindings that are independent whether an input - // control is selected or not. - if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) { - // either CTRL or META key with optional SHIFT. - switch (evt.keyCode) { - case 70: - if (!PDFView.supportsIntegratedFind) { - PDFFindBar.toggle(); - handled = true; - } - break; - case 61: // FF/Mac '=' - case 107: // FF '+' and '=' - case 187: // Chrome '+' - case 171: // FF with German keyboard - PDFView.zoomIn(); - handled = true; - break; - case 173: // FF/Mac '-' - case 109: // FF '-' - case 189: // Chrome '-' - PDFView.zoomOut(); - handled = true; - break; - case 48: // '0' - case 96: // '0' on Numpad of Swedish keyboard - PDFView.parseScale(DEFAULT_SCALE, true); - handled = false; // keeping it unhandled (to restore page zoom to 100%) - break; - } - } - - // CTRL or META with or without SHIFT. - if (cmd == 1 || cmd == 8 || cmd == 5 || cmd == 12) { - switch (evt.keyCode) { - case 71: // g - if (!PDFView.supportsIntegratedFind) { - PDFFindBar.dispatchEvent('again', cmd == 5 || cmd == 12); - handled = true; - } - break; - } - } - - if (handled) { - evt.preventDefault(); - return; - } - - // Some shortcuts should not get handled if a control/input element - // is selected. - var curElement = document.activeElement; - if (curElement && (curElement.tagName == 'INPUT' || - curElement.tagName == 'SELECT')) { - return; - } - var controlsElement = document.getElementById('toolbar'); - while (curElement) { - if (curElement === controlsElement && !PDFView.isFullscreen) - return; // ignoring if the 'toolbar' element is focused - curElement = curElement.parentNode; - } - - if (cmd === 0) { // no control key pressed at all. - switch (evt.keyCode) { - case 38: // up arrow - case 33: // pg up - case 8: // backspace - if (!PDFView.isFullscreen && PDFView.currentScaleValue !== 'page-fit') { - break; - } - /* in fullscreen mode */ - /* falls through */ - case 37: // left arrow - // horizontal scrolling using arrow keys - if (PDFView.isHorizontalScrollbarEnabled) { - break; - } - /* falls through */ - case 75: // 'k' - case 80: // 'p' - PDFView.page--; - handled = true; - break; - case 27: // esc key - if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) { - PDFFindBar.close(); - handled = true; - } - break; - case 40: // down arrow - case 34: // pg down - case 32: // spacebar - if (!PDFView.isFullscreen && PDFView.currentScaleValue !== 'page-fit') { - break; - } - /* falls through */ - case 39: // right arrow - // horizontal scrolling using arrow keys - if (PDFView.isHorizontalScrollbarEnabled) { - break; - } - /* falls through */ - case 74: // 'j' - case 78: // 'n' - PDFView.page++; - handled = true; - break; - - case 36: // home - if (PDFView.isFullscreen) { - PDFView.page = 1; - handled = true; - } - break; - case 35: // end - if (PDFView.isFullscreen) { - PDFView.page = PDFView.pdfDocument.numPages; - handled = true; - } - break; - - case 82: // 'r' - PDFView.rotatePages(90); - break; - } - } - - if (cmd == 4) { // shift-key - switch (evt.keyCode) { - case 82: // 'r' - PDFView.rotatePages(-90); - break; - } - } - - if (handled) { - evt.preventDefault(); - PDFView.clearMouseScrollState(); - } -}); - -window.addEventListener('beforeprint', function beforePrint(evt) { - PDFView.beforePrint(); -}); - -window.addEventListener('afterprint', function afterPrint(evt) { - PDFView.afterPrint(); -}); - -(function fullscreenClosure() { - function fullscreenChange(e) { - var isFullscreen = document.fullscreenElement || document.mozFullScreen || - document.webkitIsFullScreen; - - if (!isFullscreen) { - PDFView.exitFullscreen(); - } - } - - window.addEventListener('fullscreenchange', fullscreenChange, false); - window.addEventListener('mozfullscreenchange', fullscreenChange, false); - window.addEventListener('webkitfullscreenchange', fullscreenChange, false); -})(); - -(function animationStartedClosure() { - // The offsetParent is not set until the pdf.js iframe or object is visible. - // Waiting for first animation. - var requestAnimationFrame = window.requestAnimationFrame || - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function startAtOnce(callback) { callback(); }; - PDFView.animationStartedPromise = new PDFJS.Promise(); - requestAnimationFrame(function onAnimationFrame() { - PDFView.animationStartedPromise.resolve(); - }); -})(); - -//#if B2G -//window.navigator.mozSetMessageHandler('activity', function(activity) { -// var url = activity.source.data.url; -// PDFView.open(url); -// var cancelButton = document.getElementById('activityClose'); -// cancelButton.addEventListener('click', function() { -// activity.postResult('close'); -// }); -//}); -//#endif diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 11707a03..7c0708ed 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -19,6 +19,7 @@ import mediagoblin.mg_globals as mg_globals from os.path import splitext import logging +import uuid _log = logging.getLogger(__name__) @@ -34,6 +35,8 @@ from mediagoblin.media_types import sniff_media, \ from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ run_process_media, new_upload_entry +from mediagoblin.notifications import add_comment_subscription + @require_active_login @user_has_privilege(u'uploader') @@ -52,6 +55,10 @@ def submit_start(request): try: filename = request.files['file'].filename + # If the filename contains non ascii generate a unique name + if not all(ord(c) < 128 for c in filename): + filename = unicode(uuid.uuid4()) + splitext(filename)[-1] + # Sniff the submitted media to determine which # media plugin should handle processing media_type, media_manager = sniff_media( @@ -62,7 +69,7 @@ def submit_start(request): entry.media_type = unicode(media_type) entry.title = ( unicode(submit_form.title.data) - or unicode(splitext(filename)[0])) + or unicode(splitext(request.files['file'].filename)[0])) entry.description = unicode(submit_form.description.data) @@ -93,6 +100,8 @@ def submit_start(request): run_process_media(entry, feed_url) add_message(request, SUCCESS, _('Woohoo! Submitted!')) + add_comment_subscription(request.user, entry) + return redirect(request, "mediagoblin.user_pages.user_home", user=request.user.username) except Exception as e: @@ -130,9 +139,9 @@ def add_collection(request, media=None): collection.generate_slug() # Make sure this user isn't duplicating an existing collection - existing_collection = request.db.Collection.find_one({ - 'creator': request.user.id, - 'title':collection.title}) + existing_collection = request.db.Collection.query.filter_by( + creator=request.user.id, + title=collection.title).first() if existing_collection: add_message(request, messages.ERROR, diff --git a/mediagoblin/templates/mediagoblin/auth/change_fp.html b/mediagoblin/templates/mediagoblin/auth/change_fp.html index 1f7d9aca..a3cf9cb9 100644 --- a/mediagoblin/templates/mediagoblin/auth/change_fp.html +++ b/mediagoblin/templates/mediagoblin/auth/change_fp.html @@ -34,11 +34,10 @@ {{ csrf_token }} <div class="form_box"> <h1>{% trans %}Set your new password{% endtrans %}</h1> - {{ wtforms_util.render_divs(cp_form) }} + {{ wtforms_util.render_divs(cp_form, True) }} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Set password{% endtrans %}" class="button_form"/> </div> </div> - </form> {% 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 }} <div class="form_box"> <h1>{% trans %}Recover password{% endtrans %}</h1> - {{ wtforms_util.render_divs(fp_form) }} + {{ wtforms_util.render_divs(fp_form, True) }} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Send instructions{% endtrans %}" class="button_form"/> </div> 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 %} - <form action="{{ request.urlgen('mediagoblin.auth.login') }}" + <form action="{{ post_url }}" method="POST" enctype="multipart/form-data"> {{ csrf_token }} <div class="form_box"> @@ -41,15 +41,19 @@ {% endif %} {% if allow_registration %} <p> - {% trans %}Don't have an account yet?{% endtrans %} <a href="{{ request.urlgen('mediagoblin.auth.register') }}"> + {% trans %}Don't have an account yet?{% endtrans %} + <a href="{{ request.urlgen('mediagoblin.auth.register') }}"> {%- trans %}Create one here!{% endtrans %}</a> </p> {% endif %} - {{ wtforms_util.render_divs(login_form) }} - <p> - <a href="{{ request.urlgen('mediagoblin.auth.forgot_password') }}" id="forgot_password"> - {% trans %}Forgot your password?{% endtrans %}</a> - </p> + {% template_hook("login_link") %} + {{ wtforms_util.render_divs(login_form, True) }} + {% if pass_auth %} + <p> + <a href="{{ request.urlgen('mediagoblin.auth.forgot_password') }}" id="forgot_password"> + {% trans %}Forgot your password?{% endtrans %}</a> + </p> + {% endif %} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/> </div> 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 %} - <form action="{{ request.urlgen('mediagoblin.auth.register') }}" + <form action="{{ post_url }}" method="POST" enctype="multipart/form-data"> <div class="form_box"> <h1>{% trans %}Create an account!{% endtrans %}</h1> - {{ wtforms_util.render_divs(register_form) }} + {% template_hook("register_link") %} + {{ wtforms_util.render_divs(register_form, True) }} {{ csrf_token }} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Create{% endtrans %}" @@ -42,6 +43,4 @@ </div> </div> </form> -<!-- Focus the username field by default --> -<script>$(document).ready(function(){$("#username").focus();});</script> {% 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') }}"></script> <script type="text/javascript" src="{{ request.staticdirect('/js/header_dropdown.js') }}"></script> + <script type="text/javascript" + src="{{ request.staticdirect('/js/notifications.js') }}"></script> {# 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 @@ <div class="header_right"> {%- 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 %} + <a href="#notifications" class="notification-gem button_action" title="Notifications"> + {{ notification_count }}</a> + {% endif %} <div class="button_action header_dropdown_down">▼</div> <div class="button_action header_dropdown_up">▲</div> {% elif request.user and request.user.status == "needs_email_verification" %} @@ -67,7 +75,7 @@ {% trans %}Verify your email!{% endtrans %}</a> or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a> {% endif %} - {%- else %} + {%- elif auth %} <a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{ request.base_url|urlencode }}"> {%- trans %}Log in{% endtrans -%} @@ -112,6 +120,7 @@ </a> </p> {% endif %} + {% include 'mediagoblin/fragments/header_notifications.html' %} </div> {% endif %} </header> 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 %} - <h1>{% trans %}Explore{% endtrans %}</h1> -{% else %} - <h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1> - <img class="right_align" src="{{ request.staticdirect('/images/frontpage_image.png') }}" /> - <p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p> - <p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p> - {% if allow_registration %} - <p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p> - {% trans register_url=request.urlgen('mediagoblin.auth.register') -%} - <a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a> - or - <a class="button_action" href="http://wiki.mediagoblin.org/HackingHowto">Set up MediaGoblin on your own server</a> - {%- endtrans %} + <h1>{% trans %}Explore{% endtrans %}</h1> + {% else %} + <h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1> + <img class="right_align" src="{{ request.staticdirect('/images/frontpage_image.png') }}" /> + <p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p> + {% if auth %} + <p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p> + {% if allow_registration %} + <p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p> + {% trans register_url=request.urlgen('mediagoblin.auth.register') -%} + <a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a> + or + {%- endtrans %} + {% endif %} + {% endif %} + {% trans %} + <a class="button_action" href="http://wiki.mediagoblin.org/HackingHowto">Set up MediaGoblin on your own server</a> + {%- endtrans %} + + <div class="clear"></div> {% endif %} - <div class="clear"></div> -{% 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 -%} </h1> - {{ wtforms_util.render_divs(form) }} + {{ wtforms_util.render_divs(form, True) }} {{ csrf_token }} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Save{% endtrans %}" diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html index 4c4aaf95..51293acb 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_account.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html @@ -41,17 +41,16 @@ Changing {{ username }}'s account settings {%- endtrans -%} </h1> + {% if pass_auth is defined %} <p> <a href="{{ request.urlgen('mediagoblin.edit.pass') }}"> {% trans %}Change your password.{% endtrans %} </a> </p> - <div class="form_field_input"> - <p>{{ form.wants_comment_notification }} - {{ wtforms_util.render_label(form.wants_comment_notification) }}</p> - </div> - {{- wtforms_util.render_field_div(form.license_preference) }} - <div class="form_submit_buttons"> + {% endif %} + {% template_hook("edit_link") %} + {{ wtforms_util.render_divs(form, True) }} + <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Save changes{% endtrans %}" class="button_form" /> {{ csrf_token }} </div> 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 <http://www.gnu.org/licenses/>. +-#} + +{% 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 %} + <div class="header_notifications"> + <h3>{% trans %}New comments{% endtrans %}</h3> + <ul> + {% for notification in notifications %} + {% set comment = notification.subject %} + {% set comment_author = comment.get_author %} + {% set media = comment.get_entry %} + <li class="comment_wrapper"> + <div class="comment_author"> + <img src="{{ request.staticdirect('/images/icon_comment.png') }}" /> + <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', + user=comment_author.username) }}" + class="comment_authorlink"> + {{- comment_author.username -}} + </a> + <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment', + comment=comment.id, + user=media.get_uploader.username, + media=media.slug_or_id) }}#comment" + class="comment_whenlink"> + <span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'> + {%- trans formatted_time=timesince(comment.created) -%} + {{ formatted_time }} ago + {%- endtrans -%} + </span> + </a>: + </div> + <div class="comment_content"> + {% autoescape False -%} + {{ comment.content_html }} + {%- endautoescape %} + </div> + + </li> + {% endfor %} + </ul> + </div> +{% 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 %} -<iframe width=640px height=480px - src="{{ request.staticdirect('/extlib/pdf.js/web/viewer.html') }}?file={{ pdf_view }} "> -</iframe> - -{% else %} - <a href="{{ pdf_view }}"> - <img id="medium" - class="media_image" - src="{{ medium_view }}" - alt="{% trans media_title=media.title -%} Image for {{ media_title}}{% endtrans %}"/> - </a> -{% endif %} + {% if pdf_js %} + <iframe width="640px" height="480px" + src="{{ request.staticdirect('/extlib/pdf.js/web/viewer.html') }}?file={{ pdf_view }} "> + </iframe> + {% else %} + <a href="{{ pdf_view }}"> + <img id="medium" + class="media_image" + src="{{ medium_view }}" + alt=" + {%- trans media_title=media.title -%} + Image for {{ media_title}} + {%- endtrans %}"/> + </a> + {% 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 () { <div style="padding: 4px;"> - <a class="button_action" onclick="show('perspective');" - title="{%- trans %}Toggle Rotate{% endtrans -%}"> + <a class="button_action" onclick="show('perspective');"> {%- trans %}Perspective{% endtrans -%} </a> - <a class="button_action" onclick="show('front_view');" - title="{%- trans %}Front{% endtrans -%}"> + <a class="button_action" onclick="show('front_view');"> {%- trans %}Front{% endtrans -%} </a> - <a class="button_action" onclick="show('top_view');" - title="{%- trans %}Top{% endtrans -%}"> + <a class="button_action" onclick="show('top_view');"> {%- trans %}Top{% endtrans -%} </a> - <a class="button_action" onclick="show('side_view');" - title="{%- trans %}Side{% endtrans -%}"> + <a class="button_action" onclick="show('side_view');"> {%- trans %}Side{% endtrans -%} </a> {% if media.media_data.file_type == "stl" %} <a id="webgl_button" class="button_action" - onclick="show_things();" - title="{%- trans %}WebGL{% endtrans -%}"> + onclick="show_things();"> {%- trans %}WebGL{% endtrans -%} </a> {% endif %} <a class="button_action" href="{{ model_download }}" - title="{%- trans %}Download{% endtrans -%}" style="float:right;"> {%- trans %}Download model{% endtrans -%} </a> 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) %} <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a> + {% endif %} {% autoescape False %} <p>{{ media.description_html }}</p> @@ -94,6 +95,8 @@ class="button_action" id="button_addcomment" title="Add a comment"> {% trans %}Add a comment{% endtrans %} </a> + {% include "mediagoblin/utils/comment-subscription.html" %} + {% endif %} {% if request.user %} <form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment', diff --git a/mediagoblin/templates/mediagoblin/utils/comment-subscription.html b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html new file mode 100644 index 00000000..bd367e80 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html @@ -0,0 +1,34 @@ +{# +# 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 <http://www.gnu.org/licenses/>. +#} +{%- if request.user %} + {% set subscription = request.notifications.get_comment_subscription( + request.user.id, media.id) %} + {% if not subscription or not subscription.notify %} + <a type="submit" href="{{ request.urlgen('mediagoblin.notifications.subscribe_comments', + user=media.get_uploader.username, + media=media.slug_or_id)}}" + class="button_action">Subscribe to comments + </a> + {% else %} + <a type="submit" href="{{ request.urlgen('mediagoblin.notifications.silence_comments', + user=media.get_uploader.username, + media=media.slug_or_id)}}" + class="button_action">Silence comments + </a> + {% 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) }} - <div class="form_field_input"> - {{ field }} - {%- if field.errors -%} - {% for error in field.errors %} - <p class="form_field_error">{{ error }}</p> - {% endfor %} - {%- endif %} - {%- if field.description %} - <p class="form_field_description">{{ field.description|safe }}</p> - {%- endif %} - </div> +{% macro render_field_div(field, autofocus_first=False) %} + {% if field.type == 'BooleanField' %} + {{ render_bool(field) }} + {% else %} + {{- render_label_p(field) }} + <div class="form_field_input"> + {% if autofocus_first %} + {{ field(autofocus=True) }} + {% else %} + {{ field }} + {% endif %} + {%- if field.errors -%} + {% for error in field.errors %} + <p class="form_field_error">{{ error }}</p> + {% endfor %} + {%- endif %} + {%- if field.description %} + <p class="form_field_description">{{ field.description|safe }}</p> + {%- endif %} + </div> + {% 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 @@ </tr> {% endfor %} {%- endmacro %} + +{# Render a boolean field #} +{% macro render_bool(field) %} + <div class="boolean"> + <label for="{{ field.label.field_id }}"> + {{ field }}</input> + {{ field.description|safe }} + </label> + {%- if field.errors -%} + {% for error in field.errors %} + <p class="form_field_error">{{ error }}</p> + {% endfor %} + {% endif %} + </div> +{% 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 --- /dev/null +++ b/mediagoblin/tests/auth_configs/__init__.py 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 <http://www.gnu.org/licenses/>. +[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 <http://www.gnu.org/licenses/>. - 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 <http://www.gnu.org/licenses/>. +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 <http://www.gnu.org/licenses/>. 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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. -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) @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from 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', |