Ejemplo n.º 1
0
def send_comment_notification(media_obj, comment):
    """
    Helper method to send a email notification that a comment has been posted.

    Sends to the address configured in the 'email_comment_posted' setting,
    if it is configured.

    :param media_obj: The media object to send a notification about.
    :type media_obj: :class:`~mediacore.model.media.Media` instance

    :param comment: The newly posted comment.
    :type comment: :class:`~mediacore.model.comments.Comment` instance
    """
    send_to = request.settings['email_comment_posted']
    if not send_to:
        # Comment notification emails are disabled!
        return

    author_name = media_obj.author.name
    comment_subject = comment.subject
    post_url = url_for_media(media_obj, qualified=True)
    comment_body = strip_xhtml(line_break_xhtml(line_break_xhtml(comment.body)))
    subject = _('New Comment: %(comment_subject)s') % locals()
    body = _("""A new comment has been posted!

Author: %(author_name)s
Post: %(post_url)s

Body: %(comment_body)s
""") % locals()

    send(send_to, request.settings['email_send_from'], subject, body)
Ejemplo n.º 2
0
    def _parse(self, url, **kwargs):
        """Return metadata for the given URL that matches :attr:`url_pattern`.

        :type url: unicode
        :param url: A remote URL string.

        :param \*\*kwargs: The named matches from the url match object.

        :rtype: dict
        :returns: Any extracted metadata.

        """
        id = kwargs['id']

        yt_service = gdata.youtube.service.YouTubeService()
        yt_service.ssl = False

        try:
            entry = yt_service.GetYouTubeVideoEntry(video_id=id)
        except gdata.service.RequestError, e:
            if e['status'] == 403 and e['body'] == 'Private video':
                raise UserStorageError(
                    _('This video is private and cannot be embedded.'))
            elif e['status'] == 400 and e['body'] == 'Invalid id':
                raise UserStorageError(
                    _('Invalid YouTube URL. This video does not exist.'))
Ejemplo n.º 3
0
    def comment(self, slug, name="", email=None, body="", **kwargs):
        """Post a comment from :class:`~mediacore.forms.comments.PostCommentForm`.

        :param slug: The media :attr:`~mediacore.model.media.Media.slug`
        :returns: Redirect to :meth:`view` page for media.

        """

        def result(success, message=None, comment=None):
            if request.is_xhr:
                result = dict(success=success, message=message)
                if comment:
                    result["comment"] = render("comments/_list.html", {"comment_to_render": comment}, method="xhtml")
                return result
            elif success:
                return redirect(action="view")
            else:
                return self.view(slug, name=name, email=email, body=body, **kwargs)

        akismet_key = request.settings["akismet_key"]
        if akismet_key:
            akismet = Akismet(agent=USER_AGENT)
            akismet.key = akismet_key
            akismet.blog_url = request.settings["akismet_url"] or url_for("/", qualified=True)
            akismet.verify_key()
            data = {
                "comment_author": name.encode("utf-8"),
                "user_ip": request.environ.get("REMOTE_ADDR"),
                "user_agent": request.environ.get("HTTP_USER_AGENT", ""),
                "referrer": request.environ.get("HTTP_REFERER", "unknown"),
                "HTTP_ACCEPT": request.environ.get("HTTP_ACCEPT"),
            }

            if akismet.comment_check(body.encode("utf-8"), data):
                return result(False, _(u"Your comment has been rejected."))

        media = fetch_row(Media, slug=slug)
        request.perm.assert_permission(u"view", media.resource)

        c = Comment()

        name = filter_vulgarity(name)
        c.author = AuthorWithIP(name, email, request.environ["REMOTE_ADDR"])
        c.subject = "Re: %s" % media.title
        c.body = filter_vulgarity(body)

        require_review = request.settings["req_comment_approval"]
        if not require_review:
            c.reviewed = True
            c.publishable = True

        media.comments.append(c)
        DBSession.flush()
        send_comment_notification(media, c)

        if require_review:
            message = _("Thank you for your comment! We will post it just as " "soon as a moderator approves it.")
            return result(True, message=message)
        else:
            return result(True, comment=c)
Ejemplo n.º 4
0
def boolean_radiobuttonlist(name, **kwargs):
    return RadioButtonList(
        name,
        options=lambda: (('true', _('Yes')), ('false', _('No'))),
        validator=OneOf(['true', 'false']),
        **kwargs
    )
Ejemplo n.º 5
0
class HTML5OrFlashPrefsForm(PlayerPrefsForm):
    fields = [
        RadioButtonList(
            'prefer_flash',
            options=lambda: (
                (False,
                 _('Yes, use the Flash Player when the device supports it.')),
                (True,
                 _('No, use the HTML5 Player when the device supports it.')),
            ),
            css_classes=['options'],
            label_text=N_('Prefer the Flash Player when possible'),
            validator=StringBool,
        ),
    ] + PlayerPrefsForm.buttons

    event = events.Admin.Players.HTML5OrFlashPrefsForm

    def display(self, value, player, **kwargs):
        value.setdefault('prefer_flash',
                         player.data.get('prefer_flash', False))
        return PlayerPrefsForm.display(self, value, player, **kwargs)

    def save_data(self, player, prefer_flash, **kwargs):
        player.data['prefer_flash'] = prefer_flash
Ejemplo n.º 6
0
def boolean_radiobuttonlist(name, **kwargs):
    return RadioButtonList(
        name,
        options=lambda: ((True, _('Yes')), (False, _('No'))),
        validator=StringBool,
        **kwargs
    )
Ejemplo n.º 7
0
def real_boolean_radiobuttonlist(name, **kwargs):
    # TODO: replace uses of boolean_radiobuttonlist with this, then scrap the old one.
    return RadioButtonList(name,
                           options=lambda: ((True, _('Yes')),
                                            (False, _('No'))),
                           validator=StringBool,
                           **kwargs)
