def bulk(self, type=None, ids=None, **kwargs):
        """Perform bulk operations on media items

        :param type: The type of bulk action to perform (delete)
        :param ids: A list of IDs.

        """
        if not ids:
            ids = []
        elif not isinstance(ids, list):
            ids = [ids]


        if type == 'delete':
            Category.query.filter(Category.id.in_(ids)).delete(False)
            DBSession.commit()
            success = True
        else:
            success = False

        return dict(
            success = success,
            ids = ids,
            parent_options = unicode(category_form.c['parent_id'].display()),
        )
Example #2
0
def fetch_and_create_multi_setting(key, value):
    multisettings = MultiSetting.query.filter(MultiSetting.key == key).all()
    for ms in multisettings:
        if ms.value == value:
            return ms
    ms = MultiSetting(key, value)
    DBSession.add(ms)
    return ms
Example #3
0
 def setUp(self):
     super(DBTestCase, self).setUp()
     self.env_dir = self._create_environment_folders()
     self.pylons_config = setup_environment_and_database(self.env_dir, 
         enabled_plugins=self.enabled_plugins)
     add_default_data()
     DBSession.commit()
     
     config.push_process_config(self.pylons_config)
def _autocommit_commit(req):
    from mediadrop.model.meta import DBSession
    try:
        DBSession.commit()
    except:
        _autocommit_rollback(req)
        raise
    else:
        _autocommit_fire_callbacks(req, req.commit_callbacks)
Example #5
0
 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
Example #6
0
    def update_status(self, id, status=None, publish_on=None, publish_until=None, **values):
        """Update the publish status for the given media.

        :param id: Media ID
        :type id: ``int``
        :param update_status: The text of the submit button which indicates
            that the :attr:`~mediadrop.model.media.Media.status` should change.
        :type update_status: ``unicode`` or ``None``
        :param publish_on: A date to set to
            :attr:`~mediadrop.model.media.Media.publish_on`
        :type publish_on: :class:`datetime.datetime` or ``None``
        :param publish_until: A date to set to
            :attr:`~mediadrop.model.media.Media.publish_until`
        :type publish_until: :class:`datetime.datetime` or ``None``
        :rtype: JSON dict
        :returns:
            success
                bool
            message
                Error message, if unsuccessful
            status_form
                Rendered XHTML for the status form, updated to reflect the
                changes made.

        """
        media = fetch_row(Media, id)
        new_slug = None

        # Make the requested change assuming it will be allowed
        if status == 'unreviewed':
            media.reviewed = True
        elif status == 'draft':
            self._publish_media(media, publish_on)
        elif publish_on:
            media.publish_on = publish_on
            media.update_popularity()
        elif publish_until:
            media.publish_until = publish_until

        # Verify the change is valid by re-determining the status
        media.update_status()
        DBSession.flush()

        if request.is_xhr:
            # Return the rendered widget for injection
            status_form_xhtml = unicode(update_status_form.display(
                action=url_for(action='update_status'), media=media))
            return dict(
                success = True,
                status_form = status_form_xhtml,
                slug = new_slug,
            )
        else:
            redirect(action='edit')
Example #7
0
 def example(cls, **kwargs):
     defaults = dict(
         name=u'foo',
         description = u'foo permission',
         groups = None,
     )
     defaults.update(kwargs)
     permission = Permission(**defaults)
     DBSession.add(permission)
     DBSession.flush()
     return permission
def fetch_and_create_tags(tag_names):
    """Return a list of Tag instances that match the given names.

    Tag names that don't yet exist are created automatically and
    returned alongside the results that did already exist.

    If you try to create a new tag that would have the same slug
    as an already existing tag, the existing tag is used instead.

    :param tag_names: The display :attr:`Tag.name`
    :type tag_names: list
    :returns: A list of :class:`Tag` instances.
    :rtype: :class:`TagList` instance

    """
    lower_names = [name.lower() for name in tag_names]
    slugs = [slugify(name) for name in lower_names]

    # Grab all the tags that exist already, whether its the name or slug
    # that matches. Slugs can be changed by the tag settings UI so we can't
    # rely on each tag name evaluating to the same slug every time.
    results = Tag.query.filter(sql.or_(func.lower(Tag.name).in_(lower_names),
                                       Tag.slug.in_(slugs))).all()

    # Filter out any tag names that already exist (case insensitive), and
    # any tag names evaluate to slugs that already exist.
    for tag in results:
        # Remove the match from our three lists until its completely gone
        while True:
            try:
                try:
                    index = slugs.index(tag.slug)
                except ValueError:
                    index = lower_names.index(tag.name.lower())
                tag_names.pop(index)
                lower_names.pop(index)
                slugs.pop(index)
            except ValueError:
                break

    # Any remaining tag names need to be created.
    if tag_names:
        # We may still have multiple tag names which evaluate to the same slug.
        # Load it into a dict so that duplicates are overwritten.
        uniques = dict((slug, name) for slug, name in izip(slugs, tag_names))
        # Do a bulk insert to create the tag rows.
        new_tags = [{'name': n, 'slug': s} for s, n in uniques.iteritems()]
        DBSession.execute(tags.insert(), new_tags)
        DBSession.flush()
        # Query for our newly created rows and append them to our result set.
        results += Tag.query.filter(Tag.slug.in_(uniques.keys())).all()

    return results
Example #9
0
    def save(self, id, slug, title, author_name, author_email,
             description, notes, podcast, tags, categories,
             delete=None, **kwargs):
        """Save changes or create a new :class:`~mediadrop.model.media.Media` instance.

        Form handler the :meth:`edit` action and the
        :class:`~mediadrop.forms.admin.media.MediaForm`.

        Redirects back to :meth:`edit` after successful editing
        and :meth:`index` after successful deletion.

        """
        media = fetch_row(Media, id)

        if delete:
            self._delete_media(media)
            redirect(action='index', id=None)

        if not slug:
            slug = slugify(title)
        elif slug.startswith('_stub_'):
            slug = slug[len('_stub_'):]
        if slug != media.slug:
            media.slug = get_available_slug(Media, slug, media)
        media.title = title
        media.author = Author(author_name, author_email)
        media.description = description
        media.notes = notes
        media.podcast_id = podcast
        media.set_tags(tags)
        media.set_categories(categories)

        media.update_status()
        DBSession.add(media)
        DBSession.flush()

        if id == 'new' and not has_thumbs(media):
            create_default_thumbs_for(media)

        if request.is_xhr:
            status_form_xhtml = unicode(update_status_form.display(
                action=url_for(action='update_status', id=media.id),
                media=media))

            return dict(
                media_id = media.id,
                values = {'slug': slug},
                link = url_for(action='edit', id=media.id),
                status_form = status_form_xhtml,
            )
        else:
            redirect(action='edit', id=media.id)
Example #10
0
    def delete(self, id, **kwargs):
        """Delete a user.

        :param id: User ID.
        :type id: ``int``
        :returns: Redirect back to :meth:`index` after successful delete.
        """
        user = fetch_row(User, id)
        DBSession.delete(user)

        if request.is_xhr:
            return dict(success=True)
        redirect(action='index', id=None)
    def delete(self, id, **kwargs):
        """Delete a group.

        :param id: Group ID.
        :type id: ``int``
        :returns: Redirect back to :meth:`index` after successful delete.
        """
        group = fetch_row(Group, id)
        DBSession.delete(group)

        if request.is_xhr:
            return dict(success=True)
        redirect(action='index', id=None)
def init_model(engine, table_prefix=None):
    """Call me before using any of the tables or classes in the model."""
    DBSession.configure(bind=engine)
    from mediadrop.model import meta
    meta.metadata.bind = engine
    meta.engine = engine
    # Change all table names to include the given prefix. This can't be
    # easily done before the models are added to the metadata because
    # that happens on import, before the config is available.
    if table_prefix:
        table_prefix = table_prefix.rstrip('_') + '_'
        for table in meta.metadata.sorted_tables:
            table.name = table_prefix + table.name
