Merge remote-tracking branch 'gsoc2016/Subtitle-1'
authorBoris Bobrov <breton@cynicmansion.ru>
Tue, 10 Jul 2018 16:29:30 +0000 (18:29 +0200)
committerBoris Bobrov <breton@cynicmansion.ru>
Tue, 10 Jul 2018 16:29:30 +0000 (18:29 +0200)
105 files changed:
.gitignore
.gitmodules
AUTHORS
MANIFEST.in
bower.json
docs/source/api/authentication.rst
docs/source/api/objects.rst
docs/source/devel/codebase.rst
docs/source/devel/migrations.rst
docs/source/devel/originaldesigndecisions.rst
docs/source/devel/storage.rst
docs/source/index.rst
docs/source/pluginwriter/api.rst
docs/source/pluginwriter/authhooks.rst
docs/source/pluginwriter/database.rst
docs/source/pluginwriter/hooks.rst
docs/source/pluginwriter/quickstart.rst
docs/source/pluginwriter/tests.rst
docs/source/siteadmin/commandline-upload.rst
docs/source/siteadmin/configuration.rst
docs/source/siteadmin/deploying.rst
docs/source/siteadmin/foreword.rst
docs/source/siteadmin/media-types.rst
docs/source/siteadmin/plugins.rst
docs/source/siteadmin/production-deployments.rst
docs/source/siteadmin/relnotes.rst
docs/source/siteadmin/theming.rst
lazystarter.sh
mediagoblin/api/views.py
mediagoblin/auth/tools.py
mediagoblin/auth/views.py
mediagoblin/config_spec.ini
mediagoblin/db/migration_tools.py
mediagoblin/db/migrations/alembic.ini [moved from alembic.ini with 95% similarity]
mediagoblin/db/migrations/env.py
mediagoblin/db/models.py
mediagoblin/db/models_v0.py [deleted file]
mediagoblin/decorators.py
mediagoblin/edit/views.py
mediagoblin/gmg_commands/addmedia.py
mediagoblin/gmg_commands/batchaddmedia.py
mediagoblin/gmg_commands/dbupdate.py
mediagoblin/i18n/es/mediagoblin.po
mediagoblin/init/config.py
mediagoblin/listings/views.py
mediagoblin/media_types/blog/models.py
mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_admin_dashboard.html
mediagoblin/media_types/blog/templates/mediagoblin/blog/blogpost_draft_view.html
mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html
mediagoblin/media_types/blog/views.py
mediagoblin/media_types/video/models.py
mediagoblin/media_types/video/processing.py
mediagoblin/media_types/video/transcoders.py
mediagoblin/plugins/api/views.py
mediagoblin/plugins/basic_auth/README.rst
mediagoblin/plugins/basic_auth/forms.py
mediagoblin/plugins/flatpagesfile/README.rst
mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html
mediagoblin/plugins/ldap/README.rst
mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html.orig [deleted file]
mediagoblin/plugins/openid/README.rst
mediagoblin/plugins/piwigo/views.py
mediagoblin/plugins/trim_whitespace/README.rst
mediagoblin/processing/task.py
mediagoblin/static/css/audio.css
mediagoblin/static/css/base.css
mediagoblin/static/js/audio.js
mediagoblin/static/js/geolocation-map.js
mediagoblin/static/js/header_dropdown.js
mediagoblin/static/js/post_comment.js [new file with mode: 0644]
mediagoblin/static/js/show_password.js
mediagoblin/submit/lib.py
mediagoblin/submit/views.py
mediagoblin/templates/mediagoblin/api/host-meta.xml [moved from mediagoblin/templates/mediagoblin/federation/host-meta.xml with 100% similarity]
mediagoblin/templates/mediagoblin/api/oob.html
mediagoblin/templates/mediagoblin/auth/register.html
mediagoblin/templates/mediagoblin/media_displays/audio.html
mediagoblin/templates/mediagoblin/moderation/media_panel.html
mediagoblin/templates/mediagoblin/user_pages/blog_media.html
mediagoblin/templates/mediagoblin/user_pages/media.html
mediagoblin/templates/mediagoblin/user_pages/processing_panel.html
mediagoblin/templates/mediagoblin/utils/collection_gallery.html
mediagoblin/templates/mediagoblin/utils/prev_next.html
mediagoblin/templates/mediagoblin/utils/wtforms.html
mediagoblin/tests/.gitignore [new file with mode: 0644]
mediagoblin/tests/fake_carrot_conf_good.ini
mediagoblin/tests/resources.py
mediagoblin/tests/test_api.py
mediagoblin/tests/test_auth.py
mediagoblin/tests/test_celery_setup.py
mediagoblin/tests/test_config.py
mediagoblin/tests/test_exif.py
mediagoblin/tests/test_exif/bad-gps.jpg [new file with mode: 0644]
mediagoblin/tests/test_tools.py
mediagoblin/tests/test_util.py
mediagoblin/tests/tools.py
mediagoblin/themes/airy/assets/css/airy.css
mediagoblin/tools/exif.py
mediagoblin/tools/licenses.py
mediagoblin/tools/mail.py
mediagoblin/tools/pagination.py
mediagoblin/tools/theme.py
mediagoblin/user_pages/views.py
setup.py
tox.ini

index c4a1497f20ec4cb8ed2286c730ce8ca73fe2e949..4eb61d664fb3a52a4b90ae829af37a39809ef5c5 100644 (file)
@@ -50,6 +50,7 @@
 *~
 *.swp
 *.mo
+*.patch
 
 # The legacy of buildout
 .installed.cfg
index 562ad4e40af9e8abc4a712f7d899464ea12b7a95..d9171995c3180111d242facad773a21f413f5fe4 100644 (file)
@@ -1,12 +1,12 @@
 [submodule "pdf.js"]
        path = pdf.js
-       url = git://github.com/mozilla/pdf.js.git
+       url = https://github.com/mozilla/pdf.js.git
 [submodule "extlib/pdf.js"]
        path = extlib/pdf.js
-       url = git://github.com/mozilla/pdf.js.git
+       url = https://github.com/mozilla/pdf.js.git
 [submodule "extlib/skeleton"]
        path = extlib/skeleton
-       url = git://github.com/dhg/Skeleton.git
+       url = https://github.com/dhg/Skeleton.git
 [submodule "extlib/sandyseventiesspeedboat"]
        path = extlib/sandyseventiesspeedboat
        url = https://github.com/jpope777/sandyseventiesspeedboat-mg.git
diff --git a/AUTHORS b/AUTHORS
index f7ee02db5e354fe7402eb64c5e730d1ef6feaaa5..1808fa4a25e435d6511212ea8045feae0b11d5cc 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,6 +13,7 @@ Thank you!
 * Alejandro Villanueva
 * Aleksandar Micovic
 * Aleksej Serdjukov
+* Alexandre Franke
 * Alon Levy
 * Alex Camelio
 * Amirouche Boubekki
@@ -31,10 +32,12 @@ Thank you!
 * Corey Farwell
 * Chris Moylan
 * Christopher Allan Webber
+* chrysn
 * Dan Callahan
 * David Thompson
 * Daniel Krol
 * Daniel Neel
+* Matthew Deal
 * Deb Nicholson
 * Devan Goodwin
 * Derek Moore
@@ -56,10 +59,13 @@ Thank you!
 * Jiyda Mint Moussa
 * Jim Campbell
 * Joar Wandborg
+* Jonathan Sandoval
 * Jorge Araya Navarro
 * Josephine Bartholoma
+* Josh Crompton
 * Karen Rustad
 * Kenneth Dombrowski
+* Kesara Rathnayake
 * Kushal Kumaran
 * Kuno Woudt
 * Laura Arjona
@@ -76,6 +82,7 @@ Thank you!
 * Matt Lee
 * Matt Molyneaux
 * Meg Ford
+* mi
 * Michele Azzolari
 * Mike Linksvayer
 * Natalie Foust-Pilcher
@@ -85,8 +92,10 @@ Thank you!
 * Pablo J. Urbano Santos
 * Praveen Kumar
 * Rasmus Larsson
+* Robert Smith
 * Rodney Ewing
 * Rodrigo Rodrigues da Silva
+* Romain Porte
 * Runar Petursson
 * Sacha De'Angeli
 * Sam Clegg
@@ -98,11 +107,13 @@ Thank you!
 * Shawn Khan
 * Simon Fondrie-Teitler
 * Stefano Zacchiroli
+* Stéphane Péchard
 * thallian
 * Tiberiu C. Turbureanu
 * Tom Fay
 * Tran Thanh Bao
 * Tryggvi Björgvinsson
+* Vijeth Aradhya
 * Will Kahn-Greene
 * 宋文武
 
index 675c80810b3eecba307d7f0fa47932243fcac64f..dd2fd23ab71387c1e9f3cfe1ab22afcbc07725ab 100644 (file)
@@ -3,8 +3,17 @@ recursive-include mediagoblin *.js *.css *.png *.svg *.ico
 recursive-include mediagoblin *.ini
 recursive-include mediagoblin *.html *.txt
 recursive-include docs *.rst *.html
+recursive-include mediagoblin/db/migrations/versions *.py
+recursive-include mediagoblin/media_types/ascii/migrations *.py
+recursive-include mediagoblin/media_types/audio/migrations *.py
+recursive-include mediagoblin/media_types/blog/migrations *.py
+recursive-include mediagoblin/media_types/image/migrations *.py
+recursive-include mediagoblin/media_types/pdf/migrations *.py
+recursive-include mediagoblin/media_types/stl/migrations *.py
+recursive-include mediagoblin/media_types/video/migrations *.py
 include mediagoblin.example.ini mediagoblin/config_spec.ini paste.ini
 include mediagoblin/config_spec.ini
+include mediagoblin/db/migrations/alembic.ini
 graft extlib
 graft licenses
 graft devtools
index 5f7fff5d0fa2f006c21280380c65915c2bdf79a4..993921bdc5fbb3e4e46575317c949aa145632139 100644 (file)
@@ -14,7 +14,6 @@
   "dependencies": {
     "jquery": "~2.1.3",
     "video.js": "~4.11.4",
-    "leaflet": "~0.7.3",
-    "tinymce": "~4.1.7"
+    "leaflet": "~0.7.3"
   }
 }
index 1d52d300185d2b7ead0a3df2a0b36c820145c8cd..2db4a7bdaadd9a012396d6814500b2cb9ec2e32f 100644 (file)
@@ -144,7 +144,7 @@ Only set client_id for update.
 Only set client_secret for update.
     This should only be given when you update.
 
-Logo URL <url> is not a valid URL
+Logo URL <URL> is not a valid URL
     This is when the URL specified did not meet the validation.
 
 contacts must be a string of space-separated email addresses.
@@ -171,7 +171,7 @@ We are not using OAuth2 as we want to stay completely compatible with pump.io.
 Endpoints
 ---------
 
-These are the endpoints you need to use for the oauth requests:
+These are the endpoints you need to use for the OAuth requests:
 
 `/oauth/request_token` is for getting the request token.
 
index 4c19961621a4d38673b45205e3bda405c91d4294..854febe1aacc7fa032ad8df5190cae6a9ae8bd49 100644 (file)
@@ -19,7 +19,7 @@ Objects
 
 Using any the APIs mentioned in this document you will required
 :doc:`authentication`. There are many ways to interact with objects, some of
-which aren't supported yet by mediagoblin such as liking or sharing objects
+which aren't supported yet by MediaGoblin such as liking or sharing objects
 however you can interact with them by updating them, deleting them and
 commenting on them.
 
@@ -49,16 +49,16 @@ timestamps or any other data the server creates. My activity comment is::
 
 This should be posted to the users feed (outbox) which you can find out about
 :doc:`activities`. You will get back the full activity containing all of
-attributes including ID, urls, etc.
+attributes including ID, URLs, etc.
 
 Posting Media
 ~~~~~~~~~~~~~
 
 Posting media is a special case from posting all other objects. This is because
 you need to submit more than just the JSON image representation, you need to
-actually subject the image itself. There is also strange behavour around media
+actually subject the image itself. There is also strange behavior around media
 postings where if you want to give the media you're posting a title or
-description you need to peform an update too. A full media posting in order of
+description you need to perform an update too. A full media posting in order of
 steps to do is as follows:
 
 1) Uploads the data to the server
@@ -74,7 +74,7 @@ To upload media you should use the URL `/api/user/<username>/uploads`.
 A POST request should be made to the media upload URL submitting at least two
 headers:
 
-* `Content-Type` - This being a valid mimetype for the media.
+* `Content-Type` - This being a valid MIME type for the media.
 * `Content-Length` - size in bytes of the media.
 
 The media data should be submitted as POST data to the image upload URL.
@@ -196,8 +196,8 @@ You should get the full delete activity in response.
 
 .. warning::
     While deletion works, currently because of the way deletion is implemented
-    deletion either via the API or the webUI causes any activities to be broken
-    and will be skipped and unaccessible. A migration to remove the broken
+    deletion either via the API or the web UI causes any activities to be broken
+    and will be skipped and inaccessible. A migration to remove the broken
     activities will come in a future release when soft-deletion has been
     implemented.
 
index 122a329732e1760037bc16938d5e52be96e46945..068c18734f3ebc4a1efe7605cd380a105cdafed4 100644 (file)
@@ -44,7 +44,7 @@ development.
 What's where
 ============
 
-After you've run checked out mediagoblin and followed the virtualenv
+After you've run checked out MediaGoblin and followed the virtualenv
 instantiation instructions, you're faced with the following directory
 tree::
 
@@ -86,8 +86,8 @@ As you can see, all the code for GNU MediaGoblin is in the
 
 Here are some interesting files and what they do:
 
-:routing.py: maps url paths to views
-:views.py:   views handle http requests
+:routing.py: maps URL paths to views
+:views.py:   views handle HTTP requests
 :forms.py:   wtforms stuff for this submodule
 
 You'll notice that there are several sub-directories: tests,
@@ -97,7 +97,7 @@ templates, auth, submit, ...
 
 ``templates`` holds all the templates for the output.
 
-``auth`` and ``submit`` are modules that enacpsulate authentication
+``auth`` and ``submit`` are modules that encapsulate authentication
 and media item submission.  If you look in these directories, you'll
 see they have their own ``routing.py``, ``view.py``, and forms.py in
 addition to some other code.
@@ -123,15 +123,15 @@ Software Stack
     for unit tests
 
   * `virtualenv <http://www.virtualenv.org/>`_: for setting up an
-    isolated environment to keep mediagoblin and related packages
+    isolated environment to keep MediaGoblin and related packages
     (potentially not required if MediaGoblin is packaged for your
     distro)
 
 * Data storage
 
   * `SQLAlchemy <http://sqlalchemy.org/>`_: SQL ORM and database
-    interaction library for Python. Currently we support sqlite and
-    postgress as backends.
+    interaction library for Python. Currently we support SQLite and
+    PostgreSQL as backends.
 
 * Web application
 
@@ -158,10 +158,10 @@ Software Stack
 
   * `Markdown (for python) <http://pypi.python.org/pypi/Markdown>`_:
     implementation of `Markdown <http://daringfireball.net/projects/markdown/>`_
-    text-to-html tool to make it easy for people to write richtext
+    text-to-html tool to make it easy for people to write rich text
     comments, descriptions, and etc.
 
-  * `lxml <http://lxml.de/>`_: nice xml and html processing for
+  * `lxml <http://lxml.de/>`_: nice XML and HTML processing for
     python.
 
 * Media processing libraries
@@ -174,10 +174,10 @@ Software Stack
     future, probably audio too.
 
   * `chardet <http://pypi.python.org/pypi/chardet>`_: (Optional, for
-    ascii art hosting sites only)  Used to make ascii art thumbnails.
+    ASCII art hosting sites only)  Used to make ASCII art thumbnails.
 
 * Front end
 
-  * `JQuery <http://jquery.com/>`_: for groovy JavaScript things
+  * `jQuery <http://jquery.com/>`_: for groovy JavaScript things
 
 
index 5e0a6beeebaaa99eb306b46424594f65a744f17f..ec0f7d29322e1f24b35a1a2862ccec76edea2798 100644 (file)
@@ -31,7 +31,7 @@ There's a few things you need to know:
 - We use `Alembic <https://bitbucket.org/zzzeek/alembic>`_ to run
   migrations.  We also make heavy use of the
   `branching model <http://alembic.readthedocs.org/en/latest/branches.html>`_
-  for our plugins.  Every plugin gets its own migration branc.
+  for our plugins.  Every plugin gets its own migration branch.
 - We used to use `sqlalchemy-migrate
   <http://code.google.com/p/sqlalchemy-migrate/>`_.
   See `their docs <https://sqlalchemy-migrate.readthedocs.org/>`_.
@@ -41,7 +41,7 @@ There's a few things you need to know:
 - SQLAlchemy has two parts to it, the ORM and the "core" interface.
   We DO NOT use the ORM when running migrations.  Think about it: the
   ORM is set up with an expectation that the models already reflect a
-  certain pattern.  But if a person is moving from their old patern
+  certain pattern.  But if a person is moving from their old pattern
   and are running tools to *get to* the current pattern, of course
   their current database structure doesn't match the state of the ORM!
   Anyway, Alembic has its own conventions for migrations; follow those.
index 2843870c8e179cdc7932566402289a8f73c3ce15..b60db77667e2c57af6f2c13dc9c4feba50e2e85a 100644 (file)
@@ -38,7 +38,7 @@ Chris and Will on "Why GNU MediaGoblin":
     .. figure:: ../_static/goblin.png
        :alt: Cute goblin with a beret.
 
-       *Figure 1: Cute goblin with a beret.  llustrated by Chris
+       *Figure 1: Cute goblin with a beret.  Illustrated by Chris
        Webber*
 
     .. figure:: ../_static/snugglygoblin.png
@@ -61,7 +61,7 @@ Chris and Will on "Why GNU MediaGoblin":
     1. "GNU MediaGoblin" is the name we're going to use in all official
        capacities: web site, documentation, press releases, ...
 
-    2. In casual conversation, it's ok to use more casual names.
+    2. In casual conversation, it's OK to use more casual names.
 
     3. If you're writing about the project, we ask that you call it GNU 
        MediaGoblin.
@@ -113,7 +113,7 @@ Why WSGI Minimalism
 Chris Webber on "Why WSGI Minimalism":
 
     If you notice in the technology list I list a lot of components
-    that are very "django-like", but not actually `Django`_
+    that are very "Django-like", but not actually `Django`_
     components.  What can I say, I really like a lot of the ideas in
     Django!  Which leads to the question: why not just use Django?
 
@@ -176,7 +176,7 @@ Chris Webber on "Why MongoDB":
     ideal universe where everyone ran servers out of their own
     housing.  As a memory-mapped database, MongoDB is pretty hungry,
     so actually I spent a lot of time debating whether the inability
-    to scale down as nicely as something like SQL has with sqlite
+    to scale down as nicely as something like SQL has with SQLite
     meant that it was out.
 
     But I decided in the end that I really want MongoDB, not for
@@ -199,7 +199,7 @@ Chris Webber on "Why MongoDB":
 
 
     Being able to just dump media-specific information in a media_data
-    hashtable is pretty great, and even better is having a plugin
+    hash table is pretty great, and even better is having a plugin
     system where you can just let plugins have their own entire
     key-value space cleanly inside the document that doesn't interfere
     with anyone else's stuff.  If we were to let plugins to deposit
index 215f95796b8745c33fadd2eb70cf2c539d99c4ed..46e163de3670ba7a664182ffd87c61aeb32f1eed 100644 (file)
@@ -21,7 +21,7 @@ are:
 
 + **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
+  implementation, and is for stuff that's intended to be seen by
   site visitors.
 
 The workbench
@@ -43,11 +43,9 @@ Static assets / staticdirect
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 On top of all that, there is some static media that comes bundled with your
-application. This stuff is kept in
+application. This stuff is kept in ``mediagoblin/static/``.
 
-   mediagoblin/static/
-
-These files are for mediagoblin base assets. Things like the CSS files, 
+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 
@@ -55,7 +53,7 @@ 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:
+mediagoblin/templates/mediagoblin/base.html main template::
 
     <link rel="stylesheet" type="text/css" 
         href="Template:Request.staticdirect('/css/extlib/text.css')"/>