Ejemplo n.º 8
0
def boolean_radiobuttonlist(name, **kwargs):
    return RadioButtonList(
        name,
        options=lambda: (('true', _('Yes')), ('false', _('No'))),
        validator=OneOf(['true', 'false']),
        **kwargs
    )
def boolean_radiobuttonlist(name, **kwargs):
    return RadioButtonList(
        name,
        options=lambda: ((True, _('Yes')), (False, _('No'))),
        validator=StringBool,
        **kwargs
    )
Ejemplo n.º 10
0
    def _parse(self, url, **kwargs):
        """Return metadata for the given URL that matches :attr:`url_pattern`.

        :type url: unicode
        :param url: A remote URL string.

        :param \*\*kwargs: The named matches from the url match object.

        :rtype: dict
        :returns: Any extracted metadata.

        """
        id = kwargs['id']

        yt_service = gdata.youtube.service.YouTubeService()
        yt_service.ssl = False

        try:
            entry = yt_service.GetYouTubeVideoEntry(video_id=id)
        except gdata.service.RequestError, request_error:
            e = request_error.args[0]
            if e['status'] == 403 and e['body'] == 'Private video':
                raise UserStorageError(
                    _('This video is private and cannot be embedded.'))
            elif e['status'] == 400 and e['body'] == 'Invalid id':
                raise UserStorageError(
                    _('Invalid YouTube URL. This video does not exist.'))
            raise UserStorageError(_('YouTube Error: %s') % e['body'])
Ejemplo n.º 11
0
def real_boolean_radiobuttonlist(name, **kwargs):
    # TODO: replace uses of boolean_radiobuttonlist with this, then scrap the old one.
    return RadioButtonList(
        name,
        options=lambda: ((True, _('Yes')), (False, _('No'))),
        validator=StringBool,
        **kwargs
    )
Ejemplo n.º 12
0
def register_default_types():
    default_types = [
        (VIDEO, _('Video')),
        (AUDIO, _('Audio')),
        (AUDIO_DESC, _('Audio Description')),
        (CAPTIONS, _('Captions')),
    ]
    for t in default_types:
        yield t
Ejemplo n.º 13
0
def register_default_types():
    default_types = [
        (VIDEO, _('Video')),
        (AUDIO, _('Audio')),
        (AUDIO_DESC, _('Audio Description')),
        (CAPTIONS, _('Captions')),
    ]
    for t in default_types:
        yield t
Ejemplo n.º 14
0
    def index(self, page=1, show='latest', q=None, tag=None, **kwargs):
        """List media with pagination.

        The media paginator may be accessed in the template with
        :attr:`c.paginators.media`, see :class:`webhelpers.paginate.Page`.

        :param page: Page number, defaults to 1.
        :type page: int
        :param show: 'latest', 'popular' or 'featured'
        :type show: unicode or None
        :param q: A search query to filter by
        :type q: unicode or None
        :param tag: A tag slug to filter for
        :type tag: unicode or None
        :rtype: dict
        :returns:
            media
                The list of :class:`~mediacore.model.media.Media` instances
                for this page.
            result_count
                The total number of media items for this query
            search_query
                The query the user searched for, if any

        """
        media = Media.query.published()

        media, show = helpers.filter_library_controls(media, show)

        if q:
            media = media.search(q, bool=True)

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

        if (request.settings['rss_display'] == 'True') and (not (q or tag)):
            if show == 'latest':
                response.feed_links.extend([
                    (url_for(controller='/sitemaps',
                             action='latest'), _(u'Latest RSS')),
                ])
            elif show == 'featured':
                response.feed_links.extend([
                    (url_for(controller='/sitemaps',
                             action='featured'), _(u'Featured RSS')),
                ])

        media = viewable_media(media)
        return dict(
            media=media,
            result_count=media.count(),
            search_query=q,
            show=show,
            tag=tag,
        )
Ejemplo n.º 15
0
    def index(self, page=1, show='latest', q=None, tag=None, **kwargs):
        """List media with pagination.

        The media paginator may be accessed in the template with
        :attr:`c.paginators.media`, see :class:`webhelpers.paginate.Page`.

        :param page: Page number, defaults to 1.
        :type page: int
        :param show: 'latest', 'popular' or 'featured'
        :type show: unicode or None
        :param q: A search query to filter by
        :type q: unicode or None
        :param tag: A tag slug to filter for
        :type tag: unicode or None
        :rtype: dict
        :returns:
            media
                The list of :class:`~mediacore.model.media.Media` instances
                for this page.
            result_count
                The total number of media items for this query
            search_query
                The query the user searched for, if any

        """
        media = Media.query.published()

        media, show = helpers.filter_library_controls(media, show)

        if q:
            media = media.search(q, bool=True)

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

        if (request.settings['rss_display'] == 'True') and (not (q or tag)):
            if show == 'latest':
                response.feed_links.extend([
                    (url_for(controller='/sitemaps', action='latest'), _(u'Latest RSS')),
                ])
            elif show == 'featured':
                response.feed_links.extend([
                    (url_for(controller='/sitemaps', action='featured'), _(u'Featured RSS')),
                ])

        media = viewable_media(media)
        return dict(
            media = media,
            result_count = media.count(),
            search_query = q,
            show = show,
            tag = tag,
        )
Ejemplo n.º 16
0
class AddFileForm(ListForm):
    template = 'admin/media/file-add-form.html'
    id = 'add-file-form'
    submit_text = None
    fields = [
        FileField(
            'file',
            label_text=N_(
                'Select an encoded video or audio file on your computer'),
            validator=FieldStorageUploadConverter(not_empty=False,
                                                  label_text=N_('Upload'))),
        SubmitButton('add_url',
                     default=N_('Add URL'),
                     named_button=True,
                     css_class='btn grey btn-add-url f-rgt'),
        TextField(
            'url',
            validator=URL,
            suppress_label=True,
            attrs=lambda: {
                'title':
                _('YouTube, Vimeo, Google Video, Amazon S3 or any other link')
            },
            maxlength=255),
    ]

    def post_init(self, *args, **kwargs):
        events.Admin.AddFileForm(self)