Example #13
0
def cleanup_players_table(enabled=False):
    """
    Ensure that all available players are added to the database
    and that players are prioritized in incrementally increasing order.

    :param enabled: Should the default players be enabled upon creation?
    :type enabled: bool
    """
    from mediadrop.lib.players import (BlipTVFlashPlayer,
        DailyMotionEmbedPlayer, GoogleVideoFlashPlayer, JWPlayer,
        VimeoUniversalEmbedPlayer, YoutubePlayer)

    # When adding players, prefer them in the following order:
    default_players = [
        JWPlayer,
        YoutubePlayer,
        VimeoUniversalEmbedPlayer,
        GoogleVideoFlashPlayer,
        BlipTVFlashPlayer,
        DailyMotionEmbedPlayer,
    ]
    unordered_players = [p for p in AbstractPlayer if p not in default_players]
    all_players = default_players + unordered_players

    # fetch the players that are already in the database
    s = players.select().order_by('priority')
    existing_players_query = DBSession.execute(s)
    existing_player_rows = [p for p in existing_players_query]
    existing_player_names = [p['name'] for p in existing_player_rows]

    # Ensure all priorities are monotonically increasing from 1..n
    priority = 0
    for player_row in existing_player_rows:
        priority += 1
        if player_row['priority'] != priority:
            u = players.update()\
                       .where(players.c.id == player_row['id'])\
                       .values(priority=priority)
            DBSession.execute(u)

    # Ensure that all available players are in the database
    for player_cls in all_players:
        if player_cls.name not in existing_player_names:
            enable_player = enabled and player_cls in default_players
            priority += 1
            DBSession.execute(players.insert().values(
                name=player_cls.name,
                enabled=enable_player,
                data=player_cls.default_data,
                priority=priority,
            ))
Example #14
0
 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
Example #15
0
    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
Example #16
0
 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 get_available_slug(mapped_class, string, ignore=None, slug_attr='slug', slug_length=SLUG_LENGTH):
    """Return a unique slug based on the provided string.

    Works by appending an int in sequence starting with 2:

        1. awesome-stuff
        2. awesome-stuff-2
        3. awesome-stuff-3

    :param mapped_class: The ORM-controlled model that the slug is for
    :param string: A title, name, etc
    :type string: unicode
    :param ignore: A record which doesn't count as a collision
    :type ignore: Int ID, ``mapped_class`` instance or None
    :returns: A unique slug
    :rtype: unicode
    """
    if isinstance(ignore, mapped_class):
        ignore = ignore.id
    elif ignore is not None:
        ignore = int(ignore)

    new_slug = slug = slugify(string)
    appendix = 2
    while DBSession.query(mapped_class.id)\
            .filter(getattr(mapped_class, slug_attr) == new_slug)\
            .filter(mapped_class.id != ignore)\
            .first():
        str_appendix = u'-%s' % appendix
        max_substr_len = slug_length - len(str_appendix)
        new_slug = slug[:max_substr_len] + str_appendix
        appendix += 1

    return new_slug
Example #18
0
def fetch_enabled_players():
    """Return player classes and their data dicts in ascending priority.

    Warnings are logged any time a row is found that does not match up to
    one of the classes that are currently registered. A warning will also
    be raised if there are no players configured/enabled.

    :rtype: list of tuples
    :returns: :class:`~mediadrop.lib.players.AbstractPlayer` subclasses
        and the configured data associated with them.

    """
    player_classes = dict((p.name, p) for p in AbstractPlayer)
    query = sql.select((players.c.name, players.c.data))\
        .where(players.c.enabled == True)\
        .order_by(players.c.priority.asc(), players.c.id.desc())
    query_data = DBSession.execute(query).fetchall()
    while query_data:
        try:
            return [(player_classes[name], data) for name, data in query_data]
        except KeyError:
            log.warn('Player name %r exists in the database but has not '
                     'been registered.' % name)
            query_data.remove((name, data))
    log.warn('No registered players are configured in your database.')
    return []
Example #19
0
    def save_status(self, id, status, ids=None, **kwargs):
        """Approve or delete a comment or comments.

        :param id: A :attr:`~mediadrop.model.comments.Comment.id` if we are
            acting on a single comment, or ``"bulk"`` if we should refer to
            ``ids``.
        :type id: ``int`` or ``"bulk"``
        :param status: ``"approve"`` or ``"trash"`` depending on what action
            the user requests.
        :param ids: An optional string of IDs separated by commas.
        :type ids: ``unicode`` or ``None``
        :rtype: JSON dict
        :returns:
            success
                bool
            ids
                A list of :attr:`~mediadrop.model.comments.Comment.id`
                that have changed.

        """
        if id != 'bulk':
            ids = [id]
        if not isinstance(ids, list):
            ids = [ids]

        if status == 'approve':
            publishable = True
        elif status == 'trash':
            publishable = False
        else:
            # XXX: This form should never be submitted without a valid status.
            raise AssertionError('Unexpected status: %r' % status)

        comments = Comment.query.filter(Comment.id.in_(ids)).all()

        for comment in comments:
            comment.reviewed = True
            comment.publishable = publishable
            DBSession.add(comment)

        DBSession.flush()

        if request.is_xhr:
            return dict(success=True, ids=ids)
        else:
            redirect(action='index')
Example #20
0
    def popularity_save(self, **kwargs):
        """Save :class:`~mediadrop.forms.admin.settings.PopularityForm`.

        Updates the popularity for every media item based on the submitted
        values.
        """
        self._save(popularity_form, values=kwargs)
        # ".util.calculate_popularity()" uses the popularity settings from
        # the request.settings which are only updated when a new request
        # comes in.
        # update the settings manually so the popularity is actually updated
        # correctly.
        for key in ('popularity_decay_exponent', 'popularity_decay_lifetime'):
            request.settings[key] = kwargs['popularity.'+key]
        for m in Media.query:
            m.update_popularity()
            DBSession.add(m)
        redirect(action='popularity')
Example #21
0
    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
Example #22
0
    def save_edit(self, id, body, **kwargs):
        """Save an edit from :class:`~mediadrop.forms.admin.comments.EditCommentForm`.

        :param id: Comment ID
        :type id: ``int``
        :rtype: JSON dict
        :returns:
            success
                bool
            body
                The edited comment body after validation/filtering

        """
        comment = fetch_row(Comment, id)
        comment.body = body
        DBSession.add(comment)
        return dict(
            success = True,
            body = comment.body,
        )
Example #23
0
    def comments_save(self, **kwargs):
        """Save :class:`~mediadrop.forms.admin.settings.CommentsForm`."""
        old_vulgarity_filter = c.settings['vulgarity_filtered_words'].value

        self._save(comments_form, values=kwargs)

        # Run the filter now if it has changed
        if old_vulgarity_filter != c.settings['vulgarity_filtered_words'].value:
            for comment in DBSession.query(Comment):
                comment.body = filter_vulgarity(comment.body)

        redirect(action='comments')
 def test_metagroup_assignment_does_not_fail_if_groups_are_not_found_in_db(self):
     DBSession.delete(self.anonymous)
     DBSession.delete(self.authenticated)
     DBSession.flush()
     
     user = User.example()
     self.assert_user_groups([], user)