@@ -76,9 +74,7 @@ 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
+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
@@ -95,12 +91,12 @@ 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 
+[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. 
+Beowulf cluster of radioactive monkey brains, whatever. 
 
 Writing code to store stuff
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -112,10 +108,10 @@ 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:
+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']
+    ['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.
index a4fd24559f1feabe051906f39eb4c7a06229011a..fbc57154187dff4cc8a1bddac56c75bdf7d88e77 100644 (file)
@@ -20,7 +20,7 @@ GNU MediaGoblin is a platform for sharing photos, video and other media
 in an environment that respects our freedom and independence.
 
 This is a Free Software project. It is built by contributors for all
-to use and enjoy. If you're intrested in contributing, see `the wiki
+to use and enjoy. If you're interested in contributing, see `the wiki
 <http://wiki.mediagoblin.org/>`_ which has pages that talk about the
 ways someone can contribute.
 
index a220eac102f0e9ce7e7dc164d17fc98cc2bc8227..03761a38c2877cbbb10ede0e26fd324ca79ba7db 100644 (file)
@@ -83,7 +83,7 @@ can use a shortcut to get your plugin's config section::
   >>> floobie_dir = floobie_config['floobie_dir']
   
 A tip: you have access to the `%(here)s` variable in your config,
-which is the directory that the user's mediagoblin config is running
+which is the directory that the user's MediaGoblin config is running
 out of.  So for example, your plugin may need a "floobie" directory to
 store floobs in.  You could give them a reasonable default that makes
 use of the default `user_dev` location, but allow users to override
@@ -92,7 +92,7 @@ it, like so::
   [plugin_spec]
   floobie_dir = string(default="%(here)s/user_dev/floobs/")
 
-Note, this is relative to the user's mediagoblin config directory,
+Note, this is relative to the user's MediaGoblin config directory,
 *not* your plugin directory!
 
 
@@ -178,7 +178,7 @@ Adding static resources
 -----------------------
 
 It's possible to add static resources for your plugin.  Say your
-plugin needs some special javascript and images... how to provide
+plugin needs some special JavaScript and images... how to provide
 them?  Then how to access them?  MediaGoblin has a way!
 
 
@@ -230,7 +230,7 @@ the MediaGoblin repository.
 WTForms hooks
 +++++++++++++
 
-We haven't totally settled on a way to tranform wtforms form objects,
+We haven't totally settled on a way to transform wtforms form objects,
 but here's one way.  In your view::
 
   from mediagoblin.foo.forms import SomeForm
index 9721d729833206c3435c2dc49cdc7a618f5d3ccd..66d9b9da14c9b99e04716c6aedf2ec86e5ea9a74 100644 (file)
@@ -44,7 +44,7 @@ This hook is called in ``mediagoblin.auth.views`` in both the ``login`` and
 if :ref:`basic_auth-chapter` is not enabled, the user will be redirected to the
 correct login and registration views for your plugin.
 
-The code assumes that it can generate a valid url given
+The code assumes that it can generate a valid URL given
 ``mediagoblin.plugins.{{ your_plugin_here }}.login`` and
 ``mediagoblin.plugins.{{ your_plugin_here }}.register``. This is only needed if
 you will not be using the ``login`` and ``register`` views in 
@@ -82,5 +82,5 @@ the ``stored_hash`` and return either ``True`` or ``False``.
 
 This hook is called in ``mediagoblin.auth.tools.check_login_simple``. It is
 called if a user is not found and should do something that takes the same amount
-of time as your ``check_password`` function. This is to help prevent timining
+of time as your ``check_password`` function. This is to help prevent timing
 attacks.
index c0698a240f3b579ae43e8df4fb48d2d76864abd3..90c2cee3c995a32bfbdeca836d892776f7847777 100644 (file)
@@ -24,7 +24,7 @@ Accessing Existing Data
 =======================
 
 If your plugin wants to access existing data, this is quite
-straight forward. Just import the appropiate models and use
+straight forward. Just import the appropriate models and use
 the full power of SQLAlchemy. Take a look at the (upcoming)
 database section in the Developer's Chapter.
 
@@ -134,13 +134,13 @@ plugin's models.py and then run::
   ./bin/gmg alembic --with-plugins revision \
        --head REVISION_HERE \
        --autogenerate \
-       -m "YOUR_PLUGIN_NAME: Change explaination here."
+       -m "YOUR_PLUGIN_NAME: Change explanation here."
 
 Once again, this will dump the migration into the wrong place, so move
 to your plugin's migrations directory.  Open the file, adjust
 accordingly, and read carefully!  Now you should also test your
-migration with some real data.  Be sure to test it both on sqlite
-*AND* on postgresql!
+migration with some real data.  Be sure to test it both on SQLite
+*AND* on PostgreSQL!
 
 One last *very critical* note: you must never, never modify core
 tables with your plugin.  To do that is to put you and all your users
index 4aa062e86171e52c926fefb9f874396f8565a47d..66fe3b4da4572451d2d2f556815e1cd3ad7cc928 100644 (file)
@@ -28,7 +28,7 @@ What hooks are available?
 This hook is used by ``add_media_to_collection``
 in ``mediagoblin.user_pages.lib``.
 It gets a ``CollectionItem`` as its argument.
-It's the newly created item just before getting commited.
+It's the newly created item just before getting committed.
 So the item can be modified by the hook, if needed.
 Changing the session regarding this item is currently
 undefined behaviour, as the SQL Session might contain other
index 6d45ea36104ad5fe1381f5b496cef43ff5005fa1..212c91ad11e6cfa8caddda0039b8c09ff9bf40b1 100644 (file)
@@ -111,7 +111,7 @@ The code for ``__init__.py`` looks like this:
    :emphasize-lines: 12,23
 
     import logging
-    from mediagoblin.tools.pluginapi import Plugin, get_config
+    from mediagoblin.tools.pluginapi import PluginManager, get_config
 
 
     # This creates a logger that you can use to log information to
index fe99688f6cd6662cf4cc7e82542e146b8c00a11f..560a8899436ebc395ce06400b9f73753262fbc14 100644 (file)
@@ -19,7 +19,7 @@ Here's a brief guide to writing unit tests for plugins.  However, it
 isn't really ideal.  It also hasn't been well tested... yes, there's
 some irony there :)
 
-Some notes: we're using py.test and webtest for unit testing stuff.
+Some notes: we're using py.test and WebTest for unit testing stuff.
 Keep that in mind.
 
 My suggestion is to mime the behavior of `mediagoblin/tests/` and put
@@ -44,7 +44,7 @@ In any test module in your tests directory you can then do::
       # real code goes here
       pass
 
-And you'll get a mediagoblin application wrapped in webtest passed in
+And you'll get a MediaGoblin application wrapped in WebTest passed in
 to your environment.
 
 If your plugin needs to define multiple configuration setups, you can
@@ -52,8 +52,8 @@ actually set up multiple fixtures very easily for this.  You can just
 set up multiple fixtures with different names that point to different
 configs and pass them in as that named argument.
 
-To run the tests, from mediagoblin's directory (make sure that your
-plugin has been added to your mediagoblin checkout's virtualenv!) do::
+To run the tests, from MediaGoblin's directory (make sure that your
+plugin has been added to your MediaGoblin checkout's virtualenv!) do::
 
   ./runtests.sh /path/to/myplugin/tests/
 
index 690983124ab24669e102b3f2826b4c7382631664..756f5fa83a93f59084599e7f04a25b55d474e7c5 100644 (file)
@@ -37,6 +37,7 @@ Here's a longer example that makes use of more options::
   ./bin/gmg addmedia aveyah awesome_spaceship.png \
       --title "My awesome spaceship" \
       --description "Flying my awesome spaceship, since I'm an awesome pilot" \
+      --collection-slug i-m-an-awesome-pilot \
       --license "http://creativecommons.org/licenses/by-sa/3.0/" \
       --tags "spaceships, pilots, awesome" \
       --slug "awesome-spaceship"
@@ -57,7 +58,7 @@ it is a bit more complex.
 This is an example of what a script may look like. The important part here is
 that you have to create the 'metadata.csv' file.::
 
-  media:location,dcterms:title,dcterms:creator,dcterms:type
+  location,dcterms:title,dcterms:creator,dcterms:type
   "http://www.example.net/path/to/nap.png","Goblin taking a nap",,"Image"
   "http://www.example.net/path/to/snore.ogg","Goblin Snoring","Me","Audio"
 
@@ -65,14 +66,14 @@ The above is an example of a very simple metadata.csv file. The batchaddmedia
 script would read this and attempt to upload only two pieces of media, and would
 be able to automatically name them appropriately.
 
-The csv file
+The CSV file
 ============
 The location column
 -------------------
 The location column is the one column that is absolutely necessary for
 uploading your media. This gives a path to each piece of media you upload. This
 can either a path to a local file or a direct link to remote media (with the
-link in http format). As you can see in the example above the (fake) media was
+link in HTTP format). As you can see in the example above the (fake) media was
 stored remotely on "www.example.net".
 
 Other internal nodes
@@ -83,9 +84,10 @@ provide default information for your media entry, but as you'll see below, it's
 just as easy to provide this information through the correct metadata columns.
 
 - **id** is used to identify the media entry to the user in case of an error in the batchaddmedia script.
-- **license** is used to set a license for your piece a media for mediagoblin's use. This must be a URI.
-- **title** will set the title displayed to mediagoblin users.
+- **license** is used to set a license for your piece a media for MediaGoblin's use. This must be a URI.
+- **title** will set the title displayed to MediaGoblin users.
 - **description** will set a description of your media.
+- **collection-slug** will add the media to a collection, if a collection with the given slug exists.
 
 Metadata columns
 ----------------
@@ -106,7 +108,7 @@ information of uploaded media entries.
 - **dc:title** sets a title for your media entry.
 - **dc:description** sets a description of your media entry.
 
-If both a metadata column and an internal node for the title are provided, mediagoblin
+If both a metadata column and an internal node for the title are provided, MediaGoblin
 will use the internal node as the media entry's display name. This makes it so
 that if you want to display a piece of media with a different title
 than the one provided in its metadata, you can just provide different data for
index dd0d6cd9747f144cdd339c871f351e9598e85232..1b8dc9bb3be4b2d16ebf59f65265958f2fe2ff51 100644 (file)
@@ -39,12 +39,12 @@ paste.ini
   <http://pythonpaste.org/script/>`_).  It also sets up some
   middleware that you can mostly ignore, except to configure
   sessions... more on that later.  If you are adding a different
-  Python server other than fastcgi / plain HTTP, you might configure
+  Python server other than FastCGI / plain HTTP, you might configure
   it here.  You probably won't need to change this file very much.
 
 
 There's one more file that you certainly won't change unless you're
-making coding contributions to mediagoblin, but which can be useful to
+making coding contributions to MediaGoblin, but which can be useful to
 read and reference:
 
 mediagoblin/config_spec.ini
@@ -54,30 +54,6 @@ mediagoblin/config_spec.ini
   option that we didn't tell you about. :)
 
 
-Making local copies
-===================
-
-Let's assume you're doing the virtualenv setup described elsewhere in this
-manual, and you need to make local tweaks to the config files. How do you do 
-that? Let's see.
-
-To make changes to mediagoblin.ini ::
-
-    cp mediagoblin.ini mediagoblin_local.ini
-
-To make changes to paste.ini ::
-
-    cp paste.ini paste_local.ini
-
-From here you should be able to make direct adjustments to the files,
-and most of the commands described elsewhere in this manual will "notice"
-your local config files and use those instead of the non-local version.
-
-.. note::
-
-   Note that all commands provide a way to pass in a specific config
-   file also, usually by a ``-cf`` flag.
-
 
 Common changes
 ==============
@@ -128,7 +104,7 @@ If you use ``lazyserver.sh`` you need to change the ``paste.ini`` file::
     [app:mediagoblin]
     /mgoblin_media = /var/mediagoblin/user_data
 
-If you use nginx you need to change the config::
+If you use Nginx you need to change the config::
 
      # Instance specific media:
      location /mgoblin_media/ {
index 47901da951590b13081dce24fa3e1d83c82efd44..42fe177241cef44c33f49d111f9e5e133a3d8e7f 100644 (file)
@@ -25,7 +25,7 @@ will take you step-by-step through setting up your own instance of MediaGoblin.
 Of course, when it comes to setting up web applications like MediaGoblin,
 there's an almost infinite way to deploy things, so for now, we'll keep it
 simple with some assumptions. We recommend a setup that combines MediaGoblin +
-virtualenv + fastcgi + nginx on a .deb- or .rpm-based GNU/Linux distro.
+virtualenv + FastCGI + Nginx on a .deb- or .rpm-based GNU/Linux distro.
 
 Other deployment options (e.g., deploying on FreeBSD, Arch Linux, using
 Apache, etc.) are possible, though! If you'd prefer a different deployment
@@ -65,25 +65,30 @@ MediaGoblin has the following core dependencies:
 - `virtualenv <http://www.virtualenv.org/>`_
 - `nodejs <https://nodejs.org>`_
 
-On a DEB-based system (e.g Debian, gNewSense, Trisquel, *buntu, and
+On a DEB-based system (e.g Debian, gNewSense, Trisquel, \*buntu, and
 derivatives) issue the following command::
 
     sudo apt-get install git-core python python-dev python-lxml \
         python-imaging python-virtualenv npm nodejs-legacy automake \
-        nginx
+        nginx rabbitmq-server
 
 On a RPM-based system (e.g. Fedora, RedHat, and derivatives) issue the
 following command::
 
     sudo yum install python-paste-deploy python-paste-script \
         git-core python python-devel python-lxml python-imaging \
-        python-virtualenv npm automake nginx
+        python-virtualenv npm automake nginx rabbitmq-server
 
 (Note: MediaGoblin now officially supports Python 3.  You may instead
 substitute from "python" to "python3" for most package names in the
 Debian instructions and this should cover dependency installation.
 These instructions have not yet been tested on Fedora.)
 
+(Note: you might have to include additional repositories to a RPM-
+based system, because rabbitmq-server might be not included in
+official repositories. As an alternative, you can try installing
+redis-server and configure it as celery broker)
+
 Configure PostgreSQL
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -94,7 +99,7 @@ Configure PostgreSQL
 
    For medium to large deployments we recommend PostgreSQL.
 
-   If you don't want/need postgres, skip this section.
+   If you don't want/need PostgreSQL, skip this section.
 
 These are the packages needed for Debian Jessie (stable)::
 
@@ -105,7 +110,7 @@ These are the packages needed for an RPM-based system::
     sudo yum install postgresql postgresql-server python-psycopg2
 
 An rpm-based system also requires that you initialize and start the
-PostgresSQL database with a few commands. The following commands are
+PostgreSQL database with a few commands. The following commands are
 not needed on a Debian-based platform, however::
 
     sudo /usr/bin/postgresql-setup initdb
@@ -113,8 +118,8 @@ not needed on a Debian-based platform, however::
     sudo systemctl start postgresql
 
 The installation process will create a new *system* user named ``postgres``,
-which will have privilegies sufficient to manage the database. We will create a
-new database user with restricted privilegies and a new database owned by our
+which will have privileges sufficient to manage the database. We will create a
+new database user with restricted privileges and a new database owned by our
 restricted database user for our MediaGoblin instance.
 
 In this example, the database user will be ``mediagoblin`` and the database
@@ -200,10 +205,10 @@ Create a MediaGoblin Directory
 You should create a working directory for MediaGoblin. This document
 assumes your local git repository will be located at 
 ``/srv/mediagoblin.example.org/mediagoblin/``.
-Substitute your prefered local deployment path as needed.
+Substitute your preferred local deployment path as needed.
 
 Setting up the working directory requires that we first create the directory
-with elevated priviledges, and then assign ownership of the directory
+with elevated privileges, and then assign ownership of the directory
 to the unprivileged system account.
 
 To do this, enter the following command, changing the defaults to suit your
@@ -235,7 +240,7 @@ Change to the MediaGoblin directory that you just created::
 
 Clone the MediaGoblin repository and set up the git submodules::
 
-    $ git clone git://git.savannah.gnu.org/mediagoblin.git -b stable
+    $ git clone https://git.savannah.gnu.org/git/mediagoblin.git -b stable
     $ cd mediagoblin
     $ git submodule init && git submodule update
 
@@ -245,7 +250,7 @@ Clone the MediaGoblin repository and set up the git submodules::
    gitorious.org shut down, we had to move.  We are presently on
    Savannah.  You may need to update your git repository location::
 
-    $ git remote set-url origin git://git.savannah.gnu.org/mediagoblin.git
+    $ git remote set-url origin https://git.savannah.gnu.org/git/mediagoblin.git
 
 Set up the hacking environment::
 
@@ -314,25 +319,28 @@ Edit site configuration
 ~~~~~~~~~~~~~~~~~~~~~~~
 
 A few basic properties must be set before MediaGoblin will work. First
-make a copy of ``mediagoblin.ini`` and ``paste.ini`` for editing so the original
+make a copy of ``paste.ini`` for editing so the original
 config files aren't lost (you likely won't need to edit the paste configuration,
 but we'll make a local copy of it just in case)::
 
-    $ cp -av mediagoblin.ini mediagoblin_local.ini && cp -av paste.ini paste_local.ini
+    $ cp -av paste.ini paste_local.ini
+
+``mediagoblin.ini`` does not need to be copied, because original config is
+stored in ``mediagoblin.example.ini``.
 
-Then edit mediagoblin_local.ini:
+Then edit ``mediagoblin.ini``:
  - Set ``email_sender_address`` to the address you wish to be used as
    the sender for system-generated emails
  - Edit ``direct_remote_path``, ``base_dir``, and ``base_url`` if
    your mediagoblin directory is not the root directory of your
-   vhost.
+   site.
 
 
 Configure MediaGoblin to use the PostgreSQL database
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-If you are using postgres, edit the ``[mediagoblin]`` section in your
-``mediagoblin_local.ini`` and put in::
+If you are using PostgreSQL, edit the ``[mediagoblin]`` section in your
+``mediagoblin.ini`` and put in::
 
     sql_engine = postgresql:///mediagoblin
 
@@ -361,7 +369,7 @@ test the deployment with the following command::
 You should be able to connect to the machine on port 6543 in your
 browser to confirm that the service is operable.
 
-The next series of commands will need to be run as a priviledged user. Type
+The next series of commands will need to be run as a privileged user. Type
 exit to return to the root/sudo account.::
 
     exit
@@ -372,9 +380,9 @@ exit to return to the root/sudo account.::
 FastCGI and nginx
 ~~~~~~~~~~~~~~~~~
 
-This configuration example will use nginx, however, you may
+This configuration example will use Nginx, however, you may
 use any webserver of your choice as long as it supports the FastCGI
-protocol. If you do not already have a web server, consider nginx, as
+protocol. If you do not already have a web server, consider Nginx, as
 the configuration files may be more clear than the
 alternatives.
 
@@ -397,7 +405,7 @@ following commands::
     sudo systemctl enable nginx
 
 You can modify these commands and locations depending on your preferences and
-the existing configuration of your nginx instance. The contents of
+the existing configuration of your Nginx instance. The contents of
 this ``nginx.conf`` file should be modeled on the following::
 
     server {
@@ -457,7 +465,7 @@ this ``nginx.conf`` file should be modeled on the following::
         fastcgi_pass 127.0.0.1:26543;
         include /etc/nginx/fastcgi_params;
 
-        # our understanding vs nginx's handling of script_name vs
+        # our understanding vs Nginx's handling of script_name vs
         # path_info don't match :)
         fastcgi_param PATH_INFO $fastcgi_script_name;
         fastcgi_param SCRIPT_NAME "";
@@ -481,8 +489,8 @@ test to ensure that this configuration works::
 
     nginx -t
 
-If you encounter any errors, review your nginx configuration files, and try to
-resolve them. If you do not encounter any errors, you can start your nginx
+If you encounter any errors, review your Nginx configuration files, and try to
+resolve them. If you do not encounter any errors, you can start your Nginx
 server with one of the following commands (depending on your environment)::
 
     sudo /etc/init.d/nginx restart
@@ -529,7 +537,7 @@ a) Disable registration on your instance and just make
      [mediagoblin]
      allow_registration = false
 
-b) Enable a captcha plugin.  But unfortunately, though some captcha
+b) Enable a CAPTCHA plugin.  But unfortunately, though some CAPTCHA
    plugins exist, for various reasons we do not have any general
    recommendations we can make at this point.
 
index 4c425f8df209724516c5e646eb74ccabc2a51685..24282f9692de549452874687ccb239c670049d0e 100644 (file)
@@ -34,13 +34,14 @@ Improving the Site Administrator's Guide
 There are a few ways---please pick whichever method is convenient for
 you!
 
-1. Write up a bug report in the bug tracker
+1. Write up a bug report in the `bug tracker`_.
 2. Tell someone on IRC ``#mediagoblin`` on Freenode.
-3. Write an email to the devel mailing list.
+3. Write an email to the `devel mailing list`_.
 
-Information about the bugtracker, IRC and the mailing list is all on
-the `join page`_.
+More information about contributing is available on the `join page`_.
 
+.. _bug tracker: https://issues.mediagoblin.org/
+.. _devel mailing list: http://lists.mediagoblin.org/listinfo/devel
 .. _join page: http://mediagoblin.org/join/
 
 Patches are the most helpful, but even feedback on what you think
index 3877063859c2fd54872c299d27d70ce6d4e748ed..8f9239be7b8655cdd35c3fb247d93e76bfdc73eb 100644 (file)
@@ -19,10 +19,10 @@ Media Types
 
 In the future, there will be all sorts of media types you can enable,
 but in the meanwhile there are six additional media types: video, audio,
-raw image, ascii art, STL/3d models, PDF and Document.
+raw image, ASCII art, STL/3D models, PDF and Document.
 
 First, you should probably read ":doc:`configuration`" to make sure
-you know how to modify the mediagoblin config file.
+you know how to modify the MediaGoblin config file.
 
 Enabling Media Types
 ====================
@@ -30,17 +30,14 @@ Enabling Media Types
 .. note::
     Media types are now plugins
 
-Media types are enabled in your mediagoblin configuration file, typically it is
-created by copying ``mediagoblin.ini`` to ``mediagoblin_local.ini`` and then
-applying your changes to ``mediagoblin_local.ini``. If you don't already have a
-``mediagoblin_local.ini``, create one in the way described.
+Media types are enabled in your MediaGoblin configuration file.
 
 Most media types have additional dependencies that you will have to install.
 You will find descriptions on how to satisfy the requirements of each media type
 on this page.
 
 To enable a media type, add the the media type under the ``[plugins]`` section
-in you ``mediagoblin_local.ini``. For example, if your system supported image
+in you ``mediagoblin.ini``. For example, if your system supported image
 and video media types, then it would look like this::
 
     [plugins]
@@ -81,8 +78,8 @@ instance the ``video`` media type configuration can be found in
 Video
 =====
 
-To enable video, first install gstreamer and the python-gstreamer
-bindings (as well as whatever gstremaer extensions you want,
+To enable video, first install GStreamer and the python-gstreamer
+bindings (as well as whatever GStreamer extensions you want,
 good/bad/ugly).  On Debianoid systems
 
 .. code-block:: bash
@@ -99,7 +96,7 @@ good/bad/ugly).  On Debianoid systems
 
 
 Add ``[[mediagoblin.media_types.video]]`` under the ``[plugins]`` section in
-your ``mediagoblin_local.ini`` and restart MediaGoblin.
+your ``mediagoblin.ini`` and restart MediaGoblin.
 
 Run
 
@@ -107,7 +104,7 @@ Run
 
     ./bin/gmg dbupdate
 
-Now you should be able to submit videos, and mediagoblin should
+Now you should be able to submit videos, and MediaGoblin should
 transcode them.
 
 .. note::
@@ -121,8 +118,8 @@ transcode them.
 Audio
 =====
 
-To enable audio, install the gstreamer and python-gstreamer bindings (as well
-as whatever gstreamer plugins you want, good/bad/ugly), scipy and numpy are
+To enable audio, install the GStreamer and python-gstreamer bindings (as well
+as whatever GStreamer plugins you want, good/bad/ugly), SciPy and NumPy are
 also needed for the audio spectrograms.
 To install these on Debianoid systems, run::
 
