Beispiel #1
0
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: %s>' % self.name

    def __unicode__(self):
        return self.name

    @validates('slug')
    def validate_slug(self, key, slug):
        return slugify(slug)
Beispiel #2
0
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:`mediacore.lib.storage.StorageURI` instances.

        """
        return self.storage.get_uris(self)
class Vote(object):
    """Vote Model
    """

    query = DBSession.query_property(VoteQuery)

    def __repr__(self):
        return '<Vote: %r media=%r user=%r>' % (self.id, self.media_id, self.user_name)

    def __unicode__(self):
        return 'Vote %r' % self.id

    def increment_likes(self):
        self.likes += 1
        return self.likes

    def increment_dislikes(self):
        self.dislikes += 1
        return self.dislikes

    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 Vote belongs to, provided for convenience mostly.
        If the parent has not been eagerloaded, a query is executed automatically.
    """)
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

    @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())
Beispiel #5
0
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
Beispiel #6
0
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)
Beispiel #7
0
class PlayerPrefs(object):
    """
    Player Preferences

    A wrapper containing the administrator's preferences for an individual
    player. Each row maps to a :class:`mediacore.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:`mediacore.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:`mediacore.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.
    """)
Beispiel #9
0
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 PlayerPrefs(object):
    """
    Player Preferences

    A wrapper containing the administrator's preferences for an individual
    player. Each row maps to a :class:`mediacore.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:`pylons.i18n._`.

        :rtype: unicode
        :returns: A i18n-ready string 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
Beispiel #11
0
class MediaFullText(object):
    query = DBSession.query_property()
Beispiel #12
0
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
        else:
            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
Beispiel #13
0
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()
Beispiel #14
0
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: %s>' % self.slug

    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.

        """
        self.type = self._update_type()
        self.encoded = self._update_encoding()

    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
        else:
            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())

    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

        # Don't raise an exception should concurrency problems occur.
        # Views will not actually be incremented in this case, but thats
        # relatively unimportant compared to rendering the page for the user.
        # We may be able to remove this after we improve our triggers to not
        # issue an UPDATE on media_fulltext unless one of its columns are
        # actually changed. Even when just media.views is updated, all the
        # columns in the corresponding media_fulltext row are updated, and
        # media_fulltext's MyISAM engine must lock the whole table to do so.
        transaction = DBSession.begin_nested()
        try:
            DBSession.query(self.__class__)\
                .filter(self.__class__.id == self.id)\
                .update({self.__class__.views: self.__class__.views + 1})
            transaction.commit()
        except OperationalError, e:
            transaction.rollback()
            # (OperationalError) (1205, 'Lock wait timeout exceeded, try restarting the transaction')
            if not '1205' in e.message:
                raise

        # 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
Beispiel #15
0
class User(object):
    """
    Basic User definition
    """
    query = DBSession.query_property()

    def __repr__(self):
        return '<User: email="%s", display name="%s">' % (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

    @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()

    def _set_password(self, password):
        """Hash password on the fly."""
        hashed_password = password

        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 hased 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()