aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules6
-rw-r--r--AUTHORS1
-rw-r--r--docs/source/devel/codebase.rst2
-rw-r--r--docs/source/devel/storage.rst94
-rw-r--r--docs/source/pluginwriter/api.rst27
-rw-r--r--docs/source/siteadmin/deploying.rst14
-rw-r--r--docs/source/siteadmin/media-types.rst37
-rw-r--r--docs/source/siteadmin/relnotes.rst14
-rw-r--r--extlib/README13
-rw-r--r--extlib/freesound/audioprocessing.py2
m---------extlib/pdf.js0
-rw-r--r--mediagoblin.ini2
-rw-r--r--mediagoblin/app.py8
-rw-r--r--mediagoblin/auth/forms.py6
-rw-r--r--mediagoblin/auth/views.py12
-rw-r--r--mediagoblin/config_spec.ini9
-rw-r--r--mediagoblin/db/migrations.py38
-rw-r--r--mediagoblin/db/mixin.py4
-rw-r--r--mediagoblin/db/models.py6
-rw-r--r--mediagoblin/db/open.py16
-rw-r--r--mediagoblin/edit/forms.py2
-rw-r--r--mediagoblin/edit/views.py10
-rw-r--r--mediagoblin/gmg_commands/dbupdate.py2
-rw-r--r--mediagoblin/init/celery/__init__.py4
-rw-r--r--mediagoblin/init/celery/from_celery.py4
-rw-r--r--mediagoblin/init/config.py42
-rw-r--r--mediagoblin/init/plugins/__init__.py2
-rw-r--r--mediagoblin/media_types/__init__.py63
-rw-r--r--mediagoblin/media_types/ascii/__init__.py20
-rw-r--r--mediagoblin/media_types/ascii/asciitoimage.py11
-rw-r--r--mediagoblin/media_types/ascii/processing.py7
-rw-r--r--mediagoblin/media_types/audio/__init__.py17
-rw-r--r--mediagoblin/media_types/audio/spectrogram.py5
-rw-r--r--mediagoblin/media_types/audio/transcoders.py5
-rw-r--r--mediagoblin/media_types/image/__init__.py22
-rw-r--r--mediagoblin/media_types/image/processing.py91
-rw-r--r--mediagoblin/media_types/pdf/__init__.py31
-rw-r--r--mediagoblin/media_types/pdf/migrations.py17
-rw-r--r--mediagoblin/media_types/pdf/models.py58
-rw-r--r--mediagoblin/media_types/pdf/processing.py277
-rw-r--r--mediagoblin/media_types/stl/__init__.py18
-rw-r--r--mediagoblin/media_types/stl/processing.py2
-rw-r--r--mediagoblin/media_types/video/__init__.py28
-rw-r--r--mediagoblin/media_types/video/models.py2
-rw-r--r--mediagoblin/media_types/video/transcoders.py5
-rw-r--r--mediagoblin/plugins/api/__init__.py4
-rw-r--r--mediagoblin/plugins/httpapiauth/__init__.py3
-rw-r--r--mediagoblin/plugins/oauth/__init__.py2
-rw-r--r--mediagoblin/plugins/oauth/forms.py2
-rw-r--r--mediagoblin/plugins/oauth/migrations.py34
-rw-r--r--mediagoblin/plugins/oauth/models.py87
-rw-r--r--mediagoblin/plugins/oauth/tools.py73
-rw-r--r--mediagoblin/plugins/oauth/views.py150
-rw-r--r--mediagoblin/plugins/piwigo/__init__.py5
-rw-r--r--mediagoblin/plugins/piwigo/forms.py16
-rw-r--r--mediagoblin/plugins/piwigo/tools.py51
-rw-r--r--mediagoblin/plugins/piwigo/views.py68
-rw-r--r--mediagoblin/processing/__init__.py8
-rw-r--r--mediagoblin/processing/task.py2
-rw-r--r--mediagoblin/static/css/base.css20
-rw-r--r--mediagoblin/static/css/pdf_viewer.css1448
l---------mediagoblin/static/extlib/pdf.js1
-rw-r--r--mediagoblin/static/js/pdf_viewer.js3615
-rw-r--r--mediagoblin/submit/forms.py2
-rw-r--r--mediagoblin/submit/lib.py2
-rw-r--r--mediagoblin/submit/views.py47
-rw-r--r--mediagoblin/templates/mediagoblin/admin/panel.html2
-rw-r--r--mediagoblin/templates/mediagoblin/base.html5
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/pdf.html84
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html7
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html7
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_list.html12
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html24
-rw-r--r--mediagoblin/templates/mediagoblin/utils/wtforms.html8
-rw-r--r--mediagoblin/tests/appconfig_plugin_specs.ini21
-rw-r--r--mediagoblin/tests/resources.py41
-rw-r--r--mediagoblin/tests/test_api.py18
-rw-r--r--mediagoblin/tests/test_exif.py29
-rw-r--r--mediagoblin/tests/test_http_callback.py29
-rw-r--r--mediagoblin/tests/test_mgoblin_app.ini2
-rw-r--r--mediagoblin/tests/test_oauth.py115
-rw-r--r--mediagoblin/tests/test_pdf.py39
-rw-r--r--mediagoblin/tests/test_pluginapi.py104
-rw-r--r--mediagoblin/tests/test_storage.py29
-rw-r--r--mediagoblin/tests/test_submission.py79
-rw-r--r--mediagoblin/tests/test_submission/good.pdfbin0 -> 194007 bytes
-rw-r--r--mediagoblin/tests/test_timesince.py57
-rw-r--r--mediagoblin/tests/test_util.py22
-rw-r--r--mediagoblin/tests/test_workbench.py4
-rw-r--r--mediagoblin/tests/testplugins/callables1/__init__.py8
-rw-r--r--mediagoblin/tests/testplugins/callables2/__init__.py3
-rw-r--r--mediagoblin/tests/testplugins/callables3/__init__.py3
-rw-r--r--mediagoblin/tests/testplugins/pluginspec/__init__.py22
-rw-r--r--mediagoblin/tests/testplugins/pluginspec/config_spec.ini4
-rw-r--r--mediagoblin/tests/tools.py8
-rw-r--r--mediagoblin/tools/pluginapi.py112
-rw-r--r--mediagoblin/tools/processing.py2
-rw-r--r--mediagoblin/tools/response.py7
-rw-r--r--mediagoblin/tools/routing.py3
-rw-r--r--mediagoblin/tools/template.py10
-rw-r--r--mediagoblin/tools/timesince.py95
-rw-r--r--mediagoblin/tools/translate.py45
-rw-r--r--mediagoblin/user_pages/forms.py2
-rw-r--r--mediagoblin/user_pages/lib.py20
-rw-r--r--mediagoblin/user_pages/views.py43
-rw-r--r--setup.py2
106 files changed, 7220 insertions, 603 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..e6a7464c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "pdf.js"]
+ path = pdf.js
+ url = git://github.com/mozilla/pdf.js.git
+[submodule "extlib/pdf.js"]
+ path = extlib/pdf.js
+ url = git://github.com/mozilla/pdf.js.git
diff --git a/AUTHORS b/AUTHORS
index ef70e8a5..b6dd1cf2 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -30,6 +30,7 @@ Thank you!
* Greg Grossmeier
* Jakob Kramer
* Jef van Schendel
+* Jessica Tallon
* Jim Campbell
* Joar Wandborg
* Jorge Araya Navarro
diff --git a/docs/source/devel/codebase.rst b/docs/source/devel/codebase.rst
index 9718a097..122a3297 100644
--- a/docs/source/devel/codebase.rst
+++ b/docs/source/devel/codebase.rst
@@ -119,7 +119,7 @@ Software Stack
* `Python <http://python.org/>`_: the language we're using to write
this
- * `Nose <http://somethingaboutorange.com/mrl/projects/nose/>`_:
+ * `Py.Test <http://pytest.org/>`_:
for unit tests
* `virtualenv <http://www.virtualenv.org/>`_: for setting up an
diff --git a/docs/source/devel/storage.rst b/docs/source/devel/storage.rst
index 52406c4e..215f9579 100644
--- a/docs/source/devel/storage.rst
+++ b/docs/source/devel/storage.rst
@@ -2,18 +2,28 @@
Storage
=========
-
-See for now: http://wiki.mediagoblin.org/Storage
-
-Things get moved here.
-
-
The storage systems attached to your app
----------------------------------------
Dynamic content: queue_store and public_store
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Two instances of the StorageInterface come attached to your app. These
+are:
+
++ **queue_store:** When a user submits a fresh piece of media for
+ their gallery, before the Processing stage, that piece of media sits
+ here in the queue_store. (It's possible that we'll rename this to
+ "private_store" and start storing more non-publicly-stored stuff in
+ the future...). This is a StorageInterface implementation
+ instance. Visitors to your site probably cannot see it... it isn't
+ designed to be seen, anyway.
+
++ **public_store:** After your media goes through processing it gets
+ moved to the public store. This is also a StorageInterface
+ implelementation, and is for stuff that's intended to be seen by
+ site visitors.
+
The workbench
~~~~~~~~~~~~~
@@ -32,6 +42,28 @@ See the workbench module documentation for more.
Static assets / staticdirect
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+On top of all that, there is some static media that comes bundled with your
+application. This stuff is kept in:
+
+ mediagoblin/static/
+
+These files are for mediagoblin base assets. Things like the CSS files,
+logos, etc. You can mount these at whatever location is appropriate to you
+(see the direct_remote_path option in the config file) so if your users
+are keeping their static assets at http://static.mgoblin.example.org/ but
+their actual site is at http://mgoblin.example.org/, you need to be able
+to get your static files in a where-it's-mounted agnostic way. There's a
+"staticdirector" attached to the request object. It's pretty easy to use;
+just look at this bit taken from the
+mediagoblin/templates/mediagoblin/base.html main template:
+
+ <link rel="stylesheet" type="text/css"
+ href="Template:Request.staticdirect('/css/extlib/text.css')"/>
+
+see? Not too hard. As expected, if you configured direct_remote_path to be
+http://static.mgoblin.example.org/ you'll get back
+http://static.mgoblin.example.org/css/extlib/text.css just as you'd
+probably expect.
StorageInterface and implementations
------------------------------------
@@ -39,5 +71,55 @@ StorageInterface and implementations
The guts of StorageInterface and friends
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+So, the StorageInterface!
+
+So, the public and queue stores both use StorageInterface implementations
+... but what does that mean? It's not too hard.
+
+Open up:
+
+ mediagoblin/storage.py
+
+In here you'll see a couple of things. First of all, there's the
+StorageInterface class. What you'll see is that this is just a very simple
+python class. A few of the methods actually implement things, but for the
+most part, they don't. What really matters about this class is the
+docstrings. Each expected method is documented as to how it should be
+constructed. Want to make a new StorageInterface? Simply subclass it. Want
+to know how to use the methods of your storage system? Read these docs,
+they span all implementations.
+
+There are a couple of implementations of these classes bundled in
+storage.py as well. The most simple of these is BasicFileStorage, which is
+also the default storage system used. As expected, this stores files
+locally on your machine.
+
+There's also a CloudFileStorage system. This provides a mapping to
+[OpenStack's swift http://swift.openstack.org/] storage system (used by
+RackSpace Cloud files and etc).
+
+Between these two examples you should be able to get a pretty good idea of
+how to write your own storage systems, for storing data across your
+beowulf cluster of radioactive monkey brains, whatever.
+
Writing code to store stuff
~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+So what does coding for StorageInterface implementations actually look
+like? It's pretty simple, really. For one thing, the design is fairly
+inspired by [Django's file storage API
+https://docs.djangoproject.com/en/dev/ref/files/storage/]... with some
+differences.
+
+Basically, you access files on "file paths", which aren't exactly like
+unix file paths, but are close. If you wanted to store a file on a path
+like dir1/dir2/filename.jpg you'd actually write that file path like:
+
+['dir1', 'dir2', 'filename.jpg']
+
+This way we can be *sure* that each component is actually a component of
+the path that's expected... we do some filename cleaning on each component.
+
+Your StorageInterface should pass in and out "file like objects". In other
+words, they should provide .read() and .write() at minimum, and probably
+also .seek() and .close().
diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst
index 44ffd6e8..df933511 100644
--- a/docs/source/pluginwriter/api.rst
+++ b/docs/source/pluginwriter/api.rst
@@ -16,10 +16,35 @@
Plugin API
==========
+This documents the general plugin API.
+
+Please note, at this point OUR PLUGIN HOOKS MAY AND WILL CHANGE.
+Authors are encouraged to develop plugins and work with the
+MediaGoblin community to keep them up to date, but this API will be a
+moving target for a few releases.
+
+Please check the release notes for updates!
+
:mod:`pluginapi` Module
-----------------------
.. automodule:: mediagoblin.tools.pluginapi
:members: get_config, register_routes, register_template_path,
register_template_hooks, get_hook_templates,
- callable_runone, callable_runall
+ hook_handle, hook_runall, hook_transform
+
+Configuration
+-------------
+
+Your plugin may define its own configuration defaults.
+
+Simply add to the directory of your plugin a config_spec.ini file. An
+example might look like::
+
+ [plugin_spec]
+ some_string = string(default="blork")
+ some_int = integer(default=50)
+
+This means that when people enable your plugin in their config you'll
+be able to provide defaults as well as type validation.
+
diff --git a/docs/source/siteadmin/deploying.rst b/docs/source/siteadmin/deploying.rst
index 77e60037..f2f71e01 100644
--- a/docs/source/siteadmin/deploying.rst
+++ b/docs/source/siteadmin/deploying.rst
@@ -345,3 +345,17 @@ Visit the site you've set up in your browser by visiting
smaller deployments. However, for larger production deployments
with larger processing requirements, see the
":doc:`production-deployments`" documentation.
+
+
+Security Considerations
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. warning::
+
+ The directory ``user_dev/crypto/`` contains some very
+ sensitive files.
+ Especially the ``itsdangeroussecret.bin`` is very important
+ for session security. Make sure not to leak its contents anywhere.
+ If the contents gets leaked nevertheless, delete your file
+ and restart the server, so that it creates a new secret key.
+ All previous sessions will be invalifated then.
diff --git a/docs/source/siteadmin/media-types.rst b/docs/source/siteadmin/media-types.rst
index 23d3f3b9..210094b9 100644
--- a/docs/source/siteadmin/media-types.rst
+++ b/docs/source/siteadmin/media-types.rst
@@ -195,3 +195,40 @@ Run
You should now be able to upload .obj and .stl files and MediaGoblin
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 install this on Fedora:
+
+.. code-block:: bash
+
+ sudo yum install -y poppler-utils unoconv libreoffice-headless
+
+pdf.js relies on git submodules, so be sure you have fetched them:
+
+.. code-block:: bash
+
+ git submodule init
+ git submodule update
+
+This feature has been tested on Fedora with:
+ poppler-utils-0.20.2-9.fc18.x86_64
+ unoconv-0.5-2.fc18.noarch
+ libreoffice-headless-3.6.5.2-8.fc18.x86_64
+
+It may work on some earlier versions, but that is not guaranteed.
+
+Add ``mediagoblin.media_types.pdf`` to the ``media_types`` list in your
+``mediagoblin_local.ini`` and restart MediaGoblin.
+
+Run
+
+.. code-block:: bash
+
+ ./bin/gmg dbupdate
+
+
diff --git a/docs/source/siteadmin/relnotes.rst b/docs/source/siteadmin/relnotes.rst
index 6962dc5a..04863ec6 100644
--- a/docs/source/siteadmin/relnotes.rst
+++ b/docs/source/siteadmin/relnotes.rst
@@ -100,7 +100,19 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
**Do this to upgrade**
-1. Make sure to run ``bin/gmg dbupdate`` after upgrading.
+ # directory of your mediagoblin install
+ cd /srv/mediagoblin.example.org
+
+ # copy source for this release
+ git fetch
+ git checkout tags/v0.3.2
+
+ # perform any needed database updates
+ bin/gmg dbupdate
+
+ # restart your servers however you do that, e.g.,
+ sudo service mediagoblin-paster restart
+ sudo service mediagoblin-celeryd restart
**New features**
diff --git a/extlib/README b/extlib/README
index a2cc6ec0..45ee5b46 100644
--- a/extlib/README
+++ b/extlib/README
@@ -63,6 +63,19 @@ FAQ
This is a last resort; consult with the rest of the dev group
before taking this radical step.
+:Q: What about submodules?
+
+:A: pdf.js is supplied as a submodule, and other software may use that too,
+ to add a new submodule:
+ git submodule add <git-repo-of-fun-project> extlib/fun-project
+
+ Use it just like a snapshotted extlib directory. When a new clone of mediagoblin
+ is made you need to run
+
+ git submodule init
+ git submodule update
+
+ As noted in HackingHowto
Thanks
======
diff --git a/extlib/freesound/audioprocessing.py b/extlib/freesound/audioprocessing.py
index c1dfe2eb..2c2b35b5 100644
--- a/extlib/freesound/audioprocessing.py
+++ b/extlib/freesound/audioprocessing.py
@@ -20,7 +20,7 @@
# Bram de Jong <bram.dejong at domain.com where domain in gmail>
# 2012, Joar Wandborg <first name at last name dot se>
-import Image, ImageDraw, ImageColor #@UnresolvedImport
+from PIL import Image, ImageDraw, ImageColor #@UnresolvedImport
from functools import partial
import math
import numpy
diff --git a/extlib/pdf.js b/extlib/pdf.js
new file mode 160000
+Subproject 369b81b63f560b5d729da26752ca541503d8151
diff --git a/mediagoblin.ini b/mediagoblin.ini
index bed69737..43621107 100644
--- a/mediagoblin.ini
+++ b/mediagoblin.ini
@@ -20,6 +20,8 @@ email_debug_mode = true
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/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
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index 1137c0d7..bf0e0f13 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -35,7 +35,7 @@ from mediagoblin.init.plugins import setup_plugins
from mediagoblin.init import (get_jinja_loader, get_staticdirector,
setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
setup_storage)
-from mediagoblin.tools.pluginapi import PluginManager
+from mediagoblin.tools.pluginapi import PluginManager, hook_transform
from mediagoblin.tools.crypto import setup_crypto
@@ -227,7 +227,7 @@ class MediaGoblinApp(object):
for m in self.meddleware[::-1]:
m.process_response(request, response)
except HTTPException as e:
- response = render_http_exeption(
+ response = render_http_exception(
request, e, e.get_description(environ))
session_manager.save_session_to_cookie(request.session,
@@ -259,8 +259,6 @@ def paste_app_factory(global_config, **app_config):
raise IOError("Usable mediagoblin config not found.")
mgoblin_app = MediaGoblinApp(mediagoblin_config)
-
- for callable_hook in PluginManager().get_hook_callables('wrap_wsgi'):
- mgoblin_app = callable_hook(mgoblin_app)
+ mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
return mgoblin_app
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py
index 8f091d21..33e1f45c 100644
--- a/mediagoblin/auth/forms.py
+++ b/mediagoblin/auth/forms.py
@@ -17,7 +17,7 @@
import wtforms
from mediagoblin.tools.mail import normalize_email
-from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
def normalize_user_or_email_field(allow_email=True, allow_user=True):
"""Check if we were passed a field that matches a username and/or email pattern
@@ -64,9 +64,9 @@ class RegistrationForm(wtforms.Form):
class LoginForm(wtforms.Form):
username = wtforms.TextField(
- _('Username'),
+ _('Username or Email'),
[wtforms.validators.Required(),
- normalize_user_or_email_field(allow_email=False)])
+ normalize_user_or_email_field()])
password = wtforms.PasswordField(
_('Password'),
[wtforms.validators.Required(),
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index 354b48c1..dc408911 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -25,7 +25,7 @@ from mediagoblin.auth import lib as auth_lib
from mediagoblin.auth import forms as auth_forms
from mediagoblin.auth.lib import send_verification_email, \
send_fp_verification_email
-
+from sqlalchemy import or_
def email_debug_message(request):
"""
@@ -113,8 +113,16 @@ def login(request):
login_failed = False
if request.method == 'POST':
+
+ username = login_form.data['username']
+
if login_form.validate():
- user = User.query.filter_by(username=login_form.data['username']).first()
+ user = User.query.filter(
+ or_(
+ User.username == username,
+ User.email == username,
+
+ )).first()
if user and user.check_login(login_form.password.data):
# set up login in session
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index 545d02e2..6c00aa58 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -32,7 +32,7 @@ email_smtp_pass = string(default=None)
allow_registration = boolean(default=True)
# tag parsing
-tags_max_length = integer(default=50)
+tags_max_length = integer(default=255)
# Enable/disable comments
allow_comments = boolean(default=True)
@@ -94,6 +94,8 @@ max_height = integer(default=180)
[media_type:mediagoblin.media_types.image]
# One of BICUBIC, BILINEAR, NEAREST, ANTIALIAS
resize_filter = string(default="ANTIALIAS")
+#level of compression used when resizing images
+quality = integer(default=90)
[media_type:mediagoblin.media_types.video]
# Should we keep the original file?
@@ -116,7 +118,6 @@ video_codecs = string_list(default=list("VP8 video"))
audio_codecs = string_list(default=list("Vorbis"))
dimensions_match = boolean(default=True)
-
[media_type:mediagoblin.media_types.audio]
keep_original = boolean(default=True)
# vorbisenc quality
@@ -124,10 +125,12 @@ quality = float(default=0.3)
create_spectrogram = boolean(default=True)
spectrogram_fft_size = integer(default=4096)
-
[media_type:mediagoblin.media_types.ascii]
thumbnail_font = string(default=None)
+[media_type:mediagoblin.media_types.pdf]
+pdf_js = boolean(default=False)
+
[celery]
# default result stuff
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 167c4f87..2c553396 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
+import uuid
from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
Integer, Unicode, UnicodeText, DateTime,
@@ -212,7 +213,6 @@ def mediaentry_new_slug_era(db):
- slugs with = (or also : which is now also not allowed) to have those
stripped out (small possibility of breakage here sadly)
"""
- import uuid
def slug_and_user_combo_exists(slug, uploader):
return db.execute(
@@ -251,3 +251,39 @@ def mediaentry_new_slug_era(db):
row, row.slug.replace(u"=", u"-").replace(u":", u"-"))
db.commit()
+
+
+@RegisterMigration(10, MIGRATIONS)
+def unique_collections_slug(db):
+ """Add unique constraint to collection slug"""
+ metadata = MetaData(bind=db.bind)
+ collection_table = inspect_table(metadata, "core__collections")
+ existing_slugs = {}
+ slugs_to_change = []
+
+ for row in db.execute(collection_table.select()):
+ # if duplicate slug, generate a unique slug
+ if row.creator in existing_slugs and row.slug in \
+ existing_slugs[row.creator]:
+ slugs_to_change.append(row.id)
+ else:
+ if not row.creator in existing_slugs:
+ existing_slugs[row.creator] = [row.slug]
+ else:
+ existing_slugs[row.creator].append(row.slug)
+
+ for row_id in slugs_to_change:
+ new_slug = unicode(uuid.uuid4())
+ db.execute(collection_table.update().
+ where(collection_table.c.id == row_id).
+ values(slug=new_slug))
+ # sqlite does not like to change the schema when a transaction(update) is
+ # not yet completed
+ db.commit()
+
+ constraint = UniqueConstraint('creator', 'slug',
+ name='core__collection_creator_slug_key',
+ table=collection_table)
+ constraint.create()
+
+ db.commit()
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py
index 0dc3bc85..388bac89 100644
--- a/mediagoblin/db/mixin.py
+++ b/mediagoblin/db/mixin.py
@@ -149,7 +149,7 @@ class MediaEntryMixin(GenerateSlugMixin):
or, if not found, None.
"""
- fetch_order = self.media_manager.get("media_fetch_order")
+ fetch_order = self.media_manager.media_fetch_order
# No fetching order found? well, give up!
if not fetch_order:
@@ -212,7 +212,7 @@ class MediaEntryMixin(GenerateSlugMixin):
# than iterating through all media managers.
for media_type, manager in get_media_managers():
if media_type == self.media_type:
- return manager
+ return manager(self)
# Not found? Then raise an error
raise FileTypeNotSupported(
"MediaManager not in enabled types. Check media_types in config?")
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index fcfd0f61..2412706e 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -410,7 +410,7 @@ class Collection(Base, CollectionMixin):
title = Column(Unicode, nullable=False)
slug = Column(Unicode)
created = Column(DateTime, nullable=False, default=datetime.datetime.now,
- index=True)
+ index=True)
description = Column(UnicodeText)
creator = Column(Integer, ForeignKey(User.id), nullable=False)
# TODO: No of items in Collection. Badly named, can we migrate to num_items?
@@ -421,6 +421,10 @@ class Collection(Base, CollectionMixin):
backref=backref("collections",
cascade="all, delete-orphan"))
+ __table_args__ = (
+ UniqueConstraint('creator', 'slug'),
+ {})
+
def get_collection_items(self, ascending=False):
#TODO, is this still needed with self.collection_items being available?
order_col = CollectionItem.position
diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py
index 5fd5ed03..0b1679fb 100644
--- a/mediagoblin/db/open.py
+++ b/mediagoblin/db/open.py
@@ -71,12 +71,24 @@ def _sqlite_fk_pragma_on_connect(dbapi_con, con_record):
dbapi_con.execute('pragma foreign_keys=on')
-def setup_connection_and_db_from_config(app_config):
+def _sqlite_disable_fk_pragma_on_connect(dbapi_con, con_record):
+ """
+ Disable foreign key checking on each new sqlite connection
+ (Good for migrations!)
+ """
+ dbapi_con.execute('pragma foreign_keys=off')
+
+
+def setup_connection_and_db_from_config(app_config, migrations=False):
engine = create_engine(app_config['sql_engine'])
# Enable foreign key checking for sqlite
if app_config['sql_engine'].startswith('sqlite://'):
- event.listen(engine, 'connect', _sqlite_fk_pragma_on_connect)
+ if migrations:
+ event.listen(engine, 'connect',
+ _sqlite_disable_fk_pragma_on_connect)
+ else:
+ event.listen(engine, 'connect', _sqlite_fk_pragma_on_connect)
# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index 2673967b..ef270237 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -17,7 +17,7 @@
import wtforms
from mediagoblin.tools.text import tag_length_validator, TOO_LONG_TAG_WARNING
-from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.tools.licenses import licenses_as_choices
class EditForm(wtforms.Form):
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index 34b7aaca..bfcf65b5 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -28,7 +28,8 @@ 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
+from mediagoblin.tools.response import render_to_response, \
+ redirect, redirect_obj
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.text import (
convert_to_tag_list_of_dicts, media_tags_as_string)
@@ -74,8 +75,7 @@ def edit_media(request, media):
media.slug = slug
media.save()
- return redirect(request,
- location=media.url_for_self(request.urlgen))
+ return redirect_obj(request, media)
if request.user.is_admin \
and media.uploader != request.user.id \
@@ -331,9 +331,7 @@ def edit_collection(request, collection):
collection.save()
- return redirect(request, "mediagoblin.user_pages.user_collection",
- user=collection.get_creator.username,
- collection=collection.slug)
+ return redirect_obj(request, collection)
if request.user.is_admin \
and collection.creator != request.user.id \
diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py
index 65b3f922..32700c40 100644
--- a/mediagoblin/gmg_commands/dbupdate.py
+++ b/mediagoblin/gmg_commands/dbupdate.py
@@ -114,7 +114,7 @@ def run_dbupdate(app_config, global_config):
global_config.get('plugins', {}).keys())
# Set up the database
- db = setup_connection_and_db_from_config(app_config)
+ db = setup_connection_and_db_from_config(app_config, migrations=True)
Session = sessionmaker(bind=db.engine)
diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py
index bb0d5989..169cc935 100644
--- a/mediagoblin/init/celery/__init__.py
+++ b/mediagoblin/init/celery/__init__.py
@@ -18,7 +18,7 @@ import os
import sys
from celery import Celery
-from mediagoblin.tools.pluginapi import callable_runall
+from mediagoblin.tools.pluginapi import hook_runall
MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task']
@@ -66,7 +66,7 @@ def setup_celery_app(app_config, global_config,
celery_app = Celery()
celery_app.config_from_object(celery_settings)
- callable_runall('celery_setup', celery_app)
+ hook_runall('celery_setup', celery_app)
def setup_celery_from_config(app_config, global_config,
diff --git a/mediagoblin/init/celery/from_celery.py b/mediagoblin/init/celery/from_celery.py
index e2899c0b..b395a826 100644
--- a/mediagoblin/init/celery/from_celery.py
+++ b/mediagoblin/init/celery/from_celery.py
@@ -22,7 +22,7 @@ from celery.signals import setup_logging
from mediagoblin import app, mg_globals
from mediagoblin.init.celery import setup_celery_from_config
-from mediagoblin.tools.pluginapi import callable_runall
+from mediagoblin.tools.pluginapi import hook_runall
OUR_MODULENAME = __name__
@@ -47,7 +47,7 @@ def setup_logging_from_paste_ini(loglevel, **kw):
logging.config.fileConfig(logging_conf_file)
- callable_runall('celery_logging_setup')
+ hook_runall('celery_logging_setup')
setup_logging.connect(setup_logging_from_paste_ini)
diff --git a/mediagoblin/init/config.py b/mediagoblin/init/config.py
index ac4ab9bf..11a91cff 100644
--- a/mediagoblin/init/config.py
+++ b/mediagoblin/init/config.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/>.
+import logging
import os
import pkg_resources
@@ -21,6 +22,9 @@ from configobj import ConfigObj, flatten_errors
from validate import Validator
+_log = logging.getLogger(__name__)
+
+
CONFIG_SPEC_PATH = pkg_resources.resource_filename(
'mediagoblin', 'config_spec.ini')
@@ -42,6 +46,9 @@ def read_mediagoblin_config(config_path, config_spec=CONFIG_SPEC_PATH):
Also provides %(__file__)s and %(here)s values of this file and
its directory respectively similar to paste deploy.
+ Also reads for [plugins] section, appends all config_spec.ini
+ files from said plugins into the general config_spec specification.
+
This function doesn't itself raise any exceptions if validation
fails, you'll have to do something
@@ -57,10 +64,45 @@ def read_mediagoblin_config(config_path, config_spec=CONFIG_SPEC_PATH):
"""
config_path = os.path.abspath(config_path)
+ # PRE-READ of config file. This allows us to fetch the plugins so
+ # we can add their plugin specs to the general config_spec.
+ config = ConfigObj(
+ config_path,
+ interpolation='ConfigParser')
+
+ plugins = config.get("plugins", {}).keys()
+ plugin_configs = {}
+
+ for plugin in plugins:
+ try:
+ plugin_config_spec_path = pkg_resources.resource_filename(
+ plugin, "config_spec.ini")
+ if not os.path.exists(plugin_config_spec_path):
+ continue
+
+ plugin_config_spec = ConfigObj(
+ plugin_config_spec_path,
+ encoding='UTF8', list_values=False, _inspec=True)
+ _setup_defaults(plugin_config_spec, config_path)
+
+ if not "plugin_spec" in plugin_config_spec:
+ continue
+
+ plugin_configs[plugin] = plugin_config_spec["plugin_spec"]
+
+ except ImportError:
+ _log.warning(
+ "When setting up config section, could not import '%s'" %
+ plugin)
+
+ # Now load the main config spec
config_spec = ConfigObj(
config_spec,
encoding='UTF8', list_values=False, _inspec=True)
+ # append the plugin specific sections of the config spec
+ config_spec['plugins'] = plugin_configs
+
_setup_defaults(config_spec, config_path)
config = ConfigObj(
diff --git a/mediagoblin/init/plugins/__init__.py b/mediagoblin/init/plugins/__init__.py
index 72bd5c7d..0df4f381 100644
--- a/mediagoblin/init/plugins/__init__.py
+++ b/mediagoblin/init/plugins/__init__.py
@@ -59,4 +59,4 @@ def setup_plugins():
pman.register_hooks(plugin.hooks)
# Execute anything registered to the setup hook.
- pluginapi.callable_runall('setup')
+ pluginapi.hook_runall('setup')
diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py
index 0abb38d3..20e1918e 100644
--- a/mediagoblin/media_types/__init__.py
+++ b/mediagoblin/media_types/__init__.py
@@ -20,6 +20,7 @@ import logging
import tempfile
from mediagoblin import mg_globals
+from mediagoblin.tools.common import import_component
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
_log = logging.getLogger(__name__)
@@ -31,6 +32,56 @@ class InvalidFileType(Exception):
pass
+class MediaManagerBase(object):
+ "Base class for all media managers"
+
+ # Please override in actual media managers
+ media_fetch_order = None
+
+ @staticmethod
+ def sniff_handler(*args, **kwargs):
+ return False
+
+ def __init__(self, entry):
+ self.entry = entry
+
+ def __getitem__(self, i):
+ return getattr(self, i)
+
+ def __contains__(self, i):
+ 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
@@ -49,8 +100,7 @@ def sniff_media(media):
for media_type, manager in get_media_managers():
_log.info('Sniffing {0}'.format(media_type))
- if 'sniff_handler' in manager and \
- manager['sniff_handler'](media_file, media=media):
+ if manager.sniff_handler(media_file, media=media):
_log.info('{0} accepts the file'.format(media_type))
return media_type, manager
else:
@@ -74,9 +124,12 @@ def get_media_managers():
Generator, yields all enabled media managers
'''
for media_type in get_media_types():
- __import__(media_type)
+ mm = import_component(media_type + ":MEDIA_MANAGER")
+
+ if isinstance(mm, dict):
+ mm = CompatMediaManager(mm)
- yield media_type, sys.modules[media_type].MEDIA_MANAGER
+ yield media_type, mm
def get_media_type_and_manager(filename):
@@ -92,7 +145,7 @@ def get_media_type_and_manager(filename):
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']:
+ if ext[1:] in manager.accepted_extensions:
return media_type, manager
else:
_log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format(
diff --git a/mediagoblin/media_types/ascii/__init__.py b/mediagoblin/media_types/ascii/__init__.py
index 856d1d7b..0931e83a 100644
--- a/mediagoblin/media_types/ascii/__init__.py
+++ b/mediagoblin/media_types/ascii/__init__.py
@@ -14,16 +14,18 @@
# 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.media_types import MediaManagerBase
from mediagoblin.media_types.ascii.processing import process_ascii, \
sniff_handler
-MEDIA_MANAGER = {
- "human_readable": "ASCII",
- "processor": process_ascii, # alternately a string,
- # 'mediagoblin.media_types.image.processing'?
- "sniff_handler": sniff_handler,
- "display_template": "mediagoblin/media_displays/ascii.html",
- "default_thumb": "images/media_thumbs/ascii.jpg",
- "accepted_extensions": [
- "txt", "asc", "nfo"]}
+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
diff --git a/mediagoblin/media_types/ascii/asciitoimage.py b/mediagoblin/media_types/ascii/asciitoimage.py
index 108de023..786941f6 100644
--- a/mediagoblin/media_types/ascii/asciitoimage.py
+++ b/mediagoblin/media_types/ascii/asciitoimage.py
@@ -14,9 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import Image
-import ImageFont
-import ImageDraw
+try:
+ from PIL import Image
+ from PIL import ImageFont
+ from PIL import ImageDraw
+except ImportError:
+ import Image
+ import ImageFont
+ import ImageDraw
import logging
import pkg_resources
import os
diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py
index 309aab0a..2f6079be 100644
--- a/mediagoblin/media_types/ascii/processing.py
+++ b/mediagoblin/media_types/ascii/processing.py
@@ -15,7 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import chardet
import os
-import Image
+try:
+ from PIL import Image
+except ImportError:
+ import Image
import logging
from mediagoblin import mg_globals as mgg
@@ -42,7 +45,7 @@ def process_ascii(proc_state):
"""Code to process a txt file. Will be run by celery.
A Workbench() represents a local tempory dir. It is automatically
- cleaned up when this function exits.
+ cleaned up when this function exits.
"""
entry = proc_state.entry
workbench = proc_state.workbench
diff --git a/mediagoblin/media_types/audio/__init__.py b/mediagoblin/media_types/audio/__init__.py
index 4f3ead60..2eb7300e 100644
--- a/mediagoblin/media_types/audio/__init__.py
+++ b/mediagoblin/media_types/audio/__init__.py
@@ -14,12 +14,17 @@
# 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.media_types import MediaManagerBase
from mediagoblin.media_types.audio.processing import process_audio, \
sniff_handler
-MEDIA_MANAGER = {
- 'human_readable': 'Audio',
- 'processor': process_audio,
- 'sniff_handler': sniff_handler,
- 'display_template': 'mediagoblin/media_displays/audio.html',
- 'accepted_extensions': ['mp3', 'flac', 'wav', 'm4a']}
+
+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
diff --git a/mediagoblin/media_types/audio/spectrogram.py b/mediagoblin/media_types/audio/spectrogram.py
index 458855c1..dd4d0299 100644
--- a/mediagoblin/media_types/audio/spectrogram.py
+++ b/mediagoblin/media_types/audio/spectrogram.py
@@ -19,7 +19,10 @@
# Bram de Jong <bram.dejong at domain.com where domain in gmail>
# 2012, Joar Wandborg <first name at last name dot se>
-from PIL import Image
+try:
+ from PIL import Image
+except ImportError:
+ import Image
import math
import numpy
diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py
index 3a9a2125..84e6af7e 100644
--- a/mediagoblin/media_types/audio/transcoders.py
+++ b/mediagoblin/media_types/audio/transcoders.py
@@ -15,7 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
-import Image
+try:
+ from PIL import Image
+except ImportError:
+ import Image
from mediagoblin.processing import BadMediaFail
from mediagoblin.media_types.audio import audioprocessing
diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py
index 3e167db1..15cc8dda 100644
--- a/mediagoblin/media_types/image/__init__.py
+++ b/mediagoblin/media_types/image/__init__.py
@@ -14,19 +14,19 @@
# 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.media_types import MediaManagerBase
from mediagoblin.media_types.image.processing import process_image, \
sniff_handler
-MEDIA_MANAGER = {
- "human_readable": "Image",
- "processor": process_image, # alternately a string,
- # 'mediagoblin.media_types.image.processing'?
- "sniff_handler": sniff_handler,
- "display_template": "mediagoblin/media_displays/image.html",
- "default_thumb": "images/media_thumbs/image.png",
- "accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"],
+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']
+
- # Used by the media_entry.get_display_media method
- "media_fetch_order": [u'medium', u'original', u'thumb'],
-}
+MEDIA_MANAGER = ImageMediaManager
diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py
index e951ef29..bc0ce3f8 100644
--- a/mediagoblin/media_types/image/processing.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -14,13 +14,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 Image
+try:
+ from PIL import Image
+except ImportError:
+ import Image
import os
import logging
from mediagoblin import mg_globals as mgg
-from mediagoblin.processing import BadMediaFail, \
- create_pub_filepath, FilenameBuilder
+from mediagoblin.processing import BadMediaFail, FilenameBuilder
from mediagoblin.tools.exif import exif_fix_image_orientation, \
extract_exif, clean_exif, get_gps_data, get_useful, \
exif_image_needs_rotation
@@ -34,29 +36,25 @@ PIL_FILTERS = {
'ANTIALIAS': Image.ANTIALIAS}
-def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
- size_limits=(0, 0)):
+def resize_image(proc_state, resized, keyname, target_name, new_size,
+ exif_tags, workdir):
"""
Store a resized version of an image and return its pathname.
Arguments:
- entry -- the entry for the image to resize
- filename -- the filename of the original image being resized
- new_path -- public file path for the new resized image
+ proc_state -- the processing state for the image to resize
+ resized -- an image from Image.open() of the original image being resized
+ keyname -- Under what key to save in the db.
+ target_name -- public file path for the new resized image
exif_tags -- EXIF data for the original image
workdir -- directory path for storing converted image files
new_size -- 2-tuple size for the resized image
"""
- try:
- resized = Image.open(filename)
- except IOError:
- raise BadMediaFail()
- resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation
+ config = mgg.global_config['media_type:mediagoblin.media_types.image']
- filter_config = \
- mgg.global_config['media_type:mediagoblin.media_types.image']\
- ['resize_filter']
+ resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation
+ filter_config = config['resize_filter']
try:
resize_filter = PIL_FILTERS[filter_config.upper()]
except KeyError:
@@ -67,10 +65,34 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
resized.thumbnail(new_size, resize_filter)
# Copy the new file to the conversion subdir, then remotely.
- tmp_resized_filename = os.path.join(workdir, new_path[-1])
+ tmp_resized_filename = os.path.join(workdir, target_name)
with file(tmp_resized_filename, 'w') as resized_file:
- resized.save(resized_file)
- mgg.public_store.copy_local_to_storage(tmp_resized_filename, new_path)
+ resized.save(resized_file, quality=config['quality'])
+ proc_state.store_public(keyname, tmp_resized_filename, target_name)
+
+
+def resize_tool(proc_state, force, keyname, target_name,
+ conversions_subdir, exif_tags):
+ # filename -- the filename of the original image being resized
+ filename = proc_state.get_queued_filename()
+ max_width = mgg.global_config['media:' + keyname]['max_width']
+ max_height = mgg.global_config['media:' + keyname]['max_height']
+ # If the size of the original file exceeds the specified size for the desized
+ # file, a target_name file is created and later associated with the media
+ # entry.
+ # Also created if the file needs rotation, or if forced.
+ try:
+ im = Image.open(filename)
+ except IOError:
+ raise BadMediaFail()
+ if force \
+ or im.size[0] > max_width \
+ or im.size[1] > max_height \
+ or exif_image_needs_rotation(exif_tags):
+ resize_image(
+ proc_state, im, unicode(keyname), target_name,
+ (max_width, max_height),
+ exif_tags, conversions_subdir)
SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg']
@@ -116,29 +138,14 @@ def process_image(proc_state):
gps_data = get_gps_data(exif_tags)
# Always create a small thumbnail
- thumb_filepath = create_pub_filepath(
- entry, name_builder.fill('{basename}.thumbnail{ext}'))
- resize_image(entry, queued_filename, thumb_filepath,
- exif_tags, conversions_subdir,
- (mgg.global_config['media:thumb']['max_width'],
- mgg.global_config['media:thumb']['max_height']))
- entry.media_files[u'thumb'] = thumb_filepath
-
- # If the size of the original file exceeds the specified size of a `medium`
- # file, a `.medium.jpg` files is created and later associated with the media
- # entry.
- medium = Image.open(queued_filename)
- if medium.size[0] > mgg.global_config['media:medium']['max_width'] \
- or medium.size[1] > mgg.global_config['media:medium']['max_height'] \
- or exif_image_needs_rotation(exif_tags):
- medium_filepath = create_pub_filepath(
- entry, name_builder.fill('{basename}.medium{ext}'))
- resize_image(
- entry, queued_filename, medium_filepath,
- exif_tags, conversions_subdir,
- (mgg.global_config['media:medium']['max_width'],
- mgg.global_config['media:medium']['max_height']))
- entry.media_files[u'medium'] = medium_filepath
+ resize_tool(proc_state, True, 'thumb',
+ name_builder.fill('{basename}.thumbnail{ext}'),
+ conversions_subdir, exif_tags)
+
+ # Possibly create a medium
+ resize_tool(proc_state, False, 'medium',
+ name_builder.fill('{basename}.medium{ext}'),
+ conversions_subdir, exif_tags)
# Copy our queued local workbench to its final destination
proc_state.copy_original(name_builder.fill('{basename}{ext}'))
diff --git a/mediagoblin/media_types/pdf/__init__.py b/mediagoblin/media_types/pdf/__init__.py
new file mode 100644
index 00000000..f0ba7867
--- /dev/null
+++ b/mediagoblin/media_types/pdf/__init__.py
@@ -0,0 +1,31 @@
+# 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.media_types import MediaManagerBase
+from mediagoblin.media_types.pdf.processing import process_pdf, \
+ sniff_handler
+
+
+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
diff --git a/mediagoblin/media_types/pdf/migrations.py b/mediagoblin/media_types/pdf/migrations.py
new file mode 100644
index 00000000..f54c23ea
--- /dev/null
+++ b/mediagoblin/media_types/pdf/migrations.py
@@ -0,0 +1,17 @@
+# 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/>.
+
+MIGRATIONS = {}
diff --git a/mediagoblin/media_types/pdf/models.py b/mediagoblin/media_types/pdf/models.py
new file mode 100644
index 00000000..c39262d1
--- /dev/null
+++ b/mediagoblin/media_types/pdf/models.py
@@ -0,0 +1,58 @@
+# 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.db.base import Base
+
+from sqlalchemy import (
+ Column, Float, Integer, String, DateTime, ForeignKey)
+from sqlalchemy.orm import relationship, backref
+
+
+BACKREF_NAME = "pdf__media_data"
+
+
+class PdfData(Base):
+ __tablename__ = "pdf__mediadata"
+
+ # The primary key *and* reference to the main media_entry
+ media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
+ primary_key=True)
+ get_media_entry = relationship("MediaEntry",
+ backref=backref(BACKREF_NAME, uselist=False,
+ cascade="all, delete-orphan"))
+ pages = Column(Integer)
+
+ # These are taken from what pdfinfo can do, perhaps others make sense too
+ pdf_author = Column(String)
+ pdf_title = Column(String)
+ # note on keywords: this is the pdf parsed string, it should be considered a cached
+ # value like the rest of these values, since they can be deduced at query time / client
+ # side too.
+ pdf_keywords = Column(String)
+ pdf_creator = Column(String)
+ pdf_producer = Column(String)
+ pdf_creation_date = Column(DateTime)
+ pdf_modified_date = Column(DateTime)
+ pdf_version_major = Column(Integer)
+ pdf_version_minor = Column(Integer)
+ pdf_page_size_width = Column(Float) # unit: pts
+ pdf_page_size_height = Column(Float)
+ pdf_pages = Column(Integer)
+
+
+DATA_MODEL = PdfData
+MODELS = [PdfData]
diff --git a/mediagoblin/media_types/pdf/processing.py b/mediagoblin/media_types/pdf/processing.py
new file mode 100644
index 00000000..49742fd7
--- /dev/null
+++ b/mediagoblin/media_types/pdf/processing.py
@@ -0,0 +1,277 @@
+# 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 logging
+import dateutil.parser
+from subprocess import PIPE, Popen
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import (create_pub_filepath,
+ FilenameBuilder, BadMediaFail)
+from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+
+_log = logging.getLogger(__name__)
+
+# TODO - cache (memoize) util
+
+# This is a list created via uniconv --show and hand removing some types that
+# we already support via other media types better.
+unoconv_supported = [
+ 'bib', # - BibTeX [.bib]
+ #bmp - Windows Bitmap [.bmp]
+ 'csv', # - Text CSV [.csv]
+ 'dbf', # - dBASE [.dbf]
+ 'dif', # - Data Interchange Format [.dif]
+ 'doc6', # - Microsoft Word 6.0 [.doc]
+ 'doc95', # - Microsoft Word 95 [.doc]
+ 'docbook', # - DocBook [.xml]
+ 'doc', # - Microsoft Word 97/2000/XP [.doc]
+ 'docx7', # - Microsoft Office Open XML [.docx]
+ 'docx', # - Microsoft Office Open XML [.docx]
+ #emf - Enhanced Metafile [.emf]
+ 'eps', # - Encapsulated PostScript [.eps]
+ 'fodp', # - OpenDocument Presentation (Flat XML) [.fodp]
+ 'fods', # - OpenDocument Spreadsheet (Flat XML) [.fods]
+ 'fodt', # - OpenDocument Text (Flat XML) [.fodt]
+ #gif - Graphics Interchange Format [.gif]
+ 'html', # - HTML Document (OpenOffice.org Writer) [.html]
+ #jpg - Joint Photographic Experts Group [.jpg]
+ 'latex', # - LaTeX 2e [.ltx]
+ 'mediawiki', # - MediaWiki [.txt]
+ 'met', # - OS/2 Metafile [.met]
+ 'odd', # - OpenDocument Drawing [.odd]
+ 'odg', # - ODF Drawing (Impress) [.odg]
+ 'odp', # - ODF Presentation [.odp]
+ 'ods', # - ODF Spreadsheet [.ods]
+ 'odt', # - ODF Text Document [.odt]
+ 'ooxml', # - Microsoft Office Open XML [.xml]
+ 'otg', # - OpenDocument Drawing Template [.otg]
+ 'otp', # - ODF Presentation Template [.otp]
+ 'ots', # - ODF Spreadsheet Template [.ots]
+ 'ott', # - Open Document Text [.ott]
+ #pbm - Portable Bitmap [.pbm]
+ #pct - Mac Pict [.pct]
+ 'pdb', # - AportisDoc (Palm) [.pdb]
+ #pdf - Portable Document Format [.pdf]
+ #pgm - Portable Graymap [.pgm]
+ #png - Portable Network Graphic [.png]
+ 'pot', # - Microsoft PowerPoint 97/2000/XP Template [.pot]
+ 'potm', # - Microsoft PowerPoint 2007/2010 XML Template [.potm]
+ #ppm - Portable Pixelmap [.ppm]
+ 'pps', # - Microsoft PowerPoint 97/2000/XP (Autoplay) [.pps]
+ 'ppt', # - Microsoft PowerPoint 97/2000/XP [.ppt]
+ 'pptx', # - Microsoft PowerPoint 2007/2010 XML [.pptx]
+ 'psw', # - Pocket Word [.psw]
+ 'pwp', # - PlaceWare [.pwp]
+ 'pxl', # - Pocket Excel [.pxl]
+ #ras - Sun Raster Image [.ras]
+ 'rtf', # - Rich Text Format [.rtf]
+ 'sda', # - StarDraw 5.0 (OpenOffice.org Impress) [.sda]
+ 'sdc3', # - StarCalc 3.0 [.sdc]
+ 'sdc4', # - StarCalc 4.0 [.sdc]
+ 'sdc', # - StarCalc 5.0 [.sdc]
+ 'sdd3', # - StarDraw 3.0 (OpenOffice.org Impress) [.sdd]
+ 'sdd4', # - StarImpress 4.0 [.sdd]
+ 'sdd', # - StarImpress 5.0 [.sdd]
+ 'sdw3', # - StarWriter 3.0 [.sdw]
+ 'sdw4', # - StarWriter 4.0 [.sdw]
+ 'sdw', # - StarWriter 5.0 [.sdw]
+ 'slk', # - SYLK [.slk]
+ 'stc', # - OpenOffice.org 1.0 Spreadsheet Template [.stc]
+ 'std', # - OpenOffice.org 1.0 Drawing Template [.std]
+ 'sti', # - OpenOffice.org 1.0 Presentation Template [.sti]
+ 'stw', # - Open Office.org 1.0 Text Document Template [.stw]
+ #svg - Scalable Vector Graphics [.svg]
+ 'svm', # - StarView Metafile [.svm]
+ 'swf', # - Macromedia Flash (SWF) [.swf]
+ 'sxc', # - OpenOffice.org 1.0 Spreadsheet [.sxc]
+ 'sxd3', # - StarDraw 3.0 [.sxd]
+ 'sxd5', # - StarDraw 5.0 [.sxd]
+ 'sxd', # - OpenOffice.org 1.0 Drawing (OpenOffice.org Impress) [.sxd]
+ 'sxi', # - OpenOffice.org 1.0 Presentation [.sxi]
+ 'sxw', # - Open Office.org 1.0 Text Document [.sxw]
+ #text - Text Encoded [.txt]
+ #tiff - Tagged Image File Format [.tiff]
+ #txt - Text [.txt]
+ 'uop', # - Unified Office Format presentation [.uop]
+ 'uos', # - Unified Office Format spreadsheet [.uos]
+ 'uot', # - Unified Office Format text [.uot]
+ 'vor3', # - StarDraw 3.0 Template (OpenOffice.org Impress) [.vor]
+ 'vor4', # - StarWriter 4.0 Template [.vor]
+ 'vor5', # - StarDraw 5.0 Template (OpenOffice.org Impress) [.vor]
+ 'vor', # - StarCalc 5.0 Template [.vor]
+ #wmf - Windows Metafile [.wmf]
+ 'xhtml', # - XHTML Document [.html]
+ 'xls5', # - Microsoft Excel 5.0 [.xls]
+ 'xls95', # - Microsoft Excel 95 [.xls]
+ 'xls', # - Microsoft Excel 97/2000/XP [.xls]
+ 'xlt5', # - Microsoft Excel 5.0 Template [.xlt]
+ 'xlt95', # - Microsoft Excel 95 Template [.xlt]
+ 'xlt', # - Microsoft Excel 97/2000/XP Template [.xlt]
+ #xpm - X PixMap [.xpm]
+]
+
+def is_unoconv_working():
+ # TODO: must have libreoffice-headless installed too, need to check for it
+ unoconv = where('unoconv')
+ if not unoconv:
+ return False
+ try:
+ proc = Popen([unoconv, '--show'], stderr=PIPE)
+ output = proc.stderr.read()
+ except OSError, e:
+ _log.warn(_('unoconv failing to run, check log file'))
+ return False
+ if 'ERROR' in output:
+ return False
+ return True
+
+def supported_extensions(cache=[None]):
+ if cache[0] == None:
+ cache[0] = 'pdf'
+ if is_unoconv_working():
+ cache.extend(unoconv_supported)
+ return cache
+
+def where(name):
+ for p in os.environ['PATH'].split(os.pathsep):
+ fullpath = os.path.join(p, name)
+ if os.path.exists(fullpath):
+ return fullpath
+ return None
+
+def check_prerequisites():
+ if not where('pdfinfo'):
+ _log.warn('missing pdfinfo')
+ return False
+ if not where('pdftocairo'):
+ _log.warn('missing pdfcairo')
+ return False
+ return True
+
+def sniff_handler(media_file, **kw):
+ if not check_prerequisites():
+ return False
+ 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 False
+
+def create_pdf_thumb(original, thumb_filename, width, height):
+ # Note: pdftocairo adds '.png', remove it
+ thumb_filename = thumb_filename[:-4]
+ executable = where('pdftocairo')
+ args = [executable, '-scale-to', str(min(width, height)),
+ '-singlefile', '-png', original, thumb_filename]
+ _log.debug('calling {0}'.format(repr(' '.join(args))))
+ Popen(executable=executable, args=args).wait()
+
+def pdf_info(original):
+ """
+ Extract dictionary of pdf information. This could use a library instead
+ of a process.
+
+ Note: I'm assuming pdfinfo output is sanitized (integers where integers are
+ expected, etc.) - if this is wrong then an exception will be raised and caught
+ leading to the dreaded error page. It seems a safe assumption.
+ """
+ ret_dict = {}
+ pdfinfo = where('pdfinfo')
+ try:
+ proc = Popen(executable=pdfinfo,
+ args=[pdfinfo, original], stdout=PIPE)
+ lines = proc.stdout.readlines()
+ except OSError:
+ _log.debug('pdfinfo could not read the pdf file.')
+ raise BadMediaFail()
+
+ info_dict = dict([[part.strip() for part in l.strip().split(':', 1)]
+ for l in lines if ':' in l])
+
+ for date_key in [('pdf_mod_date', 'ModDate'),
+ ('pdf_creation_date', 'CreationDate')]:
+ if date_key in info_dict:
+ ret_dict[date_key] = dateutil.parser.parse(info_dict[date_key])
+ for db_key, int_key in [('pdf_pages', 'Pages')]:
+ if int_key in info_dict:
+ ret_dict[db_key] = int(info_dict[int_key])
+
+ # parse 'PageSize' field: 595 x 842 pts (A4)
+ page_size_parts = info_dict['Page size'].split()
+ ret_dict['pdf_page_size_width'] = float(page_size_parts[0])
+ ret_dict['pdf_page_size_height'] = float(page_size_parts[2])
+
+ for db_key, str_key in [('pdf_keywords', 'Keywords'),
+ ('pdf_creator', 'Creator'), ('pdf_producer', 'Producer'),
+ ('pdf_author', 'Author'), ('pdf_title', 'Title')]:
+ ret_dict[db_key] = info_dict.get(str_key, None)
+ ret_dict['pdf_version_major'], ret_dict['pdf_version_minor'] = \
+ map(int, info_dict['PDF version'].split('.'))
+
+ return ret_dict
+
+def process_pdf(proc_state):
+ """Code to process a pdf file. Will be run by celery.
+
+ A Workbench() represents a local tempory dir. It is automatically
+ cleaned up when this function exits.
+ """
+ entry = proc_state.entry
+ workbench = proc_state.workbench
+
+ queued_filename = proc_state.get_queued_filename()
+ name_builder = FilenameBuilder(queued_filename)
+
+ # Copy our queued local workbench to its final destination
+ original_dest = name_builder.fill('{basename}{ext}')
+ proc_state.copy_original(original_dest)
+
+ # Create a pdf if this is a different doc, store pdf for viewer
+ ext = queued_filename.rsplit('.', 1)[-1].lower()
+ if ext == 'pdf':
+ pdf_filename = queued_filename
+ else:
+ pdf_filename = queued_filename.rsplit('.', 1)[0] + '.pdf'
+ unoconv = where('unoconv')
+ call(executable=unoconv,
+ args=[unoconv, '-v', '-f', 'pdf', queued_filename])
+ if not os.path.exists(pdf_filename):
+ _log.debug('unoconv failed to convert file to pdf')
+ raise BadMediaFail()
+ proc_state.store_public(keyname=u'pdf', local_file=pdf_filename)
+
+ pdf_info_dict = pdf_info(pdf_filename)
+
+ for name, width, height in [
+ (u'thumb', mgg.global_config['media:thumb']['max_width'],
+ mgg.global_config['media:thumb']['max_height']),
+ (u'medium', mgg.global_config['media:medium']['max_width'],
+ mgg.global_config['media:medium']['max_height']),
+ ]:
+ filename = name_builder.fill('{basename}.%s.png' % name)
+ path = workbench.joinpath(filename)
+ create_pdf_thumb(pdf_filename, path, width, height)
+ assert(os.path.exists(path))
+ proc_state.store_public(keyname=name, local_file=path)
+
+ proc_state.delete_queue_file()
+
+ entry.media_data_init(**pdf_info_dict)
+ entry.save()
diff --git a/mediagoblin/media_types/stl/__init__.py b/mediagoblin/media_types/stl/__init__.py
index edffc633..6ae8a8b9 100644
--- a/mediagoblin/media_types/stl/__init__.py
+++ b/mediagoblin/media_types/stl/__init__.py
@@ -14,14 +14,18 @@
# 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.media_types import MediaManagerBase
from mediagoblin.media_types.stl.processing import process_stl, \
sniff_handler
-MEDIA_MANAGER = {
- "human_readable": "stereo lithographics",
- "processor": process_stl,
- "sniff_handler": sniff_handler,
- "display_template": "mediagoblin/media_displays/stl.html",
- "default_thumb": "images/media_thumbs/video.jpg",
- "accepted_extensions": ["obj", "stl"]}
+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
diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py
index e41df395..49382495 100644
--- a/mediagoblin/media_types/stl/processing.py
+++ b/mediagoblin/media_types/stl/processing.py
@@ -64,8 +64,6 @@ def blender_render(config):
"""
Called to prerender a model.
"""
- arg_string = "blender -b blender_render.blend -F "
- arg_string +="JPEG -P blender_render.py"
env = {"RENDER_SETUP" : json.dumps(config), "DISPLAY":":0"}
subprocess.call(
["blender",
diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py
index fab601f6..569cf11a 100644
--- a/mediagoblin/media_types/video/__init__.py
+++ b/mediagoblin/media_types/video/__init__.py
@@ -14,21 +14,23 @@
# 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.media_types import MediaManagerBase
from mediagoblin.media_types.video.processing import process_video, \
sniff_handler
-MEDIA_MANAGER = {
- "human_readable": "Video",
- "processor": process_video, # alternately a string,
- # 'mediagoblin.media_types.image.processing'?
- "sniff_handler": 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"],
-
+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_fetch_order = [u'webm_640', u'original']
+ default_webm_type = 'video/webm; codecs="vp8, vorbis"'
+
+
+MEDIA_MANAGER = VideoMediaManager
diff --git a/mediagoblin/media_types/video/models.py b/mediagoblin/media_types/video/models.py
index f696a892..0b52c53f 100644
--- a/mediagoblin/media_types/video/models.py
+++ b/mediagoblin/media_types/video/models.py
@@ -90,7 +90,7 @@ class VideoData(Base):
return '%s; codecs="%s, %s"' % (
mimetype, video_codec, audio_codec)
else:
- return video.MEDIA_MANAGER["default_webm_type"]
+ return video.VideoMediaManager.default_webm_type
DATA_MODEL = VideoData
diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py
index 58b2c0d4..90a767dd 100644
--- a/mediagoblin/media_types/video/transcoders.py
+++ b/mediagoblin/media_types/video/transcoders.py
@@ -26,7 +26,10 @@ import pygst
pygst.require('0.10')
import gst
import struct
-import Image
+try:
+ from PIL import Image
+except ImportError:
+ import Image
from gst.extend import discoverer
diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py
index d3fdf2ef..1eddd9e0 100644
--- a/mediagoblin/plugins/api/__init__.py
+++ b/mediagoblin/plugins/api/__init__.py
@@ -23,11 +23,11 @@ _log = logging.getLogger(__name__)
PLUGIN_DIR = os.path.dirname(__file__)
-config = pluginapi.get_config(__name__)
-
def setup_plugin():
_log.info('Setting up API...')
+ config = pluginapi.get_config(__name__)
+
_log.debug('API config: {0}'.format(config))
routes = [
diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py
index 081b590e..99b6a4b0 100644
--- a/mediagoblin/plugins/httpapiauth/__init__.py
+++ b/mediagoblin/plugins/httpapiauth/__init__.py
@@ -15,9 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
-import base64
-from werkzeug.exceptions import BadRequest, Unauthorized
+from werkzeug.exceptions import Unauthorized
from mediagoblin.plugins.api.tools import Auth
diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py
index 4714d95d..5762379d 100644
--- a/mediagoblin/plugins/oauth/__init__.py
+++ b/mediagoblin/plugins/oauth/__init__.py
@@ -34,7 +34,7 @@ def setup_plugin():
_log.debug('OAuth config: {0}'.format(config))
routes = [
- ('mediagoblin.plugins.oauth.authorize',
+ ('mediagoblin.plugins.oauth.authorize',
'/oauth/authorize',
'mediagoblin.plugins.oauth.views:authorize'),
('mediagoblin.plugins.oauth.authorize_client',
diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py
index d0a4e9b8..5edd992a 100644
--- a/mediagoblin/plugins/oauth/forms.py
+++ b/mediagoblin/plugins/oauth/forms.py
@@ -19,7 +19,7 @@ import wtforms
from urlparse import urlparse
from mediagoblin.tools.extlib.wtf_html5 import URLField
-from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
class AuthorizationForm(wtforms.Form):
diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py
index 6aa0d7cb..d7b89da3 100644
--- a/mediagoblin/plugins/oauth/migrations.py
+++ b/mediagoblin/plugins/oauth/migrations.py
@@ -102,6 +102,21 @@ class OAuthCode_v0(declarative_base()):
client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False)
+class OAuthRefreshToken_v0(declarative_base()):
+ __tablename__ = 'oauth__refresh_tokens'
+
+ id = Column(Integer, primary_key=True)
+ created = Column(DateTime, nullable=False,
+ default=datetime.now)
+
+ token = Column(Unicode, index=True)
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+
+ # XXX: Is it OK to use OAuthClient_v0.id in this way?
+ client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False)
+
+
@RegisterMigration(1, MIGRATIONS)
def remove_and_replace_token_and_code(db):
metadata = MetaData(bind=db.bind)
@@ -122,3 +137,22 @@ def remove_and_replace_token_and_code(db):
OAuthCode_v0.__table__.create(db.bind)
db.commit()
+
+
+@RegisterMigration(2, MIGRATIONS)
+def remove_refresh_token_field(db):
+ metadata = MetaData(bind=db.bind)
+
+ token_table = Table('oauth__tokens', metadata, autoload=True,
+ autoload_with=db.bind)
+
+ refresh_token = token_table.columns['refresh_token']
+
+ refresh_token.drop()
+ db.commit()
+
+@RegisterMigration(3, MIGRATIONS)
+def create_refresh_token_table(db):
+ OAuthRefreshToken_v0.__table__.create(db.bind)
+
+ db.commit()
diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py
index 695dad31..439424d3 100644
--- a/mediagoblin/plugins/oauth/models.py
+++ b/mediagoblin/plugins/oauth/models.py
@@ -14,17 +14,17 @@
# 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 bcrypt
from datetime import datetime, timedelta
-from mediagoblin.db.base import Base
-from mediagoblin.db.models import User
from sqlalchemy import (
Column, Unicode, Integer, DateTime, ForeignKey, Enum)
-from sqlalchemy.orm import relationship
+from sqlalchemy.orm import relationship, backref
+from mediagoblin.db.base import Base
+from mediagoblin.db.models import User
+from mediagoblin.plugins.oauth.tools import generate_identifier, \
+ generate_secret, generate_token, generate_code, generate_refresh_token
# Don't remove this, I *think* it applies sqlalchemy-migrate functionality onto
# the models.
@@ -41,11 +41,14 @@ class OAuthClient(Base):
name = Column(Unicode)
description = Column(Unicode)
- identifier = Column(Unicode, unique=True, index=True)
- secret = Column(Unicode, index=True)
+ identifier = Column(Unicode, unique=True, index=True,
+ default=generate_identifier)
+ secret = Column(Unicode, index=True, default=generate_secret)
owner_id = Column(Integer, ForeignKey(User.id))
- owner = relationship(User, backref='registered_clients')
+ owner = relationship(
+ User,
+ backref=backref('registered_clients', cascade='all, delete-orphan'))
redirect_uri = Column(Unicode)
@@ -54,14 +57,8 @@ class OAuthClient(Base):
u'public',
name=u'oauth__client_type'))
- def generate_identifier(self):
- self.identifier = unicode(uuid.uuid4())
-
- def generate_secret(self):
- self.secret = unicode(
- bcrypt.hashpw(
- unicode(uuid.uuid4()),
- bcrypt.gensalt()))
+ def update_secret(self):
+ self.secret = generate_secret()
def __repr__(self):
return '<{0} {1}:{2} ({3})>'.format(
@@ -76,10 +73,15 @@ class OAuthUserClient(Base):
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey(User.id))
- user = relationship(User, backref='oauth_clients')
+ user = relationship(
+ User,
+ backref=backref('oauth_client_relations',
+ cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id))
- client = relationship(OAuthClient, backref='users')
+ client = relationship(
+ OAuthClient,
+ backref=backref('oauth_user_relations', cascade='all, delete-orphan'))
state = Column(Enum(
u'approved',
@@ -103,15 +105,18 @@ class OAuthToken(Base):
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(days=30))
- token = Column(Unicode, index=True)
- refresh_token = Column(Unicode, index=True)
+ token = Column(Unicode, index=True, default=generate_token)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
- user = relationship(User)
+ user = relationship(
+ User,
+ backref=backref('oauth_tokens', cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
- client = relationship(OAuthClient)
+ client = relationship(
+ OAuthClient,
+ backref=backref('oauth_tokens', cascade='all, delete-orphan'))
def __repr__(self):
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
@@ -121,6 +126,34 @@ class OAuthToken(Base):
self.user,
self.client)
+class OAuthRefreshToken(Base):
+ __tablename__ = 'oauth__refresh_tokens'
+
+ id = Column(Integer, primary_key=True)
+ created = Column(DateTime, nullable=False,
+ default=datetime.now)
+
+ token = Column(Unicode, index=True,
+ default=generate_refresh_token)
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+
+ user = relationship(User, backref=backref('oauth_refresh_tokens',
+ cascade='all, delete-orphan'))
+
+ client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
+ client = relationship(OAuthClient,
+ backref=backref(
+ 'oauth_refresh_tokens',
+ cascade='all, delete-orphan'))
+
+ def __repr__(self):
+ return '<{0} #{1} [{3}, {4}]>'.format(
+ self.__class__.__name__,
+ self.id,
+ self.user,
+ self.client)
+
class OAuthCode(Base):
__tablename__ = 'oauth__codes'
@@ -130,14 +163,17 @@ class OAuthCode(Base):
default=datetime.now)
expires = Column(DateTime, nullable=False,
default=lambda: datetime.now() + timedelta(minutes=5))
- code = Column(Unicode, index=True)
+ code = Column(Unicode, index=True, default=generate_code)
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
index=True)
- user = relationship(User)
+ user = relationship(User, backref=backref('oauth_codes',
+ cascade='all, delete-orphan'))
client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False)
- client = relationship(OAuthClient)
+ client = relationship(OAuthClient, backref=backref(
+ 'oauth_codes',
+ cascade='all, delete-orphan'))
def __repr__(self):
return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
@@ -150,6 +186,7 @@ class OAuthCode(Base):
MODELS = [
OAuthToken,
+ OAuthRefreshToken,
OAuthCode,
OAuthClient,
OAuthUserClient]
diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py
index d21c8a5b..27ff32b4 100644
--- a/mediagoblin/plugins/oauth/tools.py
+++ b/mediagoblin/plugins/oauth/tools.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
@@ -14,13 +15,26 @@
# 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
+
+from random import getrandbits
+
+from datetime import datetime
+
from functools import wraps
-from mediagoblin.plugins.oauth.models import OAuthClient
from mediagoblin.plugins.api.tools import json_response
def require_client_auth(controller):
+ '''
+ View decorator
+
+ - Requires the presence of ``?client_id``
+ '''
+ # Avoid circular import
+ from mediagoblin.plugins.oauth.models import OAuthClient
+
@wraps(controller)
def wrapper(request, *args, **kw):
if not request.GET.get('client_id'):
@@ -41,3 +55,60 @@ def require_client_auth(controller):
return controller(request, client)
return wrapper
+
+
+def create_token(client, user):
+ '''
+ Create an OAuthToken and an OAuthRefreshToken entry in the database
+
+ Returns the data structure expected by the OAuth clients.
+ '''
+ from mediagoblin.plugins.oauth.models import OAuthToken, OAuthRefreshToken
+
+ token = OAuthToken()
+ token.user = user
+ token.client = client
+ token.save()
+
+ refresh_token = OAuthRefreshToken()
+ refresh_token.user = user
+ refresh_token.client = client
+ refresh_token.save()
+
+ # expire time of token in full seconds
+ # timedelta.total_seconds is python >= 2.7 or we would use that
+ td = token.expires - datetime.now()
+ exp_in = 86400*td.days + td.seconds # just ignore µsec
+
+ return {'access_token': token.token, 'token_type': 'bearer',
+ 'refresh_token': refresh_token.token, 'expires_in': exp_in}
+
+
+def generate_identifier():
+ ''' Generates a ``uuid.uuid4()`` '''
+ return unicode(uuid.uuid4())
+
+
+def generate_token():
+ ''' Uses generate_identifier '''
+ return generate_identifier()
+
+
+def generate_refresh_token():
+ ''' Uses generate_identifier '''
+ return generate_identifier()
+
+
+def generate_code():
+ ''' Uses generate_identifier '''
+ return generate_identifier()
+
+
+def generate_secret():
+ '''
+ Generate a long string of pseudo-random characters
+ '''
+ # XXX: We might not want it to use bcrypt, since bcrypt takes its time to
+ # generate the result.
+ return unicode(getrandbits(192))
+
diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py
index ea45c209..d6fd314f 100644
--- a/mediagoblin/plugins/oauth/views.py
+++ b/mediagoblin/plugins/oauth/views.py
@@ -16,21 +16,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
-import json
from urllib import urlencode
-from uuid import uuid4
-from datetime import datetime
+
+from werkzeug.exceptions import BadRequest
from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.decorators import require_active_login
-from mediagoblin.messages import add_message, SUCCESS, ERROR
+from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _
-from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken, \
- OAuthClient, OAuthUserClient
+from mediagoblin.plugins.oauth.models import OAuthCode, OAuthClient, \
+ OAuthUserClient, OAuthRefreshToken
from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm
-from mediagoblin.plugins.oauth.tools import require_client_auth
+from mediagoblin.plugins.oauth.tools import require_client_auth, \
+ create_token
from mediagoblin.plugins.api.tools import json_response
_log = logging.getLogger(__name__)
@@ -51,9 +51,6 @@ def register_client(request):
client.owner_id = request.user.id
client.redirect_uri = unicode(form.redirect_uri.data)
- client.generate_identifier()
- client.generate_secret()
-
client.save()
add_message(request, SUCCESS, _('The client {0} has been registered!')\
@@ -92,9 +89,9 @@ def authorize_client(request):
form.client_id.data).first()
if not client:
- _log.error('''No such client id as received from client authorization
- form.''')
- return BadRequest()
+ _log.error('No such client id as received from client authorization \
+form.')
+ raise BadRequest()
if form.validate():
relation = OAuthUserClient()
@@ -105,7 +102,7 @@ def authorize_client(request):
elif form.deny.data:
relation.state = u'rejected'
else:
- return BadRequest
+ raise BadRequest()
relation.save()
@@ -136,7 +133,7 @@ def authorize(request, client):
return json_response({
'status': 400,
'errors':
- [u'Public clients MUST have a redirect_uri pre-set']},
+ [u'Public clients should have a redirect_uri pre-set.']},
_disable_cors=True)
redirect_uri = client.redirect_uri
@@ -146,11 +143,10 @@ def authorize(request, client):
if not redirect_uri:
return json_response({
'status': 400,
- 'errors': [u'Can not find a redirect_uri for client: {0}'\
- .format(client.name)]}, _disable_cors=True)
+ 'errors': [u'No redirect_uri supplied!']},
+ _disable_cors=True)
code = OAuthCode()
- code.code = unicode(uuid4())
code.user = request.user
code.client = client
code.save()
@@ -180,59 +176,79 @@ def authorize(request, client):
def access_token(request):
+ '''
+ Access token endpoint provides access tokens to any clients that have the
+ right grants/credentials
+ '''
+
+ client = None
+ user = None
+
if request.GET.get('code'):
+ # Validate the code arg, then get the client object from the db.
code = OAuthCode.query.filter(OAuthCode.code ==
request.GET.get('code')).first()
- if code:
- if code.client.type == u'confidential':
- client_identifier = request.GET.get('client_id')
-
- if not client_identifier:
- return json_response({
- 'error': 'invalid_request',
- 'error_description':
- 'Missing client_id in request'})
-
- client_secret = request.GET.get('client_secret')
-
- if not client_secret:
- return json_response({
- 'error': 'invalid_request',
- 'error_description':
- 'Missing client_secret in request'})
-
- if not client_secret == code.client.secret or \
- not client_identifier == code.client.identifier:
- return json_response({
- 'error': 'invalid_client',
- 'error_description':
- 'The client_id or client_secret does not match the'
- ' code'})
-
- token = OAuthToken()
- token.token = unicode(uuid4())
- token.user = code.user
- token.client = code.client
- token.save()
-
- # expire time of token in full seconds
- # timedelta.total_seconds is python >= 2.7 or we would use that
- td = token.expires - datetime.now()
- exp_in = 86400*td.days + td.seconds # just ignore µsec
-
- access_token_data = {
- 'access_token': token.token,
- 'token_type': 'bearer',
- 'expires_in': exp_in}
- return json_response(access_token_data, _disable_cors=True)
- else:
+ if not code:
return json_response({
'error': 'invalid_request',
'error_description':
- 'Invalid code'})
- else:
- return json_response({
- 'error': 'invalid_request',
- 'error_descriptin':
- 'Missing `code` parameter in request'})
+ 'Invalid code.'})
+
+ client = code.client
+ user = code.user
+
+ elif request.args.get('refresh_token'):
+ # Validate a refresh token, then get the client object from the db.
+ refresh_token = OAuthRefreshToken.query.filter(
+ OAuthRefreshToken.token ==
+ request.args.get('refresh_token')).first()
+
+ if not refresh_token:
+ return json_response({
+ 'error': 'invalid_request',
+ 'error_description':
+ 'Invalid refresh token.'})
+
+ client = refresh_token.client
+ user = refresh_token.user
+
+ if client:
+ client_identifier = request.GET.get('client_id')
+
+ if not client_identifier:
+ return json_response({
+ 'error': 'invalid_request',
+ 'error_description':
+ 'Missing client_id in request.'})
+
+ if not client_identifier == client.identifier:
+ return json_response({
+ 'error': 'invalid_client',
+ 'error_description':
+ 'Mismatching client credentials.'})
+
+ if client.type == u'confidential':
+ client_secret = request.GET.get('client_secret')
+
+ if not client_secret:
+ return json_response({
+ 'error': 'invalid_request',
+ 'error_description':
+ 'Missing client_secret in request.'})
+
+ if not client_secret == client.secret:
+ return json_response({
+ 'error': 'invalid_client',
+ 'error_description':
+ 'Mismatching client credentials.'})
+
+
+ access_token_data = create_token(client, user)
+
+ return json_response(access_token_data, _disable_cors=True)
+
+ return json_response({
+ 'error': 'invalid_request',
+ 'error_description':
+ 'Missing `code` or `refresh_token` parameter in request.'})
diff --git a/mediagoblin/plugins/piwigo/__init__.py b/mediagoblin/plugins/piwigo/__init__.py
index 73326e9e..c4da708a 100644
--- a/mediagoblin/plugins/piwigo/__init__.py
+++ b/mediagoblin/plugins/piwigo/__init__.py
@@ -17,6 +17,8 @@
import logging
from mediagoblin.tools import pluginapi
+from mediagoblin.tools.session import SessionManager
+from .tools import PWGSession
_log = logging.getLogger(__name__)
@@ -32,6 +34,9 @@ def setup_plugin():
pluginapi.register_routes(routes)
+ PWGSession.session_manager = SessionManager("pwg_id", "plugins.piwigo")
+
+
hooks = {
'setup': setup_plugin
}
diff --git a/mediagoblin/plugins/piwigo/forms.py b/mediagoblin/plugins/piwigo/forms.py
index 5bb12e62..18cbd5c5 100644
--- a/mediagoblin/plugins/piwigo/forms.py
+++ b/mediagoblin/plugins/piwigo/forms.py
@@ -26,3 +26,19 @@ class AddSimpleForm(wtforms.Form):
# tags = wtforms.FieldList(wtforms.TextField())
category = wtforms.IntegerField()
level = wtforms.IntegerField()
+
+
+_md5_validator = wtforms.validators.Regexp(r"^[0-9a-fA-F]{32}$")
+
+
+class AddForm(wtforms.Form):
+ original_sum = wtforms.TextField(None,
+ [_md5_validator,
+ wtforms.validators.Required()])
+ thumbnail_sum = wtforms.TextField(None,
+ [wtforms.validators.Optional(False),
+ _md5_validator])
+ file_sum = wtforms.TextField(None, [_md5_validator])
+ name = wtforms.TextField()
+ date_creation = wtforms.TextField()
+ categories = wtforms.TextField()
diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py
index 85d77310..400be615 100644
--- a/mediagoblin/plugins/piwigo/tools.py
+++ b/mediagoblin/plugins/piwigo/tools.py
@@ -16,9 +16,11 @@
import logging
+import six
import lxml.etree as ET
-from werkzeug.exceptions import MethodNotAllowed
+from werkzeug.exceptions import MethodNotAllowed, BadRequest
+from mediagoblin.tools.request import setup_user_in_request
from mediagoblin.tools.response import Response
@@ -43,7 +45,7 @@ class PwgNamedArray(list):
def _fill_element_dict(el, data, as_attr=()):
for k, v in data.iteritems():
if k in as_attr:
- if not isinstance(v, basestring):
+ if not isinstance(v, six.string_types):
v = str(v)
el.set(k, v)
else:
@@ -57,7 +59,7 @@ def _fill_element(el, data):
el.text = "1"
else:
el.text = "0"
- elif isinstance(data, basestring):
+ elif isinstance(data, six.string_types):
el.text = data
elif isinstance(data, int):
el.text = str(data)
@@ -105,3 +107,46 @@ class CmdTable(object):
_log.warn("Method %s only allowed for POST", cmd_name)
raise MethodNotAllowed()
return func
+
+
+def check_form(form):
+ if not form.validate():
+ _log.error("form validation failed for form %r", form)
+ for f in form:
+ if len(f.error):
+ _log.error("Errors for %s: %r", f.name, f.errors)
+ raise BadRequest()
+ dump = []
+ for f in form:
+ dump.append("%s=%r" % (f.name, f.data))
+ _log.debug("form: %s", " ".join(dump))
+
+
+class PWGSession(object):
+ session_manager = None
+
+ def __init__(self, request):
+ self.request = request
+ self.in_pwg_session = False
+
+ def __enter__(self):
+ # Backup old state
+ self.old_session = self.request.session
+ self.old_user = self.request.user
+ # Load piwigo session into state
+ self.request.session = self.session_manager.load_session_from_cookie(
+ self.request)
+ setup_user_in_request(self.request)
+ self.in_pwg_session = True
+ return self
+
+ def __exit__(self, *args):
+ # Restore state
+ self.request.session = self.old_session
+ self.request.user = self.old_user
+ self.in_pwg_session = False
+
+ def save_to_cookie(self, response):
+ assert self.in_pwg_session
+ self.session_manager.save_session_to_cookie(self.request.session,
+ self.request, response)
diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py
index 3dee09cd..b59247ad 100644
--- a/mediagoblin/plugins/piwigo/views.py
+++ b/mediagoblin/plugins/piwigo/views.py
@@ -17,14 +17,15 @@
import logging
import re
-from werkzeug.exceptions import MethodNotAllowed, BadRequest
+from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented
from werkzeug.wrappers import BaseResponse
-from mediagoblin import mg_globals
from mediagoblin.meddleware.csrf import csrf_exempt
-from mediagoblin.tools.response import render_404
-from .tools import CmdTable, PwgNamedArray, response_xml
-from .forms import AddSimpleForm
+from mediagoblin.submit.lib import check_file_field
+from mediagoblin.auth.lib import fake_login_attempt
+from .tools import CmdTable, PwgNamedArray, response_xml, check_form, \
+ PWGSession
+from .forms import AddSimpleForm, AddForm
_log = logging.getLogger(__name__)
@@ -34,13 +35,25 @@ _log = logging.getLogger(__name__)
def pwg_login(request):
username = request.form.get("username")
password = request.form.get("password")
- _log.info("Login for %r/%r...", username, password)
+ _log.debug("Login for %r/%r...", username, password)
+ user = request.db.User.query.filter_by(username=username).first()
+ if not user:
+ _log.info("User %r not found", username)
+ fake_login_attempt()
+ return False
+ if not user.check_login(password):
+ _log.warn("Wrong password for %r", username)
+ return False
+ _log.info("Logging %r in", username)
+ request.session["user_id"] = user.id
+ request.session.save()
return True
@CmdTable("pwg.session.logout")
def pwg_logout(request):
_log.info("Logout")
+ request.session.delete()
return True
@@ -51,7 +64,11 @@ def pwg_getversion(request):
@CmdTable("pwg.session.getStatus")
def pwg_session_getStatus(request):
- return {'username': "fake_user"}
+ if request.user:
+ username = request.user.username
+ else:
+ username = "guest"
+ return {'username': username}
@CmdTable("pwg.categories.getList")
@@ -92,6 +109,9 @@ def pwg_images_addSimple(request):
dump.append("%s=%r" % (f.name, f.data))
_log.info("addimple: %r %s %r", request.form, " ".join(dump), request.files)
+ if not check_file_field(request, 'image'):
+ raise BadRequest()
+
return {'image_id': 123456, 'url': ''}
@@ -130,17 +150,13 @@ def pwg_images_addChunk(request):
return True
-def possibly_add_cookie(request, response):
- # TODO: We should only add a *real* cookie, if
- # authenticated. And if there is no cookie already.
- if True:
- response.set_cookie(
- 'pwg_id',
- "some_fake_for_now",
- path=request.environ['SCRIPT_NAME'],
- domain=mg_globals.app_config.get('csrf_cookie_domain'),
- secure=(request.scheme.lower() == 'https'),
- httponly=True)
+@CmdTable("pwg.images.add", True)
+def pwg_images_add(request):
+ _log.info("add: %r", request.form)
+ form = AddForm(request.form)
+ check_form(form)
+
+ return {'image_id': 123456, 'url': ''}
@csrf_exempt
@@ -153,15 +169,15 @@ def ws_php(request):
if not func:
_log.warn("wsphp: Unhandled %s %r %r", request.method,
request.args, request.form)
- return render_404(request)
-
- result = func(request)
+ raise NotImplemented()
- if isinstance(result, BaseResponse):
- return result
+ with PWGSession(request) as session:
+ result = func(request)
- response = response_xml(result)
+ if isinstance(result, BaseResponse):
+ return result
- possibly_add_cookie(request, response)
+ response = response_xml(result)
+ session.save_to_cookie(response)
- return response
+ return response
diff --git a/mediagoblin/processing/__init__.py b/mediagoblin/processing/__init__.py
index a1fd3fb7..f3a85940 100644
--- a/mediagoblin/processing/__init__.py
+++ b/mediagoblin/processing/__init__.py
@@ -75,6 +75,14 @@ class FilenameBuilder(object):
class ProcessingState(object):
+ """
+ The first and only argument to the "processor" of a media type
+
+ This could be thought of as a "request" to the processor
+ function. It has the main info for the request (media entry)
+ and a bunch of tools for the request on it.
+ It can get more fancy without impacting old media types.
+ """
def __init__(self, entry):
self.entry = entry
self.workbench = None
diff --git a/mediagoblin/processing/task.py b/mediagoblin/processing/task.py
index aec50aab..9af192ed 100644
--- a/mediagoblin/processing/task.py
+++ b/mediagoblin/processing/task.py
@@ -89,7 +89,7 @@ class ProcessMedia(task.Task):
with mgg.workbench_manager.create() as workbench:
proc_state.set_workbench(workbench)
# run the processing code
- entry.media_manager['processor'](proc_state)
+ entry.media_manager.processor(proc_state)
# We set the state to processed and save the entry here so there's
# no need to save at the end of the processing stage, probably ;)
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 8df9f2e1..0cb36753 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -360,6 +360,25 @@ textarea#description, textarea#bio {
font-size: 0.9em;
}
+a.comment_authorlink {
+ text-decoration: none;
+ padding-right: 5px;
+ font-weight: bold;
+ padding-left: 2px;
+}
+
+a.comment_authorlink:hover {
+ text-decoration: underline;
+}
+
+a.comment_whenlink {
+ text-decoration: none;
+}
+
+a.comment_whenlink:hover {
+ text-decoration: underline;
+}
+
.comment_content {
margin-left: 8px;
margin-top: 8px;
@@ -540,6 +559,7 @@ table.media_panel {
table.media_panel th {
font-weight: bold;
padding-bottom: 4px;
+ text-align: left;
}
diff --git a/mediagoblin/static/css/pdf_viewer.css b/mediagoblin/static/css/pdf_viewer.css
new file mode 100644
index 00000000..c04c8981
--- /dev/null
+++ b/mediagoblin/static/css/pdf_viewer.css
@@ -0,0 +1,1448 @@
+/* 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/extlib/pdf.js b/mediagoblin/static/extlib/pdf.js
new file mode 120000
index 00000000..f829660a
--- /dev/null
+++ b/mediagoblin/static/extlib/pdf.js
@@ -0,0 +1 @@
+../../../extlib/pdf.js \ No newline at end of file
diff --git a/mediagoblin/static/js/pdf_viewer.js b/mediagoblin/static/js/pdf_viewer.js
new file mode 100644
index 00000000..79c1e708
--- /dev/null
+++ b/mediagoblin/static/js/pdf_viewer.js
@@ -0,0 +1,3615 @@
+/* -*- 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/forms.py b/mediagoblin/submit/forms.py
index bd1e904f..e9bd93fd 100644
--- a/mediagoblin/submit/forms.py
+++ b/mediagoblin/submit/forms.py
@@ -18,7 +18,7 @@
import wtforms
from mediagoblin.tools.text import tag_length_validator
-from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.tools.licenses import licenses_as_choices
diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py
index a5483471..7c3b8ab3 100644
--- a/mediagoblin/submit/lib.py
+++ b/mediagoblin/submit/lib.py
@@ -40,7 +40,7 @@ def prepare_queue_task(app, entry, filename):
"""
Prepare a MediaEntry for the processing queue and get a queue file
"""
- # We generate this ourselves so we know what the taks id is for
+ # We generate this ourselves so we know what the task id is for
# retrieval later.
# (If we got it off the task's auto-generation, there'd be
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index 9d31c844..e964ec12 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -114,6 +114,7 @@ def submit_start(request):
{'submit_form': submit_form,
'app_config': mg_globals.app_config})
+
@require_active_login
def add_collection(request, media=None):
"""
@@ -122,32 +123,30 @@ def add_collection(request, media=None):
submit_form = submit_forms.AddCollectionForm(request.form)
if request.method == 'POST' and submit_form.validate():
- try:
- collection = request.db.Collection()
-
- collection.title = unicode(submit_form.title.data)
- collection.description = unicode(submit_form.description.data)
- collection.creator = request.user.id
- 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})
-
- if existing_collection:
- messages.add_message(
- request, messages.ERROR, _('You already have a collection called "%s"!' % collection.title))
- else:
- collection.save()
-
- add_message(request, SUCCESS, _('Collection "%s" added!' % collection.title))
+ collection = request.db.Collection()
+
+ collection.title = unicode(submit_form.title.data)
+ collection.description = unicode(submit_form.description.data)
+ collection.creator = request.user.id
+ 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})
+
+ if existing_collection:
+ add_message(request, messages.ERROR,
+ _('You already have a collection called "%s"!') \
+ % collection.title)
+ else:
+ collection.save()
- return redirect(request, "mediagoblin.user_pages.user_home",
- user=request.user.username)
+ add_message(request, SUCCESS,
+ _('Collection "%s" added!') % collection.title)
- except Exception as e:
- raise
+ return redirect(request, "mediagoblin.user_pages.user_home",
+ user=request.user.username)
return render_to_response(
request,
diff --git a/mediagoblin/templates/mediagoblin/admin/panel.html b/mediagoblin/templates/mediagoblin/admin/panel.html
index 6bcb5c24..1c3c866e 100644
--- a/mediagoblin/templates/mediagoblin/admin/panel.html
+++ b/mediagoblin/templates/mediagoblin/admin/panel.html
@@ -104,7 +104,7 @@
<td>{{ media_entry.id }}</td>
<td>{{ media_entry.get_uploader.username }}</td>
<td><a href="{{ media_entry.url_for_self(request.urlgen) }}">{{ media_entry.title }}</a></td>
- <td>{{ media_entry.created.strftime("%F %R") }}</td>
+ <td><span title='{{ media_entry.created.strftime("%F %R") }}'>{{ timesince(media_entry.created) }}</span></td>
</tr>
{% endfor %}
</table>
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index 66b95661..9c42a756 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -16,7 +16,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
<!doctype html>
-<html>
+<html
+{% block mediagoblin_html_tag %}
+{% endblock mediagoblin_html_tag %}
+>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
diff --git a/mediagoblin/templates/mediagoblin/media_displays/pdf.html b/mediagoblin/templates/mediagoblin/media_displays/pdf.html
new file mode 100644
index 00000000..e946f3ab
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/media_displays/pdf.html
@@ -0,0 +1,84 @@
+{#
+# 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/user_pages/media.html' %}
+
+{% set medium_view = request.app.public_store.file_url(
+ media.media_files['medium']) %}
+
+{% if 'pdf' in media.media_files %}
+ {% set pdf_view = request.app.public_store.file_url(
+ media.media_files['pdf']) %}
+{% else %}
+ {% set pdf_view = request.app.public_store.file_url(
+ media.media_files['original']) %}
+{% endif %}
+
+{% set pdf_js = global_config.get('media_type:mediagoblin.media_types.pdf', {}).get('pdf_js', False) %}
+
+{% if pdf_js %}
+ {% block mediagoblin_html_tag %}
+ dir="ltr" mozdisallowselectionprint moznomarginboxes
+ {% endblock mediagoblin_html_tag %}
+{% endif %}
+
+{% block mediagoblin_head -%}
+ {{ super() }}
+
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+
+{%- 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 %}
+{% endblock %}
+
+{% block mediagoblin_sidebar %}
+ <h3>{% trans %}Download{% endtrans %}</h3>
+ <ul>
+ {% if 'original' in media.media_files %}
+ <li>
+ <a href="{{ request.app.public_store.file_url(
+ media.media_files.original) }}">
+ {%- trans %}Original file{% endtrans -%}
+ </a>
+ </li>
+ {% endif %}
+ {% if 'pdf' in media.media_files %}
+ <li>
+ <a href="{{ request.app.public_store.file_url(
+ media.media_files.pdf) }}">
+ {%- trans %}PDF file{% endtrans -%}
+ </a>
+ </li>
+ {% endif %}
+ </ul>
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html b/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html
index 2b790584..694eb979 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html
@@ -41,9 +41,10 @@
<div class="form_submit_buttons">
{# TODO: This isn't a button really... might do unexpected things :) #}
- <a class="button_action" href="{{ request.urlgen('mediagoblin.user_pages.user_collection',
- collection=collection.slug,
- user=request.user.username) }}">{% trans %}Cancel{% endtrans %}</a>
+ <a class="button_action" href="
+ {{- collection.url_for_self(request.urlgen) }}">
+ {%- trans %}Cancel{% endtrans -%}
+ </a>
<input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form" />
{{ csrf_token }}
</div>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html b/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html
index 449cc3ce..dc31d90f 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html
@@ -47,9 +47,10 @@
<div class="form_submit_buttons">
{# TODO: This isn't a button really... might do unexpected things :) #}
- <a class="button_action" href="{{ request.urlgen('mediagoblin.user_pages.user_collection',
- collection=collection_item.in_collection.slug,
- user=request.user.username) }}">{% trans %}Cancel{% endtrans %}</a>
+ <a class="button_action" href="
+ {{- collection_item.in_collection.url_for_self(request.urlgen) }}">
+ {%- trans %}Cancel{% endtrans -%}
+ </a>
<input type="submit" value="{% trans %}Remove{% endtrans %}" class="button_form" />
{{ csrf_token }}
</div>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_list.html b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html
index abf22623..8ac0b988 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/collection_list.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html
@@ -15,7 +15,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/>.
#}
-{% extends "mediagoblin/base.html" %}
+{%- extends "mediagoblin/base.html" %}
{% block title %}
{%- trans username=user.username -%}
@@ -39,17 +39,17 @@
<a href="{{ request.urlgen('mediagoblin.submit.collection',
user=user.username) }}">
{%- trans %}Create new collection{% endtrans -%}
+ </a>
</p>
{% endif %}
{% endif %}
<ul>
{% for coll in collections %}
- {% set coll_url = request.urlgen(
- 'mediagoblin.user_pages.user_collection',
- user=user.username,
- collection=coll.slug) %}
- <li><a href="{{ coll_url }}">{{ coll.title }}</li>
+ {%- set coll_url = coll.url_for_self(request.urlgen) %}
+ <li>
+ <a href="{{ coll_url }}">{{ coll.title }}</a>
+ </li>
{% endfor %}
</ul>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 04be81aa..6d32d009 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -119,16 +119,20 @@
<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) }}">
+ user=comment_author.username) }}"
+ class="comment_authorlink">
{{- comment_author.username -}}
</a>
- {% trans %}at{% endtrans %}
<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">
- {{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}
- </a>:
+ 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 -%}
@@ -143,10 +147,12 @@
{% endif %}
</div>
<div class="media_sidebar">
- {% trans date=media.created.strftime("%Y-%m-%d") -%}
- <h3>Added on</h3>
- <p>{{ date }}</p>
- {%- endtrans %}
+ <h3>Added</h3>
+ <p><span title="{{ media.created.strftime("%I:%M%p %Y-%m-%d") }}">
+ {%- trans formatted_time=timesince(media.created) -%}
+ {{ formatted_time }} ago
+ {%- endtrans -%}
+ </span></p>
{% if media.tags %}
{% include "mediagoblin/utils/tags.html" %}
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html
index 35b4aa04..be6976c2 100644
--- a/mediagoblin/templates/mediagoblin/utils/wtforms.html
+++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html
@@ -19,7 +19,7 @@
{# Render the label for a field #}
{% macro render_label(field) %}
{%- if field.label.text -%}
- <label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
+ <label for="{{ field.label.field_id }}">{{ field.label.text }}</label>
{%- endif -%}
{%- endmacro %}
@@ -39,11 +39,11 @@
{{ field }}
{%- if field.errors -%}
{% for error in field.errors %}
- <p class="form_field_error">{{ _(error) }}</p>
+ <p class="form_field_error">{{ error }}</p>
{% endfor %}
{%- endif %}
{%- if field.description %}
- <p class="form_field_description">{{ _(field.description)|safe }}</p>
+ <p class="form_field_description">{{ field.description|safe }}</p>
{%- endif %}
</div>
{%- endmacro %}
@@ -59,7 +59,7 @@
{% macro render_table(form) -%}
{% for field in form %}
<tr>
- <th>{{ _(field.label.text) }}</th>
+ <th>{{ field.label.text }}</th>
<td>
{{field}}
{% if field.errors %}
diff --git a/mediagoblin/tests/appconfig_plugin_specs.ini b/mediagoblin/tests/appconfig_plugin_specs.ini
new file mode 100644
index 00000000..5511cd97
--- /dev/null
+++ b/mediagoblin/tests/appconfig_plugin_specs.ini
@@ -0,0 +1,21 @@
+[mediagoblin]
+direct_remote_path = /mgoblin_static/
+email_sender_address = "notice@mediagoblin.example.org"
+
+## Uncomment and change to your DB's appropiate setting.
+## Default is a local sqlite db "mediagoblin.db".
+# sql_engine = postgresql:///gmg
+
+# set to false to enable sending notices
+email_debug_mode = true
+
+# Set to false to disable registrations
+allow_registration = true
+
+[plugins]
+[[mediagoblin.tests.testplugins.pluginspec]]
+some_string = "not blork"
+some_int = "not an int"
+
+# this one shouldn't have its own config
+[[mediagoblin.tests.testplugins.callables1]]
diff --git a/mediagoblin/tests/resources.py b/mediagoblin/tests/resources.py
new file mode 100644
index 00000000..f7b3037d
--- /dev/null
+++ b/mediagoblin/tests/resources.py
@@ -0,0 +1,41 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2013 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 pkg_resources import resource_filename
+
+
+def resource(filename):
+ return resource_filename('mediagoblin.tests', 'test_submission/' + filename)
+
+
+GOOD_JPG = resource('good.jpg')
+GOOD_PNG = resource('good.png')
+EVIL_FILE = resource('evil')
+EVIL_JPG = resource('evil.jpg')
+EVIL_PNG = resource('evil.png')
+BIG_BLUE = resource('bigblue.png')
+GOOD_PDF = resource('good.pdf')
+
+
+def resource_exif(f):
+ return resource_filename('mediagoblin.tests', 'test_exif/' + f)
+
+
+GOOD_JPG = resource_exif('good.jpg')
+EMPTY_JPG = resource_exif('empty.jpg')
+BAD_JPG = resource_exif('bad.jpg')
+GPS_JPG = resource_exif('has-gps.jpg')
diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py
index cff25776..89cf1026 100644
--- a/mediagoblin/tests/test_api.py
+++ b/mediagoblin/tests/test_api.py
@@ -18,31 +18,17 @@
import logging
import base64
-from pkg_resources import resource_filename
-
import pytest
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.tests.tools import fixture_add_user
+from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
+ BIG_BLUE
_log = logging.getLogger(__name__)
-def resource(filename):
- '''
- Borrowed from the submission tests
- '''
- return resource_filename('mediagoblin.tests', 'test_submission/' + filename)
-
-
-GOOD_JPG = resource('good.jpg')
-GOOD_PNG = resource('good.png')
-EVIL_FILE = resource('evil')
-EVIL_JPG = resource('evil.jpg')
-EVIL_PNG = resource('evil.png')
-BIG_BLUE = resource('bigblue.png')
-
class TestAPI(object):
def setup(self):
diff --git a/mediagoblin/tests/test_exif.py b/mediagoblin/tests/test_exif.py
index 100d17f0..824de3c2 100644
--- a/mediagoblin/tests/test_exif.py
+++ b/mediagoblin/tests/test_exif.py
@@ -15,39 +15,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import pkg_resources
-import Image
+try:
+ from PIL import Image
+except ImportError:
+ import Image
from mediagoblin.tools.exif import exif_fix_image_orientation, \
extract_exif, clean_exif, get_gps_data, get_useful
+from .resources import GOOD_JPG, EMPTY_JPG, BAD_JPG, GPS_JPG
def assert_in(a, b):
assert a in b, "%r not in %r" % (a, b)
-GOOD_JPG = pkg_resources.resource_filename(
- 'mediagoblin.tests',
- os.path.join(
- 'test_exif',
- 'good.jpg'))
-EMPTY_JPG = pkg_resources.resource_filename(
- 'mediagoblin.tests',
- os.path.join(
- 'test_exif',
- 'empty.jpg'))
-BAD_JPG = pkg_resources.resource_filename(
- 'mediagoblin.tests',
- os.path.join(
- 'test_exif',
- 'bad.jpg'))
-GPS_JPG = pkg_resources.resource_filename(
- 'mediagoblin.tests',
- os.path.join(
- 'test_exif',
- 'has-gps.jpg'))
-
-
def test_exif_extraction():
'''
Test EXIF extraction from a good image
diff --git a/mediagoblin/tests/test_http_callback.py b/mediagoblin/tests/test_http_callback.py
index e2c85d0d..a0511af7 100644
--- a/mediagoblin/tests/test_http_callback.py
+++ b/mediagoblin/tests/test_http_callback.py
@@ -16,6 +16,7 @@
import json
+import pytest
from urlparse import urlparse, parse_qs
from mediagoblin import mg_globals
@@ -26,21 +27,24 @@ from mediagoblin.tests import test_oauth as oauth
class TestHTTPCallback(object):
- def _setup(self, test_app):
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
+ self.test_app = test_app
+
self.db = mg_globals.database
self.user_password = u'secret'
self.user = fixture_add_user(u'call_back', self.user_password)
- self.login(test_app)
+ self.login()
- def login(self, testapp):
- testapp.post('/auth/login/', {
+ def login(self):
+ self.test_app.post('/auth/login/', {
'username': self.user.username,
'password': self.user_password})
- def get_access_token(self, testapp, client_id, client_secret, code):
- response = testapp.get('/oauth/access_token', {
+ def get_access_token(self, client_id, client_secret, code):
+ response = self.test_app.get('/oauth/access_token', {
'code': code,
'client_id': client_id,
'client_secret': client_secret})
@@ -49,15 +53,12 @@ class TestHTTPCallback(object):
return response_data['access_token']
- def test_callback(self, test_app):
+ def test_callback(self):
''' Test processing HTTP callback '''
- self._setup(test_app)
-
self.oauth = oauth.TestOAuth()
- self.oauth._setup(test_app)
+ self.oauth.setup(self.test_app)
- redirect, client_id = self.oauth.test_4_authorize_confidential_client(
- test_app)
+ redirect, client_id = self.oauth.test_4_authorize_confidential_client()
code = parse_qs(urlparse(redirect.location).query)['code'][0]
@@ -66,11 +67,11 @@ class TestHTTPCallback(object):
client_secret = client.secret
- access_token = self.get_access_token(test_app, client_id, client_secret, code)
+ access_token = self.get_access_token(client_id, client_secret, code)
callback_url = 'https://foo.example?secrettestmediagoblinparam'
- res = test_app.post('/api/submit?client_id={0}&access_token={1}\
+ self.test_app.post('/api/submit?client_id={0}&access_token={1}\
&client_secret={2}'.format(
client_id,
access_token,
diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini
index b78abe64..9f95a398 100644
--- a/mediagoblin/tests/test_mgoblin_app.ini
+++ b/mediagoblin/tests/test_mgoblin_app.ini
@@ -16,6 +16,8 @@ allow_attachments = True
# mediagoblin.init.celery.from_celery
celery_setup_elsewhere = true
+media_types = mediagoblin.media_types.image, mediagoblin.media_types.pdf
+
[storage:publicstore]
base_dir = %(here)s/test_user_dev/media/public
base_url = /mgoblin_media/
diff --git a/mediagoblin/tests/test_oauth.py b/mediagoblin/tests/test_oauth.py
index 901556fe..ea3bd798 100644
--- a/mediagoblin/tests/test_oauth.py
+++ b/mediagoblin/tests/test_oauth.py
@@ -17,6 +17,7 @@
import json
import logging
+import pytest
from urlparse import parse_qs, urlparse
from mediagoblin import mg_globals
@@ -28,7 +29,10 @@ _log = logging.getLogger(__name__)
class TestOAuth(object):
- def _setup(self, test_app):
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
+ self.test_app = test_app
+
self.db = mg_globals.database
self.pman = pluginapi.PluginManager()
@@ -36,17 +40,17 @@ class TestOAuth(object):
self.user_password = u'4cc355_70k3N'
self.user = fixture_add_user(u'joauth', self.user_password)
- self.login(test_app)
+ self.login()
- def login(self, test_app):
- test_app.post(
- '/auth/login/', {
- 'username': self.user.username,
- 'password': self.user_password})
+ def login(self):
+ self.test_app.post(
+ '/auth/login/', {
+ 'username': self.user.username,
+ 'password': self.user_password})
- def register_client(self, test_app, name, client_type, description=None,
- redirect_uri=''):
- return test_app.post(
+ def register_client(self, name, client_type, description=None,
+ redirect_uri=''):
+ return self.test_app.post(
'/oauth/client/register', {
'name': name,
'description': description,
@@ -56,12 +60,10 @@ class TestOAuth(object):
def get_context(self, template_name):
return template.TEMPLATE_TEST_CONTEXT[template_name]
- def test_1_public_client_registration_without_redirect_uri(self, test_app):
+ def test_1_public_client_registration_without_redirect_uri(self):
''' Test 'public' OAuth client registration without any redirect uri '''
- self._setup(test_app)
-
- response = self.register_client(test_app, u'OMGOMGOMG', 'public',
- 'OMGOMG Apache License v2')
+ response = self.register_client(
+ u'OMGOMGOMG', 'public', 'OMGOMG Apache License v2')
ctx = self.get_context('oauth/client/register.html')
@@ -71,29 +73,30 @@ class TestOAuth(object):
assert response.status_int == 200
# Should display an error
- assert ctx['form'].redirect_uri.errors
+ assert len(ctx['form'].redirect_uri.errors)
# Should not pass through
assert not client
- def test_2_successful_public_client_registration(self, test_app):
+ def test_2_successful_public_client_registration(self):
''' Successfully register a public client '''
- self._setup(test_app)
- self.register_client(test_app, u'OMGOMG', 'public', 'OMG!',
- 'http://foo.example')
+ uri = 'http://foo.example'
+ self.register_client(
+ u'OMGOMG', 'public', 'OMG!', uri)
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.name == u'OMGOMG').first()
+ # redirect_uri should be set
+ assert client.redirect_uri == uri
+
# Client should have been registered
assert client
- def test_3_successful_confidential_client_reg(self, test_app):
+ def test_3_successful_confidential_client_reg(self):
''' Register a confidential OAuth client '''
- self._setup(test_app)
-
response = self.register_client(
- test_app, u'GMOGMO', 'confidential', 'NO GMO!')
+ u'GMOGMO', 'confidential', 'NO GMO!')
assert response.status_int == 302
@@ -105,18 +108,16 @@ class TestOAuth(object):
return client
- def test_4_authorize_confidential_client(self, test_app):
+ def test_4_authorize_confidential_client(self):
''' Authorize a confidential client as a logged in user '''
- self._setup(test_app)
-
- client = self.test_3_successful_confidential_client_reg(test_app)
+ client = self.test_3_successful_confidential_client_reg()
client_identifier = client.identifier
redirect_uri = 'https://foo.example'
- response = test_app.get('/oauth/authorize', {
+ response = self.test_app.get('/oauth/authorize', {
'client_id': client.identifier,
- 'scope': 'admin',
+ 'scope': 'all',
'redirect_uri': redirect_uri})
# User-agent should NOT be redirected
@@ -127,7 +128,7 @@ class TestOAuth(object):
form = ctx['form']
# Short for client authorization post reponse
- capr = test_app.post(
+ capr = self.test_app.post(
'/oauth/client/authorize', {
'client_id': form.client_id.data,
'allow': 'Allow',
@@ -142,21 +143,19 @@ class TestOAuth(object):
return authorization_response, client_identifier
def get_code_from_redirect_uri(self, uri):
+ ''' Get the value of ?code= from an URI '''
return parse_qs(urlparse(uri).query)['code'][0]
- def test_token_endpoint_successful_confidential_request(self, test_app):
+ def test_token_endpoint_successful_confidential_request(self):
''' Successful request against token endpoint '''
- self._setup(test_app)
-
- code_redirect, client_id = self.test_4_authorize_confidential_client(
- test_app)
+ code_redirect, client_id = self.test_4_authorize_confidential_client()
code = self.get_code_from_redirect_uri(code_redirect.location)
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
- token_res = test_app.get('/oauth/access_token?client_id={0}&\
+ token_res = self.test_app.get('/oauth/access_token?client_id={0}&\
code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert token_res.status_int == 200
@@ -170,19 +169,21 @@ code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert type(token_data['expires_in']) == int
assert token_data['expires_in'] > 0
- def test_token_endpont_missing_id_confidential_request(self, test_app):
- ''' Unsuccessful request against token endpoint, missing client_id '''
- self._setup(test_app)
+ # There should be a refresh token provided in the token data
+ assert len(token_data['refresh_token'])
+
+ return client_id, token_data
- code_redirect, client_id = self.test_4_authorize_confidential_client(
- test_app)
+ def test_token_endpont_missing_id_confidential_request(self):
+ ''' Unsuccessful request against token endpoint, missing client_id '''
+ code_redirect, client_id = self.test_4_authorize_confidential_client()
code = self.get_code_from_redirect_uri(code_redirect.location)
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
- token_res = test_app.get('/oauth/access_token?\
+ token_res = self.test_app.get('/oauth/access_token?\
code={0}&client_secret={1}'.format(code, client.secret))
assert token_res.status_int == 200
@@ -192,4 +193,30 @@ code={0}&client_secret={1}'.format(code, client.secret))
assert 'error' in token_data
assert not 'access_token' in token_data
assert token_data['error'] == 'invalid_request'
- assert token_data['error_description'] == 'Missing client_id in request'
+ assert len(token_data['error_description'])
+
+ def test_refresh_token(self):
+ ''' Try to get a new access token using the refresh token '''
+ # Get an access token and a refresh token
+ client_id, token_data =\
+ self.test_token_endpoint_successful_confidential_request()
+
+ client = self.db.OAuthClient.query.filter(
+ self.db.OAuthClient.identifier == client_id).first()
+
+ token_res = self.test_app.get('/oauth/access_token',
+ {'refresh_token': token_data['refresh_token'],
+ 'client_id': client_id,
+ 'client_secret': client.secret
+ })
+
+ assert token_res.status_int == 200
+
+ new_token_data = json.loads(token_res.body)
+
+ assert not 'error' in new_token_data
+ assert 'access_token' in new_token_data
+ assert 'token_type' in new_token_data
+ assert 'expires_in' in new_token_data
+ assert type(new_token_data['expires_in']) == int
+ assert new_token_data['expires_in'] > 0
diff --git a/mediagoblin/tests/test_pdf.py b/mediagoblin/tests/test_pdf.py
new file mode 100644
index 00000000..b4d1940a
--- /dev/null
+++ b/mediagoblin/tests/test_pdf.py
@@ -0,0 +1,39 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2013 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 tempfile
+import shutil
+import os
+import pytest
+
+from mediagoblin.media_types.pdf.processing import (
+ pdf_info, check_prerequisites, create_pdf_thumb)
+from .resources import GOOD_PDF as GOOD
+
+
+@pytest.mark.skipif("not check_prerequisites()")
+def test_pdf():
+ good_dict = {'pdf_version_major': 1, 'pdf_title': '',
+ 'pdf_page_size_width': 612, 'pdf_author': '',
+ 'pdf_keywords': '', 'pdf_pages': 10,
+ 'pdf_producer': 'dvips + GNU Ghostscript 7.05',
+ 'pdf_version_minor': 3,
+ 'pdf_creator': 'LaTeX with hyperref package',
+ 'pdf_page_size_height': 792}
+ assert pdf_info(GOOD) == good_dict
+ temp_dir = tempfile.mkdtemp()
+ create_pdf_thumb(GOOD, os.path.join(temp_dir, 'good_256_256.png'), 256, 256)
+ shutil.rmtree(temp_dir)
diff --git a/mediagoblin/tests/test_pluginapi.py b/mediagoblin/tests/test_pluginapi.py
index d40a5081..809b5ce9 100644
--- a/mediagoblin/tests/test_pluginapi.py
+++ b/mediagoblin/tests/test_pluginapi.py
@@ -18,9 +18,12 @@ import sys
from configobj import ConfigObj
import pytest
+import pkg_resources
+from validate import VdtTypeError
from mediagoblin import mg_globals
from mediagoblin.init.plugins import setup_plugins
+from mediagoblin.init.config import read_mediagoblin_config
from mediagoblin.tools import pluginapi
@@ -177,19 +180,22 @@ def test_disabled_plugin():
assert len(pman.plugins) == 0
+CONFIG_ALL_CALLABLES = [
+ ('mediagoblin', {}, []),
+ ('plugins', {}, [
+ ('mediagoblin.tests.testplugins.callables1', {}, []),
+ ('mediagoblin.tests.testplugins.callables2', {}, []),
+ ('mediagoblin.tests.testplugins.callables3', {}, []),
+ ])
+ ]
+
+
@with_cleanup()
-def test_callable_runone():
+def test_hook_handle():
"""
- Test the callable_runone method
+ Test the hook_handle method
"""
- cfg = build_config([
- ('mediagoblin', {}, []),
- ('plugins', {}, [
- ('mediagoblin.tests.testplugins.callables1', {}, []),
- ('mediagoblin.tests.testplugins.callables2', {}, []),
- ('mediagoblin.tests.testplugins.callables3', {}, []),
- ])
- ])
+ cfg = build_config(CONFIG_ALL_CALLABLES)
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
@@ -198,50 +204,42 @@ def test_callable_runone():
# Just one hook provided
call_log = []
- assert pluginapi.callable_runone(
+ assert pluginapi.hook_handle(
"just_one", call_log) == "Called just once"
assert call_log == ["expect this one call"]
# Nothing provided and unhandled not okay
call_log = []
- with pytest.raises(pluginapi.UnhandledCallable):
- pluginapi.callable_runone(
- "nothing_handling", call_log)
+ pluginapi.hook_handle(
+ "nothing_handling", call_log) == None
assert call_log == []
# Nothing provided and unhandled okay
call_log = []
- assert pluginapi.callable_runone(
+ assert pluginapi.hook_handle(
"nothing_handling", call_log, unhandled_okay=True) is None
assert call_log == []
# Multiple provided, go with the first!
call_log = []
- assert pluginapi.callable_runone(
+ assert pluginapi.hook_handle(
"multi_handle", call_log) == "the first returns"
assert call_log == ["Hi, I'm the first"]
# Multiple provided, one has CantHandleIt
call_log = []
- assert pluginapi.callable_runone(
+ assert pluginapi.hook_handle(
"multi_handle_with_canthandle",
call_log) == "the second returns"
assert call_log == ["Hi, I'm the second"]
@with_cleanup()
-def test_callable_runall():
+def test_hook_runall():
"""
- Test the callable_runall method
+ Test the hook_runall method
"""
- cfg = build_config([
- ('mediagoblin', {}, []),
- ('plugins', {}, [
- ('mediagoblin.tests.testplugins.callables1', {}, []),
- ('mediagoblin.tests.testplugins.callables2', {}, []),
- ('mediagoblin.tests.testplugins.callables3', {}, []),
- ])
- ])
+ cfg = build_config(CONFIG_ALL_CALLABLES)
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
@@ -250,19 +248,19 @@ def test_callable_runall():
# Just one hook, check results
call_log = []
- assert pluginapi.callable_runall(
- "just_one", call_log) == ["Called just once", None, None]
+ assert pluginapi.hook_runall(
+ "just_one", call_log) == ["Called just once"]
assert call_log == ["expect this one call"]
# None provided, check results
call_log = []
- assert pluginapi.callable_runall(
+ assert pluginapi.hook_runall(
"nothing_handling", call_log) == []
assert call_log == []
# Multiple provided, check results
call_log = []
- assert pluginapi.callable_runall(
+ assert pluginapi.hook_runall(
"multi_handle", call_log) == [
"the first returns",
"the second returns",
@@ -275,7 +273,7 @@ def test_callable_runall():
# Multiple provided, one has CantHandleIt, check results
call_log = []
- assert pluginapi.callable_runall(
+ assert pluginapi.hook_runall(
"multi_handle_with_canthandle", call_log) == [
"the second returns",
"the third returns",
@@ -283,3 +281,45 @@ def test_callable_runall():
assert call_log == [
"Hi, I'm the second",
"Hi, I'm the third"]
+
+
+@with_cleanup()
+def test_hook_transform():
+ """
+ Test the hook_transform method
+ """
+ cfg = build_config(CONFIG_ALL_CALLABLES)
+
+ mg_globals.app_config = cfg['mediagoblin']
+ mg_globals.global_config = cfg
+
+ setup_plugins()
+
+ assert pluginapi.hook_transform(
+ "expand_tuple", (-1, 0)) == (-1, 0, 1, 2, 3)
+
+
+def test_plugin_config():
+ """
+ Make sure plugins can set up their own config
+ """
+ config, validation_result = read_mediagoblin_config(
+ pkg_resources.resource_filename(
+ 'mediagoblin.tests', 'appconfig_plugin_specs.ini'))
+
+ pluginspec_section = config['plugins'][
+ 'mediagoblin.tests.testplugins.pluginspec']
+ assert pluginspec_section['some_string'] == 'not blork'
+ assert pluginspec_section['dont_change_me'] == 'still the default'
+
+ # Make sure validation works... this should be an error
+ assert isinstance(
+ validation_result[
+ 'plugins'][
+ 'mediagoblin.tests.testplugins.pluginspec'][
+ 'some_int'],
+ VdtTypeError)
+
+ # the callables thing shouldn't really have anything though.
+ assert len(config['plugins'][
+ 'mediagoblin.tests.testplugins.callables1']) == 0
diff --git a/mediagoblin/tests/test_storage.py b/mediagoblin/tests/test_storage.py
index 749f7b07..f6f1d18f 100644
--- a/mediagoblin/tests/test_storage.py
+++ b/mediagoblin/tests/test_storage.py
@@ -95,6 +95,14 @@ def get_tmp_filestorage(mount_url=None, fake_remote=False):
return tmpdir, this_storage
+def cleanup_storage(this_storage, tmpdir, *paths):
+ for p in paths:
+ while p:
+ assert this_storage.delete_dir(p) == True
+ p.pop(-1)
+ os.rmdir(tmpdir)
+
+
def test_basic_storage__resolve_filepath():
tmpdir, this_storage = get_tmp_filestorage()
@@ -111,7 +119,7 @@ def test_basic_storage__resolve_filepath():
this_storage._resolve_filepath,
['../../', 'etc', 'passwd'])
- os.rmdir(tmpdir)
+ cleanup_storage(this_storage, tmpdir)
def test_basic_storage_file_exists():
@@ -127,6 +135,7 @@ def test_basic_storage_file_exists():
assert not this_storage.file_exists(['dnedir1', 'dnedir2', 'somefile.lol'])
this_storage.delete_file(['dir1', 'dir2', 'filename.txt'])
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'])
def test_basic_storage_get_unique_filepath():
@@ -149,6 +158,7 @@ def test_basic_storage_get_unique_filepath():
assert new_filename == secure_filename(new_filename)
os.remove(filename)
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'])
def test_basic_storage_get_file():
@@ -189,6 +199,7 @@ def test_basic_storage_get_file():
this_storage.delete_file(filepath)
this_storage.delete_file(new_filepath)
this_storage.delete_file(['testydir', 'testyfile.txt'])
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'], ['testydir'])
def test_basic_storage_delete_file():
@@ -204,11 +215,15 @@ def test_basic_storage_delete_file():
assert os.path.exists(
os.path.join(tmpdir, 'dir1/dir2/ourfile.txt'))
+ assert this_storage.delete_dir(['dir1', 'dir2']) == False
this_storage.delete_file(filepath)
+ assert this_storage.delete_dir(['dir1', 'dir2']) == True
assert not os.path.exists(
os.path.join(tmpdir, 'dir1/dir2/ourfile.txt'))
+ cleanup_storage(this_storage, tmpdir, ['dir1'])
+
def test_basic_storage_url_for_file():
# Not supplying a base_url should actually just bork.
@@ -217,7 +232,7 @@ def test_basic_storage_url_for_file():
storage.NoWebServing,
this_storage.file_url,
['dir1', 'dir2', 'filename.txt'])
- os.rmdir(tmpdir)
+ cleanup_storage(this_storage, tmpdir)
# base_url without domain
tmpdir, this_storage = get_tmp_filestorage('/media/')
@@ -225,7 +240,7 @@ def test_basic_storage_url_for_file():
['dir1', 'dir2', 'filename.txt'])
expected = '/media/dir1/dir2/filename.txt'
assert result == expected
- os.rmdir(tmpdir)
+ cleanup_storage(this_storage, tmpdir)
# base_url with domain
tmpdir, this_storage = get_tmp_filestorage(
@@ -234,7 +249,7 @@ def test_basic_storage_url_for_file():
['dir1', 'dir2', 'filename.txt'])
expected = 'http://media.example.org/ourmedia/dir1/dir2/filename.txt'
assert result == expected
- os.rmdir(tmpdir)
+ cleanup_storage(this_storage, tmpdir)
def test_basic_storage_get_local_path():
@@ -248,13 +263,13 @@ def test_basic_storage_get_local_path():
assert result == expected
- os.rmdir(tmpdir)
+ cleanup_storage(this_storage, tmpdir)
def test_basic_storage_is_local():
tmpdir, this_storage = get_tmp_filestorage()
assert this_storage.local_storage is True
- os.rmdir(tmpdir)
+ cleanup_storage(this_storage, tmpdir)
def test_basic_storage_copy_locally():
@@ -275,6 +290,7 @@ def test_basic_storage_copy_locally():
os.remove(new_file_dest)
os.rmdir(dest_tmpdir)
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'])
def _test_copy_local_to_storage_works(tmpdir, this_storage):
@@ -292,6 +308,7 @@ def _test_copy_local_to_storage_works(tmpdir, this_storage):
'r').read() == 'haha'
this_storage.delete_file(['dir1', 'dir2', 'copiedto.txt'])
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'])
def test_basic_storage_copy_local_to_storage():
diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py
index ac714252..162b2d19 100644
--- a/mediagoblin/tests/test_submission.py
+++ b/mediagoblin/tests/test_submission.py
@@ -20,26 +20,17 @@ sys.setdefaultencoding('utf-8')
import urlparse
import os
-
-from pkg_resources import resource_filename
+import pytest
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.pdf.processing import check_prerequisites as pdf_check_prerequisites
-def resource(filename):
- return resource_filename('mediagoblin.tests', 'test_submission/' + filename)
-
-
-GOOD_JPG = resource('good.jpg')
-GOOD_PNG = resource('good.png')
-EVIL_FILE = resource('evil')
-EVIL_JPG = resource('evil.jpg')
-EVIL_PNG = resource('evil.png')
-BIG_BLUE = resource('bigblue.png')
-from .test_exif import GPS_JPG
+from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
+ BIG_BLUE, GOOD_PDF, GPS_JPG
GOOD_TAG_STRING = u'yin,yang'
BAD_TAG_STRING = unicode('rage,' + 'f' * 26 + 'u' * 26)
@@ -49,7 +40,8 @@ REQUEST_CONTEXT = ['mediagoblin/user_pages/user.html', 'request']
class TestSubmission:
- def _setup(self, test_app):
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
self.test_app = test_app
# TODO: Possibly abstract into a decorator like:
@@ -88,9 +80,7 @@ class TestSubmission:
comments = request.db.MediaComment.find({'media_entry': media_id})
assert count == len(list(comments))
- def test_missing_fields(self, test_app):
- self._setup(test_app)
-
+ def test_missing_fields(self):
# Test blank form
# ---------------
response, form = self.do_post({}, *FORM_CONTEXT)
@@ -117,14 +107,20 @@ class TestSubmission:
self.logout()
self.test_app.get(url)
- def test_normal_jpg(self, test_app):
- self._setup(test_app)
+ def test_normal_jpg(self):
self.check_normal_upload(u'Normal upload 1', GOOD_JPG)
- def test_normal_png(self, test_app):
- self._setup(test_app)
+ def test_normal_png(self):
self.check_normal_upload(u'Normal upload 2', GOOD_PNG)
+ @pytest.mark.skipif("not pdf_check_prerequisites()")
+ def test_normal_pdf(self):
+ response, context = self.do_post({'title': u'Normal upload 3 (pdf)'},
+ do_follow=True,
+ **self.upload_data(GOOD_PDF))
+ self.check_url(response, '/u/{0}/'.format(self.test_user.username))
+ assert 'mediagoblin/user_pages/user.html' in context
+
def check_media(self, request, find_data, count=None):
media = MediaEntry.find(find_data)
if count is not None:
@@ -133,9 +129,7 @@ class TestSubmission:
return
return media[0]
- def test_tags(self, test_app):
- self._setup(test_app)
-
+ def test_tags(self):
# Good tag string
# --------
response, request = self.do_post({'title': u'Balanced Goblin 2',
@@ -160,9 +154,7 @@ class TestSubmission:
'Tags that are too long: ' \
'ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu']
- def test_delete(self, test_app):
- self._setup(test_app)
-
+ def test_delete(self):
response, request = self.do_post({'title': u'Balanced Goblin'},
*REQUEST_CONTEXT, do_follow=True,
**self.upload_data(GOOD_JPG))
@@ -207,9 +199,7 @@ class TestSubmission:
self.check_media(request, {'id': media_id}, 0)
self.check_comments(request, media_id, 0)
- def test_evil_file(self, test_app):
- self._setup(test_app)
-
+ def test_evil_file(self):
# Test non-suppoerted file with non-supported extension
# -----------------------------------------------------
response, form = self.do_post({'title': u'Malicious Upload 1'},
@@ -220,26 +210,23 @@ class TestSubmission:
str(form.file.errors[0])
- def test_get_media_manager(self, test_app):
+ def test_get_media_manager(self):
"""Test if the get_media_manger function returns sensible things
"""
- self._setup(test_app)
-
response, request = self.do_post({'title': u'Balanced Goblin'},
*REQUEST_CONTEXT, do_follow=True,
**self.upload_data(GOOD_JPG))
media = self.check_media(request, {'title': u'Balanced Goblin'}, 1)
assert media.media_type == u'mediagoblin.media_types.image'
- assert media.media_manager == img_MEDIA_MANAGER
+ assert isinstance(media.media_manager, img_MEDIA_MANAGER)
+ assert media.media_manager.entry == media
- def test_sniffing(self, test_app):
+ def test_sniffing(self):
'''
Test sniffing mechanism to assert that regular uploads work as intended
'''
- self._setup(test_app)
-
template.clear_test_template_context()
response = self.test_app.post(
'/submit/', {
@@ -269,30 +256,22 @@ class TestSubmission:
assert entry.state == 'failed'
assert entry.fail_error == u'mediagoblin.processing:BadMediaFail'
- def test_evil_jpg(self, test_app):
- self._setup(test_app)
-
+ def test_evil_jpg(self):
# Test non-supported file with .jpg extension
# -------------------------------------------
self.check_false_image(u'Malicious Upload 2', EVIL_JPG)
- def test_evil_png(self, test_app):
- self._setup(test_app)
-
+ def test_evil_png(self):
# Test non-supported file with .png extension
# -------------------------------------------
self.check_false_image(u'Malicious Upload 3', EVIL_PNG)
- def test_media_data(self, test_app):
- self._setup(test_app)
-
+ def test_media_data(self):
self.check_normal_upload(u"With GPS data", GPS_JPG)
media = self.check_media(None, {"title": u"With GPS data"}, 1)
assert media.media_data.gps_latitude == 59.336666666666666
- def test_processing(self, test_app):
- self._setup(test_app)
-
+ def test_processing(self):
public_store_dir = mg_globals.global_config[
'storage:publicstore']['base_dir']
@@ -307,7 +286,7 @@ class TestSubmission:
# Does the processed image have a good filename?
filename = os.path.join(
public_store_dir,
- *media.media_files.get(key, []))
+ *media.media_files[key])
assert filename.endswith('_' + basename)
# Is it smaller than the last processed image we looked at?
size = os.stat(filename).st_size
diff --git a/mediagoblin/tests/test_submission/good.pdf b/mediagoblin/tests/test_submission/good.pdf
new file mode 100644
index 00000000..ab5db006
--- /dev/null
+++ b/mediagoblin/tests/test_submission/good.pdf
Binary files differ
diff --git a/mediagoblin/tests/test_timesince.py b/mediagoblin/tests/test_timesince.py
new file mode 100644
index 00000000..6579eb09
--- /dev/null
+++ b/mediagoblin/tests/test_timesince.py
@@ -0,0 +1,57 @@
+# 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 datetime import datetime, timedelta
+
+from mediagoblin.tools.timesince import is_aware, timesince
+
+
+def test_timesince():
+ test_time = datetime.now()
+
+ # it should ignore second and microseconds
+ assert timesince(test_time, test_time + timedelta(microseconds=1)) == "0 minutes"
+ assert timesince(test_time, test_time + timedelta(seconds=1)) == "0 minutes"
+
+ # test minutes, hours, days, weeks, months and years (singular and plural)
+ assert timesince(test_time, test_time + timedelta(minutes=1)) == "1 minute"
+ assert timesince(test_time, test_time + timedelta(minutes=2)) == "2 minutes"
+
+ assert timesince(test_time, test_time + timedelta(hours=1)) == "1 hour"
+ assert timesince(test_time, test_time + timedelta(hours=2)) == "2 hours"
+
+ assert timesince(test_time, test_time + timedelta(days=1)) == "1 day"
+ assert timesince(test_time, test_time + timedelta(days=2)) == "2 days"
+
+ assert timesince(test_time, test_time + timedelta(days=7)) == "1 week"
+ assert timesince(test_time, test_time + timedelta(days=14)) == "2 weeks"
+
+ assert timesince(test_time, test_time + timedelta(days=30)) == "1 month"
+ assert timesince(test_time, test_time + timedelta(days=60)) == "2 months"
+
+ assert timesince(test_time, test_time + timedelta(days=365)) == "1 year"
+ assert timesince(test_time, test_time + timedelta(days=730)) == "2 years"
+
+ # okay now we want to test combinations
+ # e.g. 1 hour, 5 days
+ assert timesince(test_time, test_time + timedelta(days=5, hours=1)) == "5 days, 1 hour"
+
+ assert timesince(test_time, test_time + timedelta(days=15)) == "2 weeks, 1 day"
+
+ assert timesince(test_time, test_time + timedelta(days=97)) == "3 months, 1 week"
+
+ assert timesince(test_time, test_time + timedelta(days=2250)) == "6 years, 2 months"
+
diff --git a/mediagoblin/tests/test_util.py b/mediagoblin/tests/test_util.py
index e4c04b7a..bc14f528 100644
--- a/mediagoblin/tests/test_util.py
+++ b/mediagoblin/tests/test_util.py
@@ -104,6 +104,28 @@ def test_locale_to_lower_lower():
assert translate.locale_to_lower_lower('en_us') == 'en-us'
+def test_gettext_lazy_proxy():
+ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+ from mediagoblin.tools.translate import pass_to_ugettext, set_thread_locale
+ proxy = _(u"Password")
+ orig = u"Password"
+
+ set_thread_locale("es")
+ p1 = unicode(proxy)
+ p1_should = pass_to_ugettext(orig)
+ assert p1_should != orig, "Test useless, string not translated"
+ assert p1 == p1_should
+
+ set_thread_locale("sv")
+ p2 = unicode(proxy)
+ p2_should = pass_to_ugettext(orig)
+ assert p2_should != orig, "Test broken, string not translated"
+ assert p2 == p2_should
+
+ assert p1_should != p2_should, "Test broken, same translated string"
+ assert p1 != p2
+
+
def test_html_cleaner():
# Remove images
result = text.clean_html(
diff --git a/mediagoblin/tests/test_workbench.py b/mediagoblin/tests/test_workbench.py
index 9cd49671..6695618b 100644
--- a/mediagoblin/tests/test_workbench.py
+++ b/mediagoblin/tests/test_workbench.py
@@ -21,7 +21,7 @@ import tempfile
from mediagoblin.tools import workbench
from mediagoblin.mg_globals import setup_globals
from mediagoblin.decorators import get_workbench
-from mediagoblin.tests.test_storage import get_tmp_filestorage
+from mediagoblin.tests.test_storage import get_tmp_filestorage, cleanup_storage
class TestWorkbench(object):
@@ -76,6 +76,7 @@ class TestWorkbench(object):
assert filename == os.path.join(
tmpdir, 'dir1/dir2/ourfile.txt')
this_storage.delete_file(filepath)
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'])
# with a fake remote file storage
tmpdir, this_storage = get_tmp_filestorage(fake_remote=True)
@@ -102,6 +103,7 @@ class TestWorkbench(object):
this_workbench.dir, 'thisfile.text')
this_storage.delete_file(filepath)
+ cleanup_storage(this_storage, tmpdir, ['dir1', 'dir2'])
this_workbench.destroy()
def test_workbench_decorator(self):
diff --git a/mediagoblin/tests/testplugins/callables1/__init__.py b/mediagoblin/tests/testplugins/callables1/__init__.py
index 9c278b49..fe801a01 100644
--- a/mediagoblin/tests/testplugins/callables1/__init__.py
+++ b/mediagoblin/tests/testplugins/callables1/__init__.py
@@ -14,8 +14,6 @@
# 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 CantHandleIt
-
def setup_plugin():
pass
@@ -30,12 +28,16 @@ def multi_handle(call_log):
return "the first returns"
def multi_handle_with_canthandle(call_log):
- raise CantHandleIt("I just can't accept this stupid method")
+ return None
+
+def expand_tuple(this_tuple):
+ return this_tuple + (1,)
hooks = {
'setup': setup_plugin,
'just_one': just_one,
'multi_handle': multi_handle,
'multi_handle_with_canthandle': multi_handle_with_canthandle,
+ 'expand_tuple': expand_tuple,
}
diff --git a/mediagoblin/tests/testplugins/callables2/__init__.py b/mediagoblin/tests/testplugins/callables2/__init__.py
index aaab5b21..9d5cf950 100644
--- a/mediagoblin/tests/testplugins/callables2/__init__.py
+++ b/mediagoblin/tests/testplugins/callables2/__init__.py
@@ -29,10 +29,13 @@ def multi_handle_with_canthandle(call_log):
call_log.append("Hi, I'm the second")
return "the second returns"
+def expand_tuple(this_tuple):
+ return this_tuple + (2,)
hooks = {
'setup': setup_plugin,
'just_one': just_one,
'multi_handle': multi_handle,
'multi_handle_with_canthandle': multi_handle_with_canthandle,
+ 'expand_tuple': expand_tuple,
}
diff --git a/mediagoblin/tests/testplugins/callables3/__init__.py b/mediagoblin/tests/testplugins/callables3/__init__.py
index 8d0c9c25..04efc8fc 100644
--- a/mediagoblin/tests/testplugins/callables3/__init__.py
+++ b/mediagoblin/tests/testplugins/callables3/__init__.py
@@ -29,10 +29,13 @@ def multi_handle_with_canthandle(call_log):
call_log.append("Hi, I'm the third")
return "the third returns"
+def expand_tuple(this_tuple):
+ return this_tuple + (3,)
hooks = {
'setup': setup_plugin,
'just_one': just_one,
'multi_handle': multi_handle,
'multi_handle_with_canthandle': multi_handle_with_canthandle,
+ 'expand_tuple': expand_tuple,
}
diff --git a/mediagoblin/tests/testplugins/pluginspec/__init__.py b/mediagoblin/tests/testplugins/pluginspec/__init__.py
new file mode 100644
index 00000000..76ca2b1f
--- /dev/null
+++ b/mediagoblin/tests/testplugins/pluginspec/__init__.py
@@ -0,0 +1,22 @@
+# 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/>.
+
+def setup_plugin():
+ pass
+
+hooks = {
+ 'setup': setup_plugin,
+}
diff --git a/mediagoblin/tests/testplugins/pluginspec/config_spec.ini b/mediagoblin/tests/testplugins/pluginspec/config_spec.ini
new file mode 100644
index 00000000..5c9c3bd7
--- /dev/null
+++ b/mediagoblin/tests/testplugins/pluginspec/config_spec.ini
@@ -0,0 +1,4 @@
+[plugin_spec]
+some_string = string(default="blork")
+some_int = integer(default=50)
+dont_change_me = string(default="still the default") \ No newline at end of file
diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py
index a0498a6e..52635e18 100644
--- a/mediagoblin/tests/tools.py
+++ b/mediagoblin/tests/tools.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import sys
import os
import pkg_resources
import shutil
@@ -28,7 +29,6 @@ from mediagoblin import mg_globals
from mediagoblin.db.models import User, MediaEntry, Collection
from mediagoblin.tools import testing
from mediagoblin.init.config import read_mediagoblin_config
-from mediagoblin.db.open import setup_connection_and_db_from_config
from mediagoblin.db.base import Session
from mediagoblin.meddleware import BaseMeddleware
from mediagoblin.auth.lib import bcrypt_gen_password_hash
@@ -50,7 +50,9 @@ USER_DEV_DIRECTORIES_TO_SETUP = ['media/public', 'media/queue']
BAD_CELERY_MESSAGE = """\
Sorry, you *absolutely* must run tests with the
mediagoblin.init.celery.from_tests module. Like so:
-$ CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests ./bin/py.test"""
+
+$ CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests {0}
+""".format(sys.argv[0])
class BadCeleryEnviron(Exception): pass
@@ -230,7 +232,7 @@ def fixture_media_entry(title=u"Some title", slug=None,
entry.slug = slug
entry.uploader = uploader or fixture_add_user().id
entry.media_type = u'image'
-
+
if gen_slug:
entry.generate_slug()
if save:
diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py
index 283350a8..3f98aa8a 100644
--- a/mediagoblin/tools/pluginapi.py
+++ b/mediagoblin/tools/pluginapi.py
@@ -274,68 +274,94 @@ def get_hook_templates(hook_name):
return PluginManager().get_template_hooks(hook_name)
-###########################
-# Callable convenience code
-###########################
+#############################
+## Hooks: The Next Generation
+#############################
-class CantHandleIt(Exception):
- """
- A callable may call this method if they look at the relevant
- arguments passed and decide it's not possible for them to handle
- things.
- """
- pass
-class UnhandledCallable(Exception):
- """
- Raise this method if no callables were available to handle the
- specified hook. Only used by callable_runone.
+def hook_handle(hook_name, *args, **kwargs):
"""
- pass
+ Run through hooks attempting to find one that handle this hook.
+ All callables called with the same arguments until one handles
+ things and returns a non-None value.
-def callable_runone(hookname, *args, **kwargs):
- """
- Run the callable hook HOOKNAME... run until the first response,
- then return.
+ (If you are writing a handler and you don't have a particularly
+ useful value to return even though you've handled this, returning
+ True is a good solution.)
- This function will run stop at the first hook that handles the
- result. Hooks raising CantHandleIt will be skipped.
+ Note that there is a special keyword argument:
+ if "default_handler" is passed in as a keyword argument, this will
+ be used if no handler is found.
- Unless unhandled_okay is True, this will error out if no hooks
- have been registered to handle this function.
+ Some examples of using this:
+ - You need an interface implemented, but only one fit for it
+ - You need to *do* something, but only one thing needs to do it.
"""
- callables = PluginManager().get_hook_callables(hookname)
+ default_handler = kwargs.pop('default_handler', None)
+
+ callables = PluginManager().get_hook_callables(hook_name)
- unhandled_okay = kwargs.pop("unhandled_okay", False)
+ result = None
for callable in callables:
- try:
- return callable(*args, **kwargs)
- except CantHandleIt:
- continue
+ result = callable(*args, **kwargs)
- if unhandled_okay is False:
- raise UnhandledCallable(
- "No hooks registered capable of handling '%s'" % hookname)
+ if result is not None:
+ break
+ if result is None and default_handler is not None:
+ result = default_handler(*args, **kwargs)
+
+ return result
-def callable_runall(hookname, *args, **kwargs):
- """
- Run all callables for HOOKNAME.
- This method will run *all* hooks that handle this method (skipping
- those that raise CantHandleIt), and will return a list of all
- results.
+def hook_runall(hook_name, *args, **kwargs):
+ """
+ Run through all callable hooks and pass in arguments.
+
+ All non-None results are accrued in a list and returned from this.
+ (Other "false-like" values like False and friends are still
+ accrued, however.)
+
+ Some examples of using this:
+ - You have an interface call where actually multiple things can
+ and should implement it
+ - You need to get a list of things from various plugins that
+ handle them and do something with them
+ - You need to *do* something, and actually multiple plugins need
+ to do it separately
"""
- callables = PluginManager().get_hook_callables(hookname)
+ callables = PluginManager().get_hook_callables(hook_name)
results = []
for callable in callables:
- try:
- results.append(callable(*args, **kwargs))
- except CantHandleIt:
- continue
+ result = callable(*args, **kwargs)
+
+ if result is not None:
+ results.append(result)
return results
+
+
+def hook_transform(hook_name, arg):
+ """
+ Run through a bunch of hook callables and transform some input.
+
+ Note that unlike the other hook tools, this one only takes ONE
+ argument. This argument is passed to each function, which in turn
+ returns something that becomes the input of the next callable.
+
+ Some examples of using this:
+ - You have an object, say a form, but you want plugins to each be
+ able to modify it.
+ """
+ result = arg
+
+ callables = PluginManager().get_hook_callables(hook_name)
+
+ for callable in callables:
+ result = callable(result)
+
+ return result
diff --git a/mediagoblin/tools/processing.py b/mediagoblin/tools/processing.py
index cff4cb9d..2abe6452 100644
--- a/mediagoblin/tools/processing.py
+++ b/mediagoblin/tools/processing.py
@@ -21,8 +21,6 @@ import traceback
from urllib2 import urlopen, Request, HTTPError
from urllib import urlencode
-from mediagoblin.tools.common import TESTS_ENABLED
-
_log = logging.getLogger(__name__)
TESTS_CALLBACKS = {}
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index 80df1f5a..aaf31d0b 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -99,3 +99,10 @@ def redirect(request, *args, **kwargs):
if querystring:
location += querystring
return werkzeug.utils.redirect(location)
+
+
+def redirect_obj(request, obj):
+ """Redirect to the page for the given object.
+
+ Requires obj to have a .url_for_self method."""
+ return redirect(request, location=obj.url_for_self(request.urlgen))
diff --git a/mediagoblin/tools/routing.py b/mediagoblin/tools/routing.py
index 791cd1e6..a15795fe 100644
--- a/mediagoblin/tools/routing.py
+++ b/mediagoblin/tools/routing.py
@@ -16,6 +16,7 @@
import logging
+import six
from werkzeug.routing import Map, Rule
from mediagoblin.tools.common import import_component
@@ -43,7 +44,7 @@ def endpoint_to_controller(rule):
_log.debug('endpoint: {0} view_func: {1}'.format(endpoint, view_func))
# import the endpoint, or if it's already a callable, call that
- if isinstance(view_func, basestring):
+ if isinstance(view_func, six.string_types):
view_func = import_component(view_func)
rule.gmg_controller = view_func
diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py
index 74d811eb..54aeac92 100644
--- a/mediagoblin/tools/template.py
+++ b/mediagoblin/tools/template.py
@@ -14,7 +14,6 @@
# 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 math import ceil
import jinja2
from jinja2.ext import Extension
@@ -27,11 +26,13 @@ from mediagoblin import mg_globals
from mediagoblin import messages
from mediagoblin import _version
from mediagoblin.tools import common
-from mediagoblin.tools.translate import get_gettext_translation
+from mediagoblin.tools.translate import set_thread_locale
from mediagoblin.tools.pluginapi import get_hook_templates
+from mediagoblin.tools.timesince import timesince
from mediagoblin.meddleware.csrf import render_csrf_form_token
+
SETUP_JINJA_ENVS = {}
@@ -42,7 +43,7 @@ def get_jinja_env(template_loader, locale):
(In the future we may have another system for providing theming;
for now this is good enough.)
"""
- mg_globals.thread_scope.translations = get_gettext_translation(locale)
+ set_thread_locale(locale)
# If we have a jinja environment set up with this locale, just
# return that one.
@@ -73,6 +74,9 @@ def get_jinja_env(template_loader, locale):
template_env.filters['urlencode'] = url_quote_plus
+ # add human readable fuzzy date time
+ template_env.globals['timesince'] = timesince
+
# allow for hooking up plugin templates
template_env.globals['get_hook_templates'] = get_hook_templates
diff --git a/mediagoblin/tools/timesince.py b/mediagoblin/tools/timesince.py
new file mode 100644
index 00000000..b761c1be
--- /dev/null
+++ b/mediagoblin/tools/timesince.py
@@ -0,0 +1,95 @@
+# Copyright (c) Django Software Foundation and individual contributors.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# 3. Neither the name of Django nor the names of its contributors may be used
+# to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import unicode_literals
+
+import datetime
+import pytz
+
+from mediagoblin.tools.translate import pass_to_ugettext, lazy_pass_to_ungettext as _
+
+"""UTC time zone as a tzinfo instance."""
+utc = pytz.utc if pytz else UTC()
+
+def is_aware(value):
+ """
+ Determines if a given datetime.datetime is aware.
+
+ The logic is described in Python's docs:
+ http://docs.python.org/library/datetime.html#datetime.tzinfo
+ """
+ return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None
+
+def timesince(d, now=None, reversed=False):
+ """
+ Takes two datetime objects and returns the time between d and now
+ as a nicely formatted string, e.g. "10 minutes". If d occurs after now,
+ then "0 minutes" is returned.
+
+ Units used are years, months, weeks, days, hours, and minutes.
+ Seconds and microseconds are ignored. Up to two adjacent units will be
+ displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are
+ possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not.
+
+ Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
+ """
+ chunks = (
+ (60 * 60 * 24 * 365, lambda n: _('year', 'years', n)),
+ (60 * 60 * 24 * 30, lambda n: _('month', 'months', n)),
+ (60 * 60 * 24 * 7, lambda n : _('week', 'weeks', n)),
+ (60 * 60 * 24, lambda n : _('day', 'days', n)),
+ (60 * 60, lambda n: _('hour', 'hours', n)),
+ (60, lambda n: _('minute', 'minutes', n))
+ )
+ # Convert datetime.date to datetime.datetime for comparison.
+ if not isinstance(d, datetime.datetime):
+ d = datetime.datetime(d.year, d.month, d.day)
+ if now and not isinstance(now, datetime.datetime):
+ now = datetime.datetime(now.year, now.month, now.day)
+
+ if not now:
+ now = datetime.datetime.now(utc if is_aware(d) else None)
+
+ delta = (d - now) if reversed else (now - d)
+ # ignore microseconds
+ since = delta.days * 24 * 60 * 60 + delta.seconds
+ if since <= 0:
+ # d is in the future compared to now, stop processing.
+ return '0 ' + pass_to_ugettext('minutes')
+ for i, (seconds, name) in enumerate(chunks):
+ count = since // seconds
+ if count != 0:
+ break
+ s = pass_to_ugettext('%(number)d %(type)s') % {'number': count, 'type': name(count)}
+ if i + 1 < len(chunks):
+ # Now get the second item
+ seconds2, name2 = chunks[i + 1]
+ count2 = (since - (seconds * count)) // seconds2
+ if count2 != 0:
+ s += pass_to_ugettext(', %(number)d %(type)s') % {'number': count2, 'type': name2(count2)}
+ return s
diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py
index 1d37c4de..b20e57d1 100644
--- a/mediagoblin/tools/translate.py
+++ b/mediagoblin/tools/translate.py
@@ -42,6 +42,22 @@ def set_available_locales():
AVAILABLE_LOCALES = locales
+class ReallyLazyProxy(LazyProxy):
+ """
+ Like LazyProxy, except that it doesn't cache the value ;)
+ """
+ @property
+ def value(self):
+ return self._func(*self._args, **self._kwargs)
+
+ def __repr__(self):
+ return "<%s for %s(%r, %r)>" % (
+ self.__class__.__name__,
+ self._func,
+ self._args,
+ self._kwargs)
+
+
def locale_to_lower_upper(locale):
"""
Take a locale, regardless of style, and format it like "en_US"
@@ -112,6 +128,11 @@ def get_gettext_translation(locale):
return this_gettext
+def set_thread_locale(locale):
+ """Set the current translation for this thread"""
+ mg_globals.thread_scope.translations = get_gettext_translation(locale)
+
+
def pass_to_ugettext(*args, **kwargs):
"""
Pass a translation on to the appropriate ugettext method.
@@ -122,6 +143,16 @@ def pass_to_ugettext(*args, **kwargs):
return mg_globals.thread_scope.translations.ugettext(
*args, **kwargs)
+def pass_to_ungettext(*args, **kwargs):
+ """
+ Pass a translation on to the appropriate ungettext method.
+
+ The reason we can't have a global ugettext method is because
+ mg_globals gets swapped out by the application per-request.
+ """
+ return mg_globals.thread_scope.translations.ungettext(
+ *args, **kwargs)
+
def lazy_pass_to_ugettext(*args, **kwargs):
"""
@@ -134,7 +165,7 @@ def lazy_pass_to_ugettext(*args, **kwargs):
you would want to use the lazy version for _.
"""
- return LazyProxy(pass_to_ugettext, *args, **kwargs)
+ return ReallyLazyProxy(pass_to_ugettext, *args, **kwargs)
def pass_to_ngettext(*args, **kwargs):
@@ -156,7 +187,17 @@ def lazy_pass_to_ngettext(*args, **kwargs):
level but you need it to not translate until the time that it's
used as a string.
"""
- return LazyProxy(pass_to_ngettext, *args, **kwargs)
+ return ReallyLazyProxy(pass_to_ngettext, *args, **kwargs)
+
+def lazy_pass_to_ungettext(*args, **kwargs):
+ """
+ Lazily pass to ungettext.
+
+ This is useful if you have to define a translation on a module
+ level but you need it to not translate until the time that it's
+ used as a string.
+ """
+ return ReallyLazyProxy(pass_to_ungettext, *args, **kwargs)
def fake_ugettext_passthrough(string):
diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py
index e9746a6c..9a193680 100644
--- a/mediagoblin/user_pages/forms.py
+++ b/mediagoblin/user_pages/forms.py
@@ -16,7 +16,7 @@
import wtforms
from wtforms.ext.sqlalchemy.fields import QuerySelectField
-from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
class MediaCommentForm(wtforms.Form):
comment_content = wtforms.TextAreaField(
diff --git a/mediagoblin/user_pages/lib.py b/mediagoblin/user_pages/lib.py
index 8a064a7c..2f47e4b1 100644
--- a/mediagoblin/user_pages/lib.py
+++ b/mediagoblin/user_pages/lib.py
@@ -18,6 +18,8 @@ from mediagoblin.tools.mail import send_email
from mediagoblin.tools.template import render_template
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin import mg_globals
+from mediagoblin.db.base import Session
+from mediagoblin.db.models import CollectionItem
def send_comment_email(user, comment, media, request):
@@ -55,3 +57,21 @@ def send_comment_email(user, comment, media, request):
instance_title=mg_globals.app_config['html_title']) \
+ _('commented on your post'),
rendered_email)
+
+
+def add_media_to_collection(collection, media, note=None, commit=True):
+ collection_item = CollectionItem()
+ collection_item.collection = collection.id
+ collection_item.media_entry = media.id
+ if note:
+ collection_item.note = note
+ Session.add(collection_item)
+
+ collection.items = collection.items + 1
+ Session.add(collection)
+
+ media.collected = media.collected + 1
+ Session.add(media)
+
+ if commit:
+ Session.commit()
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index b3e613c4..738cc054 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -20,11 +20,13 @@ import datetime
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
CollectionItem, User)
-from mediagoblin.tools.response import render_to_response, render_404, redirect
+from mediagoblin.tools.response import render_to_response, render_404, \
+ redirect, redirect_obj
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
+from mediagoblin.user_pages.lib import (send_comment_email,
+ add_media_to_collection)
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
get_media_entry_by_id,
@@ -183,7 +185,7 @@ def media_post_comment(request, media):
media_uploader.wants_comment_notification):
send_comment_email(media_uploader, comment, media, request)
- return redirect(request, location=media.url_for_self(request.urlgen))
+ return redirect_obj(request, media)
@get_media_entry_by_id
@@ -254,25 +256,13 @@ def media_collect(request, media):
_('"%s" already in collection "%s"')
% (media.title, collection.title))
else: # Add item to collection
- collection_item = request.db.CollectionItem()
- collection_item.collection = collection.id
- collection_item.media_entry = media.id
- collection_item.note = form.note.data
- collection_item.save()
-
- collection.items = collection.items + 1
- collection.save()
-
- media.collected = media.collected + 1
- media.save()
+ add_media_to_collection(collection, media, form.note.data)
messages.add_message(request, messages.SUCCESS,
_('"%s" added to collection "%s"')
% (media.title, collection.title))
- return redirect(request, "mediagoblin.user_pages.media_home",
- user=media.get_uploader.username,
- media=media.slug_or_id)
+ return redirect_obj(request, media)
#TODO: Why does @user_may_delete_media not implicate @require_active_login?
@@ -297,8 +287,7 @@ def media_confirm_delete(request, media):
messages.add_message(
request, messages.ERROR,
_("The media was not deleted because you didn't check that you were sure."))
- return redirect(request,
- location=media.url_for_self(request.urlgen))
+ return redirect_obj(request, media)
if ((request.user.is_admin and
request.user.id != media.uploader)):
@@ -384,9 +373,7 @@ def collection_item_confirm_remove(request, collection_item):
request, messages.ERROR,
_("The item was not removed because you didn't check that you were sure."))
- return redirect(request, "mediagoblin.user_pages.user_collection",
- user=username,
- collection=collection.slug)
+ return redirect_obj(request, collection)
if ((request.user.is_admin and
request.user.id != collection_item.in_collection.creator)):
@@ -424,8 +411,8 @@ def collection_confirm_delete(request, collection):
item.delete()
collection.delete()
- messages.add_message(
- request, messages.SUCCESS, _('You deleted the collection "%s"' % collection_title))
+ messages.add_message(request, messages.SUCCESS,
+ _('You deleted the collection "%s"') % collection_title)
return redirect(request, "mediagoblin.user_pages.user_home",
user=username)
@@ -434,9 +421,7 @@ def collection_confirm_delete(request, collection):
request, messages.ERROR,
_("The collection was not deleted because you didn't check that you were sure."))
- return redirect(request, "mediagoblin.user_pages.user_collection",
- user=username,
- collection=collection.slug)
+ return redirect_obj(request, collection)
if ((request.user.is_admin and
request.user.id != collection.creator)):
@@ -543,9 +528,7 @@ def collection_atom_feed(request):
ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
"""
atomlinks = [{
- 'href': request.urlgen(
- 'mediagoblin.user_pages.user_collection',
- qualified=True, user=request.matchdict['user'], collection=collection.slug),
+ 'href': collection.url_for_self(request.urlgen, qualified=True),
'rel': 'alternate',
'type': 'text/html'
}]
diff --git a/setup.py b/setup.py
index a98cd013..312de2f8 100644
--- a/setup.py
+++ b/setup.py
@@ -61,6 +61,8 @@ setup(
'sqlalchemy-migrate',
'mock',
'itsdangerous',
+ 'pytz',
+ 'six',
## This is optional!
# 'translitcodec',
## For now we're expecting that users will install this from