@@ -139,7 +136,7 @@ Then install ``scikits.audiolab`` for the spectrograms::
     ./bin/pip install scikits.audiolab
 
 Add ``[[mediagoblin.media_types.audio]]`` under the ``[plugins]`` section in your
-``mediagoblin_local.ini`` and restart MediaGoblin.
+``mediagoblin.ini`` and restart MediaGoblin.
 
 Run
 
@@ -160,7 +157,7 @@ To enable raw image you need to install pyexiv2.  On Debianoid systems
     sudo apt-get install python-pyexiv2
 
 Add ``[[mediagoblin.media_types.raw_image]]`` under the ``[plugins]``
-section in your ``mediagoblin_local.ini`` and restart MediaGoblin.
+section in your ``mediagoblin.ini`` and restart MediaGoblin.
 
 Run
 
@@ -168,24 +165,23 @@ Run
 
     ./bin/gmg dbupdate
 
-Now you should be able to submit raw images, and mediagoblin should
+Now you should be able to submit raw images, and MediaGoblin should
 extract the JPEG preview from them.
 
 
-Ascii art
+ASCII art
 =========
 
-To enable ascii art support, first install the
+To enable ASCII art support, first install the
 `chardet <http://pypi.python.org/pypi/chardet>`_
-library, which is necessary for creating thumbnails of ascii art
+library, which is necessary for creating thumbnails of ASCII art
 
 .. code-block:: bash
 
     ./bin/easy_install chardet
 
 
-Next, modify (and possibly copy over from ``mediagoblin.ini``) your
-``mediagoblin_local.ini``.  In the ``[plugins]`` section, add
+Next, modify your ``mediagoblin.ini``.  In the ``[plugins]`` section, add
 ``[[mediagoblin.media_types.ascii]]``.
 
 Run
@@ -194,20 +190,20 @@ Run
 
     ./bin/gmg dbupdate
 
-Now any .txt file you uploaded will be processed as ascii art!
+Now any .txt file you uploaded will be processed as ASCII art!
 
 
-STL / 3d model support
+STL / 3D model support
 ======================
 
-To enable the "STL" 3d model support plugin, first make sure you have
-a recentish `Blender <http://blender.org>`_ installed and available on
+To enable the "STL" 3D model support plugin, first make sure you have
+a recent `Blender <http://blender.org>`_ installed and available on
 your execution path.  This feature has been tested with Blender 2.63.
 It may work on some earlier versions, but that is not guaranteed (and
 is surely not to work prior to Blender 2.5X).
 
 Add ``[[mediagoblin.media_types.stl]]`` under the ``[plugins]`` section in your
-``mediagoblin_local.ini`` and restart MediaGoblin.
+``mediagoblin.ini`` and restart MediaGoblin.
 
 Run
 
@@ -223,9 +219,9 @@ PDF and Document
 
 To enable the "PDF and Document" support plugin, you need:
 
-1. pdftocairo and pdfinfo for pdf only support.
+1. pdftocairo and pdfinfo for PDF only support.
 
-2. unoconv with headless support to support converting libreoffice supported
+2. unoconv with headless support to support converting LibreOffice supported
    documents as well, such as doc/ppt/xls/odf/odg/odp and more.
    For the full list see mediagoblin/media_types/pdf/processing.py,
    unoconv_supported.
@@ -238,7 +234,7 @@ To install this on Fedora:
 
     sudo yum install -y poppler-utils unoconv libreoffice-headless
 
-Note: You can leave out unoconv and libreoffice-headless if you want only pdf
+Note: You can leave out unoconv and libreoffice-headless if you want only PDF
 support. This will result in a much smaller list of dependencies.
 
 pdf.js relies on git submodules, so be sure you have fetched them:
@@ -256,7 +252,7 @@ This feature has been tested on Fedora with:
 It may work on some earlier versions, but that is not guaranteed.
 
 Add ``[[mediagoblin.media_types.pdf]]`` under the ``[plugins]`` section in your
-``mediagoblin_local.ini`` and restart MediaGoblin.
+``mediagoblin.ini`` and restart MediaGoblin.
 
 Run
 
@@ -271,7 +267,7 @@ Blog (HIGHLY EXPERIMENTAL)
 MediaGoblin has a blog media type, which you might notice by looking
 through the docs!  However, it is *highly experimental*.  We have not
 security reviewed this, and it acts in a way that is not like normal
-blogs (the blogposts are themselves media types!).
+blogs (the blog posts are themselves media types!).
 
 So you can play with this, but it is not necessarily recommended yet
 for production use! :)
index 67c8bad10befff53cb2be88c5ad1ce5a639257c3..8682b0c751b323a77d82143b765a152fa9413940 100644 (file)
@@ -111,7 +111,7 @@ Check the plugin's documentation for what configuration options are
 available.
 
 Once you've set up your plugin, you should be sure to update the
-database to accomodate the new plugins::
+database to accommodate the new plugins::
 
   ./bin/gmg dbupdate
 
@@ -121,7 +121,7 @@ Deactivating plugins
 
 You should be aware that once you enable a plugin, deactivating it
 might be a bit tricky, for migrations reasons.  In the future we may
-produce better tooling to accomodate this.  In short, you will need to
+produce better tooling to accommodate this.  In short, you will need to
 do a bit of database surgery by:
 
 - Removing all tables and indexes installed by the plugin
index e65ac332707cbebfc11e791f0ba55ec97ed7d4fe..3d11f02264fc5e1cc1d7448efcd46b2f29130f6d 100644 (file)
@@ -24,10 +24,10 @@ Deploy with paste
 
 The MediaGoblin WSGI application instance you get with ``./lazyserver.sh`` is
 not ideal for a production MediaGoblin deployment. Ideally, you should be able
-to use a systemd service file or an init script to launch and restart the
+to use a Systemd service file or an init script to launch and restart the
 MediaGoblin process.
 
-We will explore setting up MediaGoblin systemd service files and init scripts,
+We will explore setting up MediaGoblin Systemd service files and init scripts,
 but first we need to create the directory that will store the MediaGoblin logs.
 
 
@@ -45,10 +45,10 @@ proper permissions::
 
 .. _systemd-service-files:
 
-Use systemd service files
+Use Systemd service files
 -------------------------
 
-If your operating system uses systemd, you can use systemd ``service files``
+If your operating system uses Systemd, you can use Systemd ``service files``
 to manage both the Celery and Paste processes. Place the following service
 files in the ``/etc/systemd/system/`` directory.
 
@@ -62,18 +62,21 @@ modify it to suit your environment's setup:
     # If using Fedora/CentOS/Red Hat, mkdir and chown are located in /usr/bin/mkdir and /usr/bin/chown, respectively.
 
     [Unit]
-    Description=Mediagoblin Celeryd
+    Description=MediaGoblin Celeryd
 
     [Service]
     User=mediagoblin
     Group=mediagoblin
     Type=simple
     WorkingDirectory=/srv/mediagoblin.example.org/mediagoblin
+    # Start mg-celeryd process as root, then switch to mediagoblin user/group
+    # (This is needed to run the ExecStartPre commands)
+    PermissionsStartOnly=true
     # Create directory for PID (if needed) and set ownership
     ExecStartPre=/bin/mkdir -p /run/mediagoblin
     ExecStartPre=/bin/chown -hR mediagoblin:mediagoblin /run/mediagoblin
     # Celery process will run as the `mediagoblin` user after start.
-    Environment=MEDIAGOBLIN_CONFIG=/srv/mediagoblin.example.org/mediagoblin/mediagoblin_local.ini \
+    Environment=MEDIAGOBLIN_CONFIG=/srv/mediagoblin.example.org/mediagoblin/mediagoblin.ini \
                 CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery
     ExecStart=/srv/mediagoblin.example.org/mediagoblin/bin/celery worker \
                   --logfile=/var/log/mediagoblin/celery.log \
@@ -153,7 +156,7 @@ processes again.
 Use an init script
 ------------------
 
-If your system does not use systemd, you can use the following command as the
+If your system does not use Systemd, you can use the following command as the
 basis for an init script:
 
 .. code-block:: bash
@@ -241,7 +244,7 @@ To launch Celery separately from the MediaGoblin WSGI application:
 
         CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery ./bin/celeryd
 
-If you use our example systemd ``service files``, Celery will be set to the
+If you use our example Systemd ``service files``, Celery will be set to the
 "CELERY_ALWAYS_EAGER=false" value by default. This will provide your users
 with the best user experience, as all media processing will be done in the
 background.
index 584fd8c343f515f5550d0f8891de2b9d0a47444a..1c15f24931a70f66cc7b050ad11973c7a863d944 100644 (file)
@@ -27,7 +27,7 @@ carefully, or at least skim over it.
    running migrations!  That way if something goes wrong, we can fix
    things!
 
-   And be sure to shut down your current mediagoblin/celery processes
+   And be sure to shut down your current MediaGoblin/Celery processes
    before upgrading!
 
 .. note::
@@ -36,7 +36,7 @@ carefully, or at least skim over it.
    gitorious.org shut down, we had to move.  We are presently on
    Savannah.  You may need to update your git repository location::
 
-       git remote set-url origin git://git.savannah.gnu.org/mediagoblin.git
+       git remote set-url origin https://git.savannah.gnu.org/git/mediagoblin.git
 
 
 0.9.0
@@ -49,7 +49,7 @@ Python 3, which is pretty cool!
 **Do this to upgrade**
 
 0. If you haven't already, switch the git remote URL:
-   ``git remote set-url origin git://git.savannah.gnu.org/mediagoblin.git``
+   ``git remote set-url origin https://git.savannah.gnu.org/git/mediagoblin.git``
 1. Update to the latest release.  If checked out from git, run:
    ``git fetch && git checkout -q v0.9.0``
 2. Run
@@ -89,7 +89,7 @@ soon as possible.
 **Do this to upgrade**
 
 0. If you haven't already, switch the git remote URL:
-   ``git remote set-url origin git://git.savannah.gnu.org/mediagoblin.git``
+   ``git remote set-url origin https://git.savannah.gnu.org/git/mediagoblin.git``
 1. Update to the latest release.  If checked out from git, run:
    ``git fetch && git checkout -q v0.8.1``
 2. Run
@@ -143,7 +143,7 @@ trouble, consider pinging the MediaGoblin list or IRC channel.
 **Do this to upgrade**
 
 0. If you haven't already, switch the git remote URL:
-   ``git remote set-url origin git://git.savannah.gnu.org/mediagoblin.git``
+   ``git remote set-url origin https://git.savannah.gnu.org/git/mediagoblin.git``
 1. If you don't have node.js installed, you'll need it for handling
    MediaGoblin's static web dependencies.  Install this via your
    distribution!  (In the glorious future MediaGoblin will be simply
@@ -162,11 +162,11 @@ prior upgrade guides!
 Additionally:
 
 - Are you using audio or video media types?  In that case, you'll need
-  to update your Gstreamer instance to 1.0.
+  to update your GStreamer instance to 1.0.
 - The Pump API needs some data passed through to the WSGI application,
-  so if you are using apache with mod_wsgi you should be sure to make
+  so if you are using Apache with mod_wsgi you should be sure to make
   sure to add "WSGIPassAuthorization On" to your config.  (Using the
-  default MediaGoblin documnetation and config, things should work
+  default MediaGoblin documentation and config, things should work
   as-is.)
 
 
@@ -181,12 +181,12 @@ Additionally:
 - Clearer documentation on permissions and installation
 - Switched from Transifex, which had become proprietary, to an
   instance of Pootle hosted for GNU
-- Moved to Gstreamer 1.0!  This also adds a new thumbnailer which
+- Moved to GStreamer 1.0!  This also adds a new thumbnailer which
   gives much better results in
-- Removed terrible check-javascript-dependencies-into-your-application
+- Removed terrible check-JavaScript-dependencies-into-your-application
   setup, now using Bower for dependency tracking
 - Put some scaffolding in place for Alembic, which will be used for
-  future mitration work
+  future migration work
 - Automatically create a fresh mediagoblin.ini from
   mediagoblin.ini.example
 - no more need for mediagoblin_local.ini (though it's still supported)
@@ -199,7 +199,7 @@ Additionally:
 This is a purely bugfix release.  Important changes happened since
 0.7.0; if running MediaGoblin 0.7.0, an upgrade is highly recommended;
 see below.  This release is especially useful if you have been running
-postgres and have been receiving seemingly random database transaction
+PostgreSQL and have been receiving seemingly random database transaction
 errors.
 
 **Do this to upgrade**
@@ -220,9 +220,9 @@ That's it, probably!  If you run into problems, don't hesitate to
   database transaction issues.  (These should be back by 0.8.0.)
 
   + Disabled the "checking if the database is up to date at
-    mediagoblin startup" feature
+    MediaGoblin startup" feature
   + Disabled the garbage collection stuff by default for now
-    (You can set garbage_collection under the config mediagoblin
+    (You can set garbage_collection under the config MediaGoblin
     header to something other than 0 to turn it back on for now, but
     it's potentially risky for the moment.)
 
@@ -231,7 +231,7 @@ That's it, probably!  If you run into problems, don't hesitate to
 - Added support for cr2 files in raw_image media type
 - Added a description to setup.py
 - Collection and CollectionItem objects now have nicer in-python representations
-- Fixed unicode error with raw image mediatype logging
+- Fixed Unicode error with raw image mediatype logging
 - Fixed #945 "Host metadata does not confirm to spec (/.well-known/meta-data)"
 
   + Add XRD+XML formatting for /.well-known/host-meta
@@ -268,7 +268,7 @@ That's it, probably!  If you run into problems, don't hesitate to
   (which will be the foundation for MediaGoblin's federation)
 - New theme: Sandy 70s Speedboat!
 
-- Metadata features!  We also now have a json-ld context. 
+- Metadata features!  We also now have a JSON-LD context. 
 
 - Many improvements for archival institutions, including metadata
   support and featuring items on the homepage.  With the (new!)
@@ -283,7 +283,7 @@ That's it, probably!  If you run into problems, don't hesitate to
   uploading many files at once.  This is aimed to be useful for
   archival institutions and groups where there is an already existing
   and large set of available media that needs to be included.
-- Speaking of, the call to postgres in the makefile is fixed.
+- Speaking of, the call to PostgreSQL in the makefile is fixed.
 - We have a new, generic media-page context hook that allows for
   adding context depending on the type of media.
 - Tired of video thumbnails breaking during processing all the time?
@@ -310,10 +310,10 @@ That's it, probably!  If you run into problems, don't hesitate to
   data.  It's basically the same thing as before, but packaged
   separately from MediaGoblin.
 - Many improvements to internationalization.  Also (still rudimentary,
-  but existant!) RTL language support!
+  but existent!) RTL language support!
 
 **Known issues:**
- - The host-meta is now json by default; in the spec it should be xml by
+ - The host-meta is now JSON by default; in the spec it should be XML by
    default.  We have done this because of compatibility with the pump
    API.  We are checking with upstream to see if there is a way to
    resolve this discrepancy.
@@ -360,7 +360,7 @@ nickname "Lore of the Admin"!
 - New tools to control how much users can upload, both as a general
   user limit, or per file.
 
-  You can set this with the following options in your mediagoblin
+  You can set this with the following options in your MediaGoblin
   config file: `upload_limit` and `max_file_size`.  Both are integers
   in megabytes.
 
@@ -368,7 +368,7 @@ nickname "Lore of the Admin"!
   upload too, though an interface for this is not yet exposed.  See
   the "uploaded" field on the core__users table.
 
-- MediaGoblin now contains an authentication plugin for ldap!  You
+- MediaGoblin now contains an authentication plugin for LDAP!  You
   can turn on the mediagoblin.plugins.ldap plugin to make use of
   this.  See the documentation: :ref:`ldap-plugin`
 
@@ -423,8 +423,8 @@ v0.5.1 is a bugfix release... the steps are the same as for 0.5.1.
 =====
 
 **NOTE:** If using the API is important to you, we're in a state of
-ransition towards a new API via the Pump API.  As such, though the old
-API still probably works, some changes have happened to the way oauth
+transition towards a new API via the Pump API.  As such, though the old
+API still probably works, some changes have happened to the way OAuth
 works to make it more Pump-compatible.  If you're heavily using
 clients using the old API, you may wish to hold off on upgrading for
 now.  Otherwise, jump in and have fun! :)
@@ -469,21 +469,21 @@ If you run into problems, don't hesitate to
 * Comment preview!
 * Users now have the ability to change their email associated with their
   account.
-* New oauth code as we move closer to federation support.
-* Experimental pyconfigure support for GNU-style configue and makefile
+* New OAuth code as we move closer to federation support.
+* Experimental pyconfigure support for GNU-style configure and makefile
   deployment.
 * Database foundations! You can now pre-populate the database models.
 * Way faster unit test run-time via in-memory database.
 * All mongokit stuff has been cleaned up.
-* Fixes for non-ascii filenames.
+* Fixes for non-ASCII filenames.
 * The option to stay logged in.
-* Mediagoblin has been upgraded to use the latest `celery <http://celeryproject.org/>`_
+* MediaGoblin has been upgraded to use the latest `Celery <http://celeryproject.org/>`_
   version.
 * You can now add jinja2 extensions to your config file to use in custom
   templates.
 * Fixed video permission issues.
-* Mediagoblin docs are now hosted with multiple versions.
-* We removed redundent tooltips from the STL media display.
+* MediaGoblin docs are now hosted with multiple versions.
+* We removed redundant tooltips from the STL media display.
 * We are now using itsdangerous for verification tokens.
 
 
@@ -495,7 +495,7 @@ fix in the newly released document support which prevented the
 "conversion via libreoffice" feature.
 
 If you were running 0.4.0 you can upgrade to v0.4.1 via a simple
-switch and restarting mediagoblin/celery with no other actions.
+switch and restarting MediaGoblin/Celery with no other actions.
 
 Otherwise, follow 0.4.0 instructions.
 
@@ -514,7 +514,7 @@ Otherwise, follow 0.4.0 instructions.
    Keep on reading to hear more about new plugin features.
 4. If you want to take advantage of new plugins that have statically
    served assets, you are going to need to add the new "plugin_static"
-   section to your nginx config.  Basically the following for nginx::
+   section to your Nginx config.  Basically the following for Nginx::
 
      # Plugin static files (usually symlinked in)
      location /plugin_static/ {
@@ -557,7 +557,7 @@ please note the following:
   date of an image when available (available as the
   "original_date_visible" variable)
 * Moved unit testing system from nosetests to py.test so we can better
-  handle issues with sqlalchemy exploding with different database
+  handle issues with SQLAlchemy exploding with different database
   configurations.  Long story :)
 * You can now disable the ability to post comments.
 * Tags now can be up to length 255 characters by default.
@@ -587,7 +587,7 @@ you run into problems, don't hesitate to
 
 * New dropdown menu for accessing various features.
 
-* Significantly improved URL generation.  Now mediagoblin won't give
+* Significantly improved URL generation.  Now MediaGoblin won't give
   up on making a slug if it looks like there will be a duplicate;
   it'll try extra hard to generate a meaningful one instead.
 
@@ -595,13 +595,13 @@ you run into problems, don't hesitate to
   linking to a slug; /u/username/m/id:35/ is the kind of reference we
   now use to linking to entries with ids.  However, old links with
   entries that linked to ids should work just fine with our migration.
-  The only urls that might break in this release are ones using colons
+  The only URLs that might break in this release are ones using colons
   or equal signs.
 
 * New template hooks for plugin authoring.
 
 * As a demonstration of new template hooks for plugin authoring,
-  openstreetmap support now moved to a plugin!
+  OpenStreetMap support now moved to a plugin!
 
 * Method to add media to collections switched from icon of paperclip
   to button with "add to collection" text.
@@ -612,9 +612,9 @@ you run into problems, don't hesitate to
   waste gobs of memory.
 
 * Video transcoding now optional for videos that meet certain
-  criteria.  By default, MediaGoblin will not transcode webm videos
+  criteria.  By default, MediaGoblin will not transcode WebM videos
   that are smaller in resolution than the MediaGoblin defaults, and
-  MediaGoblin can also be configured to allow theora files to not be
+  MediaGoblin can also be configured to allow Theora files to not be
   transcoded as well.
 
 * Per-user license preference option; always want your uploads to be
@@ -644,7 +644,7 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
 
 **Do this to upgrade**
 
-    # directory of your mediagoblin install
+    # directory of your MediaGoblin install
     cd /srv/mediagoblin.example.org
 
     # copy source for this release
@@ -664,13 +664,13 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
 * **3d model support!**
 
   You can now upload STL and OBJ files and display them in
-  MediaGoblin.  Requires a recent-ish Blender; for details see:
+  MediaGoblin.  Requires a recent Blender; for details see:
   :ref:`deploying-chapter`
 
 * **trim_whitespace**
 
   We bundle the optional plugin trim_whitespace which reduces the size
-  of the delivered html output by reducing redundant whitespace.
+  of the delivered HTML output by reducing redundant whitespace.
 
   See :ref:`core-plugin-section` for plugin documentation
 
@@ -684,7 +684,7 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
   and `OMGMG <https://github.com/jwandborg/omgmg>`_, an example of
   a web application hooking up to the API.
 
-  This is a plugin, so you have to enable it in your mediagoblin
+  This is a plugin, so you have to enable it in your MediaGoblin
   config file by adding a section under [plugins] like::
 
     [plugins]
@@ -697,7 +697,7 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
 
   For applications that use OAuth to connect to the API.
 
-  This is a plugin, so you have to enable it in your mediagoblin
+  This is a plugin, so you have to enable it in your MediaGoblin
   config file by adding a section under [plugins] like::
 
     [plugins]
@@ -717,7 +717,7 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
 
   Geolocation is also now on by default.
 
-* **Miscelaneous visual improvements**
+* **Miscellaneous visual improvements**
 
   We've made a number of small visual improvements including newer and
   nicer looking thumbnails and improved checkbox placement.
@@ -732,7 +732,7 @@ MongoDB-based MediaGoblin instance to the newer SQL-based system.
 1. Make sure to run ``bin/gmg dbuptdate`` after upgrading.
 
 2. If you set up your server config with an older version of
-   mediagoblin and the mediagoblin docs, it's possible you don't
+   MediaGoblin and the MediaGoblin docs, it's possible you don't
    have the "theme static files" alias, so double check to make
    sure that section is there if you are having problems.
 
index 11ae387586de12f353cdbea039241dafe8a2c12e..24f23235ae27a506473db373ab5874ad044f4887 100644 (file)
@@ -43,7 +43,7 @@ want to install this theme!  Don't worry, it's fairly painless.
 3. ``tar -xzvf <tar-archive>``
 
 4. Open your configuration file (probably named
-   ``mediagoblin_local.ini``) and set the theme name::
+   ``mediagoblin.ini``) and set the theme name::
 
        [mediagoblin]
        # ...
@@ -73,9 +73,9 @@ want to install this theme!  Don't worry, it's fairly painless.
 Set up your webserver to serve theme assets
 -------------------------------------------
 
-If you followed the nginx setup example in :ref:`webserver-config` you
+If you followed the Nginx setup example in :ref:`webserver-config` you
 should already have theme asset setup.  However, if you set up your
-server config with an older version of mediagoblin and the mediagoblin
+server config with an older version of MediaGoblin and the MediaGoblin
 docs, it's possible you don't have the "theme static files" alias, so
 double check to make sure that section is there if you are having
 problems.
@@ -103,7 +103,7 @@ Other variables you may consider setting:
 
 `theme_web_path`
     When theme-specific assets are specified, this is where MediaGoblin
-    will set the urls.  By default this is ``"/theme_static/"`` so in
+    will set the URLs.  By default this is ``"/theme_static/"`` so in
     the case that your theme was trying to access its file 
     ``"images/shiny_button.png"`` MediaGoblin would link
     to ``/theme_static/images/shiny_button.png``.
@@ -136,7 +136,7 @@ if necessary)::
     |  |  |- im_a_hedgehog.png  # hedgehog-containing image used by theme
     |  |  '- custom_logo.png    # your theme's custom logo
     |  '- css/
-    |     '- hedgehog.css       # your site's hedgehog-specific css
+    |     '- hedgehog.css       # your site's hedgehog-specific CSS
     |- README.txt               # Optionally, a readme file (not required)
     |- AGPLv3.txt               # AGPL license file for your theme. (good practice)
     '- CC0_1.0.txt              # CC0 1.0 legalcode for the assets [if appropriate!]
@@ -164,7 +164,7 @@ Only a few things need to go in here::
     [theme]
     name = Hedgehog-ification
     description = For hedgehog lovers ONLY
-    licensing = AGPLv3 or later templates; assets (images/css) waived under CC0 1.0
+    licensing = AGPLv3 or later templates; assets (images/CSS) waived under CC0 1.0
 
 The name and description fields here are to give users an idea of what
 your theme is about.  For the moment, we don't have any listing
@@ -232,7 +232,7 @@ You should include AGPLv3.txt with your theme as this is required for
 the assets.  You can copy this from ``mediagoblin/licenses/``.
 
 In the above example, we also use CC0 to waive our copyrights to
-images and css, so we also included CC0_1.0.txt
+images and CSS, so we also included CC0_1.0.txt
 
 
 A README.txt file
@@ -247,7 +247,7 @@ Simple theming by adding CSS
 ----------------------------
 
 Many themes won't require anything other than the ability to override
-some of MediaGoblin's core css.  Thankfully, doing so is easy if you
+some of MediaGoblin's core CSS.  Thankfully, doing so is easy if you
 combine the above steps!
 
 In your theme, do the following (make sure you make the necessary
@@ -260,7 +260,7 @@ Great, now open that file and add something like this at the end::
     <link rel="stylesheet" type="text/css"
           href="{{ request.staticdirect('/css/theme.css', 'theme') }}"/>
 
-You can name the css file whatever you like.  Now make the directory
+You can name the CSS file whatever you like.  Now make the directory
 for ``assets/css/`` and add the file ``assets/css/theme.css``.
 
 You can now put custom CSS files in here and any CSS you add will
index 0ed22fd8a00b9d3c88bf09731ee36a0ad48dd950..b531b068cdab5f5ec9206d8380d93ba03e297eb0 100755 (executable)
@@ -25,7 +25,7 @@ case "$selfname" in
         ini_prefix=paste
         ;;
     lazycelery.sh)
-        starter_cmd=celeryd
+        starter_cmd=celery
         ini_prefix=mediagoblin
         ;;
     *)