Example #25
0
    def index(self, page=1, search=None, media_filter=None, **kwargs):
        """List comments with pagination and filtering.

        :param page: Page number, defaults to 1.
        :type page: int
        :param search: Optional search term to filter by
        :type search: unicode or None
        :param media_filter: Optional media ID to filter by
        :type media_filter: int or None
        :rtype: dict
        :returns:
            comments
                The list of :class:`~mediadrop.model.comments.Comment` instances
                for this page.
            edit_form
                The :class:`mediadrop.forms.admin.comments.EditCommentForm` instance,
                to be rendered for each instance in ``comments``.
            search
                The given search term, if any
            search_form
                The :class:`~mediadrop.forms.admin.SearchForm` instance
            media_filter
                The given podcast ID to filter by, if any
            media_filter_title
                The media title for rendering if a ``media_filter`` was specified.

        """
        comments = Comment.query.trash(False)\
            .order_by(Comment.reviewed.asc(),
                      Comment.created_on.desc())

        # This only works since we only have comments on one type of content.
        # It will need re-evaluation if we ever add others.
        comments = comments.options(orm.eagerload('media'))

        if search is not None:
            comments = comments.search(search)

        media_filter_title = media_filter
        if media_filter is not None:
            comments = comments.filter(Comment.media.has(Media.id == media_filter))
            media_filter_title = DBSession.query(Media.title).get(media_filter)
            media_filter = int(media_filter)

        return dict(
            comments = comments,
            edit_form = edit_form,
            media_filter = media_filter,
            media_filter_title = media_filter_title,
            search = search,
            search_form = search_form,
        )
Example #26
0
    def save(self, id, delete=None, **kwargs):
        """Save changes or create a category.

        See :class:`~mediadrop.forms.admin.settings.categories.CategoryForm` for POST vars.

        :param id: Category ID
        :param delete: If true the category is to be deleted rather than saved.
        :type delete: bool
        :rtype: JSON dict
        :returns:
            success
                bool

        """
        if tmpl_context.form_errors:
            if request.is_xhr:
                return dict(success=False, errors=tmpl_context.form_errors)
            else:
                # TODO: Add error reporting for users with JS disabled?
                return redirect(action="edit")

        cat = fetch_row(Category, id)

        if delete:
            DBSession.delete(cat)
            data = dict(success=True, id=cat.id, parent_options=unicode(category_form.c["parent_id"].display()))
        else:
            cat.name = kwargs["name"]
            cat.slug = get_available_slug(Category, kwargs["slug"], cat)

            if kwargs["parent_id"]:
                parent = fetch_row(Category, kwargs["parent_id"])
                if parent is not cat and cat not in parent.ancestors():
                    cat.parent = parent
            else:
                cat.parent = None

            DBSession.add(cat)
            DBSession.flush()

            data = dict(
                success=True,
                id=cat.id,
                name=cat.name,
                slug=cat.slug,
                parent_id=cat.parent_id,
                parent_options=unicode(category_form.c["parent_id"].display()),
                depth=cat.depth(),
                row=unicode(
                    category_row_form.display(
                        action=url_for(id=cat.id), category=cat, depth=cat.depth(), first_child=True
                    )
                ),
            )

        if request.is_xhr:
            return data
        else:
            redirect(action="index", id=None)
Example #27
0
    def index(self, page=1, **kw):
        """List podcasts with pagination.

        :param page: Page number, defaults to 1.
        :type page: int
        :rtype: Dict
        :returns:
            podcasts
                The list of :class:`~mediadrop.model.podcasts.Podcast`
                instances for this page.
        """
        podcasts = DBSession.query(Podcast).options(orm.undefer("media_count")).order_by(Podcast.title)
        return dict(podcasts=podcasts)
Example #28
0
    def index(self, page=1, **kwargs):
        """List groups with pagination.

        :param page: Page number, defaults to 1.
        :type page: int
        :rtype: Dict
        :returns:
            users
                The list of :class:`~mediadrop.model.auth.Group`
                instances for this page.

        """
        groups = DBSession.query(Group).order_by(Group.display_name, Group.group_name)
        return dict(groups=groups)
    def save(self, id, display_name, group_name, permissions, delete=None, **kwargs):
        """Save changes or create a new :class:`~mediadrop.model.auth.Group` instance.

        :param id: Group ID. If ``"new"`` a new group is created.
        :type id: ``int`` or ``"new"``
        :returns: Redirect back to :meth:`index` after successful save.

        """
        group = fetch_row(Group, id)

        if delete:
            DBSession.delete(group)
            redirect(action='index', id=None)
        
        group.display_name = display_name
        group.group_name = group_name
        if permissions:
            query = DBSession.query(Permission).filter(Permission.permission_id.in_(permissions))
            group.permissions = list(query.all())
        else:
            group.permissions = []
        DBSession.add(group)

        redirect(action='index', id=None)
Example #30
0
    def index(self, page=1, **kwargs):
        """List users with pagination.

        :param page: Page number, defaults to 1.
        :type page: int
        :rtype: Dict
        :returns:
            users
                The list of :class:`~mediadrop.model.auth.User`
                instances for this page.

        """
        users = DBSession.query(User).order_by(User.display_name,
                                               User.email_address)
        return dict(users=users)
Example #31
0
def fetch_row(mapped_class, pk=None, extra_filter=None, **kwargs):
    """Fetch a single row from the database or else trigger a 404.

    Typical usage is to fetch a single row for display or editing::

        class PageController(object):
            @expose()
            def index(self, id):
                page = fetch_row(Page, id)
                return page.name

            @expose()
            def works_with_slugs_too(self, slug):
                page = fetch_row(Page, slug=slug)
                return page.name

    If the ``pk`` is string ``new`` then an empty instance of ``mapped_class``
    is created and returned. This is helpful in admin controllers where you
    may reuse your *edit* action for *adding* too.

    :param mapped_class: An ORM-controlled model
    :param pk: A particular primary key to filter by.
    :type pk: int, ``None`` or ``"new"``
    :param extra_filter: Extra filter arguments.
    :param \*\*kwargs: Any extra args are treated as column names to filter by.
        See :meth:`sqlalchemy.orm.Query.filter_by`.
    :returns: An instance of ``mapped_class``.
    :raises webob.exc.HTTPNotFound: If no result is found

    """
    if pk == 'new':
        inst = mapped_class()
        return inst

    query = DBSession.query(mapped_class)

    if pk is not None:
        mapper = class_mapper(mapped_class, compile=False)
        query = query.filter(mapper.primary_key[0] == pk)
    if kwargs:
        query = query.filter_by(**kwargs)
    if extra_filter is not None:
        query = query.filter(extra_filter)

    try:
        return query.one()
    except NoResultFound:
        raise webob.exc.HTTPNotFound().exception
Example #32
0
    def save_thumb(self, id, thumb=None, **kwargs):
        """Save a thumbnail uploaded with :class:`~mediadrop.forms.admin.ThumbForm`.

        :param id: Media ID. If ``"new"`` a new Media stub is created.
        :type id: ``int`` or ``"new"``
        :param file: The uploaded file
        :type file: :class:`cgi.FieldStorage` or ``None``
        :rtype: JSON dict
        :returns:
            success
                bool
            message
                Error message, if unsuccessful
            id
                The :attr:`~mediadrop.model.media.Media.id` which is
                important if a new media has just been created.

        """
        user = request.perm.user
        if id == 'new':
            media = Media()
            user = request.perm.user
            media.author = Author(user.display_name, user.email_address)
            media.title = os.path.basename(kwargs['file'].filename)
            media.slug = get_available_slug(Media, '_stub_' + media.title)
            DBSession.add(media)
            DBSession.flush()
        else:
            media = fetch_row(Media, id)
            group = request.perm.user.groups[0].group_name
            if media.author.name != user.display_name and group != 'admins':
                raise

        try:
            # Create JPEG thumbs
            create_thumbs_for(media, kwargs['file'].file,
                              kwargs['file'].filename)
            success = True
            message = None
        except IOError, e:
            success = False

            if id == 'new':
                DBSession.delete(media)

            if e.errno == 13:
                message = _('Permission denied, cannot write file')
            elif e.message == 'cannot identify image file':
                message = _('Unsupported image type: %s') \
                    % os.path.splitext(kwargs['file'].filename)[1].lstrip('.')
            elif e.message == 'cannot read interlaced PNG files':
                message = _('Interlaced PNGs are not supported.')
            else:
                raise
