diff options
Diffstat (limited to 'mediagoblin/db/mixin.py')
-rw-r--r-- | mediagoblin/db/mixin.py | 352 |
1 files changed, 323 insertions, 29 deletions
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 048cc07c..e8b121d0 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -31,17 +31,96 @@ import uuid import re from datetime import datetime +from pytz import UTC from werkzeug.utils import cached_property -from mediagoblin import mg_globals from mediagoblin.media_types import FileTypeNotSupported from mediagoblin.tools import common, licenses from mediagoblin.tools.pluginapi import hook_handle from mediagoblin.tools.text import cleaned_markdown_conversion from mediagoblin.tools.url import slugify +from mediagoblin.tools.translate import pass_to_ugettext as _ +class CommentingMixin(object): + """ + Mixin that gives classes methods to get and add the comments on/to it + + This assumes the model has a "comments" class which is a ForeignKey to the + Collection model. This will hold a Collection of comments which are + associated to this model. It also assumes the model has an "actor" + ForeignKey which points to the creator/publisher/etc. of the model. + + NB: This is NOT the mixin for the Comment Model, this is for + other models which support commenting. + """ + + def get_comment_link(self): + # Import here to avoid cyclic imports + from mediagoblin.db.models import Comment, GenericModelReference + + gmr = GenericModelReference.query.filter_by( + obj_pk=self.id, + model_type=self.__tablename__ + ).first() + + if gmr is None: + return None + + link = Comment.query.filter_by(comment_id=gmr.id).first() + return link + + def get_reply_to(self): + link = self.get_comment_link() + if link is None or link.target_id is None: + return None + + return link.target() + + def soft_delete(self, *args, **kwargs): + link = self.get_comment_link() + if link is not None: + link.delete() + super(CommentingMixin, self).soft_delete(*args, **kwargs) + +class GeneratePublicIDMixin(object): + """ + Mixin that ensures that a the public_id field is populated. + + The public_id is the ID that is used in the API, this must be globally + unique and dereferencable. This will be the URL for the API view of the + object. It's used in several places, not only is it used to give out via + the API but it's also vital information stored when a soft_deletion occurs + on the `Graveyard.public_id` field, this is needed to follow the spec which + says we have to be able to provide a shell of an object and return a 410 + (rather than a 404) when a deleted object has been deleted. + + This requires a the urlgen off the request object (`request.urlgen`) to be + provided as it's the ID is a URL. + """ + + def get_public_id(self, urlgen): + # Verify that the class this is on actually has a public_id field... + if "public_id" not in self.__table__.columns.keys(): + raise Exception("Model has no public_id field") + + # Great! the model has a public id, if it's None, let's create one! + if self.public_id is None: + # We need the internal ID for this so ensure we've been saved. + self.save(commit=False) + + # Create the URL + self.public_id = urlgen( + "mediagoblin.api.object", + object_type=self.object_type, + id=str(uuid.uuid4()), + qualified=True + ) + self.save() + return self.public_id class UserMixin(object): + object_type = "person" + @property def bio_html(self): return cleaned_markdown_conversion(self.bio) @@ -49,6 +128,7 @@ class UserMixin(object): def url_for_self(self, urlgen, **kwargs): """Generate a URL for this User's home page.""" return urlgen('mediagoblin.user_pages.user_home', + user=self.username, **kwargs) @@ -84,51 +164,59 @@ class GenerateSlugMixin(object): generated bits until it's unique. That'll be a little bit of junk, but at least it has the basis of a nice slug. """ + #Is already a slug assigned? Check if it is valid if self.slug: - self.slug = slugify(self.slug) + slug = slugify(self.slug) # otherwise, try to use the title. elif self.title: # assign slug based on title - self.slug = slugify(self.title) + slug = slugify(self.title) - # We don't want any empty string slugs - if self.slug == u"": - self.slug = None + else: + # We don't have any information to set a slug + return - # Do we have anything at this point? - # If not, we're not going to get a slug - # so just return... we're not going to force one. - if not self.slug: - return # giving up! + # We don't want any empty string slugs + if slug == u"": + return # Otherwise, let's see if this is unique. - if self.check_slug_used(self.slug): + if self.check_slug_used(slug): # It looks like it's being used... lame. # Can we just append the object's id to the end? if self.id: - slug_with_id = u"%s-%s" % (self.slug, self.id) + slug_with_id = u"%s-%s" % (slug, self.id) if not self.check_slug_used(slug_with_id): self.slug = slug_with_id return # success! # okay, still no success; # let's whack junk on there till it's unique. - self.slug += '-' + uuid.uuid4().hex[:4] + slug += '-' + uuid.uuid4().hex[:4] # keep going if necessary! - while self.check_slug_used(self.slug): - self.slug += uuid.uuid4().hex[:4] + while self.check_slug_used(slug): + slug += uuid.uuid4().hex[:4] + + # self.check_slug_used(slug) must be False now so we have a slug that + # we can use now. + self.slug = slug -class MediaEntryMixin(GenerateSlugMixin): +class MediaEntryMixin(GenerateSlugMixin, GeneratePublicIDMixin): def check_slug_used(self, slug): # import this here due to a cyclic import issue # (db.models -> db.mixin -> db.util -> db.models) from mediagoblin.db.util import check_media_slug_used - return check_media_slug_used(self.uploader, slug, self.id) + return check_media_slug_used(self.actor, slug, self.id) + + @property + def object_type(self): + """ Converts media_type to pump-like type - don't use internally """ + return self.media_type.split(".")[-1] @property def description_html(self): @@ -177,7 +265,7 @@ class MediaEntryMixin(GenerateSlugMixin): Use a slug if we have one, else use our 'id'. """ - uploader = self.get_uploader + uploader = self.get_actor return urlgen( 'mediagoblin.user_pages.media_home', @@ -192,16 +280,36 @@ class MediaEntryMixin(GenerateSlugMixin): # TODO: implement generic fallback in case MEDIA_MANAGER does # not specify one? if u'thumb' in self.media_files: - thumb_url = mg_globals.app.public_store.file_url( + thumb_url = self._app.public_store.file_url( self.media_files[u'thumb']) else: # No thumbnail in media available. Get the media's # MEDIA_MANAGER for the fallback icon and return static URL # Raises FileTypeNotSupported in case no such manager is enabled manager = self.media_manager - thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb']) + thumb_url = self._app.staticdirector(manager[u'default_thumb']) return thumb_url + @property + def original_url(self): + """ Returns the URL for the original image + will return self.thumb_url if original url doesn't exist""" + if u"original" not in self.media_files: + return self.thumb_url + + return self._app.public_store.file_url( + self.media_files[u"original"] + ) + + @property + def icon_url(self): + '''Return the icon URL (for usage in templates) if it exists''' + try: + return self._app.staticdirector( + self.media_manager['type_icon']) + except AttributeError: + return None + @cached_property def media_manager(self): """Returns the MEDIA_MANAGER of the media's media_type @@ -222,7 +330,17 @@ class MediaEntryMixin(GenerateSlugMixin): Get the exception that's appropriate for this error """ if self.fail_error: - return common.import_component(self.fail_error) + try: + return common.import_component(self.fail_error) + except ImportError: + # TODO(breton): fail_error should give some hint about why it + # failed. fail_error is used as a path to import(). + # Unfortunately, I didn't know about that and put general error + # message there. Maybe it's for the best, because for admin, + # we could show even some raw python things. Anyway, this + # should be properly resolved. Now we are in a freeze, that's + # why I simply catch ImportError. + return None def get_license_data(self): """Return license dict for requested license""" @@ -248,7 +366,7 @@ class MediaEntryMixin(GenerateSlugMixin): if 'Image DateTimeOriginal' in exif_all: # format date taken - takendate = datetime.datetime.strptime( + takendate = datetime.strptime( exif_all['Image DateTimeOriginal']['printable'], '%Y:%m:%d %H:%M:%S').date() taken = takendate.strftime('%B %d %Y') @@ -285,7 +403,9 @@ class MediaEntryMixin(GenerateSlugMixin): return exif_short -class MediaCommentMixin(object): +class TextCommentMixin(GeneratePublicIDMixin): + object_type = "comment" + @property def content_html(self): """ @@ -294,21 +414,29 @@ class MediaCommentMixin(object): """ return cleaned_markdown_conversion(self.content) + def __unicode__(self): + return u'<{klass} #{id} {actor} "{comment}">'.format( + klass=self.__class__.__name__, + id=self.id, + actor=self.get_actor, + comment=self.content) + def __repr__(self): - return '<{klass} #{id} {author} "{comment}">'.format( + return '<{klass} #{id} {actor} "{comment}">'.format( klass=self.__class__.__name__, id=self.id, - author=self.get_author, + actor=self.get_actor, comment=self.content) +class CollectionMixin(GenerateSlugMixin, GeneratePublicIDMixin): + object_type = "collection" -class CollectionMixin(GenerateSlugMixin): def check_slug_used(self, slug): # 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 - return check_collection_slug_used(self.creator, slug, self.id) + return check_collection_slug_used(self.actor, slug, self.id) @property def description_html(self): @@ -328,7 +456,7 @@ class CollectionMixin(GenerateSlugMixin): Use a slug if we have one, else use our 'id'. """ - creator = self.get_creator + creator = self.get_actor return urlgen( 'mediagoblin.user_pages.user_collection', @@ -336,6 +464,28 @@ class CollectionMixin(GenerateSlugMixin): collection=self.slug_or_id, **extra_args) + def add_to_collection(self, obj, content=None, commit=True): + """ Adds an object to the collection """ + # It's here to prevent cyclic imports + from mediagoblin.db.models import CollectionItem + + # Need the ID of this collection for this so check we've got one. + self.save(commit=False) + + # Create the CollectionItem + item = CollectionItem() + item.collection = self.id + item.get_object = obj + + if content is not None: + item.note = content + + self.num_items = self.num_items + 1 + + # Save both! + self.save(commit=commit) + item.save(commit=commit) + return item class CollectionItemMixin(object): @property @@ -345,3 +495,147 @@ class CollectionItemMixin(object): Run through Markdown and the HTML cleaner. """ return cleaned_markdown_conversion(self.note) + +class ActivityMixin(GeneratePublicIDMixin): + object_type = "activity" + + VALID_VERBS = ["add", "author", "create", "delete", "dislike", "favorite", + "follow", "like", "post", "share", "unfavorite", "unfollow", + "unlike", "unshare", "update", "tag"] + + def get_url(self, request): + return request.urlgen( + "mediagoblin.user_pages.activity_view", + username=self.get_actor.username, + id=self.id, + qualified=True + ) + + def generate_content(self): + """ Produces a HTML content for object """ + # some of these have simple and targetted. If self.target it set + # it will pick the targetted. If they DON'T have a targetted version + # the information in targetted won't be added to the content. + verb_to_content = { + "add": { + "simple" : _("{username} added {object}"), + "targetted": _("{username} added {object} to {target}"), + }, + "author": {"simple": _("{username} authored {object}")}, + "create": {"simple": _("{username} created {object}")}, + "delete": {"simple": _("{username} deleted {object}")}, + "dislike": {"simple": _("{username} disliked {object}")}, + "favorite": {"simple": _("{username} favorited {object}")}, + "follow": {"simple": _("{username} followed {object}")}, + "like": {"simple": _("{username} liked {object}")}, + "post": { + "simple": _("{username} posted {object}"), + "targetted": _("{username} posted {object} to {target}"), + }, + "share": {"simple": _("{username} shared {object}")}, + "unfavorite": {"simple": _("{username} unfavorited {object}")}, + "unfollow": {"simple": _("{username} stopped following {object}")}, + "unlike": {"simple": _("{username} unliked {object}")}, + "unshare": {"simple": _("{username} unshared {object}")}, + "update": {"simple": _("{username} updated {object}")}, + "tag": {"simple": _("{username} tagged {object}")}, + } + + object_map = { + "image": _("an image"), + "comment": _("a comment"), + "collection": _("a collection"), + "video": _("a video"), + "audio": _("audio"), + "person": _("a person"), + } + obj = self.object() + target = None if self.target_id is None else self.target() + actor = self.get_actor + content = verb_to_content.get(self.verb, None) + + if content is None or self.object is None: + return + + # Decide what to fill the object with + if hasattr(obj, "title") and obj.title.strip(" "): + object_value = obj.title + elif obj.object_type in object_map: + object_value = object_map[obj.object_type] + else: + object_value = _("an object") + + # Do we want to add a target (indirect object) to content? + if target is not None and "targetted" in content: + if hasattr(target, "title") and target.title.strip(" "): + target_value = target.title + elif target.object_type in object_map: + target_value = object_map[target.object_type] + else: + target_value = _("an object") + + self.content = content["targetted"].format( + username=actor.username, + object=object_value, + target=target_value + ) + else: + self.content = content["simple"].format( + username=actor.username, + object=object_value + ) + + return self.content + + def serialize(self, request): + href = request.urlgen( + "mediagoblin.api.object", + object_type=self.object_type, + id=self.id, + qualified=True + ) + published = UTC.localize(self.published) + updated = UTC.localize(self.updated) + obj = { + "id": href, + "actor": self.get_actor.serialize(request), + "verb": self.verb, + "published": published.isoformat(), + "updated": updated.isoformat(), + "content": self.content, + "url": self.get_url(request), + "object": self.object().serialize(request), + "objectType": self.object_type, + "links": { + "self": { + "href": href, + }, + }, + } + + if self.generator: + obj["generator"] = self.get_generator.serialize(request) + + if self.title: + obj["title"] = self.title + + if self.target_id is not None: + obj["target"] = self.target().serialize(request) + + return obj + + def unseralize(self, data): + """ + Takes data given and set it on this activity. + + Several pieces of data are not written on because of security + reasons. For example changing the author or id of an activity. + """ + if "verb" in data: + self.verb = data["verb"] + + if "title" in data: + self.title = data["title"] + + if "content" in data: + self.content = data["content"] |