@@ -87,7 +87,7 @@ case "$selfname" in
     lazycelery.sh)
         MEDIAGOBLIN_CONFIG="${ini_file}" \
             CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery \
-            $starter -B "$@"
+            $starter worker -B "$@"
         ;;
     *) exit 1 ;;
 esac
index 74181fdeefacb5a7f08bf77785572f94dc6bda74..b25300f0854dca95cb1a94558d260e6686efd849 100644 (file)
@@ -115,8 +115,13 @@ def uploads_endpoint(request):
             )
 
         mimetype = request.headers["Content-Type"]
-        filename = mimetypes.guess_all_extensions(mimetype)
-        filename = 'unknown' + filename[0] if filename else filename
+
+        if "X-File-Name" in request.headers:
+            filename = request.headers["X-File-Name"]
+        else:
+            filename = mimetypes.guess_all_extensions(mimetype)
+            filename = 'unknown' + filename[0] if filename else filename
+
         file_data = FileStorage(
             stream=io.BytesIO(request.data),
             filename=filename,
index 9c16a980d7ee54d4bc1b861a8697897cbbbd8435..ae6fadf618c35ad86977e1da1cbf37a2350d55b5 100644 (file)
@@ -34,14 +34,19 @@ from mediagoblin import auth
 _log = logging.getLogger(__name__)
 
 
-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
+def normalize_user_or_email_field(allow_email=True, allow_user=True,
+                                  is_login=False):
+    """Check if we were passed a field that matches a username and/or email
     pattern.
 
     This is useful for fields that can take either a username or email
-    address. Use the parameters if you want to only allow a username for
-    instance"""
+    address. Use the parameters if you want to only allow a username
+    for instance
+
+    is_login : bool
+        If is_login is True, does not check the length of username.
+
+    """
     message = _(u'Invalid User name or email address.')
     nomail_msg = _(u"This field does not take email addresses.")
     nouser_msg = _(u"This field requires an email address.")
@@ -56,7 +61,8 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True):
         else:  # lower case user names
             if not allow_user:
                 raise wtforms.ValidationError(nouser_msg)
-            wtforms.validators.Length(min=3, max=30)(form, field)
+            if not is_login:
+                wtforms.validators.Length(min=3, max=30)(form, field)
             wtforms.validators.Regexp(r'^[-_\w]+$')(form, field)
             field.data = field.data.lower()
         if field.data is None:  # should not happen, but be cautious anyway
index 2f95fd8114ae7c4e153828d916dd1be00962ad1a..fb8e72652786f58f168a8f267bc50747ae350383 100644 (file)
@@ -14,6 +14,8 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import logging
+
 import six
 
 from itsdangerous import BadSignature
@@ -29,6 +31,8 @@ from mediagoblin.tools.pluginapi import hook_handle
 from mediagoblin.auth.tools import (send_verification_email, register_user,
                                     check_login_simple)
 
+_log = logging.getLogger(__name__)
+
 
 @allow_registration
 @auth_enabled
@@ -105,6 +109,8 @@ def login(request):
                     return redirect(request, "index")
 
             login_failed = True
+            remote_addr = request.access_route[-1] or request.remote_addr
+            _log.warn("Failed login attempt from %r", remote_addr)
 
     return render_to_response(
         request,
index 0a8da73e345541129e9c9c2f4ea6d7d47a65a75e..bd3003d0e599b701f9d4be3a258df898e5010d07 100644 (file)
@@ -153,8 +153,7 @@ CELERY_RESULT_BACKEND = string(default="database")
 CELERY_RESULT_DBURI = string(default="sqlite:///%(here)s/celery.db")
 
 # default kombu stuff
-BROKER_TRANSPORT = string(default="sqlalchemy")
-BROKER_URL = string(default="sqlite:///%(here)s/kombu.db")
+BROKER_URL = string(default="amqp://")
 
 # known booleans
 CELERY_RESULT_PERSISTENT = boolean()
index f4273fa0e37ce30293288e1fd244a2380441fa48..852f35ee012974b939de756af0fe99ca380a0540 100644 (file)
@@ -365,9 +365,8 @@ def build_alembic_config(global_config, cmd_options, session):
     configuration.  Initialize the database session appropriately
     as well.
     """
-    root_dir = os.path.abspath(os.path.dirname(os.path.dirname(
-        os.path.dirname(__file__))))
-    alembic_cfg_path = os.path.join(root_dir, 'alembic.ini')
+    alembic_dir = os.path.join(os.path.dirname(__file__), 'migrations')
+    alembic_cfg_path = os.path.join(alembic_dir, 'alembic.ini')
     cfg = Config(alembic_cfg_path,
                  cmd_opts=cmd_options)
     cfg.attributes["session"] = session
similarity index 95%
rename from alembic.ini
rename to mediagoblin/db/migrations/alembic.ini
index 7ae94f9fd01e999b01f2c6571cd68f7d6da3f385..4f7fc1155c42d789c08a22f3cce7a406022ca9b9 100644 (file)
@@ -2,7 +2,7 @@
 
 [alembic]
 # path to migration scripts
-script_location = %(here)s/mediagoblin/db/migrations
+script_location = %(here)s
 
 # template used to generate migration files
 # file_template = %%(rev)s_%%(slug)s
index 43b7b24765aebda18ce1721f4fb25d2b62f8f938..a6d05cd1fd13683c77f2ba91c471d76175115c87 100644 (file)
@@ -48,7 +48,7 @@ def run_migrations_online():
     and associate a connection with the context.
 
     """
-    connection = config.attributes["session"].get_bind()
+    connection = config.attributes["session"].connection()
     context.configure(
                 connection=connection,
                 target_metadata=target_metadata
@@ -61,4 +61,3 @@ if context.is_offline_mode():
     run_migrations_offline()
 else:
     run_migrations_online()
-
index 9bbb252b1d7a977f816ad05886033fc23de2786a..c19fe4dade6f390721084435bba0173494489836 100644 (file)
@@ -43,6 +43,7 @@ from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
 from mediagoblin.tools.files import delete_media_files
 from mediagoblin.tools.common import import_component
 from mediagoblin.tools.routing import extract_url_arguments
+from mediagoblin.tools.text import convert_to_tag_list_of_dicts
 
 import six
 from six.moves.urllib.parse import urljoin
@@ -595,6 +596,16 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
     ## TODO
     # fail_error
 
+    @property
+    def get_uploader(self):
+        # for compatibility
+        return self.get_actor
+
+    @property
+    def uploader(self):
+        # for compatibility
+        return self.actor
+
     @property
     def collections(self):
         """ Get any collections that this MediaEntry is in """
@@ -617,9 +628,9 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
             query = query.order_by(Comment.added.asc())
         else:
             query = query.order_by(Comment.added.desc())
-        
+
         return query
+
     def url_to_prev(self, urlgen):
         """get the next 'newer' entry by this user"""
         media = MediaEntry.query.filter(
@@ -769,7 +780,6 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
                 "self": {
                     "href": public_id,
                 },
-
             }
         }
 
@@ -785,6 +795,12 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
         if self.location:
             context["location"] = self.get_location.serialize(request)
 
+        # Always show tags, even if empty list
+        if self.tags:
+            context["tags"] = [tag['name'] for tag in self.tags]
+        else:
+            context["tags"] = []
+
         if show_comments:
             comments = [
                 l.comment().serialize(request) for l in self.get_comments()]
@@ -832,6 +848,9 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
         if "location" in data:
             License.create(data["location"], self)
 
+        if "tags" in data:
+            self.tags = convert_to_tag_list_of_dicts(', '.join(data["tags"]))
+
         return True
 
 class FileKeynames(Base):
@@ -966,7 +985,7 @@ class MediaTag(Base):
 class Comment(Base):
     """
     Link table between a response and another object that can have replies.
-    
+
     This acts as a link table between an object and the comments on it, it's
     done like this so that you can look up all the comments without knowing
     whhich comments are on an object before hand. Any object can be a comment
@@ -977,7 +996,7 @@ class Comment(Base):
     __tablename__ = "core__comment_links"
 
     id = Column(Integer, primary_key=True)
-    
+
     # The GMR to the object the comment is on.
     target_id = Column(
         Integer,
@@ -1006,7 +1025,25 @@ class Comment(Base):
 
     # When it was added
     added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
-    
+
+    @property
+    def get_author(self):
+        # for compatibility
+        return self.comment().get_actor  # noqa
+
+    def __getattr__(self, attr):
+        if attr.startswith('_'):
+            # if attr starts with '_', then it's probably some internal
+            # sqlalchemy variable. Since __getattr__ is called when
+            # non-existing attributes are being accessed, we should not try to
+            # fetch it from self.comment()
+            raise AttributeError
+        try:
+            _log.debug('Old attr is being accessed: {0}'.format(attr))
+            return getattr(self.comment(), attr)  # noqa
+        except Exception as e:
+            _log.error(e)
+            raise
 
 class TextComment(Base, TextCommentMixin, CommentingMixin):
     """
@@ -1040,7 +1077,7 @@ class TextComment(Base, TextCommentMixin, CommentingMixin):
         if target is None:
             target = {}
         else:
-            target = target.serialize(request, show_comments=False)        
+            target = target.serialize(request, show_comments=False)
 
 
         author = self.get_actor
@@ -1068,7 +1105,7 @@ class TextComment(Base, TextCommentMixin, CommentingMixin):
         if "location" in data:
             Location.create(data["location"], self)
 
-    
+
         # Handle changing the reply ID
         if "inReplyTo" in data:
             # Validate that the ID is correct
@@ -1099,7 +1136,7 @@ class TextComment(Base, TextCommentMixin, CommentingMixin):
             link.target = media
             link.comment = self
             link.save()
-        
+
         return True
 
 class Collection(Base, CollectionMixin, CommentingMixin):
@@ -1298,7 +1335,7 @@ class Notification(Base):
     seen = Column(Boolean, default=lambda: False, index=True)
     user = relationship(
         User,
-        backref=backref('notifications', cascade='all, delete-orphan')) 
+        backref=backref('notifications', cascade='all, delete-orphan'))
 
     def __repr__(self):
         return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
@@ -1343,7 +1380,7 @@ class Report(Base):
                                             which points to the reported object.
     """
     __tablename__ = 'core__reports'
-    
+
     id = Column(Integer, primary_key=True)
     reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
     reporter =  relationship(
@@ -1371,7 +1408,7 @@ class Report(Base):
 
     resolved = Column(DateTime)
     result = Column(UnicodeText)
-    
+
     object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
     object_helper = relationship(GenericModelReference)
     obj = association_proxy("object_helper", "get_object",
diff --git a/mediagoblin/db/models_v0.py b/mediagoblin/db/models_v0.py
deleted file mode 100644 (file)
index bdedec2..0000000
+++ /dev/null
@@ -1,342 +0,0 @@
-# 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/>.
-
-"""
-TODO: indexes on foreignkeys, where useful.
-"""
-
-###########################################################################
-# WHAT IS THIS FILE?
-# ------------------
-#
-# Upon occasion, someone runs into this file and wonders why we have
-# both a models.py and a models_v0.py.
-#
-# The short of it is: you can ignore this file.
-#
-# The long version is, in two parts:
-#
-#  - We used to use MongoDB, then we switched to SQL and SQLAlchemy.
-#    We needed to convert peoples' databases; the script we had would
-#    switch them to the first version right after Mongo, convert over
-#    all their tables, then run any migrations that were added after.
-#
-#  - That script is now removed, but there is some discussion of
-#    writing a test that would set us at the first SQL migration and
-#    run everything after.  If we wrote that, this file would still be
-#    useful.  But for now, it's legacy!
-#
-###########################################################################
-
-
-import datetime
-import sys
-
-from sqlalchemy import (
-    Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
-    UniqueConstraint, PrimaryKeyConstraint, SmallInteger, Float)
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import relationship, backref
-from sqlalchemy.orm.collections import attribute_mapped_collection
-from sqlalchemy.ext.associationproxy import association_proxy
-from sqlalchemy.util import memoized_property
-
-from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
-from mediagoblin.db.base import GMGTableBase, Session
-
-
-Base_v0 = declarative_base(cls=GMGTableBase)
-
-
-class User(Base_v0):
-    """
-    TODO: We should consider moving some rarely used fields
-    into some sort of "shadow" table.
-    """
-    __tablename__ = "core__users"
-
-    id = Column(Integer, primary_key=True)
-    username = Column(Unicode, nullable=False, unique=True)
-    email = Column(Unicode, nullable=False)
-    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
-    pw_hash = Column(Unicode, nullable=False)
-    email_verified = Column(Boolean, default=False)
-    status = Column(Unicode, default=u"needs_email_verification", nullable=False)
-    verification_key = Column(Unicode)
-    is_admin = Column(Boolean, default=False, nullable=False)
-    url = Column(Unicode)
-    bio = Column(UnicodeText)  # ??
-    fp_verification_key = Column(Unicode)
-    fp_token_expire = Column(DateTime)
-
-    ## TODO
-    # plugin data would be in a separate model
-
-
-class MediaEntry(Base_v0):
-    """
-    TODO: Consider fetching the media_files using join
-    """
-    __tablename__ = "core__media_entries"
-
-    id = Column(Integer, primary_key=True)
-    uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
-    title = Column(Unicode, nullable=False)
-    slug = Column(Unicode)
-    created = Column(DateTime, nullable=False, default=datetime.datetime.now,
-        index=True)
-    description = Column(UnicodeText) # ??
-    media_type = Column(Unicode, nullable=False)
-    state = Column(Unicode, default=u'unprocessed', nullable=False)
-        # or use sqlalchemy.types.Enum?
-    license = Column(Unicode)
-
-    fail_error = Column(Unicode)
-    fail_metadata = Column(JSONEncoded)
-
-    queued_media_file = Column(PathTupleWithSlashes)
-
-    queued_task_id = Column(Unicode)
-
-    __table_args__ = (
-        UniqueConstraint('uploader', 'slug'),
-        {})
-
-    get_uploader = relationship(User)
-
-    media_files_helper = relationship("MediaFile",
-        collection_class=attribute_mapped_collection("name"),
-        cascade="all, delete-orphan"
-        )
-
-    attachment_files_helper = relationship("MediaAttachmentFile",
-        cascade="all, delete-orphan",
-        order_by="MediaAttachmentFile.created"
-        )
-
-    tags_helper = relationship("MediaTag",
-        cascade="all, delete-orphan"
-        )
-
-    def media_data_init(self, **kwargs):
-        """
-        Initialize or update the contents of a media entry's media_data row
-        """
-        session = Session()
-
-        media_data = session.query(self.media_data_table).filter_by(
-            media_entry=self.id).first()
-
-        # No media data, so actually add a new one
-        if media_data is None:
-            media_data = self.media_data_table(
-                media_entry=self.id,
-                **kwargs)
-            session.add(media_data)
-        # Update old media data
-        else:
-            for field, value in kwargs.iteritems():
-                setattr(media_data, field, value)
-
-    @memoized_property
-    def media_data_table(self):
-        # TODO: memoize this
-        models_module = self.media_type + '.models'
-        __import__(models_module)
-        return sys.modules[models_module].DATA_MODEL
-
-
-class FileKeynames(Base_v0):
-    """
-    keywords for various places.
-    currently the MediaFile keys
-    """
-    __tablename__ = "core__file_keynames"
-    id = Column(Integer, primary_key=True)
-    name = Column(Unicode, unique=True)
-
-    def __repr__(self):
-        return "<FileKeyname %r: %r>" % (self.id, self.name)
-
-    @classmethod
-    def find_or_new(cls, name):
-        t = cls.query.filter_by(name=name).first()
-        if t is not None:
-            return t
-        return cls(name=name)
-
-
-class MediaFile(Base_v0):
-    """
-    TODO: Highly consider moving "name" into a new table.
-    TODO: Consider preloading said table in software
-    """
-    __tablename__ = "core__mediafiles"
-
-    media_entry = Column(
-        Integer, ForeignKey(MediaEntry.id),
-        nullable=False)
-    name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
-    file_path = Column(PathTupleWithSlashes)
-
-    __table_args__ = (
-        PrimaryKeyConstraint('media_entry', 'name_id'),
-        {})
-
-    def __repr__(self):
-        return "<MediaFile %s: %r>" % (self.name, self.file_path)
-
-    name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
-    name = association_proxy('name_helper', 'name',
-        creator=FileKeynames.find_or_new
-        )
-
-
-class MediaAttachmentFile(Base_v0):
-    __tablename__ = "core__attachment_files"
-
-    id = Column(Integer, primary_key=True)
-    media_entry = Column(
-        Integer, ForeignKey(MediaEntry.id),
-        nullable=False)
-    name = Column(Unicode, nullable=False)
-    filepath = Column(PathTupleWithSlashes)
-    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
-
-
-class Tag(Base_v0):
-    __tablename__ = "core__tags"
-
-    id = Column(Integer, primary_key=True)
-    slug = Column(Unicode, nullable=False, unique=True)
-
-    def __repr__(self):
-        return "<Tag %r: %r>" % (self.id, self.slug)
-
-    @classmethod
-    def find_or_new(cls, slug):
-        t = cls.query.filter_by(slug=slug).first()
-        if t is not None:
-            return t
-        return cls(slug=slug)
-
-
-class MediaTag(Base_v0):
-    __tablename__ = "core__media_tags"
-
-    id = Column(Integer, primary_key=True)
-    media_entry = Column(
-        Integer, ForeignKey(MediaEntry.id),
-        nullable=False, index=True)
-    tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
-    name = Column(Unicode)
-    # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
-
-    __table_args__ = (
-        UniqueConstraint('tag', 'media_entry'),
-        {})
-
-    tag_helper = relationship(Tag)
-    slug = association_proxy('tag_helper', 'slug',
-        creator=Tag.find_or_new
-        )
-
-    def __init__(self, name=None, slug=None):
-        Base_v0.__init__(self)
-        if name is not None:
-            self.name = name
-        if slug is not None:
-            self.tag_helper = Tag.find_or_new(slug)
-
-
-class MediaComment(Base_v0):
-    __tablename__ = "core__media_comments"
-
-    id = Column(Integer, primary_key=True)
-    media_entry = Column(
-        Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
-    author = Column(Integer, ForeignKey(User.id), nullable=False)
-    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
-    content = Column(UnicodeText, nullable=False)
-
-    get_author = relationship(User)
-
-
-class ImageData(Base_v0):
-    __tablename__ = "image__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("image__media_data", cascade="all, delete-orphan"))
-
-    width = Column(Integer)
-    height = Column(Integer)
-    exif_all = Column(JSONEncoded)
-    gps_longitude = Column(Float)
-    gps_latitude = Column(Float)
-    gps_altitude = Column(Float)
-    gps_direction = Column(Float)
-
-
-class VideoData(Base_v0):
-    __tablename__ = "video__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("video__media_data", cascade="all, delete-orphan"))
-
-    width = Column(SmallInteger)
-    height = Column(SmallInteger)
-
-
-class AsciiData(Base_v0):
-    __tablename__ = "ascii__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("ascii__media_data", cascade="all, delete-orphan"))
-
-
-class AudioData(Base_v0):
-    __tablename__ = "audio__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("audio__media_data", cascade="all, delete-orphan"))
-
-
-######################################################
-# Special, migrations-tracking table
-#
-# Not listed in MODELS because this is special and not
-# really migrated, but used for migrations (for now)
-######################################################
-
-class MigrationData(Base_v0):
-    __tablename__ = "core__migrations"
-
-    name = Column(Unicode, primary_key=True)
-    version = Column(Integer, nullable=False, default=0)
-
-######################################################
index daeddb3f199352258988eef024d4022b705cce29..2b8343b853054d43939cf29ded776f5002b0f425 100644 (file)
@@ -268,8 +268,7 @@ def get_media_entry_by_id(controller):
     @wraps(controller)
     def wrapper(request, *args, **kwargs):
         media = MediaEntry.query.filter_by(
-                id=request.matchdict['media_id'],
-                state=u'processed').first()
+                id=request.matchdict['media_id']).first()
         # Still no media?  Okay, 404.
         if not media:
             return render_404(request)
index 69f69da5e7ec9f38a340ca45d2145edb1280d7e6..717241e8b63ef48989c13a1ef36963b904bb4fd5 100644 (file)
@@ -1,4 +1,4 @@
-# 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
@@ -55,6 +55,10 @@ import mimetypes
 @get_media_entry_by_id
 @require_active_login
 def edit_media(request, media):
+    # If media is not processed, return NotFound.
+    if not media.state == u'processed':
+        return render_404(request)
+
     if not may_edit_media(request, media):
         raise Forbidden("User may not edit this media")
 
@@ -66,7 +70,7 @@ def edit_media(request, media):
         license=media.license)
 
     form = forms.EditForm(
-        request.form,
+        request.method=='POST' and request.form or None,
         **defaults)
 
     if request.method == 'POST' and form.validate():
@@ -115,6 +119,10 @@ UNSAFE_MIMETYPES = [
 @get_media_entry_by_id
 @require_active_login
 def edit_attachments(request, media):
+    # If media is not processed, return NotFound.
+    if not media.state == u'processed':
+        return render_404(request)
+
     if mg_globals.app_config['allow_attachments']:
         form = forms.EditAttachmentsForm()
 
@@ -211,7 +219,8 @@ def edit_profile(request, url_user=None):
     else:
         location = user.get_location.name
 
-    form = forms.EditProfileForm(request.form,
+    form = forms.EditProfileForm(
+        request.method == 'POST' and request.form or None,
         url=user.url,
         bio=user.bio,
         location=location)
@@ -227,6 +236,8 @@ def edit_profile(request, url_user=None):
             location = user.get_location
             location.name = six.text_type(form.location.data)
             location.save()
+        else:
+            user.location = None
 
         user.save()
 
@@ -252,7 +263,8 @@ EMAIL_VERIFICATION_TEMPLATE = (
 @require_active_login
 def edit_account(request):
     user = request.user
-    form = forms.EditAccountForm(request.form,
+    form = forms.EditAccountForm(
+        request.method == 'POST' and request.form or None,
         wants_comment_notification=user.wants_comment_notification,
         license_preference=user.license_preference,
         wants_notifications=user.wants_notifications)
@@ -350,7 +362,7 @@ def edit_collection(request, collection):
         description=collection.description)
 
     form = forms.EditCollectionForm(
-        request.form,
+        request.method == 'POST' and request.form or None,
         **defaults)
 
     if request.method == 'POST' and form.validate():
@@ -443,9 +455,11 @@ def verify_email(request):
         user=user.username)
 
 
+@require_active_login
 def change_email(request):
     """ View to change the user's email """
-    form = forms.ChangeEmailForm(request.form)
+    form = forms.ChangeEmailForm(
+        request.method == 'POST' and request.form or None)
     user = request.user
 
     # If no password authentication, no need to enter a password
@@ -498,7 +512,12 @@ def change_email(request):
 @require_active_login
 @get_media_entry_by_id
 def edit_metadata(request, media):
-    form = forms.EditMetaDataForm(request.form)
+    # If media is not processed, return NotFound.
+    if not media.state == u'processed':
+        return render_404(request)
+
+    form = forms.EditMetaDataForm(
+        request.method == 'POST' and request.form or None)
     if request.method == "POST" and form.validate():
         metadata_dict = dict([(row['identifier'],row['value'])
                             for row in form.media_metadata.data])
index 8cbfc806ba3f4932c6111da2165b068b948e01cd..026f349575cc4ed1ba2feae2b5a6c5ab53881aa8 100644 (file)
@@ -56,6 +56,11 @@ def parser_setup(subparser):
         help=(
             "Slug for this media entry. "
             "Will be autogenerated if unspecified."))
+    subparser.add_argument(
+        "-c", "--collection-slug",
+        help=(
+            "Slug of the collection for this media entry. "
+            "Should already exist."))
 
     subparser.add_argument(
         '--celery',
@@ -85,8 +90,6 @@ def addmedia(args):
         print("Can't find a file with filename '%s'" % args.filename)
         return
 
-    upload_limit, max_file_size = get_upload_file_limits(user)
-
     def maybe_unicodeify(some_string):
         # this is kinda terrible
         if some_string is None:
@@ -102,9 +105,9 @@ def addmedia(args):
             submitted_file=open(abs_filename, 'rb'), filename=filename,
             title=maybe_unicodeify(args.title),
             description=maybe_unicodeify(args.description),
+            collection_slug=args.collection_slug,
             license=maybe_unicodeify(args.license),
-            tags_string=maybe_unicodeify(args.tags) or u"",
-            upload_limit=upload_limit, max_file_size=max_file_size)
+            tags_string=maybe_unicodeify(args.tags) or u"")
     except FileUploadLimit:
         print("This file is larger than the upload limits for this site.")
     except UserUploadLimit:
index 2ad7e39e418b4493c96f37cd178ab43268aa79d6..55ed865bd3001c1a60e8e3c627365115ae8dd997 100644 (file)
@@ -19,6 +19,7 @@ from __future__ import print_function
 import codecs
 import csv
 import os
+import sys
 
 import requests
 import six
@@ -28,8 +29,7 @@ from six.moves.urllib.parse import urlparse
 from mediagoblin.db.models import LocalUser
 from mediagoblin.gmg_commands import util as commands_util
 from mediagoblin.submit.lib import (
-    submit_media, get_upload_file_limits,
-    FileUploadLimit, UserUploadLimit, UserPastUploadLimit)
+    submit_media, FileUploadLimit, UserUploadLimit, UserPastUploadLimit)
 from mediagoblin.tools.metadata import compact_and_validate
 from mediagoblin.tools.translate import pass_to_ugettext as _
 from jsonschema.exceptions import ValidationError
@@ -73,9 +73,6 @@ def batchaddmedia(args):
                     username=args.username)))
         return
 
