diff options
Diffstat (limited to 'mediagoblin')
75 files changed, 789 insertions, 681 deletions
diff --git a/mediagoblin/api/views.py b/mediagoblin/api/views.py index 74181fde..b25300f0 100644 --- a/mediagoblin/api/views.py +++ b/mediagoblin/api/views.py @@ -115,8 +115,13 @@ def uploads_endpoint(request): ) mimetype = request.headers["Content-Type"] - filename = mimetypes.guess_all_extensions(mimetype) - filename = 'unknown' + filename[0] if filename else filename + + if "X-File-Name" in request.headers: + filename = request.headers["X-File-Name"] + else: + filename = mimetypes.guess_all_extensions(mimetype) + filename = 'unknown' + filename[0] if filename else filename + file_data = FileStorage( stream=io.BytesIO(request.data), filename=filename, diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py index 9c16a980..ae6fadf6 100644 --- a/mediagoblin/auth/tools.py +++ b/mediagoblin/auth/tools.py @@ -34,14 +34,19 @@ from mediagoblin import auth _log = logging.getLogger(__name__) -def normalize_user_or_email_field(allow_email=True, allow_user=True): - """ - Check if we were passed a field that matches a username and/or email +def normalize_user_or_email_field(allow_email=True, allow_user=True, + is_login=False): + """Check if we were passed a field that matches a username and/or email pattern. This is useful for fields that can take either a username or email - address. Use the parameters if you want to only allow a username for - instance""" + address. Use the parameters if you want to only allow a username + for instance + + is_login : bool + If is_login is True, does not check the length of username. + + """ message = _(u'Invalid User name or email address.') nomail_msg = _(u"This field does not take email addresses.") nouser_msg = _(u"This field requires an email address.") @@ -56,7 +61,8 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True): else: # lower case user names if not allow_user: raise wtforms.ValidationError(nouser_msg) - wtforms.validators.Length(min=3, max=30)(form, field) + if not is_login: + wtforms.validators.Length(min=3, max=30)(form, field) wtforms.validators.Regexp(r'^[-_\w]+$')(form, field) field.data = field.data.lower() if field.data is None: # should not happen, but be cautious anyway diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 2f95fd81..fb8e7265 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,8 @@ def login(request): return redirect(request, "index") login_failed = True + remote_addr = request.access_route[-1] or request.remote_addr + _log.warn("Failed login attempt from %r", remote_addr) return render_to_response( request, diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 0a8da73e..bd3003d0 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -153,8 +153,7 @@ CELERY_RESULT_BACKEND = string(default="database") CELERY_RESULT_DBURI = string(default="sqlite:///%(here)s/celery.db") # default kombu stuff -BROKER_TRANSPORT = string(default="sqlalchemy") -BROKER_URL = string(default="sqlite:///%(here)s/kombu.db") +BROKER_URL = string(default="amqp://") # known booleans CELERY_RESULT_PERSISTENT = boolean() 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/env.py b/mediagoblin/db/migrations/env.py index 43b7b247..a6d05cd1 100644 --- a/mediagoblin/db/migrations/env.py +++ b/mediagoblin/db/migrations/env.py @@ -48,7 +48,7 @@ def run_migrations_online(): and associate a connection with the context. """ - connection = config.attributes["session"].get_bind() + connection = config.attributes["session"].connection() context.configure( connection=connection, target_metadata=target_metadata @@ -61,4 +61,3 @@ if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() - diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 9bbb252b..c19fe4da 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 @@ -596,6 +597,16 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): # fail_error @property + def get_uploader(self): + # for compatibility + return self.get_actor + + @property + def uploader(self): + # for compatibility + return self.actor + + @property def collections(self): """ Get any collections that this MediaEntry is in """ return list(Collection.query.join(Collection.collection_items).join( @@ -617,9 +628,9 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): query = query.order_by(Comment.added.asc()) else: query = query.order_by(Comment.added.desc()) - + return query - + def url_to_prev(self, urlgen): """get the next 'newer' entry by this user""" media = MediaEntry.query.filter( @@ -769,7 +780,6 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): "self": { "href": public_id, }, - } } @@ -785,6 +795,12 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): if self.location: context["location"] = self.get_location.serialize(request) + # Always show tags, even if empty list + if self.tags: + context["tags"] = [tag['name'] for tag in self.tags] + else: + context["tags"] = [] + if show_comments: comments = [ l.comment().serialize(request) for l in self.get_comments()] @@ -832,6 +848,9 @@ class MediaEntry(Base, MediaEntryMixin, CommentingMixin): if "location" in data: License.create(data["location"], self) + if "tags" in data: + self.tags = convert_to_tag_list_of_dicts(', '.join(data["tags"])) + return True class FileKeynames(Base): @@ -966,7 +985,7 @@ class MediaTag(Base): class Comment(Base): """ Link table between a response and another object that can have replies. - + This acts as a link table between an object and the comments on it, it's done like this so that you can look up all the comments without knowing whhich comments are on an object before hand. Any object can be a comment @@ -977,7 +996,7 @@ class Comment(Base): __tablename__ = "core__comment_links" id = Column(Integer, primary_key=True) - + # The GMR to the object the comment is on. target_id = Column( Integer, @@ -1006,7 +1025,25 @@ class Comment(Base): # When it was added added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) - + + @property + def get_author(self): + # for compatibility + return self.comment().get_actor # noqa + + def __getattr__(self, attr): + if attr.startswith('_'): + # if attr starts with '_', then it's probably some internal + # sqlalchemy variable. Since __getattr__ is called when + # non-existing attributes are being accessed, we should not try to + # fetch it from self.comment() + raise AttributeError + try: + _log.debug('Old attr is being accessed: {0}'.format(attr)) + return getattr(self.comment(), attr) # noqa + except Exception as e: + _log.error(e) + raise class TextComment(Base, TextCommentMixin, CommentingMixin): """ @@ -1040,7 +1077,7 @@ class TextComment(Base, TextCommentMixin, CommentingMixin): if target is None: target = {} else: - target = target.serialize(request, show_comments=False) + target = target.serialize(request, show_comments=False) author = self.get_actor @@ -1068,7 +1105,7 @@ class TextComment(Base, TextCommentMixin, CommentingMixin): if "location" in data: Location.create(data["location"], self) - + # Handle changing the reply ID if "inReplyTo" in data: # Validate that the ID is correct @@ -1099,7 +1136,7 @@ class TextComment(Base, TextCommentMixin, CommentingMixin): link.target = media link.comment = self link.save() - + return True class Collection(Base, CollectionMixin, CommentingMixin): @@ -1298,7 +1335,7 @@ class Notification(Base): seen = Column(Boolean, default=lambda: False, index=True) user = relationship( User, - backref=backref('notifications', cascade='all, delete-orphan')) + backref=backref('notifications', cascade='all, delete-orphan')) def __repr__(self): return '<{klass} #{id}: {user}: {subject} ({seen})>'.format( @@ -1343,7 +1380,7 @@ class Report(Base): which points to the reported object. """ __tablename__ = 'core__reports' - + id = Column(Integer, primary_key=True) reporter_id = Column(Integer, ForeignKey(User.id), nullable=False) reporter = relationship( @@ -1371,7 +1408,7 @@ class Report(Base): resolved = Column(DateTime) result = Column(UnicodeText) - + object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True) object_helper = relationship(GenericModelReference) obj = association_proxy("object_helper", "get_object", diff --git a/mediagoblin/db/models_v0.py b/mediagoblin/db/models_v0.py deleted file mode 100644 index bdedec2e..00000000 --- a/mediagoblin/db/models_v0.py +++ /dev/null @@ -1,342 +0,0 @@ -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -""" -TODO: indexes on foreignkeys, where useful. -""" - -########################################################################### -# WHAT IS THIS FILE? -# ------------------ -# -# Upon occasion, someone runs into this file and wonders why we have -# both a models.py and a models_v0.py. -# -# The short of it is: you can ignore this file. -# -# The long version is, in two parts: -# -# - We used to use MongoDB, then we switched to SQL and SQLAlchemy. -# We needed to convert peoples' databases; the script we had would -# switch them to the first version right after Mongo, convert over -# all their tables, then run any migrations that were added after. -# -# - That script is now removed, but there is some discussion of -# writing a test that would set us at the first SQL migration and -# run everything after. If we wrote that, this file would still be -# useful. But for now, it's legacy! -# -########################################################################### - - -import datetime -import sys - -from sqlalchemy import ( - Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey, - UniqueConstraint, PrimaryKeyConstraint, SmallInteger, Float) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship, backref -from sqlalchemy.orm.collections import attribute_mapped_collection -from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.util import memoized_property - -from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded -from mediagoblin.db.base import GMGTableBase, Session - - -Base_v0 = declarative_base(cls=GMGTableBase) - - -class User(Base_v0): - """ - TODO: We should consider moving some rarely used fields - into some sort of "shadow" table. - """ - __tablename__ = "core__users" - - id = Column(Integer, primary_key=True) - username = Column(Unicode, nullable=False, unique=True) - email = Column(Unicode, nullable=False) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) - pw_hash = Column(Unicode, nullable=False) - email_verified = Column(Boolean, default=False) - status = Column(Unicode, default=u"needs_email_verification", nullable=False) - verification_key = Column(Unicode) - is_admin = Column(Boolean, default=False, nullable=False) - url = Column(Unicode) - bio = Column(UnicodeText) # ?? - fp_verification_key = Column(Unicode) - fp_token_expire = Column(DateTime) - - ## TODO - # plugin data would be in a separate model - - -class MediaEntry(Base_v0): - """ - TODO: Consider fetching the media_files using join - """ - __tablename__ = "core__media_entries" - - id = Column(Integer, primary_key=True) - uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True) - title = Column(Unicode, nullable=False) - slug = Column(Unicode) - created = Column(DateTime, nullable=False, default=datetime.datetime.now, - index=True) - description = Column(UnicodeText) # ?? - media_type = Column(Unicode, nullable=False) - state = Column(Unicode, default=u'unprocessed', nullable=False) - # or use sqlalchemy.types.Enum? - license = Column(Unicode) - - fail_error = Column(Unicode) - fail_metadata = Column(JSONEncoded) - - queued_media_file = Column(PathTupleWithSlashes) - - queued_task_id = Column(Unicode) - - __table_args__ = ( - UniqueConstraint('uploader', 'slug'), - {}) - - get_uploader = relationship(User) - - media_files_helper = relationship("MediaFile", - collection_class=attribute_mapped_collection("name"), - cascade="all, delete-orphan" - ) - - attachment_files_helper = relationship("MediaAttachmentFile", - cascade="all, delete-orphan", - order_by="MediaAttachmentFile.created" - ) - - tags_helper = relationship("MediaTag", - cascade="all, delete-orphan" - ) - - def media_data_init(self, **kwargs): - """ - Initialize or update the contents of a media entry's media_data row - """ - session = Session() - - media_data = session.query(self.media_data_table).filter_by( - media_entry=self.id).first() - - # No media data, so actually add a new one - if media_data is None: - media_data = self.media_data_table( - media_entry=self.id, - **kwargs) - session.add(media_data) - # Update old media data - else: - for field, value in kwargs.iteritems(): - setattr(media_data, field, value) - - @memoized_property - def media_data_table(self): - # TODO: memoize this - models_module = self.media_type + '.models' - __import__(models_module) - return sys.modules[models_module].DATA_MODEL - - -class FileKeynames(Base_v0): - """ - keywords for various places. - currently the MediaFile keys - """ - __tablename__ = "core__file_keynames" - id = Column(Integer, primary_key=True) - name = Column(Unicode, unique=True) - - def __repr__(self): - return "<FileKeyname %r: %r>" % (self.id, self.name) - - @classmethod - def find_or_new(cls, name): - t = cls.query.filter_by(name=name).first() - if t is not None: - return t - return cls(name=name) - - -class MediaFile(Base_v0): - """ - TODO: Highly consider moving "name" into a new table. - TODO: Consider preloading said table in software - """ - __tablename__ = "core__mediafiles" - - media_entry = Column( - Integer, ForeignKey(MediaEntry.id), - nullable=False) - name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False) - file_path = Column(PathTupleWithSlashes) - - __table_args__ = ( - PrimaryKeyConstraint('media_entry', 'name_id'), - {}) - - def __repr__(self): - return "<MediaFile %s: %r>" % (self.name, self.file_path) - - name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True) - name = association_proxy('name_helper', 'name', - creator=FileKeynames.find_or_new - ) - - -class MediaAttachmentFile(Base_v0): - __tablename__ = "core__attachment_files" - - id = Column(Integer, primary_key=True) - media_entry = Column( - Integer, ForeignKey(MediaEntry.id), - nullable=False) - name = Column(Unicode, nullable=False) - filepath = Column(PathTupleWithSlashes) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) - - -class Tag(Base_v0): - __tablename__ = "core__tags" - - id = Column(Integer, primary_key=True) - slug = Column(Unicode, nullable=False, unique=True) - - def __repr__(self): - return "<Tag %r: %r>" % (self.id, self.slug) - - @classmethod - def find_or_new(cls, slug): - t = cls.query.filter_by(slug=slug).first() - if t is not None: - return t - return cls(slug=slug) - - -class MediaTag(Base_v0): - __tablename__ = "core__media_tags" - - id = Column(Integer, primary_key=True) - media_entry = Column( - Integer, ForeignKey(MediaEntry.id), - nullable=False, index=True) - tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True) - name = Column(Unicode) - # created = Column(DateTime, nullable=False, default=datetime.datetime.now) - - __table_args__ = ( - UniqueConstraint('tag', 'media_entry'), - {}) - - tag_helper = relationship(Tag) - slug = association_proxy('tag_helper', 'slug', - creator=Tag.find_or_new - ) - - def __init__(self, name=None, slug=None): - Base_v0.__init__(self) - if name is not None: - self.name = name - if slug is not None: - self.tag_helper = Tag.find_or_new(slug) - - -class MediaComment(Base_v0): - __tablename__ = "core__media_comments" - - id = Column(Integer, primary_key=True) - media_entry = Column( - Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) - author = Column(Integer, ForeignKey(User.id), nullable=False) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) - content = Column(UnicodeText, nullable=False) - - get_author = relationship(User) - - -class ImageData(Base_v0): - __tablename__ = "image__mediadata" - - # The primary key *and* reference to the main media_entry - media_entry = Column(Integer, ForeignKey('core__media_entries.id'), - primary_key=True) - get_media_entry = relationship("MediaEntry", - backref=backref("image__media_data", cascade="all, delete-orphan")) - - width = Column(Integer) - height = Column(Integer) - exif_all = Column(JSONEncoded) - gps_longitude = Column(Float) - gps_latitude = Column(Float) - gps_altitude = Column(Float) - gps_direction = Column(Float) - - -class VideoData(Base_v0): - __tablename__ = "video__mediadata" - - # The primary key *and* reference to the main media_entry - media_entry = Column(Integer, ForeignKey('core__media_entries.id'), - primary_key=True) - get_media_entry = relationship("MediaEntry", - backref=backref("video__media_data", cascade="all, delete-orphan")) - - width = Column(SmallInteger) - height = Column(SmallInteger) - - -class AsciiData(Base_v0): - __tablename__ = "ascii__mediadata" - - # The primary key *and* reference to the main media_entry - media_entry = Column(Integer, ForeignKey('core__media_entries.id'), - primary_key=True) - get_media_entry = relationship("MediaEntry", - backref=backref("ascii__media_data", cascade="all, delete-orphan")) - - -class AudioData(Base_v0): - __tablename__ = "audio__mediadata" - - # The primary key *and* reference to the main media_entry - media_entry = Column(Integer, ForeignKey('core__media_entries.id'), - primary_key=True) - get_media_entry = relationship("MediaEntry", - backref=backref("audio__media_data", cascade="all, delete-orphan")) - - -###################################################### -# Special, migrations-tracking table -# -# Not listed in MODELS because this is special and not -# really migrated, but used for migrations (for now) -###################################################### - -class MigrationData(Base_v0): - __tablename__ = "core__migrations" - - name = Column(Unicode, primary_key=True) - version = Column(Integer, nullable=False, default=0) - -###################################################### 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/views.py b/mediagoblin/edit/views.py index 69f69da5..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 @@ -55,6 +55,10 @@ import mimetypes @get_media_entry_by_id @require_active_login def edit_media(request, media): + # If media is not processed, return NotFound. + if not media.state == u'processed': + return render_404(request) + if not may_edit_media(request, media): raise Forbidden("User may not edit this media") @@ -66,7 +70,7 @@ def edit_media(request, media): license=media.license) form = forms.EditForm( - request.form, + request.method=='POST' and request.form or None, **defaults) if request.method == 'POST' and form.validate(): @@ -115,6 +119,10 @@ UNSAFE_MIMETYPES = [ @get_media_entry_by_id @require_active_login def edit_attachments(request, media): + # If media is not processed, return NotFound. + if not media.state == u'processed': + return render_404(request) + if mg_globals.app_config['allow_attachments']: form = forms.EditAttachmentsForm() @@ -211,7 +219,8 @@ def edit_profile(request, url_user=None): else: location = user.get_location.name - form = forms.EditProfileForm(request.form, + form = forms.EditProfileForm( + request.method == 'POST' and request.form or None, url=user.url, bio=user.bio, location=location) @@ -227,6 +236,8 @@ def edit_profile(request, url_user=None): location = user.get_location location.name = six.text_type(form.location.data) location.save() + else: + user.location = None user.save() @@ -252,7 +263,8 @@ EMAIL_VERIFICATION_TEMPLATE = ( @require_active_login def edit_account(request): user = request.user - form = forms.EditAccountForm(request.form, + form = forms.EditAccountForm( + request.method == 'POST' and request.form or None, wants_comment_notification=user.wants_comment_notification, license_preference=user.license_preference, wants_notifications=user.wants_notifications) @@ -350,7 +362,7 @@ def edit_collection(request, collection): description=collection.description) form = forms.EditCollectionForm( - request.form, + request.method == 'POST' and request.form or None, **defaults) if request.method == 'POST' and form.validate(): @@ -443,9 +455,11 @@ def verify_email(request): user=user.username) +@require_active_login def change_email(request): """ View to change the user's email """ - form = forms.ChangeEmailForm(request.form) + form = forms.ChangeEmailForm( + request.method == 'POST' and request.form or None) user = request.user # If no password authentication, no need to enter a password @@ -498,7 +512,12 @@ def change_email(request): @require_active_login @get_media_entry_by_id def edit_metadata(request, media): - form = forms.EditMetaDataForm(request.form) + # If media is not processed, return NotFound. + if not media.state == u'processed': + return render_404(request) + + form = forms.EditMetaDataForm( + request.method == 'POST' and request.form or None) if request.method == "POST" and form.validate(): metadata_dict = dict([(row['identifier'],row['value']) for row in form.media_metadata.data]) diff --git a/mediagoblin/gmg_commands/addmedia.py b/mediagoblin/gmg_commands/addmedia.py index 8cbfc806..026f3495 100644 --- a/mediagoblin/gmg_commands/addmedia.py +++ b/mediagoblin/gmg_commands/addmedia.py @@ -56,6 +56,11 @@ def parser_setup(subparser): help=( "Slug for this media entry. " "Will be autogenerated if unspecified.")) + subparser.add_argument( + "-c", "--collection-slug", + help=( + "Slug of the collection for this media entry. " + "Should already exist.")) subparser.add_argument( '--celery', @@ -85,8 +90,6 @@ def addmedia(args): print("Can't find a file with filename '%s'" % args.filename) return - upload_limit, max_file_size = get_upload_file_limits(user) - def maybe_unicodeify(some_string): # this is kinda terrible if some_string is None: @@ -102,9 +105,9 @@ def addmedia(args): submitted_file=open(abs_filename, 'rb'), filename=filename, title=maybe_unicodeify(args.title), description=maybe_unicodeify(args.description), + collection_slug=args.collection_slug, license=maybe_unicodeify(args.license), - tags_string=maybe_unicodeify(args.tags) or u"", - upload_limit=upload_limit, max_file_size=max_file_size) + tags_string=maybe_unicodeify(args.tags) or u"") except FileUploadLimit: print("This file is larger than the upload limits for this site.") except UserUploadLimit: diff --git a/mediagoblin/gmg_commands/batchaddmedia.py b/mediagoblin/gmg_commands/batchaddmedia.py index 2ad7e39e..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,9 +73,6 @@ def batchaddmedia(args): username=args.username))) return - upload_limit, max_file_size = get_upload_file_limits(user) - temp_files = [] - if os.path.isfile(args.metadata_path): metadata_path = args.metadata_path @@ -87,7 +84,6 @@ def batchaddmedia(args): abs_metadata_filename = os.path.abspath(metadata_path) abs_metadata_dir = os.path.dirname(abs_metadata_filename) - upload_limit, max_file_size = get_upload_file_limits(user) def maybe_unicodeify(some_string): # this is kinda terrible @@ -101,7 +97,7 @@ def batchaddmedia(args): contents = all_metadata.read() media_metadata = parse_csv_file(contents) - for media_id, file_metadata in media_metadata.iteritems(): + for media_id, file_metadata in media_metadata.items(): files_attempted += 1 # In case the metadata was not uploaded initialize an empty dictionary. json_ld_metadata = compact_and_validate({}) @@ -115,6 +111,7 @@ def batchaddmedia(args): title = file_metadata.get('title') or file_metadata.get('dc:title') description = (file_metadata.get('description') or file_metadata.get('dc:description')) + collection_slug = file_metadata.get('collection-slug') license = file_metadata.get('license') try: @@ -143,7 +140,7 @@ Metadata was not uploaded.""".format( file_path = os.path.join(abs_metadata_dir, path) file_abs_path = os.path.abspath(file_path) try: - media_file = file(file_abs_path, 'r') + media_file = open(file_abs_path, 'rb') except IOError: print(_(u"""\ FAIL: Local file {filename} could not be accessed. @@ -157,10 +154,10 @@ FAIL: Local file {filename} could not be accessed. filename=filename, title=maybe_unicodeify(title), description=maybe_unicodeify(description), + collection_slug=maybe_unicodeify(collection_slug), license=maybe_unicodeify(license), metadata=json_ld_metadata, - tags_string=u"", - upload_limit=upload_limit, max_file_size=max_file_size) + tags_string=u"") print(_(u"""Successfully submitted {filename}! Be sure to look at the Media Processing Panel on your website to be sure it uploaded successfully.""".format(filename=filename))) @@ -206,7 +203,12 @@ def parse_csv_file(file_contents): # Build a dictionary for index, line in enumerate(lines): if line.isspace() or line == u'': continue - values = unicode_csv_reader([line]).next() + if (sys.version_info[0] == 3): + # Python 3's csv.py supports Unicode out of the box. + reader = csv.reader([line]) + else: + reader = unicode_csv_reader([line]) + values = next(reader) line_dict = dict([(key[i], val) for i, val in enumerate(values)]) media_id = line_dict.get('id') or index diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py index bafe76bb..2700ccbc 100644 --- a/mediagoblin/gmg_commands/dbupdate.py +++ b/mediagoblin/gmg_commands/dbupdate.py @@ -133,7 +133,9 @@ def run_alembic_migrations(db, app_config, global_config): session = Session() cfg = build_alembic_config(global_config, None, session) - return command.upgrade(cfg, 'heads') + res = command.upgrade(cfg, 'heads') + session.commit() + return res def run_dbupdate(app_config, global_config): @@ -146,7 +148,7 @@ def run_dbupdate(app_config, global_config): # Set up the database db = setup_connection_and_db_from_config(app_config, migrations=True) - # Do we have migrations + # Do we have migrations should_run_sqam_migrations = db.engine.has_table("core__migrations") and \ sqam_migrations_to_run(db, app_config, global_config) diff --git a/mediagoblin/i18n/es/mediagoblin.po b/mediagoblin/i18n/es/mediagoblin.po index 8fd27b62..bdc63e56 100644 --- a/mediagoblin/i18n/es/mediagoblin.po +++ b/mediagoblin/i18n/es/mediagoblin.po @@ -2645,7 +2645,7 @@ msgstr "más antiguo" #: mediagoblin/templates/mediagoblin/utils/profile.html:36 msgid "Location" -msgstr "Locación" +msgstr "Lugar" #: mediagoblin/templates/mediagoblin/utils/report.html:25 msgid "Report media" 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/listings/views.py b/mediagoblin/listings/views.py index f640cc95..6e1528ca 100644 --- a/mediagoblin/listings/views.py +++ b/mediagoblin/listings/views.py @@ -17,9 +17,10 @@ from mediagoblin import mg_globals from mediagoblin.db.models import MediaEntry from mediagoblin.db.util import media_entries_for_tag_slug +from mediagoblin.decorators import uses_pagination +from mediagoblin.plugins.api.tools import get_media_file_paths from mediagoblin.tools.pagination import Pagination from mediagoblin.tools.response import render_to_response -from mediagoblin.decorators import uses_pagination from werkzeug.contrib.atom import AtomFeed @@ -72,19 +73,25 @@ def atom_feed(request): tag_slug = request.matchdict.get(u'tag') feed_title = "MediaGoblin Feed" if tag_slug: - cursor = media_entries_for_tag_slug(request.db, tag_slug) + feed_title += " for tag '%s'" % tag_slug link = request.urlgen('mediagoblin.listings.tags_listing', qualified=True, tag=tag_slug ) - feed_title += "for tag '%s'" % tag_slug + cursor = media_entries_for_tag_slug(request.db, tag_slug) else: # all recent item feed - cursor = MediaEntry.query.filter_by(state=u'processed') + feed_title += " for all recent items" link = request.urlgen('index', qualified=True) - feed_title += "for all recent items" + cursor = MediaEntry.query.filter_by(state=u'processed') + cursor = cursor.order_by(MediaEntry.created.desc()) + cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) + - atomlinks = [ - {'href': link, - 'rel': 'alternate', - 'type': 'text/html'}] + """ + ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI) + """ + atomlinks = [{ + 'href': link, + 'rel': 'alternate', + 'type': 'text/html'}] if mg_globals.app_config["push_urls"]: for push_url in mg_globals.app_config["push_urls"]: @@ -92,9 +99,6 @@ def atom_feed(request): 'rel': 'hub', 'href': push_url}) - cursor = cursor.order_by(MediaEntry.created.desc()) - cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) - feed = AtomFeed( feed_title, feed_url=request.url, @@ -102,19 +106,30 @@ def atom_feed(request): links=atomlinks) for entry in cursor: - feed.add(entry.get('title'), - entry.description_html, - id=entry.url_for_self(request.urlgen,qualified=True), + # Include a thumbnail image in content. + file_urls = get_media_file_paths(entry.media_files, request.urlgen) + if 'thumb' in file_urls: + content = u'<img src="{thumb}" alt='' /> {desc}'.format( + thumb=file_urls['thumb'], desc=entry.description_html) + else: + content = entry.description_html + + feed.add( + entry.get('title'), + content, + id=entry.url_for_self(request.urlgen, qualified=True), content_type='html', - author={'name': entry.get_actor.username, + author={ + 'name': entry.get_actor.username, 'uri': request.urlgen( 'mediagoblin.user_pages.user_home', - qualified=True, user=entry.get_actor.username)}, + qualified=True, + user=entry.get_actor.username)}, updated=entry.get('created'), links=[{ - 'href':entry.url_for_self( - request.urlgen, - qualified=True), + 'href': entry.url_for_self( + request.urlgen, + qualified=True), 'rel': 'alternate', 'type': 'text/html'}]) diff --git a/mediagoblin/media_types/blog/models.py b/mediagoblin/media_types/blog/models.py index 0e1ddf97..83f520c7 100644 --- a/mediagoblin/media_types/blog/models.py +++ b/mediagoblin/media_types/blog/models.py @@ -34,6 +34,7 @@ class BlogMixin(GenerateSlugMixin): def check_slug_used(self, slug): return check_blog_slug_used(self.author, slug, self.id) +BLOG_BACKREF_NAME = "mediatype__blogs" class Blog(Base, BlogMixin): __tablename__ = "mediatype__blogs" @@ -43,6 +44,7 @@ class Blog(Base, BlogMixin): author = Column(Integer, ForeignKey(User.id), nullable=False, index=True) #similar to uploader created = Column(DateTime, nullable=False, default=datetime.datetime.now, index=True) slug = Column(Unicode) + get_author = relationship("User", backref=backref(BLOG_BACKREF_NAME, cascade="all, delete-orphan")) @property def slug_or_id(self): @@ -66,7 +68,7 @@ class Blog(Base, BlogMixin): -BACKREF_NAME = "blogpost__media_data" +BLOG_POST_BACKREF_NAME = "blogpost__media_data" class BlogPostData(Base): __tablename__ = "blogpost__mediadata" @@ -75,7 +77,7 @@ class BlogPostData(Base): media_entry = Column(Integer, ForeignKey('core__media_entries.id'), primary_key=True) blog = Column(Integer, ForeignKey('mediatype__blogs.id'), nullable=False) get_media_entry = relationship("MediaEntry", - backref=backref(BACKREF_NAME, uselist=False, + backref=backref(BLOG_POST_BACKREF_NAME, uselist=False, cascade="all, delete-orphan")) diff --git a/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_admin_dashboard.html b/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_admin_dashboard.html index 3b881466..97408b59 100644 --- a/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_admin_dashboard.html +++ b/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_admin_dashboard.html @@ -53,7 +53,7 @@ {% set blog_delete_url = request.urlgen('mediagoblin.media_types.blog.blog_delete', blog_slug=blog.slug, user=request.user.username) %} -<a class="button_action" href="{{ blog_delete_url }}"> +<a class="button_action button_warning" href="{{ blog_delete_url }}"> {%- trans %}Delete Blog{% endtrans -%} </a> </p> @@ -90,7 +90,7 @@ media_id=blog_post.id) %} <td> <a class="button_action" href="{{ blogpost_edit_url }}">{% trans %}Edit{% endtrans %}</a> - <a class="button_action" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a> + <a class="button_action button_warning" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a> </td> </tr> {% endfor %} diff --git a/mediagoblin/media_types/blog/templates/mediagoblin/blog/blogpost_draft_view.html b/mediagoblin/media_types/blog/templates/mediagoblin/blog/blogpost_draft_view.html index 6d820550..7a53cc9e 100644 --- a/mediagoblin/media_types/blog/templates/mediagoblin/blog/blogpost_draft_view.html +++ b/mediagoblin/media_types/blog/templates/mediagoblin/blog/blogpost_draft_view.html @@ -33,7 +33,7 @@ user= blogpost.get_actor.username, media_id=blogpost.id) %} <a class="button_action" href="{{ blogpost_edit_url }}">{% trans %}Edit{% endtrans %}</a> - <a class="button_action" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a> + <a class="button_action button_warning" href="{{ blogpost_delete_url }}">{% trans %}Delete{% endtrans %}</a> {% endblock %} diff --git a/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html b/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html index 8c16daeb..bad33c8c 100644 --- a/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html +++ b/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html @@ -52,7 +52,7 @@ {% if request.user and request.user.username==user.username %} <p>You have not created any blog yet.</p> {% else %} - <p>No blog has been created by <strong>{{ user.username }}</strong>yet.</p> + <p>No blog has been created by <strong>{{ user.username }}</strong> yet.</p> {% endif %} {% endif %} <br/> diff --git a/mediagoblin/media_types/blog/views.py b/mediagoblin/media_types/blog/views.py index d48cf82f..288a47ae 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/models.py b/mediagoblin/media_types/video/models.py index 4742b342..da635ed7 100644 --- a/mediagoblin/media_types/video/models.py +++ b/mediagoblin/media_types/video/models.py @@ -69,7 +69,7 @@ class VideoData(Base): orig_metadata = self.orig_metadata or {} if ("webm_video" not in self.get_media_entry.media_files - and "mimetype" in orig_metadata['common']['tags'] + and "mimetype" in orig_metadata.get('common', {}).get('tags', {}) and "codec" in orig_metadata['audio'] and "codec" in orig_metadata['video']): if orig_metadata['mimetype'] == 'application/ogg': diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index ca3087a2..71204fc7 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -79,7 +79,17 @@ def sniffer(media_file): return MEDIA_TYPE +EXCLUDED_EXTS = ["nef", "svg"] + def sniff_handler(media_file, filename): + name, ext = os.path.splitext(filename) + clean_ext = ext.lower()[1:] + + if clean_ext in EXCLUDED_EXTS: + # We don't handle this filetype, though gstreamer might think we can + _log.info('Refused to process {0} due to excluded extension'.format(filename)) + return None + try: return sniffer(media_file) except: @@ -106,10 +116,13 @@ def get_tags(stream_info): # TODO: handle timezone info; gst.get_time_zone_offset + # python's tzinfo should help dt = tags['datetime'] - tags['datetime'] = datetime.datetime( - dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(), - dt.get_minute(), dt.get_second(), - dt.get_microsecond()).isoformat() + try: + tags['datetime'] = datetime.datetime( + dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(), + dt.get_minute(), dt.get_second(), + dt.get_microsecond()).isoformat() + except: + tags['datetime'] = None for k, v in tags.copy().items(): # types below are accepted by json; others must not present if not isinstance(v, (dict, list, six.string_types, int, float, bool, diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index f4b0341e..2d3392f2 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -31,7 +31,7 @@ sys.argv = [] import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import GLib, Gst Gst.init(None) # init before import to work around https://bugzilla.gnome.org/show_bug.cgi?id=736260 from gi.repository import GstPbutils @@ -154,7 +154,7 @@ class VideoTranscoder(object): def __init__(self): _log.info('Initializing VideoTranscoder...') self.progress_percentage = None - self.loop = GObject.MainLoop() + self.loop = GLib.MainLoop() def transcode(self, src, dst, **kwargs): ''' @@ -371,11 +371,11 @@ class VideoTranscoder(object): self.pipeline.set_state(Gst.State.NULL) # This kills the loop, mercifully - GObject.idle_add(self.__stop_mainloop) + GLib.idle_add(self.__stop_mainloop) def __stop_mainloop(self): ''' - Wrapper for GObject.MainLoop.quit() + Wrapper for GLib.MainLoop.quit() This wrapper makes us able to see if self.loop.quit has been called ''' diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 23341065..fdd22ace 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -52,8 +52,6 @@ def post_entry(request): _log.debug('File field not found') raise BadRequest() - upload_limit, max_file_size = get_upload_file_limits(request.user) - callback_url = request.form.get('callback_url') if callback_url: callback_url = six.text_type(callback_url) @@ -66,7 +64,6 @@ def post_entry(request): description=six.text_type(request.form.get('description')), license=six.text_type(request.form.get('license', '')), tags_string=six.text_type(request.form.get('tags', '')), - upload_limit=upload_limit, max_file_size=max_file_size, callback_url=callback_url) return json_response(get_entry_serializable(entry, request.urlgen)) diff --git a/mediagoblin/plugins/basic_auth/README.rst b/mediagoblin/plugins/basic_auth/README.rst index 82f247ed..87a7b16f 100644 --- a/mediagoblin/plugins/basic_auth/README.rst +++ b/mediagoblin/plugins/basic_auth/README.rst @@ -5,7 +5,7 @@ =================== The basic_auth plugin is enabled by default in mediagoblin.ini. This plugin -provides basic username and password authentication for GNU Mediagoblin. +provides basic username and password authentication for GNU MediaGoblin. This plugin can be enabled alongside :ref:`openid-chapter` and :ref:`persona-chapter`. diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py index 9a6db226..3d684e91 100644 --- a/mediagoblin/plugins/basic_auth/forms.py +++ b/mediagoblin/plugins/basic_auth/forms.py @@ -38,7 +38,7 @@ class LoginForm(wtforms.Form): username = wtforms.StringField( _('Username or Email'), [wtforms.validators.InputRequired(), - normalize_user_or_email_field()]) + normalize_user_or_email_field(is_login=True)]) password = wtforms.PasswordField( _('Password')) stay_logged_in = wtforms.BooleanField( diff --git a/mediagoblin/plugins/flatpagesfile/README.rst b/mediagoblin/plugins/flatpagesfile/README.rst index 59cd6217..0354a46c 100644 --- a/mediagoblin/plugins/flatpagesfile/README.rst +++ b/mediagoblin/plugins/flatpagesfile/README.rst @@ -57,7 +57,7 @@ Examples: ``flatpages-about``, ``about-view``, ``contact-view``, ... The value has two parts separated by commas: -1. **route path**: This is the url that this route matches. +1. **route path**: This is the URL that this route matches. Examples: ``/about``, ``/contact``, ``/pages/about``, ... @@ -74,7 +74,7 @@ The value has two parts separated by commas: For example: ``'/siteadmin/{adminname:\w+}'`` -2. **template**: The template to use for this url. The template is in +2. **template**: The template to use for this URL. The template is in the flatpagesfile template directory, so you just need to specify the file name. @@ -139,10 +139,10 @@ template:: Recipes ======= -Url variables +URL variables ------------- -You can handle urls like ``/about/{name}`` and access the name that's +You can handle URLs like ``/about/{name}`` and access the name that's passed in in the template. Sample route:: diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html index 87f790d1..be608ac0 100644 --- a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html +++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html @@ -43,8 +43,9 @@ href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors </li><li>Imaging ©<a - href="http://mapquest.com">MapQuest</a></li><li>Maps powered by - <a href="http://leafletjs.com/"> Leaflet</a></li></ul> + href="https://www.openstreetmap.org">OpenStreetMap + contributors</a></li><li>Maps powered by + <a href="http://leafletjs.com/">Leaflet</a></li></ul> </div> <p> <small> diff --git a/mediagoblin/plugins/ldap/README.rst b/mediagoblin/plugins/ldap/README.rst index ea9a34b3..049b5c4d 100644 --- a/mediagoblin/plugins/ldap/README.rst +++ b/mediagoblin/plugins/ldap/README.rst @@ -14,16 +14,16 @@ .. _ldap-plugin: ============= - ldap plugin + LDAP plugin ============= .. Warning:: This plugin is not compatible with the other authentication plugins. -This plugin allow your GNU Mediagoblin instance to authenticate against an +This plugin allow your GNU MediaGoblin instance to authenticate against an LDAP server. -Set up the ldap plugin +Set up the LDAP plugin ====================== 1. Install the ``python-ldap`` package. @@ -32,13 +32,13 @@ Set up the ldap plugin [[mediagoblin.plugins.ldap]] -Configuring the ldap plugin +Configuring the LDAP plugin =========================== -This plugin allows you to use multiple ldap servers for authentication. +This plugin allows you to use multiple LDAP servers for authentication. In order to configure a server, add the following to you MediaGoblin .ini file -under the ldap plugin:: +under the LDAP plugin:: [[mediagoblin.plugins.ldap]] [[[server1]]] @@ -50,15 +50,15 @@ under the ldap plugin:: Make any necessary changes to the above to work with your sever. Make sure ``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE. -If you would like to fetch the users email from the ldap server upon account +If you would like to fetch the users email from the LDAP server upon account registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and ``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your MediaGoblin .ini file. .. Warning:: By default, this plugin provides no encryption when communicating with the - ldap servers. If you would like to use an SSL connection, change - LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default ldap + LDAP servers. If you would like to use an SSL connection, change + LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default LDAP port for SSL connections is 636. If you would like to use a TLS connection, add ``LDAP_START_TLS = 'true'`` under your server configuration in your MediaGoblin .ini file. diff --git a/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html.orig b/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html.orig deleted file mode 100644 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/openid/README.rst b/mediagoblin/plugins/openid/README.rst index 870a2b58..1a777336 100644 --- a/mediagoblin/plugins/openid/README.rst +++ b/mediagoblin/plugins/openid/README.rst @@ -1,23 +1,23 @@ .. _openid-chapter: =================== - openid plugin + OpenID plugin =================== -The openid plugin allows user to login to your GNU Mediagoblin instance using -their openid url. +The OpenID plugin allows user to login to your GNU MediaGoblin instance using +their OpenID URL. This plugin can be enabled alongside :ref:`basic_auth-chapter` and :ref:`persona-chapter`. .. note:: - When :ref:`basic_auth-chapter` is enabled alongside this openid plugin, and - a user creates an account using their openid. If they would like to add a + When :ref:`basic_auth-chapter` is enabled alongside this OpenID plugin, and + a user creates an account using their OpenID. If they would like to add a password to their account, they can use the forgot password feature to do so. -Set up the openid plugin +Set up the OpenID plugin ============================ 1. Install the ``python-openid`` package. diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index ab741a72..30c7ffa2 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -128,16 +128,13 @@ def pwg_images_addSimple(request): if not check_file_field(request, 'image'): raise BadRequest() - upload_limit, max_file_size = get_upload_file_limits(request.user) - try: entry = submit_media( mg_app=request.app, user=request.user, submitted_file=request.files['image'], filename=request.files['image'].filename, title=six.text_type(form.name.data), - description=six.text_type(form.comment.data), - upload_limit=upload_limit, max_file_size=max_file_size) + description=six.text_type(form.comment.data)) collection_id = form.category.data if collection_id > 0: diff --git a/mediagoblin/plugins/trim_whitespace/README.rst b/mediagoblin/plugins/trim_whitespace/README.rst index db9a0c53..d83af06b 100644 --- a/mediagoblin/plugins/trim_whitespace/README.rst +++ b/mediagoblin/plugins/trim_whitespace/README.rst @@ -2,7 +2,7 @@ Trim whitespace plugin ======================= -Mediagoblin templates are written with 80 char limit for better +MediaGoblin templates are written with 80 char limit for better readability. However that means that the HTML output is very verbose containing *lots* of whitespace. This plugin inserts a middleware that filters out whitespace from the returned HTML in the ``Response()`` diff --git a/mediagoblin/processing/task.py b/mediagoblin/processing/task.py index 5e0e772d..bedfd32d 100644 --- a/mediagoblin/processing/task.py +++ b/mediagoblin/processing/task.py @@ -69,6 +69,9 @@ class ProcessMedia(celery.Task): """ Pass this entry off for processing. """ + + name = 'process_media' + def run(self, media_id, feed_url, reprocess_action, reprocess_info=None): """ Pass the media entry off to the appropriate processing function diff --git a/mediagoblin/static/css/audio.css b/mediagoblin/static/css/audio.css index 5c50e727..de388094 100644 --- a/mediagoblin/static/css/audio.css +++ b/mediagoblin/static/css/audio.css @@ -24,10 +24,11 @@ font-size: 40px; width: 50px; text-shadow: 0 0 10px black; + background: none; + border: none; } .audio-control-play-pause.playing { color: #b71500; - letter-spacing: -17px; margin-left: -7px; } .audio-control-play-pause.paused { diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index 7852cae9..6da19f94 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -394,6 +394,12 @@ text-align: center; margin-right: auto; } +.form_box > h1, .form_box_xl > h1 { + /* Fix header overflowing issue. */ + overflow: hidden; + text-overflow: ellipsis +} + .form_box_xl { max-width: 460px; } @@ -457,11 +463,9 @@ text-align: center; } .form_field_label { - margin-bottom: 4px; -} - -.form_field_label { font-size:1.125em; + margin-bottom: 0; + padding: 10px 0; } .form_field_description { @@ -585,7 +589,6 @@ ul#action_to_resolve {list-style:none; margin-left:10px;} border-radius: 0 0 5px 5px; padding: 0 0 6px; text-overflow: ellipsis; - white-space: nowrap; overflow: hidden; border-color: #0D0D0D; border-style: solid; diff --git a/mediagoblin/static/js/audio.js b/mediagoblin/static/js/audio.js index 50d58cd9..59a8c801 100644 --- a/mediagoblin/static/js/audio.js +++ b/mediagoblin/static/js/audio.js @@ -116,6 +116,10 @@ var audioPlayer = new Object(); var im = audioPlayer.imageElement; var pos = (e.offsetX || e.originalEvent.layerX) / im.width(); + console.log('pos', (e.offsetX || e.originalEvent.layerX) / im.width()) + console.log('setting current time to', + pos * audioPlayer.audioElement.duration) + audioPlayer.audioElement.currentTime = pos * audioPlayer.audioElement.duration; audioPlayer.audioElement.play(); audioPlayer.setState(audioPlayer.PLAYING); @@ -151,14 +155,16 @@ var audioPlayer = new Object(); switch (state) { case audioPlayer.PLAYING: - $('.audio-spectrogram .audio-control-play-pause') + el = $('.audio-spectrogram .audio-control-play-pause') .removeClass('paused').addClass('playing') - .text('▮▮'); + .text('▮▮').attr('aria-label', 'Pause'); + el[0].setAttribute('aria-label', 'Pause') break; case audioPlayer.PAUSED: - $('.audio-spectrogram .audio-control-play-pause') + el = $('.audio-spectrogram .audio-control-play-pause') .removeClass('playing').addClass('paused') - .text('▶'); + .text('▶').attr('aria-label', 'Play'); + el[0].setAttribute('aria-label', 'Play') break; } }; @@ -200,19 +206,9 @@ var audioPlayer = new Object(); * Attach the player to an image element */ console.log(imageElement); - var im = $(imageElement); - audioPlayer.imageElement = im; - $('<div class="playhead"></div>').appendTo(im.parent()); - $('<div class="buffered-indicators"></div>').appendTo(im.parent()); - $('<div class="seekbar"></div>').appendTo(im.parent()); - $('<div class="audio-control-play-pause paused">▶</div>').appendTo(im.parent()); - $('<div class="audio-currentTime">00:00</div>').appendTo(im.parent()); - $('<input type="range" class="audio-volume"' - +'value="1" min="0" max="1" step="0.001" />').appendTo(im.parent()); - $('.audio-spectrogram').trigger('attachedControls'); }; })(audioPlayer); diff --git a/mediagoblin/static/js/geolocation-map.js b/mediagoblin/static/js/geolocation-map.js index 26d94c5d..c30788f7 100644 --- a/mediagoblin/static/js/geolocation-map.js +++ b/mediagoblin/static/js/geolocation-map.js @@ -30,13 +30,11 @@ $(document).ready(function () { // Get a new map instance attached and element with id="tile-map" var map = new L.Map('tile-map'); - var mqtileUrl = 'http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg'; + var mqtileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; var mqtileAttrib = '<a id="osm_license_link">see map license</a>'; var mqtile = new L.TileLayer( mqtileUrl, - {maxZoom: 18, - attribution: mqtileAttrib, - subdomains: '1234'}); + {maxZoom: 18}); map.attributionControl.setPrefix(''); var location = new L.LatLng(latitude, longitude); diff --git a/mediagoblin/static/js/header_dropdown.js b/mediagoblin/static/js/header_dropdown.js index 3ee46228..979d2690 100644 --- a/mediagoblin/static/js/header_dropdown.js +++ b/mediagoblin/static/js/header_dropdown.js @@ -17,9 +17,27 @@ */ $(document).ready(function(){ - $("#header_dropdown").hide(); - $(".header_dropdown_up").hide(); - $(".header_dropdown_down,.header_dropdown_up").click(function() { + // The header drop-down header panel defaults to open until you explicitly + // close it. After that, the panel open/closed setting will persist across + // page loads. + + // Initialise the panel status when page is loaded. + if (localStorage.getItem("panel_closed")) { + $("#header_dropdown").hide(); + $(".header_dropdown_up").hide(); + } + else { + $(".header_dropdown_down").hide(); + } + + // Toggle and persist the panel status. + $(".header_dropdown_down, .header_dropdown_up").click(function() { + if (localStorage.getItem("panel_closed")) { + localStorage.removeItem("panel_closed"); + } + else { + localStorage.setItem("panel_closed", "true"); + } $(".header_dropdown_down").toggle(); $(".header_dropdown_up").toggle(); $("#header_dropdown").slideToggle(); diff --git a/mediagoblin/static/js/post_comment.js b/mediagoblin/static/js/post_comment.js new file mode 100644 index 00000000..431c222f --- /dev/null +++ b/mediagoblin/static/js/post_comment.js @@ -0,0 +1,63 @@ +/** + * GNU MediaGoblin -- federated, autonomous media hosting + * Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +$(document).ready(function(){ + $(function() { + // Hide this button if script is enabled + $('.form_submit_buttons').find('input').hide(); + + // Include this link if script is enabled + $('.form_submit_buttons').append( + '<a class="button_action" id="post_comment" type="button">' + + 'Add this comment </a>'); + + $('#post_comment').click(function() { + $.ajax({ + url: $('#postCommentURL').val(), + data: $('#form_comment').serialize(), + type: 'POST', + success: function(response) { + var message = $(response).find('.mediagoblin_messages'); + var commentsInResponse = $($(response).find('.media_comments')).find('li'); + var commentsInPage = $('.media_comments').find('ul'); + + // Post the message + message.css({"position":"fixed", "top":"50px", "width":"100%"}); + $('body').append(message); + message.delay(1500).fadeOut(); + + // Checking if there is new comment + if(commentsInResponse.length != $(commentsInPage).find('li').length) { + // Post comment and scroll down to it + var newComment = commentsInResponse[commentsInResponse.length - 1]; + $('#form_comment').fadeOut('fast'); + $('#button_addcomment').fadeIn('fast'); + $('#comment_preview').replaceWith("<div id=comment_preview></div>"); + $(commentsInPage).append(newComment); + $('html, body').animate({ + scrollTop: $(newComment).offset().top + }, 1000); + } + }, + error: function(error) { + console.log(error); + } + }); + }); + }); +});
\ No newline at end of file diff --git a/mediagoblin/static/js/show_password.js b/mediagoblin/static/js/show_password.js index b3fbc862..12935124 100644 --- a/mediagoblin/static/js/show_password.js +++ b/mediagoblin/static/js/show_password.js @@ -18,7 +18,7 @@ $(document).ready(function(){ //Create a duplicate password field. We could change the input type dynamically, but this angers the IE gods (not just IE6). - $("#password").after('<input type="text" value="" name="password_clear" id="password_clear" /><label><input type="checkbox" id="password_boolean" />Show password</label>'); + $("#password").after('<input type="text" value="" name="password_clear" id="password_clear" style="width:100%" /><label><br/><input type="checkbox" id="password_boolean" />Show password</label>'); $('#password_clear').hide(); $('#password_boolean').click(function(){ if($('#password_boolean').prop("checked")) { diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py index 2edea70f..08a603e9 100644 --- a/mediagoblin/submit/lib.py +++ b/mediagoblin/submit/lib.py @@ -27,11 +27,12 @@ from mediagoblin import mg_globals from mediagoblin.tools.response import json_response from mediagoblin.tools.text import convert_to_tag_list_of_dicts from mediagoblin.tools.federation import create_activity, create_generator -from mediagoblin.db.models import MediaEntry, ProcessingMetaData +from mediagoblin.db.models import Collection, MediaEntry, ProcessingMetaData from mediagoblin.processing import mark_entry_failed from mediagoblin.processing.task import ProcessMedia from mediagoblin.notifications import add_comment_subscription from mediagoblin.media_types import sniff_media +from mediagoblin.user_pages.lib import add_media_to_collection _log = logging.getLogger(__name__) @@ -101,9 +102,8 @@ class UserPastUploadLimit(UploadLimitError): def submit_media(mg_app, user, submitted_file, filename, - title=None, description=None, + title=None, description=None, collection_slug=None, license=None, metadata=None, tags_string=u"", - upload_limit=None, max_file_size=None, callback_url=None, urlgen=None,): """ Args: @@ -116,15 +116,15 @@ def submit_media(mg_app, user, submitted_file, filename, one on disk being referenced by submitted_file. - title: title for this media entry - description: description for this media entry + - collection_slug: collection for this media entry - license: license for this media entry - tags_string: comma separated string of tags to be associated with this entry - - upload_limit: size in megabytes that's the per-user upload limit - - max_file_size: maximum size each file can be that's uploaded - callback_url: possible post-hook to call after submission - urlgen: if provided, used to do the feed_url update and assign a public ID used in the API (very important). """ + upload_limit, max_file_size = get_upload_file_limits(user) if upload_limit and user.uploaded >= upload_limit: raise UserPastUploadLimit() @@ -205,6 +205,13 @@ def submit_media(mg_app, user, submitted_file, filename, create_activity("post", entry, entry.actor) entry.save() + # add to collection + if collection_slug: + collection = Collection.query.filter_by(slug=collection_slug, + actor=user.id).first() + if collection: + add_media_to_collection(collection, entry) + # Pass off to processing # # (... don't change entry after this point to avoid race diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index be473615..7bbfb645 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -76,7 +76,6 @@ def submit_start(request): description=six.text_type(submit_form.description.data), license=six.text_type(submit_form.license.data) or None, tags_string=submit_form.tags.data, - upload_limit=upload_limit, max_file_size=max_file_size, urlgen=request.urlgen) if submit_form.collection and submit_form.collection.data: 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/api/oob.html b/mediagoblin/templates/mediagoblin/api/oob.html index 97bdd9ca..e4d3a132 100644 --- a/mediagoblin/templates/mediagoblin/api/oob.html +++ b/mediagoblin/templates/mediagoblin/api/oob.html @@ -27,7 +27,5 @@ <h4>{% trans %}Copy and paste this <strong>verifier code</strong> into your client:{% endtrans %}</h4> -<p class="verifier"> - {{ oauth_request.verifier }} -</p> +<p class="verifier">{{ oauth_request.verifier }}</p> {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/auth/register.html b/mediagoblin/templates/mediagoblin/auth/register.html index a7b8033f..b52ecff4 100644 --- a/mediagoblin/templates/mediagoblin/auth/register.html +++ b/mediagoblin/templates/mediagoblin/auth/register.html @@ -37,6 +37,7 @@ {% template_hook("register_link") %} {{ wtforms_util.render_divs(register_form, True) }} {{ csrf_token }} + {% template_hook("register_captcha") %} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Create{% endtrans %}" class="button_form" /> diff --git a/mediagoblin/templates/mediagoblin/media_displays/audio.html b/mediagoblin/templates/mediagoblin/media_displays/audio.html index 7571f863..191eff14 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/audio.html +++ b/mediagoblin/templates/mediagoblin/media_displays/audio.html @@ -30,6 +30,12 @@ <div class="audio-media"> {% if 'spectrogram' in media.media_files %} <div class="audio-spectrogram"> + <div class="playhead"></div> + <div class="buffered-indicators"></div> + <div class="seekbar"></div> + <button class="audio-control-play-pause paused" aria-label="Play">▶</button> + <div class="audio-currentTime" aria-label="current time">00:00</div> + <input type="range" class="audio-volume" value="1" min="0" max="1" step="0.001" aria-label="volume" /> <img src="{{ request.app.public_store.file_url( media.media_files.spectrogram) }}" alt="Spectrogram" /> 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/blog_media.html b/mediagoblin/templates/mediagoblin/user_pages/blog_media.html index 261b21e7..24328725 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/blog_media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/blog_media.html @@ -77,7 +77,7 @@ {% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete', user= media.get_actor.username, media_id=media.id) %} - <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a> + <a class="button_action button_warning" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a> {% endif %} </br> @@ -165,8 +165,6 @@ {% include "mediagoblin/utils/license.html" %} - {% include "mediagoblin/utils/exif.html" %} - {% template_hook("media_sideinfo") %} {% block mediagoblin_sidebar %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index d1f437d1..b93da06e 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -29,6 +29,8 @@ src="{{ request.staticdirect('/js/comment_show.js') }}"></script> <script type="text/javascript" src="{{ request.staticdirect('/js/keyboard_navigation.js') }}"></script> + <script type="text/javascript" + src="{{ request.staticdirect('/js/post_comment.js') }}"></script> {% template_hook("location_head") %} {% template_hook("media_head") %} @@ -75,6 +77,7 @@ <h2 class="media_title"> {{ media.title }} </h2> + {% template_hook("media_titleinfo") %} {% if request.user and (media.actor == request.user.id or request.user.has_privilege('admin')) %} @@ -116,6 +119,7 @@ <input type="submit" value="{% trans %}Add this comment{% endtrans %}" class="button_action" /> {{ csrf_token }} </div> + <input type="hidden" value="{{ request.urlgen('mediagoblin.user_pages.media_post_comment', user= media.get_actor.username, media_id=media.id) }}" id="postCommentURL" /> <input type="hidden" value="{{ request.urlgen('mediagoblin.user_pages.media_preview_comment') }}" id="previewURL" /> <input type="hidden" value="{% trans %}Comment Preview{% endtrans %}" id="previewText"/> </form> diff --git a/mediagoblin/templates/mediagoblin/user_pages/processing_panel.html b/mediagoblin/templates/mediagoblin/user_pages/processing_panel.html index 96786937..ee7b646a 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/processing_panel.html +++ b/mediagoblin/templates/mediagoblin/user_pages/processing_panel.html @@ -40,7 +40,7 @@ Show: <a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel', user=request.user.username, state="failed") }}">Failed</a>, <a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel', - user=request.user.username, state="processed") }}">Succesful</a> + user=request.user.username, state="processed") }}">Successful</a> </p> {% if entries.count() %} diff --git a/mediagoblin/templates/mediagoblin/utils/collection_gallery.html b/mediagoblin/templates/mediagoblin/utils/collection_gallery.html index 86680cb6..8a3f7a75 100644 --- a/mediagoblin/templates/mediagoblin/utils/collection_gallery.html +++ b/mediagoblin/templates/mediagoblin/utils/collection_gallery.html @@ -31,11 +31,17 @@ {%- if loop.first %} thumb_entry_first {%- elif loop.last %} thumb_entry_last{% endif %}"> <a href="{{ obj_url }}"> + {% if obj.icon_url %} + <img class="entry_type_icon" src="{{ obj.icon_url }}" /> + {% endif %} <img src="{{ obj.thumb_url }}" /> </a> + {% if obj.title %} + <a href="{{ obj_url }}">{{ obj.title }}</a> + {% endif %} {% if item.note %} - <a href="{{ obj_url }}">{{ item.note }}</a> + {{ item.note }} {% endif %} {% if request.user and (item.in_collection.actor == request.user.id or diff --git a/mediagoblin/templates/mediagoblin/utils/prev_next.html b/mediagoblin/templates/mediagoblin/utils/prev_next.html index 9e262ed9..fc8672fb 100644 --- a/mediagoblin/templates/mediagoblin/utils/prev_next.html +++ b/mediagoblin/templates/mediagoblin/utils/prev_next.html @@ -19,29 +19,36 @@ {# Provide navigation links to neighboring media entries, if possible #} {% set prev_entry_url = media.url_to_prev(request.urlgen) %} {% set next_entry_url = media.url_to_next(request.urlgen) %} +{% if is_rtl %} + {% set next_arrow = "→" %} + {% set prev_arrow = "←" %} +{% else %} + {% set next_arrow = "←" %} + {% set prev_arrow = "→" %} +{% endif %} {% if prev_entry_url or next_entry_url %} <div class="navigation"> {# There are no previous entries for the very first media entry #} {% if prev_entry_url %} <a class="navigation_button navigation_left" href="{{ prev_entry_url }}"> - ← {% trans %}newer{% endtrans %} + {{next_arrow}} {% trans %}newer{% endtrans %} </a> {% else %} {# This is the first entry. display greyed-out 'previous' image #} <p class="navigation_button navigation_left"> - ← {% trans %}newer{% endtrans %} + {{next_arrow}} {% trans %}newer{% endtrans %} </p> {% endif %} {# Likewise, this could be the very last media entry #} {% if next_entry_url %} <a class="navigation_button navigation_right" href="{{ next_entry_url }}"> - {% trans %}older{% endtrans %} → + {% trans %}older{% endtrans %} {{prev_arrow}} </a> {% else %} {# This is the last entry. display greyed-out 'next' image #} <p class="navigation_button navigation_right"> - {% trans %}older{% endtrans %} → + {% trans %}older{% endtrans %} {{prev_arrow}} </p> {% endif %} </div> diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html index 7e16708c..e2921258 100644 --- a/mediagoblin/templates/mediagoblin/utils/wtforms.html +++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html @@ -40,9 +40,9 @@ {{- render_label_p(field) }} <div class="form_field_input"> {% if autofocus_first %} - {{ field(autofocus=True) }} + {{ field(autofocus=True, style="width:100%;") }} {% else %} - {{ field }} + {{ field(style="width:100%;") }} {% endif %} {%- if field.errors -%} {% for error in field.errors %} diff --git a/mediagoblin/tests/.gitignore b/mediagoblin/tests/.gitignore new file mode 100644 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 cb971fdb..9cf5ccb0 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -1,4 +1,3 @@ - # GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # @@ -102,7 +101,7 @@ def test_register_views(test_app): 'password': 'iamsohappy', 'email': 'easter@egg.com'}) - ## At this point there should on user in the database + ## At this point there should be one user in the database assert User.query.count() == 1 # Successful register @@ -373,6 +372,53 @@ def test_authentication_views(test_app): assert not form.username.data == u'ANDREW' assert form.username.data == u'andrew' + # Successful login with short user + # -------------------------------- + short_user = fixture_add_user(username=u'me', password=u'sho') + template.clear_test_template_context() + response = test_app.post( + '/auth/login/', { + 'username': u'me', + 'password': 'sho'}) + + # User should be redirected + response.follow() + + assert urlparse.urlsplit(response.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + # Make sure user is in the session + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] + session = context['request'].session + assert session['user_id'] == six.text_type(short_user.id) + + # Must logout + template.clear_test_template_context() + response = test_app.get('/auth/logout/') + + # Successful login with long user + # ---------------- + long_user = fixture_add_user( + username=u'realllylonguser@reallylongdomain.com.co', password=u'sho') + template.clear_test_template_context() + response = test_app.post( + '/auth/login/', { + 'username': u'realllylonguser@reallylongdomain.com.co', + 'password': 'sho'}) + + # User should be redirected + response.follow() + assert urlparse.urlsplit(response.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + # Make sure user is in the session + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] + session = context['request'].session + assert session['user_id'] == six.text_type(long_user.id) + + template.clear_test_template_context() + response = test_app.get('/auth/logout/') + @pytest.fixture() def authentication_disabled_app(request): return get_app( diff --git a/mediagoblin/tests/test_celery_setup.py b/mediagoblin/tests/test_celery_setup.py index df0d04b0..0749c7f4 100644 --- a/mediagoblin/tests/test_celery_setup.py +++ b/mediagoblin/tests/test_celery_setup.py @@ -55,7 +55,4 @@ def test_setup_celery_from_config(): 'sqlite:///' + pkg_resources.resource_filename('mediagoblin.tests', 'celery.db')) - assert fake_celery_module.BROKER_TRANSPORT == 'sqlalchemy' - assert fake_celery_module.BROKER_URL == ( - 'sqlite:///' + - pkg_resources.resource_filename('mediagoblin.tests', 'kombu.db')) + assert fake_celery_module.BROKER_URL == 'amqp://' 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_tools.py b/mediagoblin/tests/test_tools.py index 6d3dd475..5f916400 100644 --- a/mediagoblin/tests/test_tools.py +++ b/mediagoblin/tests/test_tools.py @@ -16,10 +16,16 @@ from __future__ import absolute_import, unicode_literals +try: + import mock +except ImportError: + import unittest.mock as mock + from werkzeug.wrappers import Request from werkzeug.test import EnvironBuilder from mediagoblin.tools.request import decode_request +from mediagoblin.tools.pagination import Pagination class TestDecodeRequest(object): """Test the decode_request function.""" @@ -59,3 +65,54 @@ class TestDecodeRequest(object): request.form = {'foo': 'bar'} data = decode_request(request) assert data['foo'] == 'bar' + + +class TestPagination(object): + def _create_paginator(self, num_items, page, per_page): + """Create a Paginator with a mock database cursor.""" + mock_cursor = mock.MagicMock() + mock_cursor.count.return_value = num_items + return Pagination(page, mock_cursor, per_page) + + def test_creates_valid_page_url_from_explicit_base_url(self): + """Check that test_page_url_explicit runs. + + This is a regression test for a Python 2/3 compatibility fix. + + """ + paginator = self._create_paginator(num_items=1, page=1, per_page=30) + url = paginator.get_page_url_explicit('http://example.com', [], 1) + assert url == 'http://example.com?page=1' + + def test_iter_pages_handles_single_page(self): + """Check that iter_pages produces the expected result for single page. + + This is a regression test for a Python 2/3 compatibility fix. + + """ + paginator = self._create_paginator(num_items=1, page=1, per_page=30) + assert list(paginator.iter_pages()) == [1] + + def test_zero_items(self): + """Check that no items produces no pages.""" + paginator = self._create_paginator(num_items=0, page=1, per_page=30) + assert paginator.total_count == 0 + assert paginator.pages == 0 + + def test_single_item(self): + """Check that one item produces one page.""" + paginator = self._create_paginator(num_items=1, page=1, per_page=30) + assert paginator.total_count == 1 + assert paginator.pages == 1 + + def test_full_page(self): + """Check that a full page of items produces one page.""" + paginator = self._create_paginator(num_items=30, page=1, per_page=30) + assert paginator.total_count == 30 + assert paginator.pages == 1 + + def test_multiple_pages(self): + """Check that more than a full page produces two pages.""" + paginator = self._create_paginator(num_items=31, page=1, per_page=30) + assert paginator.total_count == 31 + assert paginator.pages == 2 diff --git a/mediagoblin/tests/test_util.py b/mediagoblin/tests/test_util.py index 8193233f..02976405 100644 --- a/mediagoblin/tests/test_util.py +++ b/mediagoblin/tests/test_util.py @@ -19,6 +19,7 @@ try: except ImportError: import unittest.mock as mock import email +import socket import pytest import smtplib import pkg_resources @@ -26,6 +27,7 @@ import pkg_resources import six from mediagoblin.tests.tools import get_app +from mediagoblin import mg_globals from mediagoblin.tools import common, url, translate, mail, text, testing testing._activate_testing() @@ -181,3 +183,30 @@ def test_html_cleaner(): '<p><a href="javascript:nasty_surprise">innocent link!</a></p>') assert result == ( '<p><a href="">innocent link!</a></p>') + + +class TestMail(object): + """ Test mediagoblin's mail tool """ + def test_no_mail_server(self): + """ Tests that no smtp server is available """ + with pytest.raises(mail.NoSMTPServerError), mock.patch("smtplib.SMTP") as smtp_mock: + smtp_mock.side_effect = socket.error + mg_globals.app_config = { + "email_debug_mode": False, + "email_smtp_use_ssl": False, + "email_smtp_host": "127.0.0.1", + "email_smtp_port": 0} + common.TESTS_ENABLED = False + mail.send_email("", "", "", "") + + def test_no_smtp_host(self): + """ Empty email_smtp_host """ + with pytest.raises(mail.NoSMTPServerError), mock.patch("smtplib.SMTP") as smtp_mock: + smtp_mock.return_value.connect.side_effect = socket.error + mg_globals.app_config = { + "email_debug_mode": False, + "email_smtp_use_ssl": False, + "email_smtp_host": "", + "email_smtp_port": 0} + common.TESTS_ENABLED = False + mail.send_email("", "", "", "") diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index 39b9ac50..82def02c 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -153,28 +153,6 @@ def install_fixtures_simple(db, fixtures): collection.insert(fixture) -def assert_db_meets_expected(db, expected): - """ - Assert a database contains the things we expect it to. - - Objects are found via 'id', so you should make sure your document - has an id. - - Args: - - db: pymongo or mongokit database connection - - expected: the data we expect. Formatted like: - {'collection_name': [ - {'id': 'foo', - 'some_field': 'some_value'},]} - """ - for collection_name, collection_data in six.iteritems(expected): - collection = db[collection_name] - for expected_document in collection_data: - document = collection.query.filter_by(id=expected_document['id']).first() - assert document is not None # make sure it exists - assert document == expected_document # make sure it matches - - def fixture_add_user(username=u'chris', password=u'toast', privileges=[], wants_comment_notification=True): # Reuse existing user or create a new one 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/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/mail.py b/mediagoblin/tools/mail.py index c11e392b..3dc180d8 100644 --- a/mediagoblin/tools/mail.py +++ b/mediagoblin/tools/mail.py @@ -16,6 +16,8 @@ from __future__ import print_function, unicode_literals +import socket +import logging import six import smtplib import sys @@ -54,6 +56,14 @@ EMAIL_TEST_INBOX = [] EMAIL_TEST_MBOX_INBOX = [] +class MailError(Exception): + """ General exception for mail errors """ + + +class NoSMTPServerError(MailError): + pass + + class FakeMhost(object): """ Just a fake mail host so we can capture and test messages @@ -101,13 +111,27 @@ def send_email(from_addr, to_addrs, subject, message_body): else: smtp_init = smtplib.SMTP - mhost = smtp_init( - mg_globals.app_config['email_smtp_host'], - mg_globals.app_config['email_smtp_port']) + try: + mhost = smtp_init( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) + except socket.error as original_error: + error_message = "Couldn't contact mail server on <{}>:<{}>".format( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) + logging.debug(original_error) + raise NoSMTPServerError(error_message) # SMTP.__init__ Issues SMTP.connect implicitly if host if not mg_globals.app_config['email_smtp_host']: # e.g. host = '' - mhost.connect() # We SMTP.connect explicitly + try: + mhost.connect() # We SMTP.connect explicitly + except socket.error as original_error: + error_message = "Couldn't contact mail server on <{}>:<{}>".format( + mg_globals.app_config['email_smtp_host'], + mg_globals.app_config['email_smtp_port']) + logging.debug(original_error) + raise NoSMTPServerError(error_message) try: mhost.starttls() diff --git a/mediagoblin/tools/pagination.py b/mediagoblin/tools/pagination.py index a525caf7..db5f69fb 100644 --- a/mediagoblin/tools/pagination.py +++ b/mediagoblin/tools/pagination.py @@ -14,13 +14,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import urllib import copy from math import ceil, floor from itertools import count from werkzeug.datastructures import MultiDict -from six.moves import zip +from six.moves import range, urllib, zip PAGINATION_DEFAULT_PER_PAGE = 30 @@ -86,7 +85,7 @@ class Pagination(object): def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2): last = 0 - for num in xrange(1, self.pages + 1): + for num in range(1, self.pages + 1): if num <= left_edge or \ (num > self.page - left_current - 1 and \ num < self.page + right_current) or \ @@ -107,7 +106,7 @@ class Pagination(object): new_get_params['page'] = page_no return "%s?%s" % ( - base_url, urllib.urlencode(new_get_params)) + base_url, urllib.parse.urlencode(new_get_params)) def get_page_url(self, request, page_no): """ diff --git a/mediagoblin/tools/theme.py b/mediagoblin/tools/theme.py index 97b041a6..79fd91e1 100644 --- a/mediagoblin/tools/theme.py +++ b/mediagoblin/tools/theme.py @@ -68,7 +68,7 @@ def register_themes(app_config, builtin_dir=BUILTIN_THEME_DIR): themedata = themedata_for_theme_dir(themedir, abs_themedir) registry[themedir] = themedata - + # Built-in themes if os.path.exists(builtin_dir): _install_themes_in_dir(builtin_dir) @@ -79,11 +79,9 @@ def register_themes(app_config, builtin_dir=BUILTIN_THEME_DIR): _install_themes_in_dir(theme_install_dir) current_theme_name = app_config.get('theme') - if current_theme_name \ - and registry.has_key(current_theme_name): + try: current_theme = registry[current_theme_name] - else: + except KeyError: current_theme = None return registry, current_theme - diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 28d3ba79..b4737ea8 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -24,6 +24,7 @@ from mediagoblin import messages, mg_globals from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, Comment, CollectionItem, LocalUser, Activity, \ GenericModelReference) +from mediagoblin.plugins.api.tools import get_media_file_paths from mediagoblin.tools.response import render_to_response, render_404, \ redirect, redirect_obj from mediagoblin.tools.text import cleaned_markdown_conversion @@ -179,6 +180,10 @@ def media_post_comment(request, media): if not request.method == 'POST': raise MethodNotAllowed() + # If media is not processed, return NotFound. + if not media.state == u'processed': + return render_404(request) + comment = request.db.TextComment() comment.actor = request.user.id comment.content = six.text_type(request.form['comment_content']) @@ -231,6 +236,10 @@ def media_preview_comment(request): def media_collect(request, media): """Add media to collection submission""" + # If media is not processed, return NotFound. + if not media.state == u'processed': + return render_404(request) + form = user_forms.MediaCollectForm(request.form) # A user's own collections: form.collection.query = Collection.query.filter_by( @@ -288,12 +297,6 @@ def media_collect(request, media): collection = None # Make sure the user actually selected a collection - item = CollectionItem.query.filter_by(collection=collection.id) - item = item.join(CollectionItem.object_helper).filter_by( - model_type=media.__tablename__, - obj_pk=media.id - ).first() - if not collection: messages.add_message( request, @@ -303,8 +306,14 @@ def media_collect(request, media): user=media.get_actor.username, media_id=media.id) + item = CollectionItem.query.filter_by(collection=collection.id) + item = item.join(CollectionItem.object_helper).filter_by( + model_type=media.__tablename__, + obj_pk=media.id + ).first() + # Check whether media already exists in collection - elif item is not None: + if item is not None: messages.add_message( request, messages.ERROR, @@ -538,23 +547,21 @@ def atom_feed(request): username = request.matchdict['user']).first() if not user or not user.has_privilege(u'active'): return render_404(request) + feed_title = "MediaGoblin Feed for user '%s'" % request.matchdict['user'] + link = request.urlgen('mediagoblin.user_pages.user_home', + qualified=True, user=request.matchdict['user']) + cursor = MediaEntry.query.filter_by(actor=user.id, state=u'processed') + cursor = cursor.order_by(MediaEntry.created.desc()) + cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) - cursor = MediaEntry.query.filter_by( - actor = user.id, - state = u'processed').\ - order_by(MediaEntry.created.desc()).\ - limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS) """ ATOM feed id is a tag URI (see http://en.wikipedia.org/wiki/Tag_URI) """ atomlinks = [{ - 'href': request.urlgen( - 'mediagoblin.user_pages.user_home', - qualified=True, user=request.matchdict['user']), - 'rel': 'alternate', - 'type': 'text/html' - }] + 'href': link, + 'rel': 'alternate', + 'type': 'text/html'}] if mg_globals.app_config["push_urls"]: for push_url in mg_globals.app_config["push_urls"]: @@ -563,25 +570,34 @@ def atom_feed(request): 'href': push_url}) feed = AtomFeed( - "MediaGoblin: Feed for user '%s'" % request.matchdict['user'], - feed_url=request.url, - id='tag:{host},{year}:gallery.user-{user}'.format( - host=request.host, - year=datetime.datetime.today().strftime('%Y'), - user=request.matchdict['user']), - links=atomlinks) + feed_title, + feed_url=request.url, + id='tag:{host},{year}:gallery.user-{user}'.format( + host=request.host, + year=datetime.datetime.today().strftime('%Y'), + user=request.matchdict['user']), + links=atomlinks) for entry in cursor: + # Include a thumbnail image in content. + file_urls = get_media_file_paths(entry.media_files, request.urlgen) + if 'thumb' in file_urls: + content = u'<img src="{thumb}" alt='' /> {desc}'.format( + thumb=file_urls['thumb'], desc=entry.description_html) + else: + content = entry.description_html + feed.add( entry.get('title'), - entry.description_html, + content, id=entry.url_for_self(request.urlgen, qualified=True), content_type='html', author={ 'name': entry.get_actor.username, 'uri': request.urlgen( 'mediagoblin.user_pages.user_home', - qualified=True, user=entry.get_actor.username)}, + qualified=True, + user=entry.get_actor.username)}, updated=entry.get('created'), links=[{ 'href': entry.url_for_self( |