diff options
Diffstat (limited to 'mediagoblin')
47 files changed, 1097 insertions, 160 deletions
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/mediagoblin/db/migrations/alembic.ini b/mediagoblin/db/migrations/alembic.ini new file mode 100644 index 00000000..4f7fc115 --- /dev/null +++ b/mediagoblin/db/migrations/alembic.ini @@ -0,0 +1,56 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = %(here)s + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%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, |