-    upload_limit, max_file_size = get_upload_file_limits(user)
-    temp_files = []
-
     if os.path.isfile(args.metadata_path):
         metadata_path = args.metadata_path
 
@@ -87,7 +84,6 @@ def batchaddmedia(args):
 
     abs_metadata_filename = os.path.abspath(metadata_path)
     abs_metadata_dir = os.path.dirname(abs_metadata_filename)
-    upload_limit, max_file_size = get_upload_file_limits(user)
 
     def maybe_unicodeify(some_string):
         # this is kinda terrible
@@ -101,7 +97,7 @@ def batchaddmedia(args):
         contents = all_metadata.read()
         media_metadata = parse_csv_file(contents)
 
-    for media_id, file_metadata in media_metadata.iteritems():
+    for media_id, file_metadata in media_metadata.items():
         files_attempted += 1
         # In case the metadata was not uploaded initialize an empty dictionary.
         json_ld_metadata = compact_and_validate({})
@@ -115,6 +111,7 @@ def batchaddmedia(args):
         title = file_metadata.get('title') or file_metadata.get('dc:title')
         description = (file_metadata.get('description') or
             file_metadata.get('dc:description'))
+        collection_slug = file_metadata.get('collection-slug')
 
         license = file_metadata.get('license')
         try:
@@ -143,7 +140,7 @@ Metadata was not uploaded.""".format(
                 file_path = os.path.join(abs_metadata_dir, path)
                 file_abs_path = os.path.abspath(file_path)
             try:
-                media_file = file(file_abs_path, 'r')
+                media_file = open(file_abs_path, 'rb')
             except IOError:
                 print(_(u"""\
 FAIL: Local file {filename} could not be accessed.
@@ -157,10 +154,10 @@ FAIL: Local file {filename} could not be accessed.
                 filename=filename,
                 title=maybe_unicodeify(title),
                 description=maybe_unicodeify(description),
+                collection_slug=maybe_unicodeify(collection_slug),
                 license=maybe_unicodeify(license),
                 metadata=json_ld_metadata,
-                tags_string=u"",
-                upload_limit=upload_limit, max_file_size=max_file_size)
+                tags_string=u"")
             print(_(u"""Successfully submitted {filename}!
 Be sure to look at the Media Processing Panel on your website to be sure it
 uploaded successfully.""".format(filename=filename)))
@@ -206,7 +203,12 @@ def parse_csv_file(file_contents):
     # Build a dictionary
     for index, line in enumerate(lines):
         if line.isspace() or line == u'': continue
-        values = unicode_csv_reader([line]).next()
+        if (sys.version_info[0] == 3):
+            # Python 3's csv.py supports Unicode out of the box.
+            reader = csv.reader([line])
+        else:
+            reader = unicode_csv_reader([line])
+        values = next(reader)
         line_dict = dict([(key[i], val)
             for i, val in enumerate(values)])
         media_id = line_dict.get('id') or index
index bafe76bb01e96e543d322d3786ecbf1b406c2225..2700ccbc926b9438ecaf24f4405391ffe20dce2e 100644 (file)
@@ -133,7 +133,9 @@ def run_alembic_migrations(db, app_config, global_config):
     session = Session()
     cfg = build_alembic_config(global_config, None, session)
 
-    return command.upgrade(cfg, 'heads')
+    res = command.upgrade(cfg, 'heads')
+    session.commit()
+    return res
 
 
 def run_dbupdate(app_config, global_config):
@@ -146,7 +148,7 @@ def run_dbupdate(app_config, global_config):
     # Set up the database
     db = setup_connection_and_db_from_config(app_config, migrations=True)
 
-    # Do we have migrations 
+    # Do we have migrations
     should_run_sqam_migrations = db.engine.has_table("core__migrations") and \
                                  sqam_migrations_to_run(db, app_config,
                                                         global_config)
index 8fd27b6285c87b936786821dfe77e2a902ad791a..bdc63e5626c5ee5cba39563e6f5312e42ba4803c 100644 (file)
@@ -2645,7 +2645,7 @@ msgstr "más antiguo"
 
 #: mediagoblin/templates/mediagoblin/utils/profile.html:36
 msgid "Location"
-msgstr "Locación"
+msgstr "Lugar"
 
 #: mediagoblin/templates/mediagoblin/utils/report.html:25
 msgid "Report media"
index a9189e8d31e6160334186ee6415f49b6f7454a59..fe4691561e944b69e24416f8be68a81c4145eb4e 100644 (file)
@@ -123,6 +123,7 @@ def read_mediagoblin_config(config_path, config_spec_path=CONFIG_SPEC_PATH):
     config = ConfigObj(
         config_path,
         configspec=config_spec,
+        encoding="UTF8",
         interpolation="ConfigParser")
 
     _setup_defaults(config, config_path, mainconfig_defaults)
index f640cc9521281ee748ca7215cd2e727bb5d99558..6e1528ca210d208a418a44ac98304770ba8aea9e 100644 (file)
 from mediagoblin import mg_globals
 from mediagoblin.db.models import MediaEntry
 from mediagoblin.db.util import media_entries_for_tag_slug
+from mediagoblin.decorators import uses_pagination
+from mediagoblin.plugins.api.tools import get_media_file_paths
 from mediagoblin.tools.pagination import Pagination
 from mediagoblin.tools.response import render_to_response
-from mediagoblin.decorators import uses_pagination
 
 from werkzeug.contrib.atom import AtomFeed
 
@@ -72,19 +73,25 @@ def atom_feed(request):
     tag_slug = request.matchdict.get(u'tag')
     feed_title = "MediaGoblin Feed"
     if tag_slug:
-        cursor = media_entries_for_tag_slug(request.db, tag_slug)
+        feed_title += " for tag '%s'" % tag_slug
         link = request.urlgen('mediagoblin.listings.tags_listing',
                               qualified=True, tag=tag_slug )
-        feed_title += "for tag '%s'" % tag_slug
+        cursor = media_entries_for_tag_slug(request.db, tag_slug)
     else: # all recent item feed
-        cursor = MediaEntry.query.filter_by(state=u'processed')
+        feed_title += " for all recent items"
         link = request.urlgen('index', qualified=True)
-        feed_title += "for all recent items"
+        cursor = MediaEntry.query.filter_by(state=u'processed')
+    cursor = cursor.order_by(MediaEntry.created.desc())
+    cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
+
 
-    atomlinks = [
-        {'href': link,
-         'rel': 'alternate',
-         'type': 'text/html'}]
+    """
+    ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
+    """
+    atomlinks = [{
+        'href': link,
+        'rel': 'alternate',
+        'type': 'text/html'}]
 
     if mg_globals.app_config["push_urls"]:
         for push_url in mg_globals.app_config["push_urls"]:
@@ -92,9 +99,6 @@ def atom_feed(request):
                 'rel': 'hub',
                 'href': push_url})
 
-    cursor = cursor.order_by(MediaEntry.created.desc())
-    cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
-
     feed = AtomFeed(
         feed_title,
         feed_url=request.url,
@@ -102,19 +106,30 @@ def atom_feed(request):
         links=atomlinks)
 
     for entry in cursor:
-        feed.add(entry.get('title'),
-            entry.description_html,
-            id=entry.url_for_self(request.urlgen,qualified=True),
+        # Include a thumbnail image in content.
+        file_urls = get_media_file_paths(entry.media_files, request.urlgen)
+        if 'thumb' in file_urls:
+            content = u'<img src="{thumb}" alt='' /> {desc}'.format(
+                thumb=file_urls['thumb'], desc=entry.description_html)
+        else:
+            content = entry.description_html
+
+        feed.add(
+            entry.get('title'),
+            content,
+            id=entry.url_for_self(request.urlgen, qualified=True),
             content_type='html',
-            author={'name': entry.get_actor.username,
+            author={
+                'name': entry.get_actor.username,
                 'uri': request.urlgen(
                     'mediagoblin.user_pages.user_home',
-                    qualified=True, user=entry.get_actor.username)},
+                    qualified=True,
+                    user=entry.get_actor.username)},
             updated=entry.get('created'),
             links=[{
-                'href':entry.url_for_self(
-                   request.urlgen,
-                   qualified=True),
+                'href': entry.url_for_self(
+                    request.urlgen,
+                    qualified=True),
                 'rel': 'alternate',
                 'type': 'text/html'}])
 
index 0e1ddf974350f3f72ce724655a63a167b76a5f85..83f520c7ae86a9aad288574bd2f221bb59bf84ea 100644 (file)
@@ -34,6 +34,7 @@ class BlogMixin(GenerateSlugMixin):
     def check_slug_used(self, slug):
         return check_blog_slug_used(self.author, slug, self.id)
 
+BLOG_BACKREF_NAME = "mediatype__blogs"
 
 class Blog(Base, BlogMixin):
     __tablename__ = "mediatype__blogs"
@@ -43,6 +44,7 @@ class Blog(Base, BlogMixin):
     author = Column(Integer, ForeignKey(User.id), nullable=False, index=True) #similar to uploader
     created = Column(DateTime, nullable=False, default=datetime.datetime.now, index=True)
     slug = Column(Unicode)
+    get_author = relationship("User", backref=backref(BLOG_BACKREF_NAME, cascade="all, delete-orphan"))
 
     @property
     def slug_or_id(self):
@@ -66,7 +68,7 @@ class Blog(Base, BlogMixin):
         
     
     
-BACKREF_NAME = "blogpost__media_data"
+BLOG_POST_BACKREF_NAME = "blogpost__media_data"
 
 class BlogPostData(Base):
     __tablename__ = "blogpost__mediadata"
@@ -75,7 +77,7 @@ class BlogPostData(Base):
     media_entry = Column(Integer, ForeignKey('core__media_entries.id'), primary_key=True)
     blog = Column(Integer, ForeignKey('mediatype__blogs.id'), nullable=False)
     get_media_entry = relationship("MediaEntry",
-        backref=backref(BACKREF_NAME, uselist=False,
+        backref=backref(BLOG_POST_BACKREF_NAME, uselist=False,
                         cascade="all, delete-orphan"))
 
 
index 3b881466ab7b7f7a53ce2d686ea3718c2bc3ef44..97408b59ac2849d177971343cbd75311926ab26c 100644 (file)
@@ -53,7 +53,7 @@
     {% set blog_delete_url = request.urlgen('mediagoblin.media_types.blog.blog_delete',
                                       blog_slug=blog.slug,
                                       user=request.user.username) %}
-<a class="button_action" href="{{ blog_delete_url }}">
+<a class="button_action button_warning" href="{{ blog_delete_url }}">
 {%- trans %}Delete Blog{% endtrans -%}
 </a>
 </p>
@@ -90,7 +90,7 @@
                                  media_id=blog_post.id) %}
             <td>
             <a class="button_action" href="{{ blogpost_edit_url }}">{% trans %}Edit{% endtrans %}</a>
-            <a class="button_action" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a>
+            <a class="button_action button_warning" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a>
             </td>
         </tr>
         {% endfor %}
index 6d82055042d261048f491a90c1fd8cf158b47f90..7a53cc9ed1e5bb87382a8053eacabdc40fad9f18 100644 (file)
@@ -33,7 +33,7 @@
                              user= blogpost.get_actor.username,
                              media_id=blogpost.id) %}
             <a class="button_action" href="{{ blogpost_edit_url }}">{% trans %}Edit{% endtrans %}</a>
-            <a class="button_action" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a>
+            <a class="button_action button_warning" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a>
 
 {% endblock %}
   
index 8c16daebcec0457a2ea6eca67c1bcf386886b519..bad33c8c7a1cabfa4d46c8c711855e2ff6e259f0 100644 (file)
@@ -52,7 +52,7 @@
         {% if request.user and request.user.username==user.username %}
             <p>You have not created any blog yet.</p>
         {% else %}
-            <p>No blog has been created by <strong>{{ user.username }}</strong>yet.</p>
+            <p>No blog has been created by <strong>{{ user.username }}</strong> yet.</p>
         {% endif %}
     {% endif %}
     <br/>
index d48cf82f8a9900a7b6bb32b7e9904245f8c67459..288a47ae08451239447a76e7c20dcda0f066b6d2 100644 (file)
@@ -376,7 +376,9 @@ def blog_about_view(request):
     user = request.db.LocalUser.query.filter(
         LocalUser.username==url_user
     ).first()
-    blog = get_blog_by_slug(request, blog_slug, author=user.id)
+
+    if user:
+        blog = get_blog_by_slug(request, blog_slug, author=user.id)
 
     if not user or not blog:
         return render_404(request)
index 4742b3427d6d4f9cc04a515b5f7141435192ff0a..da635ed7b1eef4bc1e9df4d25ef1ace0c8d7a9fe 100644 (file)
@@ -69,7 +69,7 @@ class VideoData(Base):
         orig_metadata = self.orig_metadata or {}
 
         if ("webm_video" not in self.get_media_entry.media_files
-           and "mimetype" in orig_metadata['common']['tags']
+           and "mimetype" in orig_metadata.get('common', {}).get('tags', {})
            and "codec" in orig_metadata['audio']
            and "codec" in orig_metadata['video']):
             if orig_metadata['mimetype'] == 'application/ogg':
index ca3087a2c399564cf2f2f18557dee6d76c9fde83..71204fc775c362f3c5ba2824e9250af0aa7e13d8 100644 (file)
@@ -79,7 +79,17 @@ def sniffer(media_file):
     return MEDIA_TYPE
 
 
+EXCLUDED_EXTS = ["nef", "svg"]
+
 def sniff_handler(media_file, filename):
+    name, ext = os.path.splitext(filename)
+    clean_ext = ext.lower()[1:]
+
+    if clean_ext in EXCLUDED_EXTS:
+        # We don't handle this filetype, though gstreamer might think we can
+        _log.info('Refused to process {0} due to excluded extension'.format(filename))
+        return None
+
     try:
         return sniffer(media_file)
     except:
@@ -106,10 +116,13 @@ def get_tags(stream_info):
         # TODO: handle timezone info; gst.get_time_zone_offset +
         # python's tzinfo should help
         dt = tags['datetime']