Example #33
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)
Example #34
0
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
Example #35
0
    def save(self,
             id,
             email_address,
             display_name,
             login_details,
             delete=None,
             **kwargs):
        """Save changes or create a new :class:`~mediadrop.model.auth.User` instance.

        :param id: User ID. If ``"new"`` a new user is created.
        :type id: ``int`` or ``"new"``
        :returns: Redirect back to :meth:`index` after successful save.

        """
        user = fetch_row(User, id)

        if delete:
            DBSession.delete(user)
            redirect(action='index', id=None)

        user.display_name = display_name
        user.email_address = email_address
        user.user_name = login_details['user_name']

        password = login_details['password']
        if password is not None and password != '':
            user.password = password

        if login_details['groups']:
            query = DBSession.query(Group).filter(
                Group.group_id.in_(login_details['groups']))
            user.groups = list(query.all())
        else:
            user.groups = []

        DBSession.add(user)

        # Check if we're changing the logged in user's own password
        if user.id == request.perm.user.id \
        and password is not None and password != '':
            DBSession.commit()
            # repoze.who sees the Unauthorized response and clears the cookie,
            # forcing a fresh login with the new password
            raise webob.exc.HTTPUnauthorized().exception

        redirect(action='index', id=None)
Example #36
0
    def index(self, page=1, **kw):
        """List podcasts with pagination.

        :param page: Page number, defaults to 1.
        :type page: int
        :rtype: Dict
        :returns:
            podcasts
                The list of :class:`~mediadrop.model.podcasts.Podcast`
                instances for this page.
        """
        user = request.perm.user.display_name
        group = request.perm.user.groups[0].group_name
        podcasts = DBSession.query(Podcast)\
            .options(orm.undefer('media_count'))\
            .order_by(Podcast.title)
        if group != 'admins':
            podcasts = podcasts.filter(Podcast.author_name == user)
        return dict(podcasts=podcasts)
