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)
示例#2
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')
示例#3
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)
示例#4
0
    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)
示例#5
0
文件: auth.py 项目: z0x010/mediadrop
 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
示例#6
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
示例#7
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
示例#8
0
文件: tags.py 项目: z0x010/mediadrop
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
示例#9
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
示例#10
0
文件: media.py 项目: knyar/mediadrop
    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')
示例#11
0
文件: auth.py 项目: z0x010/mediadrop
 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
示例#12
0
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
示例#13
0
文件: media.py 项目: knyar/mediadrop
    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)
示例#14
0
    def save(self,
             id,
             slug,
             title,
             subtitle,
             author_name,
             author_email,
             description,
             details,
             feed,
             delete=None,
             **kwargs):
        """Save changes or create a new :class:`~mediadrop.model.podcasts.Podcast` instance.

        Form handler the :meth:`edit` action and the
        :class:`~mediadrop.forms.admin.podcasts.PodcastForm`.

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

        """
        podcast = fetch_row(Podcast, id)

        if delete:
            DBSession.delete(podcast)
            DBSession.commit()
            delete_thumbs(podcast)
            redirect(action='index', id=None)

        if not slug:
            slug = title
        if slug != podcast.slug:
            podcast.slug = get_available_slug(Podcast, slug, podcast)
        podcast.title = title
        podcast.subtitle = subtitle
        podcast.author = Author(author_name, author_email)
        podcast.description = description
        podcast.copyright = details['copyright']
        podcast.category = details['category']
        podcast.itunes_url = feed['itunes_url']
        podcast.feedburner_url = feed['feedburner_url']
        podcast.explicit = {
            'yes': True,
            'clean': False
        }.get(details['explicit'], None)

        if id == 'new':
            DBSession.add(podcast)
            DBSession.flush()
            create_default_thumbs_for(podcast)

        redirect(action='edit', id=podcast.id)
示例#15
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)
示例#16
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.

        """
        group = request.perm.user.groups[0].group_name
        user = request.perm.user.display_name
        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 statu
            raise AssertionError('Unexpected status: %r' % status)

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

        for comment in comments:
            auth = Media.query.filter(Media.id == comment.media_id)
            for each in auth:
                if group == 'admins' or each.author_name == user:
                    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')
示例#17
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
示例#18
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
示例#19
0
文件: media.py 项目: knyar/mediadrop
 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)
示例#20
0
文件: auth.py 项目: z0x010/mediadrop
    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
示例#21
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
示例#22
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)
示例#23
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
示例#24
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
示例#25
0
文件: media.py 项目: knyar/mediadrop
    def save_thumb(self, id, thumb, **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.

        """
        if id == 'new':
            media = Media()
            user = request.perm.user
            media.author = Author(user.display_name, user.email_address)
            media.title = os.path.basename(thumb.filename)
            media.slug = get_available_slug(Media, '_stub_' + media.title)
            DBSession.add(media)
            DBSession.flush()
        else:
            media = fetch_row(Media, id)

        try:
            # Create JPEG thumbs
            create_thumbs_for(media, thumb.file, thumb.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(thumb.filename)[1].lstrip('.')
            elif e.message == 'cannot read interlaced PNG files':
                message = _('Interlaced PNGs are not supported.')
            else:
                raise
示例#26
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')
示例#27
0
    def save(self,
             name,
             email,
             title,
             description,
             tags,
             uploaded_file,
             url,
             publish=False):
        # Update media object metadata
        self.author = Author(name, email)
        self.title = title
        self.slug = get_available_slug(Media, title)
        self.description = description
        self.set_tags(tags)

        # Give the Media object an ID.
        DBSession.add(self)
        DBSession.flush()

        # Create a MediaFile object, add it to the media_obj, and store the file permanently.
        media_file = add_new_media_file(self, file=uploaded_file, url=url)

        # The thumbs may have been created already by add_new_media_file
        if not has_thumbs(self):
            create_default_thumbs_for(self)

        self.update_status()

        ## Save to database
        DBSession.add(self)
        DBSession.commit()

        ## Optional publish
        if publish:
            self.reviewed = True
            self.publishable = True
            self.publish_on = datetime.now()
            self.update_popularity()
            self.update_status()
            DBSession.add(self)
            DBSession.commit()

        return self
示例#28
0
文件: tags.py 项目: z0x010/mediadrop
    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)
示例#29
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)
示例#30
0
    def save(
        self, id, slug, title, subtitle, author_name, author_email, description, details, feed, delete=None, **kwargs
    ):
        """Save changes or create a new :class:`~mediadrop.model.podcasts.Podcast` instance.

        Form handler the :meth:`edit` action and the
        :class:`~mediadrop.forms.admin.podcasts.PodcastForm`.

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

        """
        podcast = fetch_row(Podcast, id)

        if delete:
            DBSession.delete(podcast)
            DBSession.commit()
            delete_thumbs(podcast)
            redirect(action="index", id=None)

        if not slug:
            slug = title
        if slug != podcast.slug:
            podcast.slug = get_available_slug(Podcast, slug, podcast)
        podcast.title = title
        podcast.subtitle = subtitle
        podcast.author = Author(author_name, author_email)
        podcast.description = description
        podcast.copyright = details["copyright"]
        podcast.category = details["category"]
        podcast.itunes_url = feed["itunes_url"]
        podcast.feedburner_url = feed["feedburner_url"]
        podcast.explicit = {"yes": True, "clean": False}.get(details["explicit"], None)

        if id == "new":
            DBSession.add(podcast)
            DBSession.flush()
            create_default_thumbs_for(podcast)

        redirect(action="edit", id=podcast.id)
示例#31
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)
示例#32
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)
示例#33
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
示例#34
0
文件: media.py 项目: knyar/mediadrop
    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,
        )
示例#35
0
文件: media.py 项目: knyar/mediadrop
    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)
示例#36
0
文件: media.py 项目: knyar/mediadrop
    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
示例#37
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,
        )
示例#38
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