-        tags['datetime'] = datetime.datetime(
-            dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(),
-            dt.get_minute(), dt.get_second(),
-            dt.get_microsecond()).isoformat()
+        try:
+            tags['datetime'] = datetime.datetime(
+                dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(),
+                dt.get_minute(), dt.get_second(),
+                dt.get_microsecond()).isoformat()
+        except:
+            tags['datetime'] = None
     for k, v in tags.copy().items():
         # types below are accepted by json; others must not present
         if not isinstance(v, (dict, list, six.string_types, int, float, bool,
index f4b0341ed296a0f5aa2e1bb702cf726687661bf9..2d3392f231d30efc268a4532c162daa2f6585db6 100644 (file)
@@ -31,7 +31,7 @@ sys.argv = []
 
 import gi
 gi.require_version('Gst', '1.0')
-from gi.repository import GObject, Gst
+from gi.repository import GLib, Gst
 Gst.init(None)
 # init before import to work around https://bugzilla.gnome.org/show_bug.cgi?id=736260
 from gi.repository import GstPbutils
@@ -154,7 +154,7 @@ class VideoTranscoder(object):
     def __init__(self):
         _log.info('Initializing VideoTranscoder...')
         self.progress_percentage = None
-        self.loop = GObject.MainLoop()
+        self.loop = GLib.MainLoop()
 
     def transcode(self, src, dst, **kwargs):
         '''
@@ -371,11 +371,11 @@ class VideoTranscoder(object):
             self.pipeline.set_state(Gst.State.NULL)
 
         # This kills the loop, mercifully
-        GObject.idle_add(self.__stop_mainloop)
+        GLib.idle_add(self.__stop_mainloop)
 
     def __stop_mainloop(self):
         '''
-        Wrapper for GObject.MainLoop.quit()
+        Wrapper for GLib.MainLoop.quit()
 
         This wrapper makes us able to see if self.loop.quit has been called
         '''
index 233410654711bfbf67a2d14df36d09f8e5760ac2..fdd22acebc2f0890071b8c1ed902e065abc1c15d 100644 (file)
@@ -52,8 +52,6 @@ def post_entry(request):
         _log.debug('File field not found')
         raise BadRequest()
 
-    upload_limit, max_file_size = get_upload_file_limits(request.user)
-
     callback_url = request.form.get('callback_url')
     if callback_url:
         callback_url = six.text_type(callback_url)
@@ -66,7 +64,6 @@ def post_entry(request):
             description=six.text_type(request.form.get('description')),
             license=six.text_type(request.form.get('license', '')),
             tags_string=six.text_type(request.form.get('tags', '')),
-            upload_limit=upload_limit, max_file_size=max_file_size,
             callback_url=callback_url)
 
         return json_response(get_entry_serializable(entry, request.urlgen))
index 82f247ed4269a308848c1dff8e7aeed2d57e5c86..87a7b16f7aabb0810d6e1e018b0f5c6d7b566e76 100644 (file)
@@ -5,7 +5,7 @@
 ===================
 
 The basic_auth plugin is enabled by default in mediagoblin.ini. This plugin
-provides basic username and password authentication for GNU Mediagoblin.
+provides basic username and password authentication for GNU MediaGoblin.
 
 This plugin can be enabled alongside :ref:`openid-chapter` and
 :ref:`persona-chapter`.
index 9a6db226b2b32be64cd730347fbd61582c061b24..3d684e91b18fbcf81464771bec81f270dd3f164c 100644 (file)
@@ -38,7 +38,7 @@ class LoginForm(wtforms.Form):
     username = wtforms.StringField(
         _('Username or Email'),
         [wtforms.validators.InputRequired(),
-         normalize_user_or_email_field()])
+         normalize_user_or_email_field(is_login=True)])
     password = wtforms.PasswordField(
         _('Password'))
     stay_logged_in = wtforms.BooleanField(
index 59cd6217a94481ecb6c3aee14283e90e333e0353..0354a46c506a20df49c69d46159376faed8ac86d 100644 (file)
@@ -57,7 +57,7 @@ Examples: ``flatpages-about``, ``about-view``, ``contact-view``, ...
 
 The value has two parts separated by commas:
 
-1. **route path**: This is the url that this route matches.
+1. **route path**: This is the URL that this route matches.
 
    Examples: ``/about``, ``/contact``, ``/pages/about``, ...
 
@@ -74,7 +74,7 @@ The value has two parts separated by commas:
 
       For example: ``'/siteadmin/{adminname:\w+}'``
 
-2. **template**: The template to use for this url. The template is in
+2. **template**: The template to use for this URL. The template is in
    the flatpagesfile template directory, so you just need to specify
    the file name.
 
@@ -139,10 +139,10 @@ template::
 Recipes
 =======
 
-Url variables
+URL variables
 -------------
 
-You can handle urls like ``/about/{name}`` and access the name that's
+You can handle URLs like ``/about/{name}`` and access the name that's
 passed in in the template.
 
 Sample route::
index 87f790d1cb303cbf082d91f5596653fc2b750138..be608ac0e2f3db0cf95b35ac600ff23ae8d84de3 100644 (file)
@@ -43,8 +43,9 @@
             href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>
           contributors
           </li><li>Imaging &copy;<a
-          href="http://mapquest.com">MapQuest</a></li><li>Maps powered by
-          <a href="http://leafletjs.com/"> Leaflet</a></li></ul>
+          href="https://www.openstreetmap.org">OpenStreetMap
+         contributors</a></li><li>Maps powered by
+          <a href="http://leafletjs.com/">Leaflet</a></li></ul>
         </div>
         <p>
          <small>
index ea9a34b3c553dd6605c9e9bb9851b40a4a4b8102..049b5c4d93c405450162120257acfe08ac4c7e11 100644 (file)
 .. _ldap-plugin:
 
 =============
ldap plugin
LDAP plugin
 =============
 
 .. Warning::
    This plugin is not compatible with the other authentication plugins.
 
-This plugin allow your GNU Mediagoblin instance to authenticate against an
+This plugin allow your GNU MediaGoblin instance to authenticate against an
 LDAP server.
 
-Set up the ldap plugin
+Set up the LDAP plugin
 ======================
 
 1. Install the ``python-ldap`` package.
@@ -32,13 +32,13 @@ Set up the ldap plugin
 
     [[mediagoblin.plugins.ldap]]
 
-Configuring the ldap plugin
+Configuring the LDAP plugin
 ===========================
 
-This plugin allows you to use multiple ldap servers for authentication.
+This plugin allows you to use multiple LDAP servers for authentication.
 
 In order to configure a server, add the following to you MediaGoblin .ini file
-under the ldap plugin:: 
+under the LDAP plugin:: 
 
     [[mediagoblin.plugins.ldap]]
     [[[server1]]]
@@ -50,15 +50,15 @@ under the ldap plugin::
 Make any necessary changes to the above to work with your sever. Make sure
 ``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE.
    
-If you would like to fetch the users email from the ldap server upon account
+If you would like to fetch the users email from the LDAP server upon account
 registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and
 ``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your
 MediaGoblin .ini file.
 
 .. Warning::
    By default, this plugin provides no encryption when communicating with the
-   ldap servers. If you would like to use an SSL connection, change
-   LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default ldap
+   LDAP servers. If you would like to use an SSL connection, change
+   LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default LDAP
    port for SSL connections is 636. If you would like to use a TLS connection,
    add ``LDAP_START_TLS = 'true'`` under your server configuration in your
    MediaGoblin .ini file.
diff --git a/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html.orig b/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html.orig
deleted file mode 100644 (file)
index 2bd1a14..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-{#
-# 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/>.
-#}
-
-<<<<<<< HEAD:mediagoblin/templates/mediagoblin/utils/metadata_table.html
-{%- macro render_table(request, media_entry, format_predicate) %}
-  {%- set metadata=media_entry.media_metadata %}
-  {%- set metadata_context=metadata['@context'] %}
-  {%- if metadata %}
-      <h3>{% trans %}Metadata Information{% endtrans %}</h3>
-      <table class="metadata_info">
-        {%- for key, value in metadata.iteritems() if (
-            not key=='@context' and value) %}
-            <tr {% if loop.index%2 == 1 %}class="highlight"{% endif %}>
-              <th>{{ format_predicate(key) }}</th>
-              <td property="{{ key }}">
-                {{ value }}</td>
-            </tr>
-        {%- endfor %}
-      </table>
-  {% endif %}
-  {% if request.user and request.user.has_privilege('admin') %}
-      <a href="{{ request.urlgen('mediagoblin.edit.metadata',
-                    user=media_entry.get_uploader.username,
-                    media_id=media_entry.id) }}">
-        {% trans %}Edit Metadata{% endtrans %}</a>
-  {% endif %}
-{%- endmacro %}
-=======
-{%- set metadata=media.media_metadata %}
-{%- set metadata_context=metadata['@context'] %}
-{%- if metadata %}
-  {#- NOTE: In some smart future where the context is more extensible,
-        we will need to add to the prefix here-#}
-  <table>
-    {%- for key, value in metadata.iteritems() if not key=='@context' %}
-      {% if value -%}
-        <tr>
-          <td>{{ rdfa_to_readable(key) }}</td>
-          <td property="{{ key }}">{{ value }}</td>
-        </tr>
-      {%- endif -%}
-    {%- endfor %}
-  </table>
-{% endif %}
->>>>>>> acfcaf6366bd4695c1c37c7aa8ff5a176b412e2a:mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html
index 870a2b584892d1edb677fc59cf545cee7c7d87d5..1a777336d68574554fdd58e8ffda5deb07e4416b 100644 (file)
@@ -1,23 +1,23 @@
 .. _openid-chapter:
 
 ===================
openid plugin
OpenID plugin
 ===================
 
-The openid plugin allows user to login to your GNU Mediagoblin instance using
-their openid url.
+The OpenID plugin allows user to login to your GNU MediaGoblin instance using
+their OpenID URL.
 
 This plugin can be enabled alongside :ref:`basic_auth-chapter` and
 :ref:`persona-chapter`.
 
 .. note::
-    When :ref:`basic_auth-chapter` is enabled alongside this openid plugin, and
-    a user creates an account using their openid. If they would like to add a
+    When :ref:`basic_auth-chapter` is enabled alongside this OpenID plugin, and
+    a user creates an account using their OpenID. If they would like to add a
     password to their account, they can use the forgot password feature to do
     so.
 
 
-Set up the openid plugin
+Set up the OpenID plugin
 ============================
 
 1. Install the ``python-openid`` package.
index ab741a7243cca6caff177760d0252ee306c9c5a6..30c7ffa287a66b09fc7154dd3a58197796198d6b 100644 (file)
@@ -128,16 +128,13 @@ def pwg_images_addSimple(request):
     if not check_file_field(request, 'image'):
         raise BadRequest()
 
-    upload_limit, max_file_size = get_upload_file_limits(request.user)
-
     try:
         entry = submit_media(
             mg_app=request.app, user=request.user,
             submitted_file=request.files['image'],
             filename=request.files['image'].filename,
             title=six.text_type(form.name.data),
-            description=six.text_type(form.comment.data),
-            upload_limit=upload_limit, max_file_size=max_file_size)
+            description=six.text_type(form.comment.data))
 
         collection_id = form.category.data
         if collection_id > 0:
index db9a0c53700b4e67342b54ed12eb536171b1ec7e..d83af06b70cb18f037a3d5d43ca6cef48cdf5768 100644 (file)
@@ -2,7 +2,7 @@
  Trim whitespace plugin
 =======================
 
-Mediagoblin templates are written with 80 char limit for better
+MediaGoblin templates are written with 80 char limit for better
 readability. However that means that the HTML output is very verbose
 containing *lots* of whitespace. This plugin inserts a middleware that
 filters out whitespace from the returned HTML in the ``Response()``
index 5e0e772d9fde83168439ee7dae9d2538349d252e..bedfd32d8bdfc549db68c79a038cddf1d8fc436f 100644 (file)
@@ -69,6 +69,9 @@ class ProcessMedia(celery.Task):
     """
     Pass this entry off for processing.
     """
+
+    name = 'process_media'
+
     def run(self, media_id, feed_url, reprocess_action, reprocess_info=None):
         """
         Pass the media entry off to the appropriate processing function
index 5c50e7274e026a524b26b8492d7c6fa5527400cf..de388094fadf993266dddc4d0df08b1d0245eaf9 100644 (file)
     font-size: 40px;
     width: 50px;
     text-shadow: 0 0 10px black;
+    background: none;
+    border: none;
 }
     .audio-control-play-pause.playing {
         color: #b71500;
-        letter-spacing: -17px;
         margin-left: -7px;
     }
     .audio-control-play-pause.paused {
index 7852cae99e7a535dc6d62f00572ff3f461807a4c..6da19f9499d40fa7b592ca224914a3bb0aaa443d 100644 (file)
@@ -394,6 +394,12 @@ text-align: center;
   margin-right: auto;
 }
 
+.form_box > h1, .form_box_xl > h1 {
+  /* Fix header overflowing issue. */
+  overflow: hidden;
+  text-overflow: ellipsis
+}
+
 .form_box_xl {
   max-width: 460px;
 }
@@ -456,12 +462,10 @@ text-align: center;
   margin-bottom: 10px;
 }
 
-.form_field_label {
-  margin-bottom: 4px;
-}
-
 .form_field_label {
   font-size:1.125em;
+  margin-bottom: 0;
+  padding: 10px 0;
 }
 
 .form_field_description {
@@ -585,7 +589,6 @@ ul#action_to_resolve {list-style:none; margin-left:10px;}
   border-radius: 0 0 5px 5px;
   padding: 0 0 6px;
   text-overflow: ellipsis;
-  white-space: nowrap;
   overflow: hidden;
   border-color: #0D0D0D;
   border-style: solid;
index 50d58cd9b066263152062f88b15ded4893f6d2f0..59a8c801cad5a5588222f2ec2cb0e2284c6d6d46 100644 (file)
@@ -116,6 +116,10 @@ var audioPlayer = new Object();
         var im = audioPlayer.imageElement;
         var pos = (e.offsetX || e.originalEvent.layerX) / im.width();
 
+        console.log('pos', (e.offsetX || e.originalEvent.layerX) / im.width())
+        console.log('setting current time to',
+            pos * audioPlayer.audioElement.duration)
+
         audioPlayer.audioElement.currentTime = pos * audioPlayer.audioElement.duration;
         audioPlayer.audioElement.play();
         audioPlayer.setState(audioPlayer.PLAYING);
@@ -151,14 +155,16 @@ var audioPlayer = new Object();
 
         switch (state) {
             case audioPlayer.PLAYING:
-                $('.audio-spectrogram .audio-control-play-pause')
+                el = $('.audio-spectrogram .audio-control-play-pause')
                     .removeClass('paused').addClass('playing')
-                    .text('▮▮');
+                    .text('▮▮').attr('aria-label', 'Pause');
+                el[0].setAttribute('aria-label', 'Pause')
                 break;
             case audioPlayer.PAUSED:
-                $('.audio-spectrogram .audio-control-play-pause')
+                el = $('.audio-spectrogram .audio-control-play-pause')
                     .removeClass('playing').addClass('paused')
-                    .text('▶');
+                    .text('▶').attr('aria-label', 'Play');
+                el[0].setAttribute('aria-label', 'Play')
                 break;
         }
     };
@@ -200,19 +206,9 @@ var audioPlayer = new Object();
          * Attach the player to an image element
          */
         console.log(imageElement);
-
         var im = $(imageElement);
-
         audioPlayer.imageElement = im;
 
-        $('<div class="playhead"></div>').appendTo(im.parent());
-        $('<div class="buffered-indicators"></div>').appendTo(im.parent());
-        $('<div class="seekbar"></div>').appendTo(im.parent());
-        $('<div class="audio-control-play-pause paused">▶</div>').appendTo(im.parent());
-        $('<div class="audio-currentTime">00:00</div>').appendTo(im.parent());
-        $('<input type="range" class="audio-volume"'
-                +'value="1" min="0" max="1" step="0.001" />').appendTo(im.parent());
-        $('.audio-spectrogram').trigger('attachedControls');
     };
 })(audioPlayer);
 
index 26d94c5d844ec86a461946036e044f26b95d878f..c30788f77caa94c389d41ae0901c2e40c3ec35e5 100644 (file)
@@ -30,13 +30,11 @@ $(document).ready(function () {
     // Get a new map instance attached and element with id="tile-map"
     var map = new L.Map('tile-map');
 
-    var mqtileUrl = 'http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg';
+    var mqtileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
     var mqtileAttrib = '<a id="osm_license_link">see map license</a>';
     var mqtile = new L.TileLayer(
        mqtileUrl,
-       {maxZoom: 18,
-        attribution: mqtileAttrib,
-        subdomains: '1234'});
+       {maxZoom: 18});
 
     map.attributionControl.setPrefix('');
     var location = new L.LatLng(latitude, longitude);
index 3ee46228f20fed1ad8703df3448cd563d0b5aff5..979d26908107fd09ea2d3a0f38e74d5de8c25291 100644 (file)
  */
 
 $(document).ready(function(){
-  $("#header_dropdown").hide();
-  $(".header_dropdown_up").hide();
-  $(".header_dropdown_down,.header_dropdown_up").click(function() {
+  // The header drop-down header panel defaults to open until you explicitly
+  // close it. After that, the panel open/closed setting will persist across
+  // page loads.
+
+  // Initialise the panel status when page is loaded.
+  if (localStorage.getItem("panel_closed")) {
+    $("#header_dropdown").hide();
+    $(".header_dropdown_up").hide();
+  }
+  else {
+    $(".header_dropdown_down").hide();
+  }
+
+  // Toggle and persist the panel status.
+  $(".header_dropdown_down, .header_dropdown_up").click(function() {
+    if (localStorage.getItem("panel_closed")) {
+      localStorage.removeItem("panel_closed");
+    }
+    else {
+      localStorage.setItem("panel_closed", "true");
+    }
     $(".header_dropdown_down").toggle();
     $(".header_dropdown_up").toggle();
     $("#header_dropdown").slideToggle();
diff --git a/mediagoblin/static/js/post_comment.js b/mediagoblin/static/js/post_comment.js
new file mode 100644 (file)
index 0000000..431c222
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * 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/>.
+ */
+
+$(document).ready(function(){
+    $(function() {
+        // Hide this button if script is enabled
+        $('.form_submit_buttons').find('input').hide();
+
+        // Include this link if script is enabled
+        $('.form_submit_buttons').append(
+            '<a class="button_action" id="post_comment" type="button">' +
+            'Add this comment </a>');
+
+        $('#post_comment').click(function() {
+            $.ajax({
+                url: $('#postCommentURL').val(),
+                data: $('#form_comment').serialize(),
+                type: 'POST',
+                success: function(response) {
+                    var message = $(response).find('.mediagoblin_messages');
+                    var commentsInResponse = $($(response).find('.media_comments')).find('li');
+                    var commentsInPage = $('.media_comments').find('ul');
+                    
+                    // Post the message
+                    message.css({"position":"fixed", "top":"50px", "width":"100%"});
+                    $('body').append(message);
+                    message.delay(1500).fadeOut();
+
+                    // Checking if there is new comment
+                    if(commentsInResponse.length != $(commentsInPage).find('li').length) {
+                        // Post comment and scroll down to it
+                        var newComment = commentsInResponse[commentsInResponse.length - 1];
+                        $('#form_comment').fadeOut('fast');
+                        $('#button_addcomment').fadeIn('fast');
+                        $('#comment_preview').replaceWith("<div id=comment_preview></div>");
+                        $(commentsInPage).append(newComment);
+                        $('html, body').animate({
+                            scrollTop: $(newComment).offset().top
+                        }, 1000);
+                    }
+                },
+                error: function(error) {
+                    console.log(error);
+                }
+            });
+        });
+    });
+});
\ No newline at end of file
index b3fbc86224acbb895b038f82bb3bd52046e48254..12935124cd448cf36d24bcba2b164ca5f3277c65 100644 (file)
@@ -18,7 +18,7 @@
 
 $(document).ready(function(){
   //Create a duplicate password field. We could change the input type dynamically, but this angers the IE gods (not just IE6).
-  $("#password").after('<input type="text" value="" name="password_clear" id="password_clear" /><label><input type="checkbox" id="password_boolean" />Show password</label>');
+  $("#password").after('<input type="text" value="" name="password_clear" id="password_clear" style="width:100%" /><label><br/><input type="checkbox" id="password_boolean" />Show password</label>');
   $('#password_clear').hide();
   $('#password_boolean').click(function(){
     if($('#password_boolean').prop("checked")) {
index 2edea70f193242a2d36f86cd56a02e6984355bcf..08a603e9aa317cc9b41e25156db9967a2aa4a07a 100644 (file)
@@ -27,11 +27,12 @@ from mediagoblin import mg_globals
 from mediagoblin.tools.response import json_response
 from mediagoblin.tools.text import convert_to_tag_list_of_dicts
 from mediagoblin.tools.federation import create_activity, create_generator
-from mediagoblin.db.models import MediaEntry, ProcessingMetaData
+from mediagoblin.db.models import Collection, MediaEntry, ProcessingMetaData
 from mediagoblin.processing import mark_entry_failed
 from mediagoblin.processing.task import ProcessMedia
 from mediagoblin.notifications import add_comment_subscription
 from mediagoblin.media_types import sniff_media
+from mediagoblin.user_pages.lib import add_media_to_collection
 
 
 _log = logging.getLogger(__name__)
@@ -101,9 +102,8 @@ class UserPastUploadLimit(UploadLimitError):
 
 
 def submit_media(mg_app, user, submitted_file, filename,
-                 title=None, description=None,
+                 title=None, description=None, collection_slug=None,
                  license=None, metadata=None, tags_string=u"",
-                 upload_limit=None, max_file_size=None,
                  callback_url=None, urlgen=None,):
     """
     Args:
@@ -116,15 +116,15 @@ def submit_media(mg_app, user, submitted_file, filename,
        one on disk being referenced by submitted_file.
      - title: title for this media entry
      - description: description for this media entry
+     - collection_slug: collection for this media entry
      - license: license for this media entry
      - tags_string: comma separated string of tags to be associated
        with this entry
-     - upload_limit: size in megabytes that's the per-user upload limit
-     - max_file_size: maximum size each file can be that's uploaded
      - callback_url: possible post-hook to call after submission
      - urlgen: if provided, used to do the feed_url update and assign a public
                ID used in the API (very important).
     """
+    upload_limit, max_file_size = get_upload_file_limits(user)
     if upload_limit and user.uploaded >= upload_limit:
         raise UserPastUploadLimit()
 
@@ -205,6 +205,13 @@ def submit_media(mg_app, user, submitted_file, filename,
     create_activity("post", entry, entry.actor)
     entry.save()
 
+    # add to collection
+    if collection_slug:
+        collection = Collection.query.filter_by(slug=collection_slug,
+                                                actor=user.id).first()
+        if collection:
+            add_media_to_collection(collection, entry)
+
     # Pass off to processing
     #
     # (... don't change entry after this point to avoid race
index be47361534ca123ada3777f519cf03afaee716a6..7bbfb64502a252c3d71b59e33ab1fadb4e99e302 100644 (file)
@@ -76,7 +76,6 @@ def submit_start(request):
                     description=six.text_type(submit_form.description.data),
                     license=six.text_type(submit_form.license.data) or None,
                     tags_string=submit_form.tags.data,
-                    upload_limit=upload_limit, max_file_size=max_file_size,
                     urlgen=request.urlgen)
 
                 if submit_form.collection and submit_form.collection.data:
index 97bdd9cab2779977088c60315bb7d6d830487f46..e4d3a132019eca1be461dd95df26ebd2c14600b7 100644 (file)
@@ -27,7 +27,5 @@
 
 <h4>{% trans %}Copy and paste this <strong>verifier code</strong> into your client:{% endtrans %}</h4>
 
-<p class="verifier">
-   {{ oauth_request.verifier }}
-</p>
+<p class="verifier">{{ oauth_request.verifier }}</p>
 {% endblock %}
index a7b8033f9aa1092b12b3d0fa92e0c4cd9ca5dc9d..b52ecff4117f029376d3b6fe9fbf0a642f09f6cc 100644 (file)
@@ -37,6 +37,7 @@
       {% template_hook("register_link") %}
       {{ wtforms_util.render_divs(register_form, True) }}
       {{ csrf_token }}
+      {% template_hook("register_captcha") %}
       <div class="form_submit_buttons">
         <input type="submit" value="{% trans %}Create{% endtrans %}"
                class="button_form" />
index 7571f8636f5c20ec98ad0ec3b4370a7862bf606e..191eff1482d078abf04e0988bc26eab5e78d6bf3 100644 (file)
     <div class="audio-media">
       {% if 'spectrogram' in media.media_files %}
         <div class="audio-spectrogram">
+          <div class="playhead"></div>
+          <div class="buffered-indicators"></div>
+          <div class="seekbar"></div>
+          <button class="audio-control-play-pause paused" aria-label="Play">▶</button>
+          <div class="audio-currentTime" aria-label="current time">00:00</div>
+          <input type="range" class="audio-volume" value="1" min="0" max="1" step="0.001" aria-label="volume" />
          <img src="{{ request.app.public_store.file_url(
                        media.media_files.spectrogram) }}"
               alt="Spectrogram" />
index 94d4a1a0fde772d57177b3997b602df2f62ad87b..e60f5e9832c98232d3a392fb2a5436dbc57daae4 100644 (file)
@@ -35,6 +35,7 @@
 {% if processing_entries.count() %}
   <table class="media_panel processing">
     <tr>
+      <th>{% trans %}Thumbnail{% endtrans %}</th>
       <th>{% trans %}ID{% endtrans %}</th>
       <th>{% trans %}User{% endtrans %}</th>
       <th>{% trans %}Title{% endtrans %}</th>
index 261b21e7255c4b4703c87410321f1e01d33f0806..24328725717d51c89edd1ca1f774fd0b818e87a1 100644 (file)
@@ -77,7 +77,7 @@
       {% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete',
                                  user= media.get_actor.username,
                                  media_id=media.id) %}
-      <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
+      <a class="button_action button_warning" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
 
     {% endif %}
     </br>
 
     {% include "mediagoblin/utils/license.html" %}
 
-    {% include "mediagoblin/utils/exif.html" %}
-
     {% template_hook("media_sideinfo") %}
 
     {% block mediagoblin_sidebar %}
index d1f437d13a9b2ddc4b05a3fc8540fd5bf3e53823..b93da06ee0c232fd8bce1cae503f5e797c346bc9 100644 (file)
@@ -29,6 +29,8 @@
           src="{{ request.staticdirect('/js/comment_show.js') }}"></script>
   <script type="text/javascript"
           src="{{ request.staticdirect('/js/keyboard_navigation.js') }}"></script>
+  <script type="text/javascript"
+          src="{{ request.staticdirect('/js/post_comment.js') }}"></script>
 
   {% template_hook("location_head") %}
   {% template_hook("media_head") %}
@@ -75,6 +77,7 @@
     <h2 class="media_title">
       {{ media.title }}
     </h2>
+    {% template_hook("media_titleinfo") %}
     {% if request.user and
           (media.actor == request.user.id or
            request.user.has_privilege('admin')) %}
             <input type="submit" value="{% trans %}Add this comment{% endtrans %}" class="button_action" />
               {{ csrf_token }}
           </div>
+          <input type="hidden" value="{{ request.urlgen('mediagoblin.user_pages.media_post_comment', user= media.get_actor.username, media_id=media.id) }}" id="postCommentURL" />
           <input type="hidden" value="{{ request.urlgen('mediagoblin.user_pages.media_preview_comment') }}" id="previewURL" />
           <input type="hidden" value="{% trans %}Comment Preview{% endtrans %}" id="previewText"/>
         </form>
index 96786937fac9d2d7355e1816e6afc3ba434d65d7..ee7b646a636fb46df1de6a4c27e4d05d173395da 100644 (file)
@@ -40,7 +40,7 @@ Show:
 <a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
                            user=request.user.username, state="failed") }}">Failed</a>,
 <a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
-                           user=request.user.username, state="processed") }}">Succesful</a>
+                           user=request.user.username, state="processed") }}">Successful</a>
 </p>
     
 {% if entries.count() %}
index 86680cb6c639af2e6c1df8acee47b106f18b7d34..8a3f7a75c5f06f6cb6d4b04884369c4dfc469ae7 100644 (file)
                      {%- if loop.first %} thumb_entry_first
                      {%- elif loop.last %} thumb_entry_last{% endif %}">
             <a href="{{ obj_url }}">
+              {% if obj.icon_url %}
+              <img class="entry_type_icon" src="{{ obj.icon_url }}" />
+              {% endif %}
               <img src="{{ obj.thumb_url }}" />
             </a>
 
+           {% if obj.title %}
+           <a href="{{ obj_url }}">{{ obj.title }}</a>
+           {% endif %}
             {% if item.note %}
-              <a href="{{ obj_url }}">{{ item.note }}</a>
+             {{ item.note }}
             {% endif %}
            {% if request.user and
                   (item.in_collection.actor == request.user.id or
index 9e262ed946df17dffc3011acb9df2ce72474d446..fc8672fb708841be0f507685b01b2a64f67d6b4e 100644 (file)
 {# Provide navigation links to neighboring media entries, if possible #}
 {% set prev_entry_url = media.url_to_prev(request.urlgen) %}
 {% set next_entry_url = media.url_to_next(request.urlgen) %}
+{% if is_rtl %}
+  {% set next_arrow = "→" %}
+  {% set prev_arrow = "←" %}
+{% else %}
+  {% set next_arrow = "←" %}
+  {% set prev_arrow = "→" %}
+{% endif %}
 
 {% if prev_entry_url or next_entry_url %}
   <div class="navigation">
    {# There are no previous entries for the very first media entry #}
     {% if prev_entry_url %}
       <a class="navigation_button navigation_left" href="{{ prev_entry_url }}">
-        &larr; {% trans %}newer{% endtrans %}
+        {{next_arrow}} {% trans %}newer{% endtrans %}
       </a>
     {% else %}
       {# This is the first entry. display greyed-out 'previous' image #}
       <p class="navigation_button navigation_left">
-        &larr; {% trans %}newer{% endtrans %}
+        {{next_arrow}} {% trans %}newer{% endtrans %}
       </p>
     {% endif %}
     {# Likewise, this could be the very last media entry #}
     {% if next_entry_url %}
       <a class="navigation_button navigation_right" href="{{ next_entry_url }}">
-        {% trans %}older{% endtrans %} &rarr;
+        {% trans %}older{% endtrans %} {{prev_arrow}}
       </a>
     {% else %}
       {# This is the last entry. display greyed-out 'next' image #}
       <p class="navigation_button navigation_right">
-        {% trans %}older{% endtrans %} &rarr;
+        {% trans %}older{% endtrans %} {{prev_arrow}}
       </p>
     {% endif %}
   </div>
index 7e16708ca1ca7d401f7da5a9e545e6e16120d25b..e2921258e76c25e78eeefc0827b068991a9da36a 100644 (file)
@@ -40,9 +40,9 @@
     {{- render_label_p(field) }}
     <div class="form_field_input">
       {% if autofocus_first %}
-        {{ field(autofocus=True) }}
+        {{ field(autofocus=True, style="width:100%;") }}
       {% else %}
-        {{ field }}
+        {{ field(style="width:100%;") }}
       {% endif %}
       {%- if field.errors -%}
         {% for error in field.errors %}
diff --git a/mediagoblin/tests/.gitignore b/mediagoblin/tests/.gitignore
new file mode 100644 (file)
index 0000000..16d3c4d
--- /dev/null
@@ -0,0 +1 @@
+.cache
index 1377907b92f69005f7da66c8e01e6219651973a5..8dc32525e5ca4bd3b7e82b65655dc38ce1215f39 100644 (file)
@@ -7,7 +7,7 @@ num_carrots = 88
 encouragement_phrase = "I'd love it if you eat your carrots!"
 
 # Something extra!
-blah_blah = "blah!"
+blah_blah = "blæh!"
 
 [celery]
 EAT_CELERY_WITH_CARROTS = False
index 480f6d9a4e8008b07aa0953f279cc51a71d63ddd..38406d6255288bb813506a24c1d8d40e60743035 100644 (file)
@@ -41,3 +41,4 @@ 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')
+BAD_GPS_JPG = resource_exif('bad-gps.jpg')
index 90873cb905284fd68f19e61070028890ef086897..f4741fd102009941477406b49fdf2fcde750baf3 100644 (file)
@@ -25,11 +25,11 @@ from webtest import AppError
 
 from .resources import GOOD_JPG
 from mediagoblin import mg_globals
-from mediagoblin.db.models import User, Activity, MediaEntry, TextComment
-from mediagoblin.tools.routing import extract_url_arguments
+from mediagoblin.db.models import User, MediaEntry, TextComment
 from mediagoblin.tests.tools import fixture_add_user
 from mediagoblin.moderation.tools import take_away_privileges
 
+
 class TestAPI(object):
     """ Test mediagoblin's pump.io complient APIs """
 
@@ -38,7 +38,8 @@ class TestAPI(object):
         self.test_app = test_app
         self.db = mg_globals.database
 
-        self.user = fixture_add_user(privileges=[u'active', u'uploader', u'commenter'])
+        self.user = fixture_add_user(privileges=[u'active', u'uploader',
+                                                 u'commenter'])
         self.other_user = fixture_add_user(
             username="otheruser",
             privileges=[u'active', u'uploader', u'commenter']
@@ -61,7 +62,7 @@ class TestAPI(object):
 
         return response, json.loads(response.body.decode())
 
-    def _upload_image(self, test_app, image):
+    def _upload_image(self, test_app, image, custom_filename=None):
         """ Uploads and image to MediaGoblin via pump.io API """
         data = open(image, "rb").read()
         headers = {
@@ -69,6 +70,8 @@ class TestAPI(object):
             "Content-Length": str(len(data))
         }
 
+        if custom_filename is not None:
+            headers["X-File-Name"] = custom_filename
 
         with self.mock_oauth():
             response = test_app.post(
@@ -126,9 +129,48 @@ class TestAPI(object):
         assert image["objectType"] == "image"
 
         # Check that we got the response we're expecting
-        response, _ = self._post_image_to_feed(test_app, image)
+        response, data = self._post_image_to_feed(test_app, image)
+        assert response.status_code == 200
+        assert data["object"]["fullImage"]["url"].endswith("unknown.jpe")
+        assert data["object"]["image"]["url"].endswith("unknown.thumbnail.jpe")
+
+    def test_can_post_image_custom_filename(self, test_app):
+        """ Tests an image can be posted to the API with custom filename """
+        # First request we need to do is to upload the image
+        response, image = self._upload_image(test_app, GOOD_JPG,
+                                             custom_filename="hello.jpg")
+
+        # I should have got certain things back
+        assert response.status_code == 200
+
+        assert "id" in image
+        assert "fullImage" in image
+        assert "url" in image["fullImage"]
+        assert "url" in image
+        assert "author" in image
+        assert "published" in image
+        assert "updated" in image
+        assert image["objectType"] == "image"
+
+        # Check that we got the response we're expecting
+        response, data = self._post_image_to_feed(test_app, image)
+        assert response.status_code == 200
+        assert data["object"]["fullImage"]["url"].endswith("hello.jpg")
+        assert data["object"]["image"]["url"].endswith("hello.thumbnail.jpg")
+
+    def test_can_post_image_tags(self, test_app):
+        """ Tests that an image can be posted to the API """
+        # First request we need to do is to upload the image
+        response, image = self._upload_image(test_app, GOOD_JPG)
         assert response.status_code == 200
 
+        image["tags"] = ["hello", "world"]
+
+        # Check that we got the response we're expecting
+        response, data = self._post_image_to_feed(test_app, image)
+        assert response.status_code == 200
+        assert data["object"]["tags"] == ["hello", "world"]
+
     def test_unable_to_upload_as_someone_else(self, test_app):
         """ Test that can't upload as someoen else """
         data = open(GOOD_JPG, "rb").read()
@@ -172,7 +214,7 @@ class TestAPI(object):
             assert "403 FORBIDDEN" in excinfo.value.args[0]
 
     def test_only_able_to_update_own_image(self, test_app):
-        """ Test's that the uploader is the only person who can update an image """
+        """ Test uploader is the only person who can update an image """
         response, data = self._upload_image(test_app, GOOD_JPG)
         response, data = self._post_image_to_feed(test_app, data)
 
@@ -186,13 +228,16 @@ class TestAPI(object):
         }
 
         # Lets change the image uploader to be self.other_user, this is easier
-        # than uploading the image as someone else as the way self.mocked_oauth_required
-        # and self._upload_image.
-        media = MediaEntry.query.filter_by(public_id=data["object"]["id"]).first()
+        # than uploading the image as someone else as the way
+        # self.mocked_oauth_required and self._upload_image.
+        media = MediaEntry.query \
+            .filter_by(public_id=data["object"]["id"]) \
+            .first()
         media.actor = self.other_user.id
         media.save()
 
-        # Now lets try and edit the image as self.user, this should produce a 403 error.
+        # Now lets try and edit the image as self.user, this should produce a
+        # 403 error.
         with self.mock_oauth():
             with pytest.raises(AppError) as excinfo:
                 test_app.post(
@@ -242,7 +287,6 @@ class TestAPI(object):
         assert image["content"] == description
         assert image["license"] == license
 
-
     def test_only_uploaders_post_image(self, test_app):
         """ Test that only uploaders can upload images """
         # Remove uploader permissions from user
@@ -288,12 +332,15 @@ class TestAPI(object):
         image = json.loads(request.body.decode())
         entry = MediaEntry.query.filter_by(public_id=image["id"]).first()
 
+        assert entry is not None
+
         assert request.status_code == 200
 
         assert "image" in image
         assert "fullImage" in image
         assert "pump_io" in image
         assert "links" in image
+        assert "tags" in image
 
     def test_post_comment(self, test_app):
         """ Tests that I can post an comment media """
@@ -316,7 +363,9 @@ class TestAPI(object):
         assert response.status_code == 200
 
         # Find the objects in the database
-        media = MediaEntry.query.filter_by(public_id=data["object"]["id"]).first()
+        media = MediaEntry.query \
+            .filter_by(public_id=data["object"]["id"]) \
+            .first()
         comment = media.get_comments()[0].comment()
 
         # Tests that it matches in the database
@@ -378,7 +427,9 @@ class TestAPI(object):
         response, comment_data = self._activity_to_feed(test_app, activity)
 
         # change who uploaded the comment as it's easier than changing
-        comment = TextComment.query.filter_by(public_id=comment_data["object"]["id"]).first()
+        comment = TextComment.query \
+            .filter_by(public_id=comment_data["object"]["id"]) \
+            .first()
         comment.actor = self.other_user.id
         comment.save()
 
@@ -432,7 +483,7 @@ class TestAPI(object):
     def test_whoami_without_login(self, test_app):
         """ Test that whoami endpoint returns error when not logged in """
         with pytest.raises(AppError) as excinfo:
-            response = test_app.get("/api/whoami")
+            test_app.get("/api/whoami")
 
         assert "401 UNAUTHORIZED" in excinfo.value.args[0]
 
@@ -621,8 +672,11 @@ class TestAPI(object):
         delete = self._activity_to_feed(test_app, activity)[1]
 
         # Verify the comment no longer exists
-        assert TextComment.query.filter_by(public_id=comment["object"]["id"]).first() is None
-        comment_id = comment["object"]["id"]
+        assert TextComment.query \
+            .filter_by(public_id=comment["object"]["id"]) \
+            .first() is None
+
+        assert "id" in comment["object"]
 
         # Check we've got a delete activity back
         assert "id" in delete
@@ -662,6 +716,8 @@ class TestAPI(object):
         comment = self._activity_to_feed(test_app, activity)[1]
 
         # Verify the comment reflects the changes
-        model = TextComment.query.filter_by(public_id=comment["object"]["id"]).first()
+        model = TextComment.query \
+            .filter_by(public_id=comment["object"]["id"]) \
+            .first()
 
         assert model.content == activity["object"]["content"]
index cb971fdbb8783cc9aebdb3ccd238b018638fc87e..9cf5ccb07aebbe5d12f162f34e92209ed16cfecb 100644 (file)
@@ -1,4 +1,3 @@
-
 # GNU MediaGoblin -- federated, autonomous media hosting
 # Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
 #
@@ -102,7 +101,7 @@ def test_register_views(test_app):
             'password': 'iamsohappy',
             'email': 'easter@egg.com'})
 
-    ## At this point there should on user in the database
+    ## At this point there should be one user in the database
     assert User.query.count() == 1
 
     # Successful register
@@ -373,6 +372,53 @@ def test_authentication_views(test_app):
     assert not form.username.data == u'ANDREW'
     assert form.username.data == u'andrew'
 
+    # Successful login with short user
+    # --------------------------------
+    short_user = fixture_add_user(username=u'me', password=u'sho')
+    template.clear_test_template_context()
+    response = test_app.post(
+        '/auth/login/', {
+            'username': u'me',
+            'password': 'sho'})
+
+    # User should be redirected
+    response.follow()
+
+    assert urlparse.urlsplit(response.location)[2] == '/'
+    assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
+
+    # Make sure user is in the session
+    context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
+    session = context['request'].session
+    assert session['user_id'] == six.text_type(short_user.id)
+
+    # Must logout
+    template.clear_test_template_context()
+    response = test_app.get('/auth/logout/')
+
+    # Successful login with long user
+    # ----------------
+    long_user = fixture_add_user(
+        username=u'realllylonguser@reallylongdomain.com.co', password=u'sho')
+    template.clear_test_template_context()
+    response = test_app.post(
+        '/auth/login/', {
+            'username': u'realllylonguser@reallylongdomain.com.co',
+            'password': 'sho'})
+
+    # User should be redirected
+    response.follow()
+    assert urlparse.urlsplit(response.location)[2] == '/'
+    assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
+
+    # Make sure user is in the session
+    context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
+    session = context['request'].session
+    assert session['user_id'] == six.text_type(long_user.id)
+
+    template.clear_test_template_context()
+    response = test_app.get('/auth/logout/')
+
 @pytest.fixture()
 def authentication_disabled_app(request):
     return get_app(
index df0d04b0ce9add3d339a647d13a5bc2e304754cc..0749c7f420d2fd0351f7be9897ca8451f8859473 100644 (file)
@@ -55,7 +55,4 @@ def test_setup_celery_from_config():
         'sqlite:///' +
         pkg_resources.resource_filename('mediagoblin.tests', 'celery.db'))
 
-    assert fake_celery_module.BROKER_TRANSPORT == 'sqlalchemy'
-    assert fake_celery_module.BROKER_URL == (
-        'sqlite:///' +
-        pkg_resources.resource_filename('mediagoblin.tests', 'kombu.db'))
+    assert fake_celery_module.BROKER_URL == 'amqp://'
index b13adae6147cbbcce2fcdb05fbd5637b005633ca..c352741803fa1449e1b5472cbd2d15b11c02f246 100644 (file)
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+#
 # GNU MediaGoblin -- federated, autonomous media hosting
 # Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
 #
@@ -47,7 +49,7 @@ def test_read_mediagoblin_config():
     assert this_conf['carrotapp']['num_carrots'] == 88
     assert this_conf['carrotapp']['encouragement_phrase'] == \
         "I'd love it if you eat your carrots!"
-    assert this_conf['carrotapp']['blah_blah'] == "blah!"
+    assert this_conf['carrotapp']['blah_blah'] == u"blæh!"
     assert this_conf['celery']['EAT_CELERY_WITH_CARROTS'] == False
 
     # A bad file
index d0495a7aeded5d088ed4475abc07abf4747cf7ab..ad771ccac9cefe6439edc22c39e570aaa3f3b39d 100644 (file)
@@ -24,7 +24,7 @@ from collections import OrderedDict
 
 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
+from .resources import GOOD_JPG, EMPTY_JPG, BAD_JPG, GPS_JPG, BAD_GPS_JPG
 
 
 def assert_in(a, b):
@@ -437,3 +437,18 @@ def test_exif_gps_data():
         'direction': 25.674046740467404,
         'altitude': 37.64365671641791,
         'longitude': 18.016166666666667}
+
+
+def test_exif_bad_gps_data():
+    '''
+    Test extraction of GPS data from an image with bad GPS data
+    '''
+    result = extract_exif(BAD_GPS_JPG)
+    gps = get_gps_data(result)
+    print(gps)
+
+    assert gps == {
+        'latitude': 0.0,
+        'direction': 0.0,
+        'altitude': 0.0,
+        'longitude': 0.0}
diff --git a/mediagoblin/tests/test_exif/bad-gps.jpg b/mediagoblin/tests/test_exif/bad-gps.jpg
new file mode 100644 (file)
index 0000000..bd6c7bf
Binary files /dev/null and b/mediagoblin/tests/test_exif/bad-gps.jpg differ
index 6d3dd4758d87198e3c7df526176a40f2530b2225..5f9164001c34f8e07d725ac430e4d3c4041bffa3 100644 (file)
 
 from __future__ import absolute_import, unicode_literals
 
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
+
 from werkzeug.wrappers import Request
 from werkzeug.test import EnvironBuilder
 
 from mediagoblin.tools.request import decode_request
+from mediagoblin.tools.pagination import Pagination
 
 class TestDecodeRequest(object):
     """Test the decode_request function."""
@@ -59,3 +65,54 @@ class TestDecodeRequest(object):
         request.form = {'foo': 'bar'}
         data = decode_request(request)
         assert data['foo'] == 'bar'
+
+
+class TestPagination(object):
+    def _create_paginator(self, num_items, page, per_page):
+        """Create a Paginator with a mock database cursor."""
+        mock_cursor = mock.MagicMock()
+        mock_cursor.count.return_value = num_items
+        return Pagination(page, mock_cursor, per_page)
+
+    def test_creates_valid_page_url_from_explicit_base_url(self):
+        """Check that test_page_url_explicit runs.
+
+        This is a regression test for a Python 2/3 compatibility fix.
+
+        """
+        paginator = self._create_paginator(num_items=1, page=1, per_page=30)
+        url = paginator.get_page_url_explicit('http://example.com', [], 1)
+        assert url == 'http://example.com?page=1'
+
+    def test_iter_pages_handles_single_page(self):
+        """Check that iter_pages produces the expected result for single page.
+
+        This is a regression test for a Python 2/3 compatibility fix.
+
+        """
+        paginator = self._create_paginator(num_items=1, page=1, per_page=30)
+        assert list(paginator.iter_pages()) == [1]
+
+    def test_zero_items(self):
+        """Check that no items produces no pages."""
+        paginator = self._create_paginator(num_items=0, page=1, per_page=30)
+        assert paginator.total_count == 0
+        assert paginator.pages == 0
+
+    def test_single_item(self):
+        """Check that one item produces one page."""
+        paginator = self._create_paginator(num_items=1, page=1, per_page=30)
+        assert paginator.total_count == 1
+        assert paginator.pages == 1
+
+    def test_full_page(self):
+        """Check that a full page of items produces one page."""
+        paginator = self._create_paginator(num_items=30, page=1, per_page=30)
+        assert paginator.total_count == 30
+        assert paginator.pages == 1
+
+    def test_multiple_pages(self):
+        """Check that more than a full page produces two pages."""
+        paginator = self._create_paginator(num_items=31, page=1, per_page=30)
+        assert paginator.total_count == 31
+        assert paginator.pages == 2
index 8193233fc6a6287ef30c8dfad60766c486dbbf8b..029764057f21f5b056fa70c5d8435a86529d6393 100644 (file)
@@ -19,6 +19,7 @@ try:
 except ImportError:
     import unittest.mock as mock
 import email
+import socket
 import pytest
 import smtplib
 import pkg_resources
@@ -26,6 +27,7 @@ import pkg_resources
 import six
 
 from mediagoblin.tests.tools import get_app
+from mediagoblin import mg_globals
 from mediagoblin.tools import common, url, translate, mail, text, testing
 
 testing._activate_testing()
@@ -181,3 +183,30 @@ def test_html_cleaner():
         '<p><a href="javascript:nasty_surprise">innocent link!</a></p>')
     assert result == (
         '<p><a href="">innocent link!</a></p>')
+
+
+class TestMail(object):
+    """ Test mediagoblin's mail tool """
+    def test_no_mail_server(self):
+        """ Tests that no smtp server is available """
+        with pytest.raises(mail.NoSMTPServerError), mock.patch("smtplib.SMTP") as smtp_mock:
+            smtp_mock.side_effect = socket.error
+            mg_globals.app_config = {
+                "email_debug_mode": False,
+                "email_smtp_use_ssl": False,
+                "email_smtp_host": "127.0.0.1",
+                "email_smtp_port": 0}
+            common.TESTS_ENABLED = False
+            mail.send_email("", "", "", "")
+
+    def test_no_smtp_host(self):
+        """ Empty email_smtp_host """
+        with pytest.raises(mail.NoSMTPServerError), mock.patch("smtplib.SMTP") as smtp_mock:
+            smtp_mock.return_value.connect.side_effect = socket.error
+            mg_globals.app_config = {
+                "email_debug_mode": False,
+                "email_smtp_use_ssl": False,
+                "email_smtp_host": "",
+                "email_smtp_port": 0}
+            common.TESTS_ENABLED = False
+            mail.send_email("", "", "", "")
index 39b9ac5019f326054775ba98601998cc20c8fcdd..82def02c86898ec43f6e4a7897c7f2f500096333 100644 (file)
@@ -153,28 +153,6 @@ def install_fixtures_simple(db, fixtures):
             collection.insert(fixture)
 
 
-def assert_db_meets_expected(db, expected):
-    """
-    Assert a database contains the things we expect it to.
-
-    Objects are found via 'id', so you should make sure your document
-    has an id.
-
-    Args:
-     - db: pymongo or mongokit database connection
-     - expected: the data we expect.  Formatted like:
-         {'collection_name': [
-             {'id': 'foo',
-              'some_field': 'some_value'},]}
-    """
-    for collection_name, collection_data in six.iteritems(expected):
-        collection = db[collection_name]
-        for expected_document in collection_data:
-            document = collection.query.filter_by(id=expected_document['id']).first()
-            assert document is not None  # make sure it exists
-            assert document == expected_document  # make sure it matches
-
-
 def fixture_add_user(username=u'chris', password=u'toast',
                      privileges=[], wants_comment_notification=True):
     # Reuse existing user or create a new one
index 7539997e041426f531a3bb456183e0e35574a3e7..047e02dc41353cb1b60c8cb4ee0d3a7bac5be45b 100644 (file)
@@ -52,6 +52,10 @@ footer {
   border-top: 1px solid #E4E4E4;
 }
 
+table.admin_panel th {
+  color: #4a4a4a;
+}
+
 .button_action, .button_action_highlight, .button_form {
   color: #4a4a4a;
   background-color: #fff;
index fafd987d167680e267b893ab663edd7aa75f23d8..2215fb0ccd47d4dcf6777fd8ac412eb3a0fd7b78 100644 (file)
@@ -19,6 +19,11 @@ import six
 from exifread import process_file
 from exifread.utils import Ratio
 
+try:
+    from PIL import Image
+except ImportError:
+    import Image
+
 from mediagoblin.processing import BadMediaFail
 from mediagoblin.tools.translate import pass_to_ugettext as _
 
@@ -61,12 +66,12 @@ def exif_fix_image_orientation(im, exif_tags):
     # Rotate image
     if 'Image Orientation' in exif_tags:
         rotation_map = {
-            3: 180,
-            6: 270,
-            8: 90}
+            3: Image.ROTATE_180,
+            6: Image.ROTATE_270,
+            8: Image.ROTATE_90}
         orientation = exif_tags['Image Orientation'].values[0]
         if orientation in rotation_map:
-            im = im.rotate(
+            im = im.transpose(
                 rotation_map[orientation])
 
     return im
@@ -175,18 +180,14 @@ def get_gps_data(tags):
         pass
 
     try:
-        gps_data['direction'] = (
-            lambda d:
-                float(d.num) / float(d.den)
-            )(tags['GPS GPSImgDirection'].values[0])
+        direction = tags['GPS GPSImgDirection'].values[0]
+        gps_data['direction'] = safe_gps_ratio_divide(direction)
     except KeyError:
         pass
 
     try:
-        gps_data['altitude'] = (
-            lambda a:
-                float(a.num) / float(a.den)
-            )(tags['GPS GPSAltitude'].values[0])
+        altitude = tags['GPS GPSAltitude'].values[0]
+        gps_data['altitude'] = safe_gps_ratio_divide(altitude)
     except KeyError:
         pass
 
index a964980e8928946313265086b3612977abfb636e..2aff7f20c2f59024ba3481f8924f0a914bf0c355 100644 (file)
@@ -20,28 +20,45 @@ License = namedtuple("License", ["abbreviation", "name", "uri"])
 
 SORTED_LICENSES = [
     License("All rights reserved", "No license specified", ""),
+    License("CC BY 4.0", "Creative Commons Attribution 4.0 International",
+            "https://creativecommons.org/licenses/by/4.0/"),
+    License("CC BY-SA 4.0",
+            "Creative Commons Attribution-ShareAlike 4.0 International",
+            "https://creativecommons.org/licenses/by-sa/4.0/"),
+    License("CC BY-ND 4.0",
+            "Creative Commons Attribution-NoDerivs 4.0 International",
+            "https://creativecommons.org/licenses/by-nd/4.0/"),
+    License("CC BY-NC 4.0",
+            "Creative Commons Attribution-NonCommercial 4.0 International",
+            "https://creativecommons.org/licenses/by-nc/4.0/"),
+    License("CC BY-NC-SA 4.0",
+            "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International",
+            "https://creativecommons.org/licenses/by-nc-sa/4.0/"),
+    License("CC BY-NC-ND 4.0",
+            "Creative Commons Attribution-NonCommercial-NoDerivs 4.0 International",
+            "https://creativecommons.org/licenses/by-nc-nd/4.0/"),
     License("CC BY 3.0", "Creative Commons Attribution Unported 3.0",
-           "http://creativecommons.org/licenses/by/3.0/"),
+           "https://creativecommons.org/licenses/by/3.0/"),
     License("CC BY-SA 3.0",
-           "Creative Commons Attribution-ShareAlike Unported 3.0",
-           "http://creativecommons.org/licenses/by-sa/3.0/"),
+           "Creative Commons Attribution-ShareAlike 3.0 Unported",
+           "https://creativecommons.org/licenses/by-sa/3.0/"),
     License("CC BY-ND 3.0",
            "Creative Commons Attribution-NoDerivs 3.0 Unported",
-           "http://creativecommons.org/licenses/by-nd/3.0/"),
+           "https://creativecommons.org/licenses/by-nd/3.0/"),
     License("CC BY-NC 3.0",
-          "Creative Commons Attribution-NonCommercial Unported 3.0",
-          "http://creativecommons.org/licenses/by-nc/3.0/"),
+          "Creative Commons Attribution-NonCommercial 3.0 Unported",
+          "https://creativecommons.org/licenses/by-nc/3.0/"),
     License("CC BY-NC-SA 3.0",
            "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported",
-           "http://creativecommons.org/licenses/by-nc-sa/3.0/"),
+           "https://creativecommons.org/licenses/by-nc-sa/3.0/"),
     License("CC BY-NC-ND 3.0",
            "Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported",
-           "http://creativecommons.org/licenses/by-nc-nd/3.0/"),
+           "https://creativecommons.org/licenses/by-nc-nd/3.0/"),
     License("CC0 1.0",
            "Creative Commons CC0 1.0 Universal",
-           "http://creativecommons.org/publicdomain/zero/1.0/"),
+           "https://creativecommons.org/publicdomain/zero/1.0/"),
     License("Public Domain","Public Domain",
-           "http://creativecommons.org/publicdomain/mark/1.0/"),
+           "https://creativecommons.org/publicdomain/mark/1.0/"),
     ]
 
 # dict {uri: License,...} to enable fast license lookup by uri. Ideally,
index c11e392bc31ff05bfe7fe4bb3dc32969c5e174a4..3dc180d86c28e7ef3a9a51953d3fcd79dd3c4231 100644 (file)
@@ -16,6 +16,8 @@
 
 from __future__ import print_function, unicode_literals
 
+import socket
+import logging
 import six
 import smtplib
 import sys
@@ -54,6 +56,14 @@ EMAIL_TEST_INBOX = []
 EMAIL_TEST_MBOX_INBOX = []
 
 
+class MailError(Exception):
+    """ General exception for mail errors """
+
+
+class NoSMTPServerError(MailError):
+    pass
+
+
 class FakeMhost(object):
     """
     Just a fake mail host so we can capture and test messages
@@ -101,13 +111,27 @@ def send_email(from_addr, to_addrs, subject, message_body):
         else:
             smtp_init = smtplib.SMTP
 
-        mhost = smtp_init(
-            mg_globals.app_config['email_smtp_host'],
-            mg_globals.app_config['email_smtp_port'])
+        try:
+            mhost = smtp_init(
+                mg_globals.app_config['email_smtp_host'],
+                mg_globals.app_config['email_smtp_port'])
+        except socket.error as original_error:
+            error_message = "Couldn't contact mail server on <{}>:<{}>".format(
+                mg_globals.app_config['email_smtp_host'],
+                mg_globals.app_config['email_smtp_port'])
+            logging.debug(original_error)
+            raise NoSMTPServerError(error_message)
 
         # SMTP.__init__ Issues SMTP.connect implicitly if host
         if not mg_globals.app_config['email_smtp_host']:  # e.g. host = ''
-            mhost.connect()  # We SMTP.connect explicitly
+            try:
+                mhost.connect()  # We SMTP.connect explicitly
+            except socket.error as original_error:
+                error_message = "Couldn't contact mail server on <{}>:<{}>".format(
+                    mg_globals.app_config['email_smtp_host'],
+                    mg_globals.app_config['email_smtp_port'])
+                logging.debug(original_error)
+                raise NoSMTPServerError(error_message)
 
         try:
             mhost.starttls()
index a525caf721d81f3bab4e3a6b3eb6114c6e880378..db5f69fb0f825a93a7ded2912e3f3359b5523942 100644 (file)
 # 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 urllib
 import copy
 from math import ceil, floor
 from itertools import count
 from werkzeug.datastructures import MultiDict
 
-from six.moves import zip
+from six.moves import range, urllib, zip
 
 PAGINATION_DEFAULT_PER_PAGE = 30
 
@@ -86,7 +85,7 @@ class Pagination(object):
     def iter_pages(self, left_edge=2, left_current=2,
                    right_current=5, right_edge=2):
         last = 0
-        for num in xrange(1, self.pages + 1):
+        for num in range(1, self.pages + 1):
             if num <= left_edge or \
                (num > self.page - left_current - 1 and \
                 num < self.page + right_current) or \
@@ -107,7 +106,7 @@ class Pagination(object):
 
         new_get_params['page'] = page_no
         return "%s?%s" % (
-            base_url, urllib.urlencode(new_get_params))
+            base_url, urllib.parse.urlencode(new_get_params))
 
     def get_page_url(self, request, page_no):
         """
index 97b041a61a5cb2cb769dcbc9c21b51e591ddb7d6..79fd91e1b2e956d6a903d6d080fb93bb41d9ea74 100644 (file)
@@ -68,7 +68,7 @@ def register_themes(app_config, builtin_dir=BUILTIN_THEME_DIR):
 
             themedata = themedata_for_theme_dir(themedir, abs_themedir)
             registry[themedir] = themedata
-        
+
     # Built-in themes
     if os.path.exists(builtin_dir):
         _install_themes_in_dir(builtin_dir)
@@ -79,11 +79,9 @@ def register_themes(app_config, builtin_dir=BUILTIN_THEME_DIR):
         _install_themes_in_dir(theme_install_dir)
 
     current_theme_name = app_config.get('theme')
-    if current_theme_name \
-            and registry.has_key(current_theme_name):
+    try:
         current_theme = registry[current_theme_name]
-    else:
+    except KeyError:
         current_theme = None
 
     return registry, current_theme
-
index 28d3ba79bc81438c588c140f9cba54b24ce7869d..b4737ea8fb5e15e4bf948239a6e8aad4f05e5807 100644 (file)
@@ -24,6 +24,7 @@ from mediagoblin import messages, mg_globals
 from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, Comment,
                                    CollectionItem, LocalUser, Activity, \
                                    GenericModelReference)
+from mediagoblin.plugins.api.tools import get_media_file_paths
 from mediagoblin.tools.response import render_to_response, render_404, \
     redirect, redirect_obj
 from mediagoblin.tools.text import cleaned_markdown_conversion
@@ -179,6 +180,10 @@ def media_post_comment(request, media):
     if not request.method == 'POST':
         raise MethodNotAllowed()
 
+    # If media is not processed, return NotFound.
+    if not media.state == u'processed':
+        return render_404(request)
+
     comment = request.db.TextComment()
     comment.actor = request.user.id
     comment.content = six.text_type(request.form['comment_content'])
@@ -231,6 +236,10 @@ def media_preview_comment(request):
 def media_collect(request, media):
     """Add media to collection submission"""
 
+    # If media is not processed, return NotFound.
+    if not media.state == u'processed':
+        return render_404(request)
+
     form = user_forms.MediaCollectForm(request.form)
     # A user's own collections:
     form.collection.query = Collection.query.filter_by(
@@ -288,12 +297,6 @@ def media_collect(request, media):
             collection = None
 
     # Make sure the user actually selected a collection
-    item = CollectionItem.query.filter_by(collection=collection.id)
-    item = item.join(CollectionItem.object_helper).filter_by(
-        model_type=media.__tablename__,
-        obj_pk=media.id
-    ).first()
-
     if not collection:
         messages.add_message(
             request,
@@ -303,8 +306,14 @@ def media_collect(request, media):
                     user=media.get_actor.username,
                     media_id=media.id)
 
+    item = CollectionItem.query.filter_by(collection=collection.id)
+    item = item.join(CollectionItem.object_helper).filter_by(
+        model_type=media.__tablename__,
+        obj_pk=media.id
+    ).first()
+
     # Check whether media already exists in collection
-    elif item is not None:
+    if item is not None:
         messages.add_message(
             request,
             messages.ERROR,
@@ -538,23 +547,21 @@ def atom_feed(request):
         username = request.matchdict['user']).first()
     if not user or not user.has_privilege(u'active'):
         return render_404(request)
+    feed_title = "MediaGoblin Feed for user '%s'" % request.matchdict['user']
+    link = request.urlgen('mediagoblin.user_pages.user_home',
+                          qualified=True, user=request.matchdict['user'])
+    cursor = MediaEntry.query.filter_by(actor=user.id, state=u'processed')
+    cursor = cursor.order_by(MediaEntry.created.desc())
+    cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
 
-    cursor = MediaEntry.query.filter_by(
-        actor = user.id,
-        state = u'processed').\
-        order_by(MediaEntry.created.desc()).\
-        limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
 
     """
     ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI)
     """
     atomlinks = [{
-           'href': request.urlgen(
-               'mediagoblin.user_pages.user_home',
-               qualified=True, user=request.matchdict['user']),
-           'rel': 'alternate',
-           'type': 'text/html'
-           }]
+        'href': link,
+        'rel': 'alternate',
+        'type': 'text/html'}]
 
     if mg_globals.app_config["push_urls"]:
         for push_url in mg_globals.app_config["push_urls"]:
@@ -563,25 +570,34 @@ def atom_feed(request):
                 'href': push_url})
 
     feed = AtomFeed(
-               "MediaGoblin: Feed for user '%s'" % request.matchdict['user'],
-               feed_url=request.url,
-               id='tag:{host},{year}:gallery.user-{user}'.format(
-                   host=request.host,
-                   year=datetime.datetime.today().strftime('%Y'),
-                   user=request.matchdict['user']),
-               links=atomlinks)
+        feed_title,
+        feed_url=request.url,
+        id='tag:{host},{year}:gallery.user-{user}'.format(
+            host=request.host,
+            year=datetime.datetime.today().strftime('%Y'),
+            user=request.matchdict['user']),
+        links=atomlinks)
 
     for entry in cursor:
+        # Include a thumbnail image in content.
+        file_urls = get_media_file_paths(entry.media_files, request.urlgen)
+        if 'thumb' in file_urls:
+            content = u'<img src="{thumb}" alt='' /> {desc}'.format(
+                thumb=file_urls['thumb'], desc=entry.description_html)
+        else:
+            content = entry.description_html
+
         feed.add(
             entry.get('title'),
-            entry.description_html,
+            content,
             id=entry.url_for_self(request.urlgen, qualified=True),
             content_type='html',
             author={
                 'name': entry.get_actor.username,
                 'uri': request.urlgen(
                     'mediagoblin.user_pages.user_home',
-                    qualified=True, user=entry.get_actor.username)},
+                    qualified=True,
+                    user=entry.get_actor.username)},
             updated=entry.get('created'),
             links=[{
                 'href': entry.url_for_self(
index 0c2fe731c8409d0919c53875067f66016a339eb2..72c19735d185812edf32168fbab1fba9ae34c8bd 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -57,7 +57,6 @@ install_requires = [
     'pytest-xdist',
     'werkzeug>=0.7',
     'celery>=3.0',
-    'kombu',
     'jinja2',
     'Babel>=1.3',
     'WebTest>=2.0.18',
@@ -75,6 +74,7 @@ install_requires = [
     'PasteScript',
     'requests>=2.6.0',
     'pyld',
+    'ExifRead>=2.0.0'
     # This is optional:
     # 'translitcodec',
     # For now we're expecting that users will install this from
@@ -83,10 +83,8 @@ install_requires = [
     # 'Pillow',
 ] + pyversion_install_requires
 
-dependency_links = []
 if not PY2:
     # PyPI version (1.4.2) does not have proper Python 3 support
-    dependency_links.append('https://github.com/ianare/exif-py/zipball/develop#egg=ExifRead-2.0.0')
     install_requires.append('ExifRead>=2.0.0')
 
 with open(READMEFILE, encoding="utf-8") as fobj:
@@ -101,7 +99,6 @@ try:
     include_package_data = True,
     # scripts and dependencies
     install_requires=install_requires,
-    dependency_links=dependency_links,
     test_suite='nose.collector',
     entry_points="""\
         [console_scripts]
diff --git a/tox.ini b/tox.ini
index 023c53ed7a9259fd15a10c2608e0fe12511a7217..3c57e802f8d8e31bffdb5b8470cbdf8d55443482 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ sitepackages = False
 usedevelop = True
 # for ExifRead 2.0.0
 install_command = pip install --process-dependency-links --pre {opts} {packages}
-commands = py.test ./mediagoblin/tests --boxed
+commands = py.test ./mediagoblin/tests --boxed -k '{posargs}'
 deps =
  lxml
  Pillow