Example #37
0
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.
    """)
Example #38
0
 def _delete_media(self, media):
     # FIXME: Ensure that if the first file is deleted from the file system,
     #        then the second fails, the first file is deleted from the
     #        file system and not linking to a nonexistent file.
     # Delete every file from the storage engine
     for file in media.files:
         file.storage.delete(file.unique_id)
         # Remove this item from the DBSession so that the foreign key
         # ON DELETE CASCADE can take effect.
         DBSession.expunge(file)
     # Delete the media
     DBSession.delete(media)
     DBSession.flush()
     # Cleanup the thumbnails
     delete_thumbs(media)
Example #39
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
Example #40
0
def get_available_slug(mapped_class,
                       string,
                       ignore=None,
                       slug_attr='slug',
                       slug_length=SLUG_LENGTH):
    """Return a unique slug based on the provided string.

    Works by appending an int in sequence starting with 2:

        1. awesome-stuff
        2. awesome-stuff-2
        3. awesome-stuff-3

    :param mapped_class: The ORM-controlled model that the slug is for
    :param string: A title, name, etc
    :type string: unicode
    :param ignore: A record which doesn't count as a collision
    :type ignore: Int ID, ``mapped_class`` instance or None
    :returns: A unique slug
    :rtype: unicode
    """
    if isinstance(ignore, mapped_class):
        ignore = ignore.id
    elif ignore is not None:
        ignore = int(ignore)

    new_slug = slug = slugify(string)
    appendix = 2
    while DBSession.query(mapped_class.id)\
            .filter(getattr(mapped_class, slug_attr) == new_slug)\
            .filter(mapped_class.id != ignore)\
            .first():
        str_appendix = u'-%s' % appendix
        max_substr_len = slug_length - len(str_appendix)
        new_slug = slug[:max_substr_len] + str_appendix
        appendix += 1

    return new_slug
Example #41
0
    def index(self, page=1, **kwargs):
        """List tags with pagination.

        :param page: Page number, defaults to 1.
        :type page: int
        :rtype: Dict
        :returns:
            tags
                The list of :class:`~mediadrop.model.tags.Tag`
                instances for this page.
            tag_form
                The :class:`~mediadrop.forms.admin.settings.tags.TagForm` instance.

        """
        tags = DBSession.query(Tag)\
            .options(orm.undefer('media_count'))\
            .order_by(Tag.name)

        return dict(
            tags = tags,
            tag_form = tag_form,
            tag_row_form = tag_row_form,
        )
Example #42
0
    def save(self, id, delete=False, **kwargs):
        """Save changes or create a tag.

        See :class:`~mediadrop.forms.admin.settings.tags.TagForm` for POST vars.

        :param id: Tag ID
        :rtype: JSON dict
        :returns:
            success
                bool

        """
        if tmpl_context.form_errors:
            if request.is_xhr:
                return dict(success=False, errors=tmpl_context.form_errors)
            else:
                # TODO: Add error reporting for users with JS disabled?
                return redirect(action='edit')

        tag = fetch_row(Tag, id)

        if delete:
            DBSession.delete(tag)
            data = dict(success=True, id=tag.id)
        else:
            tag.name = kwargs['name']
            tag.slug = get_available_slug(Tag, kwargs['slug'], tag)
            DBSession.add(tag)
            DBSession.flush()
            data = dict(
                success = True,
                id = tag.id,
                name = tag.name,
                slug = tag.slug,
                row = unicode(tag_row_form.display(tag=tag)),
            )

        if request.is_xhr:
            return data
        else:
            redirect(action='index', id=None)
Example #43
0
def insert_settings(defaults):
    """Insert the given setting if they don't exist yet.

    XXX: Does not include any support for MultiSetting. This approach
         won't work for that. We'll need to use a migration script.

    :type defaults: list
    :param defaults: Key and value pairs
    :rtype: list
    :returns: Any settings that have just been created.
    """
    inserted = []
    try:
        settings_query = DBSession.query(Setting.key)\
            .filter(Setting.key.in_([key for key, value in defaults]))
        existing_settings = set(x[0] for x in settings_query)
    except ProgrammingError:
        # If we are running paster setup-app on a fresh database with a
        # plugin which tries to use this function every time the
        # Environment.loaded event fires, the settings table will not
        # exist and this exception will be thrown, but its safe to ignore.
        # The settings will be created the next time the event fires,
        # which will likely be the first time the app server starts up.
        return inserted
    for key, value in defaults:
        if key in existing_settings:
            continue
        transaction = DBSession.begin_nested()
        try:
            s = Setting(key, value)
            DBSession.add(s)
            transaction.commit()
            inserted.append(s)
        except IntegrityError:
            transaction.rollback()
    if inserted:
        DBSession.commit()
    return inserted
Example #44
0
    def merge_stubs(self, orig_id, input_id, **kwargs):
        """Merge in a newly created media item.

        This is merges media that has just been created. It must have:
            1. a non-default thumbnail, or
            2. a file, or
            3. a title, description, etc

        :param orig_id: Media ID to copy data to
        :type orig_id: ``int``
        :param input_id: Media ID to source files, thumbs, etc from
        :type input_id: ``int``
        :returns: JSON dict

        """
        orig = fetch_row(Media, orig_id)
        input = fetch_row(Media, input_id)
        merged_files = []

        # Merge in the file(s) from the input stub
        if input.slug.startswith('_stub_') and input.files:
            for file in input.files[:]:
                # XXX: The filename will still use the old ID
                file.media = orig
                merged_files.append(file)
            DBSession.delete(input)

        # The original is a file or thumb stub, copy in the new values
        elif orig.slug.startswith('_stub_') \
        and not input.slug.startswith('_stub_'):
            DBSession.delete(input)
            DBSession.flush()
            orig.podcast = input.podcast
            orig.title = input.title
            orig.subtitle = input.subtitle
            orig.slug = input.slug
            orig.author = input.author
            orig.description = input.description
            orig.notes = input.notes
            orig.duration = input.duration
            orig.views = input.views
            orig.likes = input.likes
            orig.publish_on = input.publish_on
            orig.publish_until = input.publish_until
            orig.categories = input.categories
            orig.tags = input.tags
            orig.update_popularity()

        # Copy the input thumb over the default thumbnail
        elif input.slug.startswith('_stub_') \
        and has_default_thumbs(orig) \
        and not has_default_thumbs(input):
            for key, dst_path in thumb_paths(orig).iteritems():
                src_path = thumb_path(input, key)
                # This will raise an OSError on Windows, but not *nix
                os.rename(src_path, dst_path)
            DBSession.delete(input)

        # Report an error
        else:
            return dict(
                success=False,
                message=u'No merge operation fits.',
            )

        orig.update_status()

        status_form_xhtml = unicode(
            update_status_form.display(action=url_for(action='update_status',
                                                      id=orig.id),
                                       media=orig))

        file_xhtml = {}
        for file in merged_files:
            file_xhtml[file.id] = unicode(
                edit_file_form.display(action=url_for(action='edit_file',
                                                      id=orig.id),
                                       file=file))

        return dict(
            success=True,
            media_id=orig.id,
            title=orig.title,
            link=url_for(action='edit', id=orig.id),
            status_form=status_form_xhtml,
            file_forms=file_xhtml,
        )
Example #45
0
def _autocommit_rollback(req):
    from mediadrop.model.meta import DBSession
    if not DBSession.is_active:
        return
    DBSession.rollback()
    _autocommit_fire_callbacks(req, req.rollback_callbacks)
Example #46
0
    def index(self, type=None, podcast=None, tag=None, category=None, search=None,
              max_age=None, min_age=None, order=None, offset=0, limit=10,
              published_after=None, published_before=None, featured=False,
              id=None, slug=None, include_embed=False, format="json", **kwargs):
        """Query for a list of media.

        :param type:
            Filter by '%s' or '%s'. Defaults to any type.

        :param podcast:
            A podcast slug (or slugs) to filter by. Use 0 to include
            only non-podcast media or 1 to include any podcast media.
            For multiple podcasts, separate the slugs with commas.

        :param tag:
            A tag slug to filter by.

        :param category:
            A category slug to filter by.

        :param search:
            A boolean search query. See
            http://dev.mysql.com/doc/refman/5.0/en/fulltext-boolean.html

        :param published_after:
            If given, only media published *on or after* this date is
            returned. The expected format is 'YYYY-MM-DD HH:MM:SS'
            (ISO 8601) and must include the year at a bare minimum.

        :param published_before:
            If given, only media published *on or before* this date is
            returned. The expected format is 'YYYY-MM-DD HH:MM:SS'
            (ISO 8601) and must include the year at a bare minimum.

        :param max_age:
            If given, only media published within this many days is
            returned. This is a convenience shortcut for publish_after
            and will override its value if both are given.
        :type max_age: int

        :param min_age:
            If given, only media published prior to this number of days
            ago will be returned. This is a convenience shortcut for
            publish_before and will override its value if both are given.
        :type min_age: int

        :param order:
            A column name and 'asc' or 'desc', seperated by a space.
            The column name can be any one of the returned columns.
            Defaults to newest media first (publish_on desc).

        :param offset:
            Where in the complete resultset to start returning results.
            Defaults to 0, the very beginning. This is useful if you've
            already fetched the first 50 results and want to fetch the
            next 50 and so on.
        :type offset: int

        :param limit:
            Number of results to return in each query. Defaults to 10.
            The maximum allowed value defaults to 50 and is set via
            :attr:`request.settings['api_media_max_results']`.
        :type limit: int

        :param featured:
            If nonzero, the results will only include media from the
            configured featured category, if there is one.
        :type featured: bool

        :param include_embed:
            If nonzero, the HTML for the embeddable player is included
            for all results.
        :type include_embed: bool

        :param id:
            Filters the results to include the one item with the given ID.
            Note that we still return a list.
        :type id: int or None

        :param slug:
            Filters the results to include the one item with the given slug.
            Note that we still return a list.
        :type slug: unicode or None

        :param api_key:
            The api access key if required in settings
        :type api_key: unicode or None

        :raises APIException:
            If there is an user error in the query params.

        :rtype: JSON-ready dict
        :returns: The returned dict has the following fields:

            count (int)
                The total number of results that match this query.
            media (list of dicts)
                A list of **media_info** dicts, as generated by the
                :meth:`_info <mediadrop.controllers.api.media.MediaController._info>`
                method. The number of dicts in this list will be the lesser
                of the number of matched items and the requested limit.
                **Note**: unless the 'include_embed' option is specified,
                The returned **media_info** dicts will not include the
                'embed' entry.

        """

        if format not in ("json", "mrss"):
            return dict(error= INVALIDFORMATERROR % format)

        query = Media.query\
            .published()\
            .options(orm.undefer('comment_count_published'))

        # Basic filters
        if id:
            query = query.filter_by(id=id)
        if slug:
            query = query.filter_by(slug=slug)

        if type:
            query = query.filter_by(type=type)

        if podcast:
            podcast_query = DBSession.query(Podcast.id)\
                .filter(Podcast.slug.in_(podcast.split(',')))
            query = query.filter(Media.podcast_id.in_(podcast_query))

        if tag:
            tag = fetch_row(Tag, slug=tag)
            query = query.filter(Media.tags.contains(tag))

        if category:
            category = fetch_row(Category, slug=category)
            query = query.filter(Media.categories.contains(category))

        if max_age:
            published_after = datetime.now() - timedelta(days=int(max_age))
        if min_age:
            published_before = datetime.now() - timedelta(days=int(min_age))

        # FIXME: Parse the date and catch formatting problems before it
        #        it hits the database. Right now support for partial
        #        dates like '2010-02' is thanks to leniancy in MySQL.
        #        Hopefully this leniancy is common to Postgres etc.
        if published_after:
            query = query.filter(Media.publish_on >= published_after)
        if published_before:
            query = query.filter(Media.publish_on <= published_before)

        query = query.order_by(get_order_by(order, order_columns))

        # Search will supercede the ordering above
        if search:
            query = query.search(search)

        if featured:
            featured_cat = get_featured_category()
            if featured_cat:
                query = query.in_category(featured_cat)

        # Preload podcast slugs so we don't do n+1 queries
        podcast_slugs = dict(DBSession.query(Podcast.id, Podcast.slug))

        # Rudimentary pagination support
        start = int(offset)
        end = start + min(int(limit), int(request.settings['api_media_max_results']))

        if format == "mrss":
            request.override_template = "sitemaps/mrss.xml"
            return dict(
                media = query[start:end],
                title = "Media Feed",
            )

        media = [self._info(m, podcast_slugs, include_embed) for m in query[start:end]]

        return dict(
            media = media,
            count = query.count(),
        )
Example #47
0
    def _info(self, media, podcast_slugs=None, include_embed=False):
        """
        Return a **media_info** dict--a JSON-ready dict for describing a media instance.

        :rtype: JSON-ready dict
        :returns: The returned dict has the following fields:

            author (unicode)
                The name of the
                :attr:`author <mediadrop.model.media.Media.author>` of the
                media instance.
            categories (dict of unicode)
                A JSON-ready dict representing the categories the media
                instance is in. Keys are the unique
                :attr:`slugs <mediadrop.model.podcasts.Podcast.slug>`
                for each category, values are the human-readable
                :attr:`title <mediadrop.model.podcasts.podcast.Title>`
                of that category.
            id (int)
                The numeric unique :attr:`id <mediadrop.model.media.Media.id>` of
                the media instance.
            slug (unicode)
                The more human readable unique identifier
                (:attr:`slug <mediadrop.model.media.Media.slug>`)
                of the media instance.
            url (unicode)
                A permalink (HTTP) to the MediaDrop view page for the media instance.
            embed (unicode)
                HTML code that can be used to embed the video in another site.
            title (unicode)
                The :attr:`title <mediadrop.model.media.Media.title>` of
                the media instance.
            type (string, one of ['%s', '%s'])
                The :attr:`type <mediadrop.model.media.Media.type>` of
                the media instance
            podcast (unicode or None)
                The :attr:`slug <mediadrop.model.podcasts.Podcast.slug>` of the
                :class:`podcast <mediadrop.model.podcasts.Podcast>` that
                the media instance has been published under, or None
            description (unicode)
                An XHTML
                :attr:`description <mediadrop.model.media.Media.description>`
                of the media instance.
            description_plain (unicode)
                A plain text
                :attr:`description <mediadrop.model.media.Media.description_plain>`
                of the media instance.
            comment_count (int)
                The number of published comments on the media instance.
            publish_on (unicode)
                The date of publishing in "YYYY-MM-DD HH:MM:SS" (ISO 8601) format.
                e.g.  "2010-02-16 15:06:49"
            likes (int)
                The number of :attr:`like votes <mediadrop.model.media.Media.likes>`
                that the media instance has received.
            views (int)
                The number of :attr:`views <mediadrop.model.media.Media.views>`
                that the media instance has received.
            thumbs (dict)
                A dict of dicts containing URLs, width and height of
                different sizes of thumbnails. The default sizes
                are 's', 'm' and 'l'. Using medium for example::

                    medium_url = thumbs['m']['url']
                    medium_width = thumbs['m']['x']
                    medium_height = thumbs['m']['y']
        """
        if media.podcast_id:
            media_url = url_for(controller='/media', action='view', slug=media.slug,
                                podcast_slug=media.podcast.slug, qualified=True)
        else:
            media_url = url_for_media(media, qualified=True)

        if media.podcast_id is None:
            podcast_slug = None
        elif podcast_slugs:
            podcast_slug = podcast_slugs[media.podcast_id]
        else:
            podcast_slug = DBSession.query(Podcast.slug)\
                .filter_by(id=media.podcast_id).scalar()

        thumbs = {}
        for size in config['thumb_sizes'][media._thumb_dir].iterkeys():
            thumbs[size] = thumb(media, size, qualified=True)

        info = dict(
            id = media.id,
            slug = media.slug,
            url = media_url,
            title = media.title,
            author = media.author.name,
            type = media.type,
            podcast = podcast_slug,
            description = media.description,
            description_plain = media.description_plain,
            comment_count = media.comment_count_published,
            publish_on = unicode(media.publish_on),
            likes = media.likes,
            views = media.views,
            thumbs = thumbs,
            categories = dict((c.slug, c.name) for c in list(media.categories)),
        )

        if include_embed:
            info['embed'] = unicode(helpers.embed_player(media))

        return info
Example #48
0
 def _tear_down_db(self):
     metadata.drop_all(bind=DBSession.bind)
     DBSession.close_all()
Example #49
0
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())
Example #50
0
        except IOError, e:
            success = False
            if id == 'new':
                DBSession.delete(media)
            if e.errno == 13:
                message = _('Permission denied, cannot write file')
            elif e.message == 'cannot identify image file':
                message = _('Unsupported image type: %s') \
                    % os.path.splitext(thumb.filename)[1].lstrip('.')
            elif e.message == 'cannot read interlaced PNG files':
                message = _('Interlaced PNGs are not supported.')
            else:
                raise
        except Exception:
            if id == 'new':
                DBSession.delete(media)
            raise

        return dict(
            success=success,
            message=message,
            id=media.id,
            title=media.title,
            slug=media.slug,
            link=url_for(action='edit', id=media.id),
        )

    @expose('json', request_method='POST')
    @validate(update_status_form, error_handler=edit)
    @autocommit
    @observable(events.Admin.MediaController.update_status)
Example #51
0
 def by_email_address(cls, email):
     # TODO: Move this function to User.query
     return DBSession.query(cls).filter(cls.email_address == email).first()
Example #52
0
class MediaFullText(object):
    query = DBSession.query_property()
Example #53
0
class MediaController(BaseController):
    allow_only = has_permission('edit')

    @expose_xhr('admin/media/index.html', 'admin/media/index-table.html')
    @paginate('media', items_per_page=15)
    @observable(events.Admin.MediaController.index)
    def index(self,
              page=1,
              search=None,
              filter=None,
              podcast=None,
              category=None,
              tag=None,
              **kwargs):
        """List media with pagination and filtering.

        :param page: Page number, defaults to 1.
        :type page: int
        :param search: Optional search term to filter by
        :type search: unicode or None
        :param podcast_filter: Optional podcast to filter by
        :type podcast_filter: int or None
        :rtype: dict
        :returns:
            media
                The list of :class:`~mediadrop.model.media.Media` instances
                for this page.
            search
                The given search term, if any
            search_form
                The :class:`~mediadrop.forms.admin.SearchForm` instance
            podcast
                The podcast object for rendering if filtering by podcast.

        """
        media = Media.query.options(orm.undefer('comment_count_published'))

        if search:
            media = media.admin_search(search)
        else:
            media = media.order_by_status()\
                         .order_by(Media.publish_on.desc(),
                                   Media.modified_on.desc())

        if not filter:
            pass
        elif filter == 'unreviewed':
            media = media.reviewed(False)
        elif filter == 'unencoded':
            media = media.reviewed().encoded(False)
        elif filter == 'drafts':
            media = media.drafts()
        elif filter == 'published':
            media = media.published()

        if category:
            category = fetch_row(Category, slug=category)
            media = media.filter(Media.categories.contains(category))
        if tag:
            tag = fetch_row(Tag, slug=tag)
            media = media.filter(Media.tags.contains(tag))
        if podcast:
            podcast = fetch_row(Podcast, slug=podcast)
            media = media.filter(Media.podcast == podcast)

        return dict(
            media=media,
            search=search,
            search_form=search_form,
            media_filter=filter,
            category=category,
            tag=tag,
            podcast=podcast,
        )

    def json_error(self, *args, **kwargs):
        validation_exception = tmpl_context._current_obj().validation_exception
        return dict(success=False, message=validation_exception.msg)

    @expose('admin/media/edit.html')
    @validate(validators={'podcast': validators.Int()})
    @autocommit
    @observable(events.Admin.MediaController.edit)
    def edit(self, id, **kwargs):
        """Display the media forms for editing or adding.

        This page serves as the error_handler for every kind of edit action,
        if anything goes wrong with them they'll be redirected here.

        :param id: Media ID
        :type id: ``int`` or ``"new"``
        :param \*\*kwargs: Extra args populate the form for ``"new"`` media
        :returns:
            media
                :class:`~mediadrop.model.media.Media` instance
            media_form
                The :class:`~mediadrop.forms.admin.media.MediaForm` instance
            media_action
                ``str`` form submit url
            media_values
                ``dict`` form values
            file_add_form
                The :class:`~mediadrop.forms.admin.media.AddFileForm` instance
            file_add_action
                ``str`` form submit url
            file_edit_form
                The :class:`~mediadrop.forms.admin.media.EditFileForm` instance
            file_edit_action
                ``str`` form submit url
            thumb_form
                The :class:`~mediadrop.forms.admin.ThumbForm` instance
            thumb_action
                ``str`` form submit url
            update_status_form
                The :class:`~mediadrop.forms.admin.media.UpdateStatusForm` instance
            update_status_action
                ``str`` form submit url

        """
        media = fetch_row(Media, id)

        if tmpl_context.action == 'save' or id == 'new':
            # Use the values from error_handler or GET for new podcast media
            media_values = kwargs
            user = request.perm.user
            media_values.setdefault('author_name', user.display_name)
            media_values.setdefault('author_email', user.email_address)
        else:
            # Pull the defaults from the media item
            media_values = dict(
                podcast=media.podcast_id,
                slug=media.slug,
                title=media.title,
                author_name=media.author.name,
                author_email=media.author.email,
                description=media.description,
                tags=', '.join((tag.name for tag in media.tags)),
                categories=[category.id for category in media.categories],
                notes=media.notes,
            )

        # Re-verify the state of our Media object in case the data is nonsensical
        if id != 'new':
            media.update_status()

        return dict(
            media=media,
            media_form=media_form,
            media_action=url_for(action='save'),
            media_values=media_values,
            category_tree=Category.query.order_by(
                Category.name).populated_tree(),
            file_add_form=add_file_form,
            file_add_action=url_for(action='add_file'),
            file_edit_form=edit_file_form,
            file_edit_action=url_for(action='edit_file'),
            thumb_form=thumb_form,
            thumb_action=url_for(action='save_thumb'),
            update_status_form=update_status_form,
            update_status_action=url_for(action='update_status'),
        )

    @expose_xhr(request_method='POST')
    @validate_xhr(media_form, error_handler=edit)
    @autocommit
    @observable(events.Admin.MediaController.save)
    def save(self,
             id,
             slug,
             title,
             author_name,
             author_email,
             description,
             notes,
             podcast,
             tags,
             categories,
             delete=None,
             **kwargs):
        """Save changes or create a new :class:`~mediadrop.model.media.Media` instance.

        Form handler the :meth:`edit` action and the
        :class:`~mediadrop.forms.admin.media.MediaForm`.

        Redirects back to :meth:`edit` after successful editing
        and :meth:`index` after successful deletion.

        """
        media = fetch_row(Media, id)

        if delete:
            self._delete_media(media)
            redirect(action='index', id=None)

        if not slug:
            slug = slugify(title)
        elif slug.startswith('_stub_'):
            slug = slug[len('_stub_'):]
        if slug != media.slug:
            media.slug = get_available_slug(Media, slug, media)
        media.title = title
        media.author = Author(author_name, author_email)
        media.description = description
        media.notes = notes
        media.podcast_id = podcast
        media.set_tags(tags)
        media.set_categories(categories)

        media.update_status()
        DBSession.add(media)
        DBSession.flush()

        if id == 'new' and not has_thumbs(media):
            create_default_thumbs_for(media)

        if request.is_xhr:
            status_form_xhtml = unicode(
                update_status_form.display(action=url_for(
                    action='update_status', id=media.id),
                                           media=media))

            return dict(
                media_id=media.id,
                values={'slug': slug},
                link=url_for(action='edit', id=media.id),
                status_form=status_form_xhtml,
            )
        else:
            redirect(action='edit', id=media.id)

    @expose('json', request_method='POST')
    @validate(add_file_form, error_handler=json_error)
    @autocommit
    @observable(events.Admin.MediaController.add_file)
    def add_file(self, id, file=None, url=None, **kwargs):
        """Save action for the :class:`~mediadrop.forms.admin.media.AddFileForm`.

        Creates a new :class:`~mediadrop.model.media.MediaFile` from the
        uploaded file or the local or remote URL.

        :param id: Media ID. If ``"new"`` a new Media stub is created.
        :type id: :class:`int` or ``"new"``
        :param file: The uploaded file
        :type file: :class:`cgi.FieldStorage` or ``None``
        :param url: A URL to a recognizable audio or video file
        :type url: :class:`unicode` or ``None``
        :rtype: JSON dict
        :returns:
            success
                bool
            message
                Error message, if unsuccessful
            media_id
                The :attr:`~mediadrop.model.media.Media.id` which is
                important if new media has just been created.
            file_id
                The :attr:`~mediadrop.model.media.MediaFile.id` for the newly
                created file.
            edit_form
                The rendered XHTML :class:`~mediadrop.forms.admin.media.EditFileForm`
                for this file.
            status_form
                The rendered XHTML :class:`~mediadrop.forms.admin.media.UpdateStatusForm`

        """
        if id == 'new':
            media = Media()
            user = request.perm.user
            media.author = Author(user.display_name, user.email_address)
            # Create a temp stub until we can set it to something meaningful
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            media.title = u'Temporary stub %s' % timestamp
            media.slug = get_available_slug(Media, '_stub_' + timestamp)
            media.reviewed = True
            DBSession.add(media)
            DBSession.flush()
        else:
            media = fetch_row(Media, id)

        try:
            media_file = add_new_media_file(media, file, url)
        except UserStorageError as e:
            return dict(success=False, message=e.message)
        if media.slug.startswith('_stub_'):
            media.title = media_file.display_name
            media.slug = get_available_slug(Media, '_stub_' + media.title)

        # The thumbs may have been created already by add_new_media_file
        if id == 'new' and not has_thumbs(media):
            create_default_thumbs_for(media)

        media.update_status()

        # Render some widgets so the XHTML can be injected into the page
        edit_form_xhtml = unicode(
            edit_file_form.display(action=url_for(action='edit_file',
                                                  id=media.id),
                                   file=media_file))
        status_form_xhtml = unicode(
            update_status_form.display(action=url_for(action='update_status',
                                                      id=media.id),
                                       media=media))

        data = dict(
            success=True,
            media_id=media.id,
            file_id=media_file.id,
            file_type=media_file.type,
            edit_form=edit_form_xhtml,
            status_form=status_form_xhtml,
            title=media.title,
            slug=media.slug,
            description=media.description,
            link=url_for(action='edit', id=media.id),
            duration=helpers.duration_from_seconds(media.duration),
        )

        return data

    @expose('json', request_method='POST')
    @autocommit
    @observable(events.Admin.MediaController.edit_file)
    def edit_file(self,
                  id,
                  file_id,
                  file_type=None,
                  duration=None,
                  delete=None,
                  bitrate=None,
                  width_height=None,
                  **kwargs):
        """Save action for the :class:`~mediadrop.forms.admin.media.EditFileForm`.

        Changes or deletes a :class:`~mediadrop.model.media.MediaFile`.

        XXX: We do NOT use the @validate decorator due to complications with
             partial validation. The JS sends only the value it wishes to
             change, so we only want to validate that one value.
             FancyValidator.if_missing seems to eat empty values and assign
             them None, but there's an important difference to us between
             None (no value from the user) and an empty value (the user
             is clearing the value of a field).

        :param id: Media ID
        :type id: :class:`int`
        :rtype: JSON dict
        :returns:
            success
                bool
            message
                Error message, if unsuccessful
            status_form
                Rendered XHTML for the status form, updated to reflect the
                changes made.

        """
        media = fetch_row(Media, id)
        data = dict(success=False)
        file_id = int(file_id)  # Just in case validation failed somewhere.

        for file in media.files:
            if file.id == file_id:
                break
        else:
            file = None

        fields = edit_file_form.c
        try:
            if file is None:
                data['message'] = _('File "%s" does not exist.') % file_id
            elif file_type:
                file.type = fields.file_type.validate(file_type)
                data['success'] = True
            elif duration is not None:
                media.duration = fields.duration.validate(duration)
                data['success'] = True
                data['duration'] = helpers.duration_from_seconds(
                    media.duration)
            elif width_height is not None:
                width_height = fields.width_height.validate(width_height)
                file.width, file.height = width_height or (0, 0)
                data['success'] = True
            elif bitrate is not None:
                file.bitrate = fields.bitrate.validate(bitrate)
                data['success'] = True
            elif delete:
                file.storage.delete(file.unique_id)
                DBSession.delete(file)
                DBSession.flush()
                # media.files must be updated to reflect the file deletion above
                DBSession.refresh(media)
                data['success'] = True
            else:
                data['message'] = _('No action to perform.')
        except Invalid, e:
            data['success'] = False
            data['message'] = unicode(e)

        if data['success']:
            data['file_type'] = file.type
            media.update_status()
            DBSession.flush()

            # Return the rendered widget for injection
            status_form_xhtml = unicode(
                update_status_form.display(
                    action=url_for(action='update_status'), media=media))
            data['status_form'] = status_form_xhtml
        return data
Example #54
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()
Example #55
0
 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']))
         )
Example #56
0
 def by_user_name(cls, username):
     # TODO: Move this function to User.query
     return DBSession.query(cls).filter(cls.user_name == username).first()
Example #57
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
        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
Example #58
0
    def edit_file(self,
                  id,
                  file_id,
                  file_type=None,
                  duration=None,
                  delete=None,
                  bitrate=None,
                  width_height=None,
                  **kwargs):
        """Save action for the :class:`~mediadrop.forms.admin.media.EditFileForm`.

        Changes or deletes a :class:`~mediadrop.model.media.MediaFile`.

        XXX: We do NOT use the @validate decorator due to complications with
             partial validation. The JS sends only the value it wishes to
             change, so we only want to validate that one value.
             FancyValidator.if_missing seems to eat empty values and assign
             them None, but there's an important difference to us between
             None (no value from the user) and an empty value (the user
             is clearing the value of a field).

        :param id: Media ID
        :type id: :class:`int`
        :rtype: JSON dict
        :returns:
            success
                bool
            message
                Error message, if unsuccessful
            status_form
                Rendered XHTML for the status form, updated to reflect the
                changes made.

        """
        media = fetch_row(Media, id)
        data = dict(success=False)
        file_id = int(file_id)  # Just in case validation failed somewhere.

        for file in media.files:
            if file.id == file_id:
                break
        else:
            file = None

        fields = edit_file_form.c
        try:
            if file is None:
                data['message'] = _('File "%s" does not exist.') % file_id
            elif file_type:
                file.type = fields.file_type.validate(file_type)
                data['success'] = True
            elif duration is not None:
                media.duration = fields.duration.validate(duration)
                data['success'] = True
                data['duration'] = helpers.duration_from_seconds(
                    media.duration)
            elif width_height is not None:
                width_height = fields.width_height.validate(width_height)
                file.width, file.height = width_height or (0, 0)
                data['success'] = True
            elif bitrate is not None:
                file.bitrate = fields.bitrate.validate(bitrate)
                data['success'] = True
            elif delete:
                file.storage.delete(file.unique_id)
                DBSession.delete(file)
                DBSession.flush()
                # media.files must be updated to reflect the file deletion above
                DBSession.refresh(media)
                data['success'] = True
            else:
                data['message'] = _('No action to perform.')
        except Invalid, e:
            data['success'] = False
            data['message'] = unicode(e)
Example #59
0
    def add_file(self, id, file=None, url=None, **kwargs):
        """Save action for the :class:`~mediadrop.forms.admin.media.AddFileForm`.

        Creates a new :class:`~mediadrop.model.media.MediaFile` from the
        uploaded file or the local or remote URL.

        :param id: Media ID. If ``"new"`` a new Media stub is created.
        :type id: :class:`int` or ``"new"``
        :param file: The uploaded file
        :type file: :class:`cgi.FieldStorage` or ``None``
        :param url: A URL to a recognizable audio or video file
        :type url: :class:`unicode` or ``None``
        :rtype: JSON dict
        :returns:
            success
                bool
            message
                Error message, if unsuccessful
            media_id
                The :attr:`~mediadrop.model.media.Media.id` which is
                important if new media has just been created.
            file_id
                The :attr:`~mediadrop.model.media.MediaFile.id` for the newly
                created file.
            edit_form
                The rendered XHTML :class:`~mediadrop.forms.admin.media.EditFileForm`
                for this file.
            status_form
                The rendered XHTML :class:`~mediadrop.forms.admin.media.UpdateStatusForm`

        """
        if id == 'new':
            media = Media()
            user = request.perm.user
            media.author = Author(user.display_name, user.email_address)
            # Create a temp stub until we can set it to something meaningful
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            media.title = u'Temporary stub %s' % timestamp
            media.slug = get_available_slug(Media, '_stub_' + timestamp)
            media.reviewed = True
            DBSession.add(media)
            DBSession.flush()
        else:
            media = fetch_row(Media, id)

        try:
            media_file = add_new_media_file(media, file, url)
        except UserStorageError as e:
            return dict(success=False, message=e.message)
        if media.slug.startswith('_stub_'):
            media.title = media_file.display_name
            media.slug = get_available_slug(Media, '_stub_' + media.title)

        # The thumbs may have been created already by add_new_media_file
        if id == 'new' and not has_thumbs(media):
            create_default_thumbs_for(media)

        media.update_status()

        # Render some widgets so the XHTML can be injected into the page
        edit_form_xhtml = unicode(
            edit_file_form.display(action=url_for(action='edit_file',
                                                  id=media.id),
                                   file=media_file))
        status_form_xhtml = unicode(
            update_status_form.display(action=url_for(action='update_status',
                                                      id=media.id),
                                       media=media))

        data = dict(
            success=True,
            media_id=media.id,
            file_id=media_file.id,
            file_type=media_file.type,
            edit_form=edit_form_xhtml,
            status_form=status_form_xhtml,
            title=media.title,
            slug=media.slug,
            description=media.description,
            link=url_for(action='edit', id=media.id),
            duration=helpers.duration_from_seconds(media.duration),
        )

        return data
Example #60
0
    def save(self,
             id,
             slug,
             title,
             author_name,
             author_email,
             description,
             notes,
             podcast,
             tags,
             categories,
             delete=None,
             **kwargs):
        """Save changes or create a new :class:`~mediadrop.model.media.Media` instance.

        Form handler the :meth:`edit` action and the
        :class:`~mediadrop.forms.admin.media.MediaForm`.

        Redirects back to :meth:`edit` after successful editing
        and :meth:`index` after successful deletion.

        """
        media = fetch_row(Media, id)

        if delete:
            self._delete_media(media)
            redirect(action='index', id=None)

        if not slug:
            slug = slugify(title)
        elif slug.startswith('_stub_'):
            slug = slug[len('_stub_'):]
        if slug != media.slug:
            media.slug = get_available_slug(Media, slug, media)
        media.title = title
        media.author = Author(author_name, author_email)
        media.description = description
        media.notes = notes
        media.podcast_id = podcast
        media.set_tags(tags)
        media.set_categories(categories)

        media.update_status()
        DBSession.add(media)
        DBSession.flush()

        if id == 'new' and not has_thumbs(media):
            create_default_thumbs_for(media)

        if request.is_xhr:
            status_form_xhtml = unicode(
                update_status_form.display(action=url_for(
                    action='update_status', id=media.id),
                                           media=media))

            return dict(
                media_id=media.id,
                values={'slug': slug},
                link=url_for(action='edit', id=media.id),
                status_form=status_form_xhtml,
            )
        else:
            redirect(action='edit', id=media.id)