Ejemplo n.º 17
0
class WXHValidator(FancyValidator):
    """
    width by height validator.
    example input 1: "800x600"
    example output 2: (800, 600)

    example input 2: ""
    example output 2: (None, None)

    example input 3: "0x0"
    example output 3:" (None, None)
    """
    def _to_python(self, value, state=None):
        if not value.strip():
            return (None, None)

        try:
            width, height = value.split('x')
        except ValueError, e:
            raise Invalid(_('Value must be in the format wxh; e.g. 200x300'),
                          value, state)
        errors = []
        try:
            width = int(width)
        except ValueError, e:
            errors.append(_('Width must be a valid integer'))
Ejemplo n.º 18
0
    def _parse(self, url, **kwargs):
        """Return metadata for the given URL that matches :attr:`url_pattern`.

        :type url: unicode
        :param url: A remote URL string.

        :param \*\*kwargs: The named matches from the url match object.

        :rtype: dict
        :returns: Any extracted metadata.

        """
        id = kwargs['id']
        # Ensure the video uses the .com TLD for the API request.
        url = 'http://www.dailymotion.com/video/%s' % id
        data_url = 'http://www.dailymotion.com/services/oembed?' + \
            urlencode({'format': 'json', 'url': url})

        headers = {'User-Agent': USER_AGENT}
        req = Request(data_url, headers=headers)

        try:
            temp_data = urlopen(req)
            try:
                data_string = temp_data.read()
                if data_string == 'This video cannot be embeded.':
                    raise UserStorageError(
                        _('This DailyMotion video does not allow embedding.'))
                data = simplejson.loads(data_string)
            finally:
                temp_data.close()
        except URLError, e:
            log.exception(e)
            data = {}
Ejemplo n.º 19
0
    def _parse(self, url, **kwargs):
        """Return metadata for the given URL that matches :attr:`url_pattern`.

        :type url: unicode
        :param url: A remote URL string.

        :param \*\*kwargs: The named matches from the url match object.

        :rtype: dict
        :returns: Any extracted metadata.

        """
        id = kwargs['id']
        # Ensure the video uses the .com TLD for the API request.
        url = 'http://www.dailymotion.com/video/%s' % id
        data_url = 'http://www.dailymotion.com/services/oembed?' + \
            urlencode({'format': 'json', 'url': url})

        headers = {'User-Agent': USER_AGENT}
        req = Request(data_url, headers=headers)

        try:
            temp_data = urlopen(req)
            try:
                data_string = temp_data.read()
                if data_string == 'This video cannot be embeded.':
                    raise UserStorageError(
                        _('This DailyMotion video does not allow embedding.'))
                data = simplejson.loads(data_string)
            finally:
                temp_data.close()
        except URLError, e:
            log.exception(e)
            data = {}
Ejemplo n.º 20
0
    def _parse(self, url, id, **kwargs):
        """Return metadata for the given URL that matches :attr:`url_pattern`.

        :type url: unicode
        :param url: A remote URL string.

        :param \*\*kwargs: The named matches from the url match object.

        :rtype: dict
        :returns: Any extracted metadata.

        """
        if '?' in url:
            url += '&skin=api'
        else:
            url += '?skin=api'

        req = Request(url)

        try:
            temp_data = urlopen(req)
            xmlstring = temp_data.read()
            try:
                try:
                    xmltree = ElementTree.fromstring(xmlstring)
                except:
                    temp_data.close()
                    raise
            except SyntaxError:
                raise UserStorageError(
                    _('Invalid BlipTV URL. This video does not exist.'))
        except URLError, e:
            log.exception(e)
            raise
Ejemplo n.º 21
0
def best_translation(a, b):
    """Return the best translation given a preferred and a fallback string.

    If we have a translation for our preferred string 'a' or if we are using
    English, return 'a'. Otherwise, return a translation for the fallback string 'b'.

    :param a: The preferred string to translate.
    :param b: The fallback string to translate.
    :returns: The best translation
    :rtype: string
    """
    translated_a = _(a)
    if a != translated_a or translator.locale.language == 'en':
        return translated_a
    else:
        return _(b)
Ejemplo n.º 22
0
def best_translation(a, b):
    """Return the best translation given a preferred and a fallback string.

    If we have a translation for our preferred string 'a' or if we are using
    English, return 'a'. Otherwise, return a translation for the fallback string 'b'.

    :param a: The preferred string to translate.
    :param b: The fallback string to translate.
    :returns: The best translation
    :rtype: string
    """
    translated_a = _(a)
    if a != translated_a or translator.locale.language == 'en':
        return translated_a
    else:
        return _(b)
Ejemplo n.º 23
0
    def login(self, came_from=None, **kwargs):
        if request.environ.get('repoze.who.identity'):
            redirect(came_from or '/')

        # the friendlyform plugin requires that these values are set in the
        # query string
        form_url = url_for('/login/submit',
                           came_from=(came_from or '').encode('utf-8'),
                           __logins=str(self._is_failed_login()))

        login_errors = None
        if self._is_failed_login():
            login_errors = Invalid('dummy',
                                   None, {},
                                   error_dict={
                                       '_form':
                                       Invalid(
                                           _('Invalid username or password.'),
                                           None, {}),
                                       'login':
                                       Invalid('dummy', None, {}),
                                       'password':
                                       Invalid('dummy', None, {}),
                                   })
        return dict(
            login_form=login_form,
            form_action=form_url,
            form_values=kwargs,
            login_errors=login_errors,
        )
    def _parse(self, url, id, **kwargs):
        """Return metadata for the given URL that matches :attr:`url_pattern`.

        :type url: unicode
        :param url: A remote URL string.

        :param \*\*kwargs: The named matches from the url match object.

        :rtype: dict
        :returns: Any extracted metadata.

        """
        if '?' in url:
            url += '&skin=api'
        else:
            url += '?skin=api'

        req = Request(url)

        try:
            temp_data = urlopen(req)
            xmlstring = temp_data.read()
            try:
                try:
                    xmltree = ElementTree.fromstring(xmlstring)
                except:
                    temp_data.close()
                    raise
            except SyntaxError:
                raise UserStorageError(
                    _('Invalid BlipTV URL. This video does not exist.'))
        except URLError, e:
            log.exception(e)
            raise
