diff options
59 files changed, 1085 insertions, 216 deletions
@@ -50,6 +50,7 @@ *~ *.swp *.mo +*.patch # The legacy of buildout .installed.cfg diff --git a/.gitmodules b/.gitmodules index 562ad4e4..d9171995 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 @@ -32,6 +32,7 @@ Thank you! * Corey Farwell * Chris Moylan * Christopher Allan Webber +* chrysn * Dan Callahan * David Thompson * Daniel Krol @@ -94,6 +95,7 @@ Thank you! * Robert Smith * Rodney Ewing * Rodrigo Rodrigues da Silva +* Romain Porte * Runar Petursson * Sacha De'Angeli * Sam Clegg diff --git a/MANIFEST.in b/MANIFEST.in index 675c8081..dd2fd23a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/docs/source/siteadmin/commandline-upload.rst b/docs/source/siteadmin/commandline-upload.rst index ef597a44..756f5fa8 100644 --- a/docs/source/siteadmin/commandline-upload.rst +++ b/docs/source/siteadmin/commandline-upload.rst @@ -58,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" @@ -87,6 +87,7 @@ just as easy to provide this information through the correct metadata columns. - **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 ---------------- diff --git a/docs/source/siteadmin/configuration.rst b/docs/source/siteadmin/configuration.rst index 6b8cd225..1b8dc9bb 100644 --- a/docs/source/siteadmin/configuration.rst +++ b/docs/source/siteadmin/configuration.rst @@ -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 ============== diff --git a/docs/source/siteadmin/deploying.rst b/docs/source/siteadmin/deploying.rst index c7cc2403..42fe1772 100644 --- a/docs/source/siteadmin/deploying.rst +++ b/docs/source/siteadmin/deploying.rst @@ -65,7 +65,7 @@ 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 \ @@ -240,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 @@ -250,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:: @@ -319,13 +319,16 @@ 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 @@ -337,7 +340,7 @@ Configure MediaGoblin to use the PostgreSQL database ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are using PostgreSQL, edit the ``[mediagoblin]`` section in your -``mediagoblin_local.ini`` and put in:: +``mediagoblin.ini`` and put in:: sql_engine = postgresql:///mediagoblin diff --git a/docs/source/siteadmin/media-types.rst b/docs/source/siteadmin/media-types.rst index 146e1aae..8f9239be 100644 --- a/docs/source/siteadmin/media-types.rst +++ b/docs/source/siteadmin/media-types.rst @@ -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] @@ -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 @@ -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 @@ -184,8 +181,7 @@ library, which is necessary for creating thumbnails of ASCII art ./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 @@ -207,7 +203,7 @@ 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 @@ -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 diff --git a/docs/source/siteadmin/production-deployments.rst b/docs/source/siteadmin/production-deployments.rst index ee915573..3d11f022 100644 --- a/docs/source/siteadmin/production-deployments.rst +++ b/docs/source/siteadmin/production-deployments.rst @@ -76,7 +76,7 @@ modify it to suit your environment's setup: 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 \ diff --git a/docs/source/siteadmin/relnotes.rst b/docs/source/siteadmin/relnotes.rst index f32ca792..1c15f249 100644 --- a/docs/source/siteadmin/relnotes.rst +++ b/docs/source/siteadmin/relnotes.rst @@ -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 diff --git a/docs/source/siteadmin/theming.rst b/docs/source/siteadmin/theming.rst index 9c01a5b3..24f23235 100644 --- a/docs/source/siteadmin/theming.rst +++ b/docs/source/siteadmin/theming.rst @@ -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] # ... diff --git a/lazystarter.sh b/lazystarter.sh index 0ed22fd8..b531b068 100755 --- a/lazystarter.sh +++ b/lazystarter.sh @@ -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 diff --git a/mediagoblin/api/views.py b/mediagoblin/api/views.py index 74181fde..dfa9dfa2 100644 --- a/mediagoblin/api/views.py +++ b/mediagoblin/api/views.py @@ -115,8 +115,16 @@ 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: + filenames = sorted(mimetypes.guess_all_extensions(mimetype)) + if not filenames: + return json_error('Unknown mimetype: {}'.format(mimetype), + status=415) + filename = 'unknown{0}'.format(filenames[0]) + file_data = FileStorage( stream=io.BytesIO(request.data), filename=filename, diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 2f95fd81..593d588d 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +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,9 @@ def login(request): return redirect(request, "index") login_failed = True + remote_addr = (request.access_route and request.access_route[-1] + or request.remote_addr) + _log.warn("Failed login attempt from %r", remote_addr) return render_to_response( request, diff --git a/mediagoblin/db/migration_tools.py b/mediagoblin/db/migration_tools.py index f4273fa0..852f35ee 100644 --- a/mediagoblin/db/migration_tools.py +++ b/mediagoblin/db/migration_tools.py @@ -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 diff --git a/alembic.ini b/mediagoblin/db/migrations/alembic.ini index 7ae94f9f..4f7fc115 100644 --- a/alembic.ini +++ b/mediagoblin/db/migrations/alembic.ini @@ -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 diff --git a/mediagoblin/db/migrations/versions/afd3d1da5e29_subtitle_plugin_initial_migration.py b/mediagoblin/db/migrations/versions/afd3d1da5e29_subtitle_plugin_initial_migration.py new file mode 100644 index 00000000..565d4864 --- /dev/null +++ b/mediagoblin/db/migrations/versions/afd3d1da5e29_subtitle_plugin_initial_migration.py @@ -0,0 +1,36 @@ +"""Subtitle plugin initial migration + +Revision ID: afd3d1da5e29 +Revises: 228916769bd2 +Create Date: 2016-06-03 11:48:03.369079 + +""" + +# revision identifiers, used by Alembic. +revision = 'afd3d1da5e29' +down_revision = '228916769bd2' +branch_labels = ('subtitles_plugin',) +depends_on = None + +from alembic import op +import sqlalchemy as sa +from mediagoblin.db.extratypes import PathTupleWithSlashes + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('core__subtitle_files', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('media_entry', sa.Integer(), nullable=False), + sa.Column('name', sa.Unicode(), nullable=False), + sa.Column('filepath', PathTupleWithSlashes(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['media_entry'], [u'core__media_entries.id'], ), + sa.PrimaryKeyConstraint('id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('core__subtitle_files') + ### end Alembic commands ### diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index b2dcb6ad..0974676a 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -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 @@ -574,6 +575,15 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): name=v["name"], filepath=v["filepath"]) ) + subtitle_files_helper = relationship("MediaSubtitleFile", + cascade="all, delete-orphan", + order_by="MediaSubtitleFile.created" + ) + subtitle_files = association_proxy("subtitle_files_helper", "dict_view", + creator=lambda v: MediaSubtitleFile( + name=v["name"], filepath=v["filepath"]) + ) + tags_helper = relationship("MediaTag", cascade="all, delete-orphan" # should be automatically deleted ) @@ -771,7 +781,6 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): "self": { "href": public_id, }, - } } @@ -787,6 +796,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()] @@ -834,6 +849,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): @@ -899,6 +917,22 @@ class MediaAttachmentFile(Base): """A dict like view on this object""" return DictReadAttrProxy(self) +class MediaSubtitleFile(Base): + __tablename__ = "core__subtitle_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.utcnow) + + @property + def dict_view(self): + """A dict like view on this object""" + return DictReadAttrProxy(self) + class Tag(Base): __tablename__ = "core__tags" @@ -1610,7 +1644,7 @@ class Graveyard(Base): return context MODELS = [ LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, Comment, TextComment, - Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile, + Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile, MediaSubtitleFile, ProcessingMetaData, Notification, Client, CommentSubscription, Report, UserBan, Privilege, PrivilegeUserAssociation, RequestToken, AccessToken, NonceTimestamp, Activity, Generator, Location, GenericModelReference, Graveyard] diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index daeddb3f..2b8343b8 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -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) diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index b349975d..d3ae5465 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -29,4 +29,4 @@ add_route('mediagoblin.edit.verify_email', '/edit/verify_email/', add_route('mediagoblin.edit.email', '/edit/email/', 'mediagoblin.edit.views:change_email') add_route('mediagoblin.edit.deauthorize_applications', '/edit/deauthorize/', - 'mediagoblin.edit.views:deauthorize_applications') + 'mediagoblin.edit.views:deauthorize_applications')
\ No newline at end of file diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index b15fb2e7..717241e8 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -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 @@ -34,7 +34,7 @@ from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import (require_active_login, active_user_from_url, get_media_entry_by_id, user_may_alter_collection, get_user_collection, user_has_privilege, - user_not_banned) + user_not_banned, user_may_delete_media) from mediagoblin.tools.crypto import get_timed_signer_url from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER, DEFAULT_SCHEMA) @@ -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(): @@ -446,7 +458,8 @@ def verify_email(request): @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 @@ -499,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]) @@ -520,4 +538,4 @@ def edit_metadata(request, media): request, 'mediagoblin/edit/metadata.html', {'form':form, - 'media':media}) + 'media':media})
\ No newline at end of file diff --git a/mediagoblin/gmg_commands/__init__.py b/mediagoblin/gmg_commands/__init__.py index 98b097a6..0034fd98 100644 --- a/mediagoblin/gmg_commands/__init__.py +++ b/mediagoblin/gmg_commands/__init__.py @@ -145,7 +145,10 @@ def main_cli(): os.path.join(parent_directory, "mediagoblin.example.ini"), os.path.join(parent_directory, "mediagoblin.ini")) - args.func(args) + try: + args.func(args) + except AttributeError: # no subcommand or no func of subcommand + parser.print_help() if __name__ == '__main__': diff --git a/mediagoblin/gmg_commands/batchaddmedia.py b/mediagoblin/gmg_commands/batchaddmedia.py index 274d72bc..55ed865b 100644 --- a/mediagoblin/gmg_commands/batchaddmedia.py +++ b/mediagoblin/gmg_commands/batchaddmedia.py @@ -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,8 +73,6 @@ def batchaddmedia(args): username=args.username))) return - temp_files = [] - if os.path.isfile(args.metadata_path): metadata_path = args.metadata_path @@ -99,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({}) @@ -113,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: @@ -141,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. @@ -155,6 +154,7 @@ 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"") @@ -203,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 diff --git a/mediagoblin/init/config.py b/mediagoblin/init/config.py index a9189e8d..fe469156 100644 --- a/mediagoblin/init/config.py +++ b/mediagoblin/init/config.py @@ -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) diff --git a/mediagoblin/media_types/blog/views.py b/mediagoblin/media_types/blog/views.py index f1d5c49d..2bf2e5be 100644 --- a/mediagoblin/media_types/blog/views.py +++ b/mediagoblin/media_types/blog/views.py @@ -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) diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index c377d100..012ba352 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -81,7 +81,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: @@ -108,10 +118,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, 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 index 2bd1a14c..00000000 --- a/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html.orig +++ /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 diff --git a/mediagoblin/plugins/subtitles/__init__.py b/mediagoblin/plugins/subtitles/__init__.py new file mode 100644 index 00000000..78f207dc --- /dev/null +++ b/mediagoblin/plugins/subtitles/__init__.py @@ -0,0 +1,50 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tools import pluginapi +import os + +PLUGIN_DIR = os.path.dirname(__file__) + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.subtitles') + + routes = [ + ('mediagoblin.plugins.subtitles.customize', + '/u/<string:user>/m/<int:media_id>/customize/<int:id>', + 'mediagoblin.plugins.subtitles.views:custom_subtitles'), + ('mediagoblin.plugins.subtitles.subtitles', + '/u/<string:user>/m/<int:media_id>/subtitles/', + 'mediagoblin.plugins.subtitles.views:edit_subtitles'), + ('mediagoblin.plugins.subtitles.delete_subtitles', + '/u/<string:user>/m/<int:media_id>/delete/<int:id>', + 'mediagoblin.plugins.subtitles.views:delete_subtitles')] + + pluginapi.register_routes(routes) + + # Register the template path. + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {"customize_subtitles": "mediagoblin/plugins/subtitles/custom_subtitles.html", + "add_subtitles": "mediagoblin/plugins/subtitles/subtitles.html", + "subtitle_sidebar": "mediagoblin/plugins/subtitles/subtitle_media_block.html"}) + + + +hooks = { + 'setup': setup_plugin + } diff --git a/mediagoblin/plugins/subtitles/forms.py b/mediagoblin/plugins/subtitles/forms.py new file mode 100644 index 00000000..de8ffbcd --- /dev/null +++ b/mediagoblin/plugins/subtitles/forms.py @@ -0,0 +1,29 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import wtforms + +class CustomizeSubtitlesForm(wtforms.Form): + subtitle = wtforms.TextAreaField( + ('Subtitle'), + [wtforms.validators.Optional()], + description=("")) + +class EditSubtitlesForm(wtforms.Form): + subtitle_language = wtforms.StringField( + 'Language') + subtitle_file = wtforms.FileField( + 'File') diff --git a/mediagoblin/plugins/subtitles/models.py b/mediagoblin/plugins/subtitles/models.py new file mode 100644 index 00000000..f71fb173 --- /dev/null +++ b/mediagoblin/plugins/subtitles/models.py @@ -0,0 +1,49 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from sqlalchemy import Column, Integer, Unicode, ForeignKey +from sqlalchemy.orm import relationship + +from mediagoblin.db.models import User +from mediagoblin.db.base import Base,MediaEntry + +class MediaSubtitleFile(Base): + __tablename__ = "core__subtitle_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.utcnow) + + @property + def dict_view(self): + """A dict like view on this object""" + return DictReadAttrProxy(self) + + subtitle_files_helper = relationship("MediaSubtitleFile", + cascade="all, delete-orphan", + order_by="MediaSubtitleFile.created" + ) + subtitle_files = association_proxy("subtitle_files_helper", "dict_view", + creator=lambda v: MediaSubtitleFile( + name=v["name"], filepath=v["filepath"]) + ) + +MODELS = [ + MediaSubtitleFile +] diff --git a/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/custom_subtitles.html b/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/custom_subtitles.html new file mode 100644 index 00000000..a62325ca --- /dev/null +++ b/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/custom_subtitles.html @@ -0,0 +1,48 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} +{%- endblock %} + + +{% block mediagoblin_content %} +<link href="{{ + request.staticdirect('/css/subtitles.css') }}" + rel="stylesheet"> + + <form action="{{ request.urlgen('mediagoblin.plugins.subtitles.customize', + user=media.get_actor.username, + media_id=media.id, + id=id) }}" method="POST" enctype="multipart/form-data"> + <div class="form_box edit_box"> + {{ wtforms_util.render_divs(form) }} + <div class="form_submit_buttons"> + {% set delete_url = request.urlgen('mediagoblin.plugins.subtitles.delete_subtitles', + user= media.get_actor.username, + media_id=media.id, + id=id) %} + <a class="button_action button_warning" href="{{ delete_url }}">{% trans %}Delete Subtitle{% endtrans %}</a> + <input type="submit" value="{% trans %}Save changes{% endtrans %}" class="button_form" /> + {{ csrf_token }} + </div> + </div> + </form> +{% endblock %} diff --git a/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/subtitle_media_block.html b/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/subtitle_media_block.html new file mode 100644 index 00000000..34c9c16f --- /dev/null +++ b/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/subtitle_media_block.html @@ -0,0 +1,50 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} + +{% block subtitle_block %} +{% if "video.html" in media.media_manager.display_template or "audio.html" in media.media_manager.display_template %} + {%- if media.subtitle_files|count %} + <h3>{% trans %}Subtitles{% endtrans %}</h3> + <ul> + {%- for subtitle in media.subtitle_files %} + <li> + <a href="{{ request.urlgen('mediagoblin.plugins.subtitles.customize', + user=media.get_actor.username, + media_id=media.id, + id=subtitle.id ) }}"> + {{- subtitle.name -}} + </li> + {%- endfor %} + </ul> + {%- endif %} + {%- if request.user + and (media.actor == request.user.id + or request.user.has_privilege('admin')) %} + {%- if not media.subtitle_files|count %} + <h3>{% trans %}Subtitles{% endtrans %}</h3> + {%- endif %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.subtitles.subtitles', + user=media.get_actor.username, + media_id=media.id) }}"> + {%- trans %}Add subtitle {% endtrans -%} + </a> + </p> + {%- endif %} + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/subtitles.html b/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/subtitles.html new file mode 100644 index 00000000..5b249403 --- /dev/null +++ b/mediagoblin/plugins/subtitles/templates/mediagoblin/plugins/subtitles/subtitles.html @@ -0,0 +1,69 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} +{%- extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans media_title=media.title -%} + Editing subtitles for {{ media_title }} + {%- endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.subtitles.subtitles', + user= media.get_actor.username, + media_id=media.id) }}" + method="POST" enctype="multipart/form-data"> + <div class="form_box"> + <h1> + {%- trans media_title=media.title -%} + Editing subtitles for {{ media_title }} + {%- endtrans -%} + </h1> + <div style="text-align: center;" > + <img src="{{ media.thumb_url }}" /> + </div> + + {% if media.subtitle_files|count %} + <h2>{% trans %}subtitles{% endtrans %}</h2> + <ul> + {%- for subtitle in media.subtitle_files %} + <li> + <a target="_blank" href="{{ request.app.public_store.file_url( + subtitle['filepath']) }}"> + {{- subtitle.name -}} + </a> + </li> + {%- endfor %} + </ul> + {% endif %} + + <h2>{% trans %}Add subtitle{% endtrans %}</h2> + {{- wtforms_util.render_divs(form) }} + <div class="form_submit_buttons"> + <a class="button_action" href="{{ media.url_for_self(request.urlgen) }}"> + {%- trans %}Cancel{% endtrans -%} + </a> + <input type="submit" value="{% trans %}Save changes{% endtrans %}" + class="button_form" /> + {{ csrf_token }} + </div> + </div> + </form> +{% endblock %} diff --git a/mediagoblin/plugins/subtitles/tools.py b/mediagoblin/plugins/subtitles/tools.py new file mode 100644 index 00000000..43f5bd6a --- /dev/null +++ b/mediagoblin/plugins/subtitles/tools.py @@ -0,0 +1,42 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin import mg_globals +import os + +def open_subtitle(path): + status = True + subtitle_public_filepath = path + try: + with mg_globals.public_store.get_file( + subtitle_public_filepath, 'rb') as subtitle_public_file: + text = subtitle_public_file.read().decode('utf-8','ignore') + return (text,status) + except: + status = False + return ('',status) + +def save_subtitle(path,text): + status = True + subtitle_public_filepath = path + try: + with mg_globals.public_store.get_file( + subtitle_public_filepath, 'wb') as subtitle_public_file: + subtitle_public_file.write(text.encode('utf-8','ignore')) + return status + except: + status = False + return (status) diff --git a/mediagoblin/plugins/subtitles/views.py b/mediagoblin/plugins/subtitles/views.py new file mode 100644 index 00000000..46b844af --- /dev/null +++ b/mediagoblin/plugins/subtitles/views.py @@ -0,0 +1,186 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2016 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import six + +from datetime import datetime + +from itsdangerous import BadSignature +from werkzeug.exceptions import Forbidden +from werkzeug.utils import secure_filename + +from mediagoblin import messages +from mediagoblin import mg_globals + +from mediagoblin.plugins.subtitles import forms +from mediagoblin.decorators import (require_active_login, active_user_from_url, + get_media_entry_by_id, user_may_delete_media) +from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER, + DEFAULT_SCHEMA) +from mediagoblin.tools.response import (render_to_response, + redirect, redirect_obj, render_404) + +import mimetypes + +from mediagoblin.plugins.subtitles.tools import open_subtitle,save_subtitle + +UNSAFE_MIMETYPES = [ + 'text/html', + 'text/svg+xml'] + +@get_media_entry_by_id +@user_may_delete_media +@require_active_login +def edit_subtitles(request, media): + allowed_extensions = ['aqt','gsub','jss','sub','ttxt','pjs','psb', + 'rt','smi','stl','ssf','srt','ssa','ass','usf','vtt','lrc'] + form = forms.EditSubtitlesForm(request.form) + + # Add any subtitles + if 'subtitle_file' in request.files \ + and request.files['subtitle_file']: + if mimetypes.guess_type( + request.files['subtitle_file'].filename)[0] in \ + UNSAFE_MIMETYPES: + public_filename = secure_filename('{0}.notsafe'.format( + request.files['subtitle_file'].filename)) + else: + public_filename = secure_filename( + request.files['subtitle_file'].filename) + filepath = request.files['subtitle_file'].filename + if filepath.split('.')[-1] not in allowed_extensions : + messages.add_message( + request, + messages.ERROR, + ("Invalid subtitle file")) + + return redirect(request, + location=media.url_for_self(request.urlgen)) + subtitle_public_filepath = mg_globals.public_store.get_unique_filepath( + ['media_entries', six.text_type(media.id), 'subtitle', + public_filename]) + + with mg_globals.public_store.get_file( + subtitle_public_filepath, 'wb') as subtitle_public_file: + subtitle_public_file.write( + request.files['subtitle_file'].stream.read()) + request.files['subtitle_file'].stream.close() + + media.subtitle_files.append(dict( + name=form.subtitle_language.data \ + or request.files['subtitle_file'].filename, + filepath=subtitle_public_filepath, + created=datetime.utcnow(), + )) + + media.save() + + messages.add_message( + request, + messages.SUCCESS, + ("You added the subtitle %s!") % + (form.subtitle_language.data or + request.files['subtitle_file'].filename)) + + return redirect(request, + location=media.url_for_self(request.urlgen)) + return render_to_response( + request, + 'mediagoblin/plugins/subtitles/subtitles.html', + {'media': media, + 'form': form}) + + +@require_active_login +@get_media_entry_by_id +@user_may_delete_media +def custom_subtitles(request,media,id=None): + id = request.matchdict['id'] + path = "" + for subtitle in media.subtitle_files: + if subtitle["id"] == id: + path = subtitle["filepath"] + text = "" + value = open_subtitle(path) + text, status = value[0], value[1] + if status == True : + form = forms.CustomizeSubtitlesForm(request.form, + subtitle=text) + if request.method == 'POST' and form.validate(): + subtitle_data = form.subtitle.data + status = save_subtitle(path,subtitle_data) + if status == True: + messages.add_message( + request, + messages.SUCCESS, + ("Subtitle file changed!!!")) + return redirect(request, + location=media.url_for_self(request.urlgen)) + else : + messages.add_message( + request, + messages.ERROR, + ("Couldn't edit the subtitles!!!")) + return redirect(request, + location=media.url_for_self(request.urlgen)) + + return render_to_response( + request, + "mediagoblin/plugins/subtitles/custom_subtitles.html", + {"id": id, + "media": media, + "form": form }) + else: + index = 0 + for subtitle in media.subtitle_files: + if subtitle["id"] == id: + delete_container = index + media.subtitle_files.pop(delete_container) + media.save() + break + index += 1 + messages.add_message( + request, + messages.ERROR, + ("File link broken! Upload the subtitle again")) + return redirect(request, + location=media.url_for_self(request.urlgen)) + + +@require_active_login +@get_media_entry_by_id +@user_may_delete_media +def delete_subtitles(request,media): + id = request.matchdict['id'] + delete_container = None + index = 0 + for subtitle in media.subtitle_files: + if subtitle["id"] == id: + path = subtitle["filepath"] + mg_globals.public_store.delete_file(path) + delete_container = index + media.subtitle_files.pop(delete_container) + media.save() + break + index += 1 + + messages.add_message( + request, + messages.SUCCESS, + ("Subtitle file deleted!!!")) + + return redirect(request, + location=media.url_for_self(request.urlgen)) diff --git a/mediagoblin/static/css/lightbox.css b/mediagoblin/static/css/lightbox.css new file mode 100644 index 00000000..e4fa4c48 --- /dev/null +++ b/mediagoblin/static/css/lightbox.css @@ -0,0 +1,21 @@ +body { + height: 100%; +} +.overlay { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + opacity: 0; + filter: alpha(opacity=0); + z-index: 50; + cursor: pointer; +} +.box { + position: absolute; + opacity: 0; + filter: alpha(opacity=0); + left: -9999em; + z-index: 51; +} diff --git a/mediagoblin/static/css/subtitles.css b/mediagoblin/static/css/subtitles.css new file mode 100644 index 00000000..73dcbc7d --- /dev/null +++ b/mediagoblin/static/css/subtitles.css @@ -0,0 +1,4 @@ +textarea#subtitle { + height: 500px; + border: 3px solid #cccccc; +}
\ No newline at end of file diff --git a/mediagoblin/static/js/lightbox.js b/mediagoblin/static/js/lightbox.js new file mode 100644 index 00000000..8d7bf31f --- /dev/null +++ b/mediagoblin/static/js/lightbox.js @@ -0,0 +1,70 @@ +$(document).ready(function() { + $(".lightbox").click(function() { + overlayLink = $(this).attr("href"); //Getting the link for the media + window.startOverlay(overlayLink); + return false; + }); +}); + +function startOverlay(overlayLink) { + + // Adding elements to the page + $("body") + .append('<div class="overlay"></div><div class="box"></div>') + .css({ + "overflow-y": "hidden" + }); + + // To create the lightbox effect + $(".container").animate({ + "opacity": "0.2" + }, 300, "linear"); + + var imgWidth = $(".box img").width(); + var imgHeight = $(".box img").height(); + + //adding the image to the box + + $(".box").html('<img height=100% width=100% src="' + overlayLink + '" alt="" />'); + //Position + $(".box img").load(function() { + var imgWidth = $(".box img").width(); + var imgHeight = $(".box img").height(); + if (imgHeight > screen.height - 170) imgHeight = screen.height - 170; + if (imgWidth > screen.width - 300) imgWidth = screen.width - 300; + $(".box") + .css({ + "position": "absolute", + "top": "50%", + "left": "50%", + "height": imgHeight + 10, + "width": imgWidth + 10, + "border": "5px solid white", + "margin-top": -(imgHeight / 2), + "margin-left": -(imgWidth / 2) //to position it in the middle + }) + .animate({ + "opacity": "1" + }, 400, "linear"); + + //To remove + window.closeOverlay(); + }); +} + +function closeOverlay() { + // allow users to be able to close the lightbox + $(".overlay").click(function() { + $(".box, .overlay").animate({ + "opacity": "0" + }, 200, "linear", function() { + $(".box, .overlay").remove(); + }); + $(".container").animate({ + "opacity": "1" + }, 200, "linear"); + $("body").css({ + "overflow-y": "scroll" + }); + }); +} diff --git a/mediagoblin/templates/mediagoblin/federation/host-meta.xml b/mediagoblin/templates/mediagoblin/api/host-meta.xml index 7970a0d2..7970a0d2 100644 --- a/mediagoblin/templates/mediagoblin/federation/host-meta.xml +++ b/mediagoblin/templates/mediagoblin/api/host-meta.xml diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html index 61baf11c..31825bfd 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/video.html +++ b/mediagoblin/templates/mediagoblin/media_displays/video.html @@ -65,6 +65,10 @@ type="{{ media.media_manager['default_webm_type'] }}" {% endif %} label="{{ each_media_path[0] }}" res="{{ each_media_path[1][1] }}" /> + {%- for subtitle in media.subtitle_files %} + <track src="{{ request.app.public_store.file_url(subtitle.filepath) }}" + label="{{ subtitle.name }}" kind="subtitles"> + {%- endfor %} {% endfor %} <div class="no_html5"> {%- trans -%}Sorry, this video will not work because diff --git a/mediagoblin/templates/mediagoblin/moderation/media_panel.html b/mediagoblin/templates/mediagoblin/moderation/media_panel.html index 94d4a1a0..e60f5e98 100644 --- a/mediagoblin/templates/mediagoblin/moderation/media_panel.html +++ b/mediagoblin/templates/mediagoblin/moderation/media_panel.html @@ -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> diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index ce19717f..b93da06e 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -233,6 +233,7 @@ </a> </p> {%- endif %} + {% template_hook("subtitle_sidebar") %} {% block mediagoblin_sidebar %} {% endblock %} diff --git a/mediagoblin/tests/.gitignore b/mediagoblin/tests/.gitignore new file mode 100644 index 00000000..16d3c4db --- /dev/null +++ b/mediagoblin/tests/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/mediagoblin/tests/fake_carrot_conf_good.ini b/mediagoblin/tests/fake_carrot_conf_good.ini index 1377907b..8dc32525 100644 --- a/mediagoblin/tests/fake_carrot_conf_good.ini +++ b/mediagoblin/tests/fake_carrot_conf_good.ini @@ -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 diff --git a/mediagoblin/tests/resources.py b/mediagoblin/tests/resources.py index 480f6d9a..38406d62 100644 --- a/mediagoblin/tests/resources.py +++ b/mediagoblin/tests/resources.py @@ -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') diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index 90873cb9..f4741fd1 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -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"] diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index 618d02b6..9cf5ccb0 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -101,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 diff --git a/mediagoblin/tests/test_config.py b/mediagoblin/tests/test_config.py index b13adae6..c3527418 100644 --- a/mediagoblin/tests/test_config.py +++ b/mediagoblin/tests/test_config.py @@ -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 diff --git a/mediagoblin/tests/test_exif.py b/mediagoblin/tests/test_exif.py index d0495a7a..ad771cca 100644 --- a/mediagoblin/tests/test_exif.py +++ b/mediagoblin/tests/test_exif.py @@ -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 Binary files differnew file mode 100644 index 00000000..bd6c7bf2 --- /dev/null +++ b/mediagoblin/tests/test_exif/bad-gps.jpg diff --git a/mediagoblin/tests/test_subtitles.py b/mediagoblin/tests/test_subtitles.py new file mode 100644 index 00000000..4e884d07 --- /dev/null +++ b/mediagoblin/tests/test_subtitles.py @@ -0,0 +1,68 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tests import tools +from mediagoblin import mg_globals +from mediagoblin.db.models import User, MediaEntry +from mediagoblin.db.base import Session +from mediagoblin.tools.testing import _activate_testing +from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry +from mediagoblin.plugins.subtitles.tools import open_subtitle, save_subtitle + +# Checking if the subtitle entry is working + +def test_add_subtitle_entry(test_app): + user_a = fixture_add_user(u"test_user") + + media = fixture_media_entry(uploader=user_a.id, save=False, expunge=False) + media.subtitle_files.append(dict( + name=u"some name", + filepath=[u"does", u"not", u"exist"], + )) + Session.add(media) + Session.flush() + + MediaEntry.query.get(media.id).delete() + User.query.get(user_a.id).delete() + +# Checking the tools written for subtitles + +def test_read_write_file(test_app): + test_filepath = ['test'] + + status = save_subtitle(test_filepath,"Testing!!!") + text = open_subtitle(test_filepath)[0] + + assert status == True + assert text == "Testing!!!" + + mg_globals.public_store.delete_file(test_filepath) + +# Checking the customize exceptions + +def test_customize_subtitle(test_app): + user_a = fixture_add_user(u"test_user") + + media = fixture_media_entry(uploader=user_a.id, save=False, expunge=False) + media.subtitle_files.append(dict( + name=u"some name", + filepath=[u"does", u"not", u"exist"], + )) + Session.add(media) + Session.flush() + + for subtitle in media.subtitle_files: + assert '' == open_subtitle(subtitle['filepath'])[0] diff --git a/mediagoblin/themes/airy/assets/css/airy.css b/mediagoblin/themes/airy/assets/css/airy.css index 7539997e..047e02dc 100644 --- a/mediagoblin/themes/airy/assets/css/airy.css +++ b/mediagoblin/themes/airy/assets/css/airy.css @@ -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; diff --git a/mediagoblin/tools/exif.py b/mediagoblin/tools/exif.py index fafd987d..2215fb0c 100644 --- a/mediagoblin/tools/exif.py +++ b/mediagoblin/tools/exif.py @@ -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 diff --git a/mediagoblin/tools/files.py b/mediagoblin/tools/files.py index 2c486ac8..0509a387 100644 --- a/mediagoblin/tools/files.py +++ b/mediagoblin/tools/files.py @@ -41,5 +41,12 @@ def delete_media_files(media): except OSError: no_such_files.append("/".join(attachment['filepath'])) + for subtitle in media.subtitle_files: + try: + mg_globals.public_store.delete_file( + subtitle['filepath']) + except OSError: + no_such_files.append("/".join(subtitle['filepath'])) + if no_such_files: raise OSError(", ".join(no_such_files)) diff --git a/mediagoblin/tools/licenses.py b/mediagoblin/tools/licenses.py index a964980e..2aff7f20 100644 --- a/mediagoblin/tools/licenses.py +++ b/mediagoblin/tools/licenses.py @@ -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, diff --git a/mediagoblin/tools/subtitles.py b/mediagoblin/tools/subtitles.py new file mode 100644 index 00000000..efafbeec --- /dev/null +++ b/mediagoblin/tools/subtitles.py @@ -0,0 +1,20 @@ +import os + +def get_path(path): + temp = ['user_dev','media','public'] + path = list(eval(path)) + file_path = os.path.abspath(__file__).split('/') # Path of current file as dictionary + subtitle_path = file_path[:-3] + temp + path # Creating the absolute path for the subtitle file + subtitle_path = "/" + os.path.join(*subtitle_path) + return subtitle_path + +def open_subtitle(path): + subtitle_path = get_path(path) + subtitle = open(subtitle_path,"r") # Opening the file using the absolute path + text = subtitle.read() + return text + +def save_subtitle(path,text): + subtitle_path = get_path(path) + subtitle = open(subtitle_path,"w") # Opening the file using the absolute path + subtitle.write(text)
\ No newline at end of file diff --git a/mediagoblin/tools/validator.py b/mediagoblin/tools/validator.py index 03598f9c..93296eab 100644 --- a/mediagoblin/tools/validator.py +++ b/mediagoblin/tools/validator.py @@ -14,21 +14,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from wtforms.validators import Email, URL +from six.moves.urllib.parse import urlparse def validate_email(email): - """ - Validates an email - - Returns True if valid and False if invalid """ + Validates an email - email_re = Email().regex - result = email_re.match(email) - if result is None: - return False - else: - return result.string + Returns True if valid and False if invalid + """ + return '@' in email def validate_url(url): """ @@ -36,11 +30,9 @@ def validate_url(url): Returns True if valid and False if invalid """ - - url_re = URL().regex - result = url_re.match(url) - if result is None: + try: + urlparse(url) + return True + except Exception as e: return False - else: - return result.string diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index 68cb0a3b..73371b6d 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -113,4 +113,4 @@ add_route('mediagoblin.edit.attachments', add_route('mediagoblin.edit.metadata', '/u/<string:user>/m/<int:media_id>/metadata/', - 'mediagoblin.edit.views:edit_metadata') + 'mediagoblin.edit.views:edit_metadata')
\ No newline at end of file diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 5e629575..62a4f151 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -179,6 +179,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 +235,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 +296,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 +305,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, |