From: Aaron Williamson Date: Fri, 17 Aug 2012 04:54:40 +0000 (-0400) Subject: Added basic collection functionality X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=be5be1154fd22c548125ce5a055af1bdfdad9526;p=mediagoblin.git Added basic collection functionality --- diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 9f9b8786..929c1c7f 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -143,3 +143,56 @@ class MediaCommentMixin(object): Run through Markdown and the HTML cleaner. """ return cleaned_markdown_conversion(self.content) + + +class CollectionMixin(object): + def generate_slug(self): + # import this here due to a cyclic import issue + # (db.models -> db.mixin -> db.util -> db.models) + from mediagoblin.db.util import check_collection_slug_used + + self.slug = slugify(self.title) + + duplicate = check_collection_slug_used(mg_globals.database, + self.creator, self.slug, self.id) + + if duplicate: + if self.id is not None: + self.slug = u"%s-%s" % (self.id, self.slug) + else: + self.slug = None + + @property + def description_html(self): + """ + Rendered version of the description, run through + Markdown and cleaned with our cleaning tool. + """ + return cleaned_markdown_conversion(self.description) + + @property + def slug_or_id(self): + return (self.slug or self._id) + + def url_for_self(self, urlgen, **extra_args): + """ + Generate an appropriate url for ourselves + + Use a slug if we have one, else use our '_id'. + """ + creator = self.get_creator + + return urlgen( + 'mediagoblin.user_pages.collections_home', + user=creator.username, + collection=self.slug_or_id, + **extra_args) + +class CollectionItemMixin(object): + @property + def note_html(self): + """ + the actual html-rendered version of the note displayed. + Run through Markdown and the HTML cleaner. + """ + return cleaned_markdown_conversion(self.note) diff --git a/mediagoblin/db/sql/migrations.py b/mediagoblin/db/sql/migrations.py index 49798a54..3db51ab9 100644 --- a/mediagoblin/db/sql/migrations.py +++ b/mediagoblin/db/sql/migrations.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import MetaData, Table, Column, Boolean, SmallInteger +from sqlalchemy import MetaData, Table, Column, Boolean, SmallInteger, Integer from mediagoblin.db.sql.util import RegisterMigration @@ -59,3 +59,15 @@ def add_transcoding_progress(db_conn): col = Column('transcoding_progress', SmallInteger) col.create(media_entry) db_conn.commit() + + +@RegisterMigration(4, MIGRATIONS) +def add_mediaentry_collections(db_conn): + metadata = MetaData(bind=db_conn.bind) + + media_entry = Table('core__media_entries', metadata, autoload=True, + autoload_with=db_conn.bind) + + col = Column('collections', Integer) + col.create(media_entry) + db_conn.commit() diff --git a/mediagoblin/db/sql/models.py b/mediagoblin/db/sql/models.py index 7c8c0f99..5862f722 100644 --- a/mediagoblin/db/sql/models.py +++ b/mediagoblin/db/sql/models.py @@ -33,7 +33,7 @@ from sqlalchemy.util import memoized_property from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded from mediagoblin.db.sql.base import Base, DictReadAttrProxy -from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin +from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin from mediagoblin.db.sql.base import Session # It's actually kind of annoying how sqlalchemy-migrate does this, if @@ -103,6 +103,7 @@ class MediaEntry(Base, MediaEntryMixin): state = Column(Unicode, default=u'unprocessed', nullable=False) # or use sqlalchemy.types.Enum? license = Column(Unicode) + collected = Column(Integer, default=0) fail_error = Column(Unicode) fail_metadata = Column(JSONEncoded) @@ -143,6 +144,11 @@ class MediaEntry(Base, MediaEntryMixin): creator=lambda v: MediaTag(name=v["name"], slug=v["slug"]) ) + collections_helper = relationship("CollectionItem", + cascade="all, delete-orphan" + ) + collections = association_proxy("collections_helper", "in_collection") + ## TODO # media_data # fail_error @@ -348,8 +354,58 @@ class MediaComment(Base, MediaCommentMixin): _id = SimpleFieldAlias("id") +class Collection(Base, CollectionMixin): + __tablename__ = "core__collections" + + id = Column(Integer, primary_key=True) + title = Column(Unicode, nullable=False) + slug = Column(Unicode) + created = Column(DateTime, nullable=False, default=datetime.datetime.now, + index=True) + description = Column(UnicodeText) + creator = Column(Integer, ForeignKey(User.id), nullable=False) + items = Column(Integer, default=0) + + get_creator = relationship(User) + + def get_collection_items(self, ascending=False): + order_col = CollectionItem.position + if not ascending: + order_col = desc(order_col) + return CollectionItem.query.filter_by( + collection=self.id).order_by(order_col) + + _id = SimpleFieldAlias("id") + + +class CollectionItem(Base, CollectionItemMixin): + __tablename__ = "core__collection_items" + + id = Column(Integer, primary_key=True) + media_entry = Column( + Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) + collection = Column(Integer, ForeignKey(Collection.id), nullable=False) + note = Column(UnicodeText, nullable=True) + added = Column(DateTime, nullable=False, default=datetime.datetime.now) + position = Column(Integer) + in_collection = relationship("Collection") + + get_media_entry = relationship(MediaEntry) + + _id = SimpleFieldAlias("id") + + __table_args__ = ( + UniqueConstraint('collection', 'media_entry'), + {}) + + @property + def dict_view(self): + """A dict like view on this object""" + return DictReadAttrProxy(self) + + MODELS = [ - User, MediaEntry, Tag, MediaTag, MediaComment, MediaFile, FileKeynames, + User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile] diff --git a/mediagoblin/db/sql/util.py b/mediagoblin/db/sql/util.py index 53260db2..74b5d73e 100644 --- a/mediagoblin/db/sql/util.py +++ b/mediagoblin/db/sql/util.py @@ -17,7 +17,7 @@ import sys from mediagoblin.db.sql.base import Session -from mediagoblin.db.sql.models import MediaEntry, Tag, MediaTag +from mediagoblin.db.sql.models import MediaEntry, Tag, MediaTag, Collection from mediagoblin.tools.common import simple_printer @@ -310,6 +310,15 @@ def clean_orphan_tags(): Session.commit() +def check_collection_slug_used(dummy_db, creator_id, slug, ignore_c_id): + filt = (Collection.creator == creator_id) \ + & (Collection.slug == slug) + if ignore_c_id is not None: + filt = filt & (Collection.id != ignore_c_id) + does_exist = Session.query(Collection.id).filter(filt).first() is not None + return does_exist + + if __name__ == '__main__': from mediagoblin.db.sql.open import setup_connection_and_db_from_config diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 540a9244..a8c8c92b 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -22,7 +22,7 @@ except ImportError: if use_sql: from mediagoblin.db.sql.fake import ObjectId, InvalidId, DESCENDING from mediagoblin.db.sql.util import atomic_update, check_media_slug_used, \ - media_entries_for_tag_slug + media_entries_for_tag_slug, check_collection_slug_used else: from mediagoblin.db.mongo.util import \ ObjectId, InvalidId, DESCENDING, atomic_update, \ diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index 9961be83..9be9d4cc 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -70,6 +70,23 @@ def user_may_delete_media(controller): return wrapper +def user_may_alter_collection(controller): + """ + Require user ownership of the Collection to modify. + """ + @wraps(controller) + def wrapper(request, *args, **kwargs): + creator_id = request.db.User.find_one( + {'username': request.matchdict['user']}).id + if not (request.user.is_admin or + request.user._id == creator_id): + return exc.HTTPForbidden() + + return controller(request, *args, **kwargs) + + return wrapper + + def uses_pagination(controller): """ Check request GET 'page' key for wrong values @@ -123,6 +140,59 @@ def get_user_media_entry(controller): return wrapper +def get_user_collection(controller): + """ + Pass in a Collection based off of a url component + """ + @wraps(controller) + def wrapper(request, *args, **kwargs): + user = request.db.User.find_one( + {'username': request.matchdict['user']}) + + if not user: + return render_404(request) + + collection = request.db.Collection.find_one( + {'slug': request.matchdict['collection'], + 'creator': user._id}) + + # Still no collection? Okay, 404. + if not collection: + return render_404(request) + + return controller(request, collection=collection, *args, **kwargs) + + return wrapper + + +def get_user_collection_item(controller): + """ + Pass in a CollectionItem based off of a url component + """ + @wraps(controller) + def wrapper(request, *args, **kwargs): + user = request.db.User.find_one( + {'username': request.matchdict['user']}) + + if not user: + return render_404(request) + + collection = request.db.Collection.find_one( + {'slug': request.matchdict['collection'], + 'creator': user._id}) + + collection_item = request.db.CollectionItem.find_one( + {'_id': request.matchdict['collection_item'] }) + + # Still no collection item? Okay, 404. + if not collection_item: + return render_404(request) + + return controller(request, collection_item=collection_item, *args, **kwargs) + + return wrapper + + def get_media_entry_by_id(controller): """ Pass in a MediaEntry based off of a url component diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index e2882ada..856852b6 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -77,3 +77,19 @@ class EditAttachmentsForm(wtforms.Form): 'Title') attachment_file = wtforms.FileField( 'File') + +class EditCollectionForm(wtforms.Form): + title = wtforms.TextField( + _('Title'), + [wtforms.validators.Length(min=0, max=500), wtforms.validators.Required(message=_("The title can't be empty"))]) + description = wtforms.TextAreaField( + _('Description of this collection'), + description=_("""You can use + + Markdown for formatting.""")) + slug = wtforms.TextField( + _('Slug'), + [wtforms.validators.Required(message=_("The slug can't be empty"))], + description=_( + "The title part of this collection's address. " + "You usually don't need to change this.")) diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index 5bcafeac..321c1f44 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -22,5 +22,5 @@ edit_routes = [ Route('mediagoblin.edit.profile', '/profile/', controller="mediagoblin.edit.views:edit_profile"), Route('mediagoblin.edit.account', '/account/', - controller="mediagoblin.edit.views:edit_account") + controller="mediagoblin.edit.views:edit_account"), ] diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 9ce70231..d4adcd0d 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -25,13 +25,14 @@ from mediagoblin import mg_globals from mediagoblin.auth import lib as auth_lib from mediagoblin.edit import forms -from mediagoblin.edit.lib import may_edit_media -from mediagoblin.decorators import require_active_login, get_user_media_entry +from mediagoblin.edit.lib import may_edit_media, may_edit_collection +from mediagoblin.decorators import require_active_login, get_user_media_entry, \ + user_may_alter_collection, get_user_collection from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.text import ( convert_to_tag_list_of_dicts, media_tags_as_string) -from mediagoblin.db.util import check_media_slug_used +from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used import mimetypes @@ -255,3 +256,58 @@ def edit_account(request): 'mediagoblin/edit/edit_account.html', {'user': user, 'form': form}) + + +@require_active_login +@user_may_alter_collection +@get_user_collection +def edit_collection(request, collection): + defaults = dict( + title=collection.title, + slug=collection.slug, + description=collection.description) + + form = forms.EditCollectionForm( + request.POST, + **defaults) + + if request.method == 'POST' and form.validate(): + # Make sure there isn't already a Collection with such a slug + # and userid. + slug_used = check_collection_slug_used(request.db, collection.creator, + request.POST['slug'], collection.id) + + # Make sure there isn't already a Collection with this title + existing_collection = request.db.Collection.find_one({ + 'creator': request.user._id, + 'title':request.POST['title']}) + + if existing_collection and existing_collection.id != collection.id: + messages.add_message( + request, messages.ERROR, _('You already have a collection called "%s"!' % request.POST['title'])) + elif slug_used: + form.slug.errors.append( + _(u'A collection with that slug already exists for this user.')) + else: + collection.title = unicode(request.POST['title']) + collection.description = unicode(request.POST.get('description')) + collection.slug = unicode(request.POST['slug']) + + collection.save() + + return redirect(request, "mediagoblin.user_pages.user_collection", + user=collection.get_creator.username, + collection=collection.slug) + + if request.user.is_admin \ + and collection.creator != request.user._id \ + and request.method != 'POST': + messages.add_message( + request, messages.WARNING, + _("You are editing another user's collection. Proceed with caution.")) + + return render_to_response( + request, + 'mediagoblin/edit/edit_collection.html', + {'collection': collection, + 'form': form}) diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index 0a14335a..6bc85674 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -250,6 +250,17 @@ footer { font-family: 'Lato', sans-serif; } +.button_collect { + background-image: url("../images/icon_collect.png"); + background-repeat: no-repeat; + background-position:top center; + height: 30px; + width: 30px; + margin: 0px; + padding: 3px 3px 2px 3px; + position: relative; +} + .pagination { text-align: center; } @@ -344,6 +355,10 @@ text-align: center; text-align: right; } +.subform { + margin: 2em; +} + #password_boolean { margin-top: 4px; width: 20px; @@ -416,6 +431,34 @@ textarea#comment_content { max-height: 135px; } +/* collection media */ + +.collection_thumbnail { + float: left; + padding: 0px; + width: 180px; + margin: 0px 4px 10px; + text-align: left; + font-size: 0.875em; + background-color: #222; + border-radius: 0 0 5px 5px; + padding: 0 0 6px; + text-overflow: ellipsis; +} + +.collection_thumbnail a { + color: #eee; + text-decoration: none; +} + +.collection_thumbnail a.remove { + color: #86D4B1; +} + +.collection_thumbnail img { + max-height: 135px; +} + /* media detail */ h2.media_title { diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index 7d9e8fcf..bd1e904f 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -41,3 +41,13 @@ class SubmitStartForm(wtforms.Form): _('License'), [wtforms.validators.Optional(),], choices=licenses_as_choices()) + +class AddCollectionForm(wtforms.Form): + title = wtforms.TextField( + _('Title'), + [wtforms.validators.Length(min=0, max=500), wtforms.validators.Required()]) + description = wtforms.TextAreaField( + _('Description of this collection'), + description=_("""You can use + + Markdown for formatting.""")) diff --git a/mediagoblin/submit/routing.py b/mediagoblin/submit/routing.py index 8ce23cd9..1e399d1e 100644 --- a/mediagoblin/submit/routing.py +++ b/mediagoblin/submit/routing.py @@ -18,4 +18,7 @@ from routes.route import Route submit_routes = [ Route('mediagoblin.submit.start', '/', - controller='mediagoblin.submit.views:submit_start')] + controller='mediagoblin.submit.views:submit_start'), + Route('mediagoblin.submit.collection', '/collection', + controller='mediagoblin.submit.views:add_collection'), + ] diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 72186136..a9b13778 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from mediagoblin import messages import mediagoblin.mg_globals as mg_globals import uuid from os.path import splitext @@ -181,3 +182,46 @@ def submit_start(request): 'mediagoblin/submit/start.html', {'submit_form': submit_form, 'app_config': mg_globals.app_config}) + +@require_active_login +def add_collection(request, media=None): + """ + View to create a new collection + """ + submit_form = submit_forms.AddCollectionForm(request.POST) + + if request.method == 'POST' and submit_form.validate(): + try: + collection = request.db.Collection() + collection.id = ObjectId() + + collection.title = unicode(request.POST['title']) + + collection.description = unicode(request.POST.get('description')) + collection.creator = request.user._id + collection.generate_slug() + + # Make sure this user isn't duplicating an existing collection + existing_collection = request.db.Collection.find_one({ + 'creator': request.user._id, + 'title':collection.title}) + + if existing_collection: + messages.add_message( + request, messages.ERROR, _('You already have a collection called "%s"!' % collection.title)) + else: + collection.save(validate=True) + + add_message(request, SUCCESS, _('Collection "%s" added!' % collection.title)) + + return redirect(request, "mediagoblin.user_pages.user_home", + user=request.user.username) + + except Exception as e: + raise + + return render_to_response( + request, + 'mediagoblin/submit/collection.html', + {'submit_form': submit_form, + 'app_config': mg_globals.app_config}) diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index 5620debd..b6ba8818 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -64,6 +64,7 @@ diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py index f17e6c00..9e8ccf01 100644 --- a/mediagoblin/user_pages/forms.py +++ b/mediagoblin/user_pages/forms.py @@ -15,16 +15,32 @@ # along with this program. If not, see . import wtforms - +from wtforms.ext.sqlalchemy.fields import QuerySelectField from mediagoblin.tools.translate import fake_ugettext_passthrough as _ - class MediaCommentForm(wtforms.Form): comment_content = wtforms.TextAreaField( '', [wtforms.validators.Required()]) - class ConfirmDeleteForm(wtforms.Form): confirm = wtforms.BooleanField( _('I am sure I want to delete this')) + +class ConfirmCollectionItemRemoveForm(wtforms.Form): + confirm = wtforms.BooleanField( + _('I am sure I want to remove this item from the collection')) + +class MediaCollectForm(wtforms.Form): + collection = QuerySelectField(allow_blank=True, blank_text=_('-- Select --'), get_label='title',) + note = wtforms.TextAreaField( + _('Include a note'), + [wtforms.validators.Optional()],) + collection_title = wtforms.TextField( + _('Title'), + [wtforms.validators.Length(min=0, max=500)]) + collection_description = wtforms.TextAreaField( + _('Description of this collection'), + description=_("""You can use + + Markdown for formatting.""")) diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index f9780edc..1cfce2dd 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -40,6 +40,21 @@ user_routes = [ Route('mediagoblin.user_pages.media_post_comment', '/{user}/m/{media}/comment/add/', controller="mediagoblin.user_pages.views:media_post_comment"), + Route('mediagoblin.user_pages.media_collect', + "/{user}/m/{media}/collect/", + controller="mediagoblin.user_pages.views:media_collect"), + Route('mediagoblin.user_pages.user_collection', "/{user}/collection/{collection}/", + controller="mediagoblin.user_pages.views:user_collection"), + Route('mediagoblin.edit.edit_collection', "/{user}/c/{collection}/edit/", + controller="mediagoblin.edit.views:edit_collection"), + Route('mediagoblin.user_pages.collection_confirm_delete', + "/{user}/c/{collection}/confirm-delete/", + controller="mediagoblin.user_pages.views:collection_confirm_delete"), + Route('mediagoblin.user_pages.collection_item_confirm_remove', + "/{user}/collection/{collection}/{collection_item}/confirm_remove/", + controller="mediagoblin.user_pages.views:collection_item_confirm_remove"), + Route('mediagoblin.user_pages.collection_atom_feed', '/{user}/collection/{collection}/atom/', + controller="mediagoblin.user_pages.views:collection_atom_feed"), Route('mediagoblin.user_pages.processing_panel', '/{user}/panel/', controller="mediagoblin.user_pages.views:processing_panel"), diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 4c86aadc..ee366c10 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -28,12 +28,13 @@ from mediagoblin.user_pages import forms as user_forms from mediagoblin.user_pages.lib import send_comment_email from mediagoblin.decorators import (uses_pagination, get_user_media_entry, - require_active_login, user_may_delete_media) + require_active_login, user_may_delete_media, user_may_alter_collection, + get_user_collection, get_user_collection_item) from werkzeug.contrib.atom import AtomFeed from mediagoblin.media_types import get_media_manager - +from sqlalchemy.exc import IntegrityError _log = logging.getLogger(__name__) _log.setLevel(logging.DEBUG) @@ -175,6 +176,91 @@ def media_post_comment(request, media): location=media.url_for_self(request.urlgen)) +@get_user_media_entry +@require_active_login +def media_collect(request, media): + + form = user_forms.MediaCollectForm(request.POST) + filt = (request.db.Collection.creator == request.user.id) + form.collection.query = request.db.Collection.query.filter(filt).order_by(request.db.Collection.title) + + if request.method == 'POST': + if form.validate(): + + collection = None + collection_item = request.db.CollectionItem() + + # If the user is adding a new collection, use that + if request.POST['collection_title']: + collection = request.db.Collection() + collection.id = ObjectId() + + collection.title = ( + unicode(request.POST['collection_title']) + or unicode(splitext(filename)[0])) + + collection.description = unicode(request.POST.get('collection_description')) + collection.creator = request.user._id + collection.generate_slug() + + # Make sure this user isn't duplicating an existing collection + existing_collection = request.db.Collection.find_one({ + 'creator': request.user._id, + 'title':collection.title}) + + if existing_collection: + messages.add_message( + request, messages.ERROR, _('You already have a collection called "%s"!' % collection.title)) + + return redirect(request, "mediagoblin.user_pages.media_home", + user=request.user.username, + media=media.id) + + collection.save(validate=True) + + collection_item.collection = collection.id + # Otherwise, use the collection selected from the drop-down + else: + collection = request.db.Collection.find_one({'_id': request.POST.get('collection')}) + collection_item.collection = collection.id + + # Make sure the user actually selected a collection + if not collection: + messages.add_message( + request, messages.ERROR, _('You have to select or add a collection')) + # Check whether media already exists in collection + elif request.db.CollectionItem.find_one({'media_entry': media.id, 'collection': collection_item.collection}): + messages.add_message( + request, messages.ERROR, _('"%s" already in collection "%s"' % (media.title, collection.title))) + else: + collection_item.media_entry = media.id + collection_item.author = request.user.id + collection_item.note = unicode(request.POST['note']) + collection_item.save(validate=True) + + collection.items = collection.items + 1 + collection.save(validate=True) + + media.collected = media.collected + 1 + media.save() + + messages.add_message( + request, messages.SUCCESS, _('"%s" added to collection "%s"' % (media.title, collection.title))) + + return redirect(request, "mediagoblin.user_pages.media_home", + user=request.user.username, + media=media.id) + else: + messages.add_message( + request, messages.ERROR, _('Please check your entries and try again.')) + + return render_to_response( + request, + 'mediagoblin/user_pages/media_collect.html', + {'media': media, + 'form': form}) + + @get_user_media_entry @require_active_login @user_may_delete_media @@ -227,6 +313,132 @@ def media_confirm_delete(request, media): 'form': form}) +@uses_pagination +def user_collection(request, page): + """A User-defined Collection""" + user = request.db.User.find_one({ + 'username': request.matchdict['user'], + 'status': u'active'}) + if not user: + return render_404(request) + + collection = request.db.Collection.find_one( + {'slug': request.matchdict['collection'] }) + + cursor = request.db.CollectionItem.find( + {'collection': collection.id }) + + pagination = Pagination(page, cursor) + collection_items = pagination() + + #if no data is available, return NotFound + if collection_items == None: + return render_404(request) + + return render_to_response( + request, + 'mediagoblin/user_pages/collection.html', + {'user': user, + 'collection': collection, + 'collection_items': collection_items, + 'pagination': pagination}) + + +@get_user_collection_item +@require_active_login +@user_may_alter_collection +def collection_item_confirm_remove(request, collection_item): + + form = user_forms.ConfirmCollectionItemRemoveForm(request.POST) + + if request.method == 'POST' and form.validate(): + username = collection_item.in_collection.get_creator.username + collection = collection_item.in_collection + + if form.confirm.data is True: + entry = collection_item.get_media_entry + entry.collected = entry.collected - 1 + entry.save() + + collection_item.delete() + collection.items = collection.items - 1; + collection.save() + + messages.add_message( + request, messages.SUCCESS, _('You deleted the item from the collection.')) + else: + messages.add_message( + request, messages.ERROR, + _("The item was not removed because you didn't check that you were sure.")) + + return redirect(request, "mediagoblin.user_pages.user_collection", + user=username, + collection=collection.slug) + + if ((request.user.is_admin and + request.user._id != collection.creator)): + messages.add_message( + request, messages.WARNING, + _("You are about to delete an item from another user's collection. " + "Proceed with caution.")) + + return render_to_response( + request, + 'mediagoblin/user_pages/collection_item_confirm_remove.html', + {'collection_item': collection_item, + 'form': form}) + + +@get_user_collection +@require_active_login +@user_may_alter_collection +def collection_confirm_delete(request, collection): + + form = user_forms.ConfirmDeleteForm(request.POST) + + if request.method == 'POST' and form.validate(): + + username = collection.get_creator.username + + if form.confirm.data is True: + collection_title = collection.title + + # Delete all the associated collection items + for item in collection.get_collection_items(): + entry = item.get_media_entry + entry.collected = entry.collected - 1 + entry.save() + item.delete() + + collection.delete() + messages.add_message( + request, messages.SUCCESS, _('You deleted the collection "%s"' % collection_title)) + + return redirect(request, "mediagoblin.user_pages.user_home", + user=username) + else: + messages.add_message( + request, messages.ERROR, + _("The collection was not deleted because you didn't check that you were sure.")) + + return redirect(request, "mediagoblin.user_pages.user_collection", + user=username, + collection=collection.slug) + + if ((request.user.is_admin and + request.user._id != collection.creator)): + messages.add_message( + request, messages.WARNING, + _("You are about to delete another user's collection. " + "Proceed with caution.")) + + return render_to_response( + request, + 'mediagoblin/user_pages/collection_confirm_delete.html', + {'collection': collection, + 'form': form}) + + ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 15 @@ -293,6 +505,74 @@ def atom_feed(request): return feed.get_response() +def collection_atom_feed(request): + """ + generates the atom feed with the newest images from a collection + """ + + user = request.db.User.find_one({ + 'username': request.matchdict['user'], + 'status': u'active'}) + if not user: + return render_404(request) + + collection = request.db.Collection.find_one({ + 'creator': user.id, + 'slug': request.matchdict['collection']}) + + cursor = request.db.CollectionItem.find({ + 'collection': collection._id}) \ + .sort('added', DESCENDING) \ + .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_collection', + qualified=True, user=request.matchdict['user'], collection=collection.slug), + 'rel': 'alternate', + 'type': 'text/html' + }] + + if mg_globals.app_config["push_urls"]: + for push_url in mg_globals.app_config["push_urls"]: + atomlinks.append({ + 'rel': 'hub', + 'href': push_url}) + + feed = AtomFeed( + "MediaGoblin: Feed for %s's collection %s" % (request.matchdict['user'], collection.title), + feed_url=request.url, + id='tag:{host},{year}:collection.user-{user}.title-{title}'.format( + host=request.host, + year=datetime.datetime.today().strftime('%Y'), + user=request.matchdict['user'], + title=collection.title), + links=atomlinks) + + for item in cursor: + entry = item.get_media_entry + feed.add(entry.get('title'), + item.note_html, + id=entry.url_for_self(request.urlgen, qualified=True), + content_type='html', + author={ + 'name': entry.get_uploader.username, + 'uri': request.urlgen( + 'mediagoblin.user_pages.user_home', + qualified=True, user=entry.get_uploader.username)}, + updated=item.get('added'), + links=[{ + 'href': entry.url_for_self( + request.urlgen, + qualified=True), + 'rel': 'alternate', + 'type': 'text/html'}]) + + return feed.get_response() + @require_active_login def processing_panel(request):