class MediaFile(object): """ Audio or Video File """ meta = association_proxy('_meta', 'value', creator=MediaFilesMeta) query = DBSession.query_property(MediaFileQuery) def __repr__(self): return '<MediaFile: %r %r unique_id=%r>' \ % (self.type, self.storage.display_name, self.unique_id) @property def mimetype(self): """The best-guess mimetype based on this file's container format. Defaults to 'application/octet-stream'. """ type = self.type if type == AUDIO_DESC: type = AUDIO return guess_mimetype(self.container, type) def get_uris(self): """Return a list all possible playback URIs for this file. :rtype: list :returns: :class:`mediadrop.lib.storage.StorageURI` instances. """ return self.storage.get_uris(self)
class Tag(object): """ Tag (keyword) for labelling content .. attribute:: id .. attribute:: name Display name .. attribute:: slug A unique URL-friendly permalink string for looking up this object. .. attribute:: media_content .. attribute:: media_count_published """ query = DBSession.query_property() def __init__(self, name=None, slug=None): self.name = name or None self.slug = slug or name or None def __repr__(self): return '<Tag: %r>' % self.name def __unicode__(self): return self.name @validates('slug') def validate_slug(self, key, slug): return slugify(slug)
class Views_Counter(object): """ Podcast Metadata """ query = DBSession.query_property() # TODO: replace '_thumb_dir' with something more generic, like 'name', # so that its other uses throughout the code make more sense. _thumb_dir = 'view_check' def __repr__(self): return '<view: %r>' % self.view_id
class MultiSetting(object): """ A MultiSetting """ query = DBSession.query_property() def __init__(self, key=None, value=None): self.key = key or None self.value = value or None def __repr__(self): return '<MultiSetting: %s = %r>' % (self.key, self.value) def __unicode__(self): return self.value
class Podcast(object): """ Podcast Metadata """ query = DBSession.query_property() # TODO: replace '_thumb_dir' with something more generic, like 'name', # so that its other uses throughout the code make more sense. _thumb_dir = 'podcasts' def __repr__(self): return '<Podcast: %r>' % self.slug @validates('slug') def validate_slug(self, key, slug): return slugify(slug)
class PlayerPrefs(object): """ Player Preferences A wrapper containing the administrator's preferences for an individual player. Each row maps to a :class:`mediadrop.lib.players.AbstractPlayer` implementation. """ query = DBSession.query_property() @property def player_cls(self): """Return the class object that is mapped to this row.""" for player_cls in reversed(tuple(AbstractPlayer)): if self.name == player_cls.name: return player_cls return None @property def display_name(self): """Return the user-friendly display name for this player class. This string is expected to be i18n-ready. Simply wrap it in a call to :func:`mediadrop.lib.i18n._`. :rtype: unicode :returns: A i18n-ready string name. """ if self.player_cls is None: # do not break the admin interface (admin/settings/players) if the # player is still in the database but the actual player class is not # available anymore (this can happen especially for players provided # by external plugins. return _(u'%s (broken)') % self.name return self.player_cls.display_name @property @memoize def settings_form(self): cls = self.player_cls if cls and cls.settings_form_class: return cls.settings_form_class() return None
class Comment(object): """Comment Model .. attribute:: type The relation name to use when looking up the parent object of this Comment. This is the name of the backref property which can be used to find the object that this Comment belongs to. Our convention is to have a controller by this name, with a 'view' action which accepts a slug, so we can auto-generate links to any comment's parent. .. attribute:: author An instance of :class:`mediadrop.model.author.AuthorWithIP`. """ query = DBSession.query_property(CommentQuery) def __repr__(self): return '<Comment: %r subject=%r>' % (self.id, self.subject) def __unicode__(self): return self.subject @property def type(self): if self.media_id: return 'media' return None def _get_parent(self): return self.media or None def _set_parent(self, parent): self.media = parent parent = property( _get_parent, _set_parent, None, """ The object this Comment belongs to, provided for convenience mostly. If the parent has not been eagerloaded, a query is executed automatically. """)
class Group(object): """ An ultra-simple group definition. """ query = DBSession.query_property() def __init__(self, name=None, display_name=None): self.group_name = name self.display_name = display_name def __repr__(self): return '<Group: name=%r>' % self.group_name def __unicode__(self): return self.group_name @classmethod def custom_groups(cls, *columns): query_object = columns or (Group, ) return DBSession.query(*query_object).\ filter( not_(Group.group_name.in_([u'anonymous', u'authenticated'])) ) @classmethod def by_name(cls, name): return cls.query.filter(cls.group_name == name).first() @classmethod def example(cls, **kwargs): defaults = dict( name=u'baz_users', display_name=u'Baz Users', ) defaults.update(kwargs) group = Group(**defaults) DBSession.add(group) DBSession.flush() return group
class User(object): """ Basic User definition """ query = DBSession.query_property() def __repr__(self): return '<User: email=%r, display name=%r>' % (self.email_address, self.display_name) def __unicode__(self): return self.display_name or self.user_name @property def permissions(self): perms = set() for g in self.groups: perms = perms | set(g.permissions) return perms def has_permission(self, permission_name): return any(perm.permission_name == permission_name for perm in self.permissions) @classmethod def by_email_address(cls, email): # TODO: Move this function to User.query return DBSession.query(cls).filter(cls.email_address == email).first() @classmethod def by_user_name(cls, username): # TODO: Move this function to User.query return DBSession.query(cls).filter(cls.user_name == username).first() @classmethod def example(cls, **kwargs): user = User() defaults = dict( user_name=u'joe', email_address=u'*****@*****.**', display_name=u'Joe Smith', created=datetime.now(), ) defaults.update(kwargs) for key, value in defaults.items(): setattr(user, key, value) DBSession.add(user) DBSession.flush() return user def _set_password(self, password): """Hash password on the fly.""" if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password salt = sha1() salt.update(os.urandom(60)) hash_ = sha1() hash_.update(password_8bit + salt.hexdigest()) hashed_password = salt.hexdigest() + hash_.hexdigest() # make sure the hashed password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') self._password = hashed_password def _get_password(self): return self._password password = property(_get_password, _set_password) def validate_password(self, password): """Check the password against existing credentials. :param password: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the hashed one in the database. :type password: unicode object. :return: Whether the password is valid. :rtype: bool """ hashed_pass = sha1() hashed_pass.update(password + self.password[:40]) return self.password[40:] == hashed_pass.hexdigest()
class Category(object): """ Category Mapped Class """ query = DBSession.query_property(CategoryQuery) def __init__(self, name=None, slug=None): self.name = name or None self.slug = slug or name or None def __repr__(self): return '<Category: %r>' % self.name def __unicode__(self): return self.name @classmethod def example(cls, **kwargs): category = Category() defaults = dict(name=u'Foo', parent_id=0) defaults.update(kwargs) defaults.setdefault('slug', get_available_slug(Category, defaults['name'])) for key, value in defaults.items(): assert hasattr(category, key) setattr(category, key, value) DBSession.add(category) DBSession.flush() return category @validates('slug') def validate_slug(self, key, slug): return slugify(slug) def traverse(self): """Iterate over all nested categories in depth-first order.""" return traverse(self.children) def descendants(self): """Return a list of descendants in depth-first order.""" return [desc for desc, depth in self.traverse()] def ancestors(self): """Return a list of ancestors, starting with the root node. This method is optimized for when all categories have already been fetched in the current DBSession:: >>> Category.query.all() # run one query >>> row = Category.query.get(50) # doesn't use a query >>> row.parent # the DBSession recognized the primary key <Category: parent> >>> print row.ancestors() [..., <Category: great-grand-parent>, <Category: grand-parent>, <Category: parent>] """ ancestors = CategoryList() anc = self.parent while anc: if anc is self: raise CategoryNestingException, 'Category %s is defined as a ' \ 'parent of one of its ancestors.' % anc ancestors.insert(0, anc) anc = anc.parent return ancestors def depth(self): """Return this category's distance from the root of the tree.""" return len(self.ancestors())
class MediaFullText(object): query = DBSession.query_property()
class Media(object): """ Media metadata and a collection of related files. """ meta = association_proxy('_meta', 'value', creator=MediaMeta) query = DBSession.query_property(MediaQuery) # TODO: replace '_thumb_dir' with something more generic, like 'name', # so that its other uses throughout the code make more sense. _thumb_dir = 'media' def __init__(self): if self.author is None: self.author = Author() def __repr__(self): return '<Media: %r>' % self.slug @classmethod def example(cls, **kwargs): media = Media() defaults = dict( title=u'Foo Media', author=Author(u'Joe', u'*****@*****.**'), type = None, ) defaults.update(kwargs) defaults.setdefault('slug', get_available_slug(Media, defaults['title'])) for key, value in defaults.items(): assert hasattr(media, key) setattr(media, key, value) DBSession.add(media) DBSession.flush() return media def set_tags(self, tags): """Set the tags relations of this media, creating them as needed. :param tags: A list or comma separated string of tags to use. """ if isinstance(tags, basestring): tags = extract_tags(tags) if isinstance(tags, list) and tags: tags = fetch_and_create_tags(tags) self.tags = tags or [] def set_categories(self, cats): """Set the related categories of this media. :param cats: A list of category IDs to set. """ if cats: cats = Category.query.filter(Category.id.in_(cats)).all() self.categories = cats or [] def update_status(self): """Ensure the type (audio/video) and encoded flag are properly set. Call this after modifying any files belonging to this item. """ was_encoded = self.encoded self.type = self._update_type() self.encoded = self._update_encoding() if self.encoded and not was_encoded: events.Media.encoding_done(self) def _update_type(self): """Update the type of this Media object. If there's a video file, mark this as a video type, else fallback to audio, if possible, or unknown (None) """ if any(file.type == VIDEO for file in self.files): return VIDEO elif any(file.type == AUDIO for file in self.files): return AUDIO return None def _update_encoding(self): # Test to see if we can find a workable file/player combination # for the most common podcasting app w/ the POOREST format support if self.podcast_id and not pick_podcast_media_file(self): return False # Test to see if we can find a workable file/player combination # for the browser w/ the BEST format support if not pick_any_media_file(self): return False return True @property def is_published(self): if self.id is None: return False return self.publishable and self.reviewed and self.encoded\ and (self.publish_on is not None and self.publish_on <= datetime.now())\ and (self.publish_until is None or self.publish_until >= datetime.now()) @property def resource(self): return Resource('media', self.id, media=self) def increment_views(self): """Increment the number of views in the database. We avoid concurrency issues by incrementing JUST the views and not allowing modified_on to be updated automatically. """ if self.id is None: self.views += 1 return self.views DBSession.execute(media.update()\ .values(views=media.c.views + 1)\ .where(media.c.id == self.id)) # Increment the views by one for the rest of the request, # but don't allow the ORM to increment the views too. attributes.set_committed_value(self, 'views', self.views + 1) return self.views def increment_likes(self): self.likes += 1 self.update_popularity() return self.likes def increment_dislikes(self): self.dislikes += 1 self.update_popularity() return self.dislikes def update_popularity(self): if self.is_published: self.popularity_points = calculate_popularity( self.publish_on, self.likes - self.dislikes, ) self.popularity_likes = calculate_popularity( self.publish_on, self.likes, ) self.popularity_dislikes = calculate_popularity( self.publish_on, self.dislikes, ) else: self.popularity_points = 0 self.popularity_likes = 0 self.popularity_dislikes = 0 @validates('description') def _validate_description(self, key, value): self.description_plain = line_break_xhtml( line_break_xhtml(value) ) return value @validates('description_plain') def _validate_description_plain(self, key, value): return strip_xhtml(value, True) def get_uris(self): uris = [] for file in self.files: uris.extend(file.get_uris()) return uris