Ejemplo n.º 25
0
def send_support_request(email, url, description, get_vars, post_vars):
    """
    Helper method to send a Support Request email in response to a server
    error.

    Sends to the address configured in the 'email_support_requests' setting,
    if it is configured.

    :param email: The requesting user's email address.
    :type email: unicode

    :param url: The url that the user requested assistance with.
    :type url: unicode

    :param description: The user's description of their problem.
    :type description: unicode

    :param get_vars: The GET variables sent with the failed request.
    :type get_vars: dict of str -> str

    :param post_vars: The POST variables sent with the failed request.
    :type post_vars: dict of str -> str
    """
    send_to = request.settings['email_support_requests']
    if not send_to:
        return

    get_vars = "\n\n  ".join(x + " :  " + get_vars[x] for x in get_vars)
    post_vars = "\n\n  ".join([x + " :  " + post_vars[x] for x in post_vars])
    subject = _('New Support Request: %(email)s') % locals()
    body = _("""A user has asked for support

Email: %(email)s

URL: %(url)s

Description: %(description)s

GET_VARS:
%(get_vars)s


POST_VARS:
%(post_vars)s
""") % locals()

    send(send_to, request.settings['email_send_from'], subject, body)
Ejemplo n.º 26
0
    def save_thumb(self, id, thumb, **kwargs):
        """Save a thumbnail uploaded with :class:`~mediacore.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:`~mediacore.model.media.Media.id` which is
                important if a new media has just been created.

        """
        if id == 'new':
            media = Media()
            user = request.environ['repoze.who.identity']['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 = _('Unsupport 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
Ejemplo n.º 27
0
    def save_thumb(self, id, thumb, **kwargs):
        """Save a thumbnail uploaded with :class:`~mediacore.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:`~mediacore.model.media.Media.id` which is
                important if a new media has just been created.

        """
        if id == 'new':
            media = Media()
            user = req.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
Ejemplo n.º 28
0
    def _to_python(self, value, state=None):
        if not value.strip():
            return (None, None)

        try:
            width, height = value.split("x")
        except ValueError, e:
            raise Invalid(_("Value must be in the format wxh; e.g. 200x300"), value, state)
Ejemplo n.º 29
0
 def _to_python(self, value, state=None):
     try:
         return helpers.duration_to_seconds(value)
     except ValueError:
         msg = _('Bad duration formatting, use Hour:Min:Sec')
         # Colons have special meaning in error messages
         msg.replace(':', ':')
         raise formencode.Invalid(msg, value, state)
Ejemplo n.º 30
0
 def _to_python(self, value, state=None):
     try:
         return helpers.duration_to_seconds(value)
     except ValueError:
         msg = _('Bad duration formatting, use Hour:Min:Sec')
         # Colons have special meaning in error messages
         msg.replace(':', ':')
         raise Invalid(msg, value, state)
    def parse(self, file=None, url=None):
        """Return metadata for the given file or raise an error.

        :type file: :class:`cgi.FieldStorage` or None
        :param file: A freshly uploaded file object.
        :type url: unicode or None
        :param url: A remote URL string.
        :rtype: dict
        :returns: Any extracted metadata.
        :raises UnsuitableEngineError: If file information cannot be parsed.

        """
        if url is None:
            raise UnsuitableEngineError

        if url.startswith('rtmp://'):
            known_server_uris = self._data.setdefault(RTMP_SERVER_URIS, ())

            if RTMP_URI_DIVIDER in url:
                # Allow the user to explicitly mark the server/file separation
                parts = url.split(RTMP_URI_DIVIDER)
                server_uri = parts[0].rstrip('/')
                file_uri = ''.join(parts[1:]).lstrip('/')
                if server_uri not in known_server_uris:
                    known_server_uris.append(server_uri)
            else:
                # Get the rtmp server from our list of known servers or fail
                for server_uri in known_server_uris:
                    if url.startswith(server_uri):
                        file_uri = url[len(server_uri.rstrip('/') + '/'):]
                        break
                else:
                    raise UserStorageError(
                        _('This RTMP server has not been configured. Add it '
                          'by going to Settings > Storage Engines > '
                          'Remote URLs.'))
            unique_id = ''.join((server_uri, RTMP_URI_DIVIDER, file_uri))
        else:
            unique_id = url

        filename = os.path.basename(url)
        name, ext = os.path.splitext(filename)
        ext = unicode(ext).lstrip('.').lower()

        container = guess_container_format(ext)

        # FIXME: Replace guess_container_format with something that takes
        #        into consideration the supported formats of all the custom
        #        players that may be installed.
#        if not container or container == 'unknown':
#            raise UnsuitableEngineError

        return {
            'type': guess_media_type(ext),
            'container': container,
            'display_name': u'%s.%s' % (name, container or ext),
            'unique_id': unique_id,
        }
Ejemplo n.º 32
0
    def parse(self, file=None, url=None):
        """Return metadata for the given file or raise an error.

        :type file: :class:`cgi.FieldStorage` or None
        :param file: A freshly uploaded file object.
        :type url: unicode or None
        :param url: A remote URL string.
        :rtype: dict
        :returns: Any extracted metadata.
        :raises UnsuitableEngineError: If file information cannot be parsed.

        """
        if url is None:
            raise UnsuitableEngineError

        if url.startswith('rtmp://'):
            known_server_uris = self._data.setdefault(RTMP_SERVER_URIS, ())

            if RTMP_URI_DIVIDER in url:
                # Allow the user to explicitly mark the server/file separation
                parts = url.split(RTMP_URI_DIVIDER)
                server_uri = parts[0].rstrip('/')
                file_uri = ''.join(parts[1:]).lstrip('/')
                if server_uri not in known_server_uris:
                    known_server_uris.append(server_uri)
            else:
                # Get the rtmp server from our list of known servers or fail
                for server_uri in known_server_uris:
                    if url.startswith(server_uri):
                        file_uri = url[len(server_uri.rstrip('/') + '/'):]
                        break
                else:
                    raise UserStorageError(
                        _('This RTMP server has not been configured. Add it '
                          'by going to Settings > Storage Engines > '
                          'Remote URLs.'))
            unique_id = ''.join((server_uri, RTMP_URI_DIVIDER, file_uri))
        else:
            unique_id = url

        filename = os.path.basename(url)
        name, ext = os.path.splitext(filename)
        ext = ext.lstrip('.').lower()

        container = guess_container_format(ext)

        # FIXME: Replace guess_container_format with something that takes
        #        into consideration the supported formats of all the custom
        #        players that may be installed.
        #        if not container or container == 'unknown':
        #            raise UnsuitableEngineError

        return {
            'type': guess_media_type(ext),
            'container': container,
            'display_name': u'%s.%s' % (name, container or ext),
            'unique_id': unique_id,
        }
Ejemplo n.º 33
0
    def _to_python(self, value, state=None):
        if not value.strip():
            return (None, None)

        try:
            width, height = value.split('x')
        except ValueError, e:
            raise Invalid(_('Value must be in the format wxh; e.g. 200x300'),
                          value, state)
Ejemplo n.º 34
0
    def explore(self, **kwargs):
        """Display the most recent 15 media.

        :rtype: Dict
        :returns:
            latest
                Latest media
            popular
                Latest media

        """
        media = Media.query.published()

        latest = media.order_by(Media.publish_on.desc())
        popular = media.order_by(Media.popularity_points.desc())

        featured = None
        featured_cat = helpers.get_featured_category()
        if featured_cat:
            featured = viewable_media(latest.in_category(featured_cat)).first()
        if not featured:
            featured = viewable_media(popular).first()

        latest = viewable_media(latest.exclude(featured))[:8]
        popular = viewable_media(popular.exclude(featured, latest))[:5]
        if request.settings['sitemaps_display'] == 'True':
            response.feed_links.extend([
                (url_for(controller='/sitemaps',
                         action='google'), _(u'Sitemap XML')),
                (url_for(controller='/sitemaps',
                         action='mrss'), _(u'Sitemap RSS')),
            ])
        if request.settings['rss_display'] == 'True':
            response.feed_links.extend([
                (url_for(controller='/sitemaps',
                         action='latest'), _(u'Latest RSS')),
            ])

        return dict(
            featured=featured,
            latest=latest,
            popular=popular,
            categories=Category.query.populated_tree(),
        )
Ejemplo n.º 35
0
    def save_thumb(self, id, thumb, **values):
        """Save a thumbnail uploaded with :class:`~mediacore.forms.admin.ThumbForm`.

        :param id: Media ID. If ``"new"`` a new Podcast 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:`~mediacore.model.podcasts.Podcast.id` which is
                important if a new podcast has just been created.

        """
        if id == 'new':
            return dict(
                success=False,
                message=
                u'You must first save the podcast before you can upload a thumbnail',
            )

        podcast = fetch_row(Podcast, id)

        try:
            # Create JPEG thumbs
            create_thumbs_for(podcast, thumb.file, thumb.filename)
            success = True
            message = None
        except IOError, e:
            success = False
            if e.errno == 13:
                message = _('Permission denied, cannot write file')
            elif e.message == 'cannot identify image file':
                message = _('Unsupport 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
Ejemplo n.º 36
0
    def _to_python(self, value, state):
        id = request.environ['pylons.routes_dict']['id']

        query = DBSession.query(User).filter_by(user_name=value)
        if id != 'new':
            query = query.filter(User.user_id != id)

        if query.count() != 0:
            raise Invalid(_('User name already exists'), value, state)
        return value
Ejemplo n.º 37
0
    def _to_python(self, value, state):
        id = request.environ["pylons.routes_dict"]["id"]

        query = DBSession.query(User).filter_by(user_name=value)
        if id != "new":
            query = query.filter(User.user_id != id)

        if query.count() != 0:
            raise Invalid(_("User name already exists"), value, state)
        return value
Ejemplo n.º 38
0
def add_settings_link():
    """Generate new links for the admin Settings menu.

    The :data:`mediacore.plugin.events.plugin_settings_links` event
    is defined as a :class:`mediacore.plugin.events.GeneratorEvent`
    which expects all observers to yield their content for use.

    """
    yield (_('Search Engine Optimization', domain='mediacore_seo'),
           url_for(controller='/seo/admin/settings'))
Ejemplo n.º 39
0
def add_settings_link():
    """Generate new links for the admin Settings menu.

    The :data:`mediacore.plugin.events.plugin_settings_links` event
    is defined as a :class:`mediacore.plugin.events.GeneratorEvent`
    which expects all observers to yield their content for use.

    """
    yield (_('Search Engine Optimization', domain='mediacore_seo'),
           url_for(controller='/seo/admin/settings'))
Ejemplo n.º 40
0
    def _to_python(self, value, state):
        id = request.environ['pylons.routes_dict']['id']

        query = DBSession.query(User).filter_by(user_name=value)
        if id != 'new':
            query = query.filter(User.user_id != id)

        if query.count() != 0:
            raise Invalid(_('User name already exists'), value, state)
        return value
Ejemplo n.º 41
0
    def save_thumb(self, id, thumb, **values):
        """Save a thumbnail uploaded with :class:`~mediacore.forms.admin.ThumbForm`.

        :param id: Media ID. If ``"new"`` a new Podcast 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:`~mediacore.model.podcasts.Podcast.id` which is
                important if a new podcast has just been created.

        """
        if id == 'new':
            return dict(
                success = False,
                message = u'You must first save the podcast before you can upload a thumbnail',
            )

        podcast = fetch_row(Podcast, id)

        try:
            # Create JPEG thumbs
            create_thumbs_for(podcast, thumb.file, thumb.filename)
            success = True
            message = None
        except IOError, e:
            success = False
            if e.errno == 13:
                message = _('Permission denied, cannot write file')
            elif e.message == 'cannot identify image file':
                message = _('Unsupport 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
Ejemplo n.º 42
0
def doc_link(page=None, anchor="", text=N_("Help"), **kwargs):
    """Return a link (anchor element) to the documentation on the project site.

    XXX: Target attribute is not XHTML compliant.
    """
    attrs = {"href": "http://mediacorecommunity.org/docs/user/%s.html#%s" % (page, anchor), "target": "_blank"}
    if kwargs:
        attrs.update(kwargs)
    attrs_string = " ".join(['%s="%s"' % (key, attrs[key]) for key in attrs])
    out = "<a %s>%s</a>" % (attrs_string, _(text))
    return literal(out)
Ejemplo n.º 43
0
def send_media_notification(media_obj):
    """
    Send a creation notification email that a new Media object has been
    created.

    Sends to the address configured in the 'email_media_uploaded' address,
    if one has been created.

    :param media_obj: The media object to send a notification about.
    :type media_obj: :class:`~mediacore.model.media.Media` instance
    """
    send_to = request.settings['email_media_uploaded']
    if not send_to:
        # media notification emails are disabled!
        return

    edit_url = url_for(controller='/admin/media',
                       action='edit',
                       id=media_obj.id,
                       qualified=True)

    clean_description = strip_xhtml(
        line_break_xhtml(line_break_xhtml(media_obj.description)))

    type = media_obj.type
    title = media_obj.title
    author_name = media_obj.author.name
    author_email = media_obj.author.email
    subject = _('New %(type)s: %(title)s') % locals()
    body = _("""A new %(type)s file has been uploaded!

Title: %(title)s

Author: %(author_name)s (%(author_email)s)

Admin URL: %(edit_url)s

Description: %(clean_description)s
""") % locals()

    send(send_to, request.settings['email_send_from'], subject, body)
Ejemplo n.º 44
0
    def explore(self, **kwargs):
        """Display the most recent 15 media.

        :rtype: Dict
        :returns:
            latest
                Latest media
            popular
                Latest media

        """
        media = Media.query.published()

        latest = media.order_by(Media.publish_on.desc())
        popular = media.order_by(Media.popularity_points.desc())

        featured = None
        featured_cat = helpers.get_featured_category()
        if featured_cat:
            featured = viewable_media(latest.in_category(featured_cat)).first()
        if not featured:
            featured = viewable_media(popular).first()

        latest = viewable_media(latest.exclude(featured))[:8]
        popular = viewable_media(popular.exclude(featured, latest))[:5]
        if request.settings['sitemaps_display'] == 'True':
            response.feed_links.extend([
                (url_for(controller='/sitemaps', action='google'), _(u'Sitemap XML')),
                (url_for(controller='/sitemaps', action='mrss'), _(u'Sitemap RSS')),
            ])
        if request.settings['rss_display'] == 'True':
            response.feed_links.extend([
                (url_for(controller='/sitemaps', action='latest'), _(u'Latest RSS')),
            ])

        return dict(
            featured = featured,
            latest = latest,
            popular = popular,
            categories = Category.query.populated_tree(),
        )
Ejemplo n.º 45
0
def send_media_notification(media_obj):
    """
    Send a creation notification email that a new Media object has been
    created.

    Sends to the address configured in the 'email_media_uploaded' address,
    if one has been created.

    :param media_obj: The media object to send a notification about.
    :type media_obj: :class:`~mediacore.model.media.Media` instance
    """
    send_to = request.settings['email_media_uploaded']
    if not send_to:
        # media notification emails are disabled!
        return

    edit_url = url_for(controller='/admin/media', action='edit',
                       id=media_obj.id, qualified=True)

    clean_description = strip_xhtml(line_break_xhtml(line_break_xhtml(media_obj.description)))

    type = media_obj.type
    title = media_obj.title
    author_name = media_obj.author.name
    author_email = media_obj.author.email
    subject = _('New %(type)s: %(title)s') % locals()
    body = _("""A new %(type)s file has been uploaded!

Title: %(title)s

Author: %(author_name)s (%(author_email)s)

Admin URL: %(edit_url)s

Description: %(clean_description)s
""") % locals()

    send(send_to, request.settings['email_send_from'], subject, body)
Ejemplo n.º 46
0
def doc_link(page=None, anchor='', text=N_('Help'), **kwargs):
    """Return a link (anchor element) to the documentation on the project site.

    XXX: Target attribute is not XHTML compliant.
    """
    attrs = {
        'href': 'http://mediadrop.net/docs/user/%s.html#%s' % (page, anchor),
        'target': '_blank',
    }
    if kwargs:
        attrs.update(kwargs)
    attrs_string = ' '.join(['%s="%s"' % (key, attrs[key]) for key in attrs])
    out = '<a %s>%s</a>' % (attrs_string, _(text))
    return literal(out)
Ejemplo n.º 47
0
def doc_link(page=None, anchor='', text=N_('Help'), **kwargs):
    """Return a link (anchor element) to the documentation on the project site.

    XXX: Target attribute is not XHTML compliant.
    """
    attrs = {
        'href': 'http://getmediacore.com/docs/user/%s.html#%s' % (page, anchor),
        'target': '_blank',
    }
    if kwargs:
        attrs.update(kwargs)
    attrs_string = ' '.join(['%s="%s"' % (key, attrs[key]) for key in attrs])
    out = '<a %s>%s</a>' % (attrs_string, _(text))
    return literal(out)
Ejemplo n.º 48
0
    def display_name(self):
        """Return the user-friendly display name for this player class.

        This string is expected to be i18n-ready. Simply wrap it in a
        call to :func:`mediacore.lib.i18n._`.

        :rtype: unicode
        :returns: A i18n-ready string name.
        """
        if self.player_cls is None:
            # do not break the admin interface (admin/settings/players) if the
            # player is still in the database but the actual player class is not
            # available anymore (this can happen especially for players provided
            # by external plugins.
            return _(u'%s (broken)') % self.name
        return self.player_cls.display_name
Ejemplo n.º 49
0
    def display_name(self):
        """Return the user-friendly display name for this player class.

        This string is expected to be i18n-ready. Simply wrap it in a
        call to :func:`mediacore.lib.i18n._`.

        :rtype: unicode
        :returns: A i18n-ready string name.
        """
        if self.player_cls is None:
            # do not break the admin interface (admin/settings/players) if the
            # player is still in the database but the actual player class is not
            # available anymore (this can happen especially for players provided
            # by external plugins.
            return _(u'%s (broken)') % self.name
        return self.player_cls.display_name
Ejemplo n.º 50
0
    def _verify_upload_integrity(self, file, file_url):
        """Download the given file from the URL and compare the SHA1s.

        :type file: :class:`cgi.FieldStorage`
        :param file: A freshly uploaded file object, that has just been
            sent to the FTP server.

        :type file_url: str
        :param file_url: A publicly accessible URL where the uploaded file
            can be downloaded.

        :returns: `True` if the integrity check succeeds or is disabled.

        :raises FTPUploadError: If the file cannot be downloaded after
            the max number of retries, or if the the downloaded file
            doesn't match the original.

        """
        max_tries = int(self._data[FTP_MAX_INTEGRITY_RETRIES])
        if max_tries < 1:
            return True

        file.seek(0)
        orig_hash = sha1(file.read()).hexdigest()

        # Try to download the file. Increase the number of retries, or the
        # timeout duration, if the server is particularly slow.
        # eg: Akamai usually takes 3-15 seconds to make an uploaded file
        #     available over HTTP.
        for i in xrange(max_tries):
            try:
                temp_file = urlopen(file_url)
                dl_hash = sha1(temp_file.read()).hexdigest()
                temp_file.close()
            except HTTPError, http_err:
                # Don't raise the exception now, wait until all attempts fail
                time.sleep(3)
            else:
                # If the downloaded file matches, success! Otherwise, we can
                # be pretty sure that it got corrupted during FTP transfer.
                if orig_hash == dl_hash:
                    return True
                else:
                    msg = _('The file transferred to your FTP server is '\
                            'corrupted. Please try again.')
                    raise FTPUploadError(msg, None, None)
Ejemplo n.º 51
0
    def display(self, value=None, **kwargs):
        if value:
            # XXX: First, try to translate this string. This is necessary for
            #      the "Legal Wording" section of the Upload settings. The
            #      default string was extracted correctly but not translated.
            value = _(value)

            value = line_break_xhtml(value)

        # Enable the rich text editor, if dictated by the settings:
        if tiny_mce_condition():
            if 'css_classes' in kwargs:
                kwargs['css_classes'].append('tinymcearea')
            else:
                kwargs['css_classes'] = ['tinymcearea']

        return TextArea.display(self, value, **kwargs)
Ejemplo n.º 52
0
    def _verify_upload_integrity(self, file, file_url):
        """Download the given file from the URL and compare the SHA1s.

        :type file: :class:`cgi.FieldStorage`
        :param file: A freshly uploaded file object, that has just been
            sent to the FTP server.

        :type file_url: str
        :param file_url: A publicly accessible URL where the uploaded file
            can be downloaded.

        :returns: `True` if the integrity check succeeds or is disabled.

        :raises FTPUploadError: If the file cannot be downloaded after
            the max number of retries, or if the the downloaded file
            doesn't match the original.

        """
        max_tries = int(self._data[FTP_MAX_INTEGRITY_RETRIES])
        if max_tries < 1:
            return True

        file.seek(0)
        orig_hash = sha1(file.read()).hexdigest()

        # Try to download the file. Increase the number of retries, or the
        # timeout duration, if the server is particularly slow.
        # eg: Akamai usually takes 3-15 seconds to make an uploaded file
        #     available over HTTP.
        for i in xrange(max_tries):
            try:
                temp_file = urlopen(file_url)
                dl_hash = sha1(temp_file.read()).hexdigest()
                temp_file.close()
            except HTTPError, http_err:
                # Don't raise the exception now, wait until all attempts fail
                time.sleep(3)
            else:
                # If the downloaded file matches, success! Otherwise, we can
                # be pretty sure that it got corrupted during FTP transfer.
                if orig_hash == dl_hash:
                    return True
                else:
                    msg = _('The file transferred to your FTP server is '\
                            'corrupted. Please try again.')
                    raise FTPUploadError(msg, None, None)
Ejemplo n.º 53
0
    def display(self, value=None, **kwargs):
        if value:
            # XXX: First, try to translate this string. This is necessary for
            #      the "Legal Wording" section of the Upload settings. The
            #      default string was extracted correctly but not translated.
            value = _(value)

            value = line_break_xhtml(value)

        # Enable the rich text editor, if dictated by the settings:
        if tiny_mce_condition():
            if 'css_classes' in kwargs:
                kwargs['css_classes'].append('tinymcearea')
            else:
                kwargs['css_classes'] = ['tinymcearea']

        return TextArea.display(self, value, **kwargs)
Ejemplo n.º 54
0
    def store(self, media_file, file=None, url=None, meta=None):
        """Store the given file or URL and return a unique identifier for it.

        :type media_file: :class:`~mediacore.model.media.MediaFile`
        :param media_file: The associated media file object.

        :type file: :class:`cgi.FieldStorage` or None
        :param file: A freshly uploaded file object.

        :type url: unicode or None
        :param url: A remote URL string.

        :type meta: dict
        :param meta: The metadata returned by :meth:`parse`.

        :rtype: unicode or None
        :returns: The unique ID string. Return None if not generating it here.

        :raises FTPUploadError: If storing the file fails.

        """
        file_name = safe_file_name(media_file, file.filename)

        file_url = os.path.join(self._data[HTTP_DOWNLOAD_URI], file_name)
        upload_dir = self._data[FTP_UPLOAD_DIR]
        stor_cmd = 'STOR ' + file_name

        ftp = self._connect()
        try:
            if upload_dir:
                ftp.cwd(upload_dir)
            ftp.storbinary(stor_cmd, file.file)

            # Raise a FTPUploadError if the file integrity check fails
            # TODO: Delete the file if the integrity check fails
            self._verify_upload_integrity(file.file, file_url)
            ftp.quit()
        except ftp_errors, e:
            log.exception(e)
            ftp.quit()
            msg = _('Could not upload the file from your FTP server: %s')\
                % e.message
            raise FTPUploadError(msg, None, None)
Ejemplo n.º 55
0
    def store(self, media_file, file=None, url=None, meta=None):
        """Store the given file or URL and return a unique identifier for it.

        :type media_file: :class:`~mediacore.model.media.MediaFile`
        :param media_file: The associated media file object.

        :type file: :class:`cgi.FieldStorage` or None
        :param file: A freshly uploaded file object.

        :type url: unicode or None
        :param url: A remote URL string.

        :type meta: dict
        :param meta: The metadata returned by :meth:`parse`.

        :rtype: unicode or None
        :returns: The unique ID string. Return None if not generating it here.

        :raises FTPUploadError: If storing the file fails.

        """
        file_name = safe_file_name(media_file, file.filename)

        file_url = os.path.join(self._data[HTTP_DOWNLOAD_URI], file_name)
        upload_dir = self._data[FTP_UPLOAD_DIR]
        stor_cmd = 'STOR ' + file_name

        ftp = self._connect()
        try:
            if upload_dir:
                ftp.cwd(upload_dir)
            ftp.storbinary(stor_cmd, file.file)

            # Raise a FTPUploadError if the file integrity check fails
            # TODO: Delete the file if the integrity check fails
            self._verify_upload_integrity(file.file, file_url)
            ftp.quit()
        except ftp_errors, e:
            log.exception(e)
            ftp.quit()
            msg = _('Could not upload the file from your FTP server: %s')\
                % e.message
            raise FTPUploadError(msg, None, None)
Ejemplo n.º 56
0
    def document(self, *args, **kwargs):
        """Render the error document for the general public.

        Essentially, when an error occurs, a second request is initiated for
        the URL ``/error/document``. The URL is set on initialization of the
        :class:`pylons.middleware.StatusCodeRedirect` object, and can be
        overridden in :func:`tg.configuration.add_error_middleware`. Also,
        before this method is called, some potentially useful environ vars
        are set in :meth:`pylons.middleware.StatusCodeRedirect.__call__`
        (access them via :attr:`tg.request.environ`).

        :rtype: Dict
        :returns:
            prefix
                The environ SCRIPT_NAME.
            vars
                A dict containing the first 2 KB of the original request.
            code
                Integer error code thrown by the original request, but it can
                also be overriden by setting ``tg.request.params['code']``.
            message
                A message to display to the user. Pulled from
                ``tg.request.params['message']``.

        """
        request = self._py_object.request
        environ = request.environ
        original_request = environ.get('pylons.original_request', None)
        original_response = environ.get('pylons.original_response', None)
        default_message = '<p>%s</p>' % _("We're sorry but we weren't able "
                                          "to process this request.")

        message = request.params.get('message', default_message)
        message = clean_xhtml(message)

        return dict(
            prefix=environ.get('SCRIPT_NAME', ''),
            code=int(
                request.params.get(
                    'code', getattr(original_response, 'status_int', 500))),
            message=message,
            vars=dict(POST_request=unicode(original_request)[:2048]),
        )
Ejemplo n.º 57
0
    def document(self, *args, **kwargs):
        """Render the error document for the general public.

        Essentially, when an error occurs, a second request is initiated for
        the URL ``/error/document``. The URL is set on initialization of the
        :class:`pylons.middleware.StatusCodeRedirect` object, and can be
        overridden in :func:`tg.configuration.add_error_middleware`. Also,
        before this method is called, some potentially useful environ vars
        are set in :meth:`pylons.middleware.StatusCodeRedirect.__call__`
        (access them via :attr:`tg.request.environ`).

        :rtype: Dict
        :returns:
            prefix
                The environ SCRIPT_NAME.
            vars
                A dict containing the first 2 KB of the original request.
            code
                Integer error code thrown by the original request, but it can
                also be overriden by setting ``tg.request.params['code']``.
            message
                A message to display to the user. Pulled from
                ``tg.request.params['message']``.

        """
        request = self._py_object.request
        environ = request.environ
        original_request = environ.get('pylons.original_request', None)
        original_response = environ.get('pylons.original_response', None)
        default_message = '<p>%s</p>' % _("We're sorry but we weren't able "
                                          "to process this request.")

        message = request.params.get('message', default_message)
        message = clean_xhtml(message)

        return dict(
            prefix = environ.get('SCRIPT_NAME', ''),
            code = int(request.params.get('code', getattr(original_response,
                                                          'status_int', 500))),
            message = message,
            vars = dict(POST_request=unicode(original_request)[:2048]),
        )
Ejemplo n.º 58
0
    def index(self, slug=None, **kwargs):
        media = Media.query.published()

        if c.category:
            media = media.in_category(c.category)
            
            response.feed_links.append((
                url_for(controller='/categories', action='feed', slug=c.category.slug),
                _('Latest media in %s') % c.category.name
            ))

        latest = media.order_by(Media.publish_on.desc())
        popular = media.order_by(Media.popularity_points.desc())

        latest = viewable_media(latest)[:5]
        popular = viewable_media(popular.exclude(latest))[:5]

        return dict(
            latest = latest,
            popular = popular,
        )