Example #1
0
class OAuthAccessToken(OAuthToken):
    class __mongometa__:
        polymorphic_identity = 'access'

    type = FieldProperty(str, if_missing='access')
    consumer_token_id = ForeignIdProperty('OAuthConsumerToken')
    request_token_id = ForeignIdProperty('OAuthToken')
    user_id = AlluraUserProperty(if_missing=lambda: c.user._id)
    is_bearer = FieldProperty(bool, if_missing=False)

    user = RelationProperty('User')
    consumer_token = RelationProperty('OAuthConsumerToken',
                                      via='consumer_token_id')
    request_token = RelationProperty('OAuthToken', via='request_token_id')

    @classmethod
    def for_user(cls, user=None):
        if user is None:
            user = c.user
        return cls.query.find(dict(user_id=user._id, type='access')).all()

    def can_import_forum(self):
        tokens = aslist(config.get('oauth.can_import_forum', ''), ',')
        if self.api_key in tokens:
            return True
        return False
Example #2
0
class ForumPost(M.Post):
    class __mongometa__:
        name = 'forum_post'
        history_class = ForumPostHistory
        indexes = [
            'timestamp',  # for the posts_24hr site_stats query
            (  # for last_post queries on thread listing page
                'thread_id',
                'deleted',
                ('timestamp', pymongo.DESCENDING),
            ),
        ]

    type_s = 'Post'

    discussion_id = ForeignIdProperty(Forum)
    thread_id = ForeignIdProperty(ForumThread)

    discussion = RelationProperty(Forum)
    thread = RelationProperty(ForumThread)

    @classmethod
    def attachment_class(cls):
        return ForumAttachment

    @property
    def email_address(self):
        return self.discussion.email_address

    def primary(self):
        return self
Example #3
0
class ForumThread(M.Thread):
    class __mongometa__:
        name = 'forum_thread'
        indexes = [
            'flags',
            'discussion_id',
            'import_id',  # may be used by external legacy systems
        ]

    type_s = 'Thread'

    discussion_id = ForeignIdProperty(Forum)
    first_post_id = ForeignIdProperty('ForumPost')
    flags = FieldProperty([str])

    discussion = RelationProperty(Forum)
    posts = RelationProperty('ForumPost')
    first_post = RelationProperty('ForumPost', via='first_post_id')

    @property
    def status(self):
        if self.first_post:
            return self.first_post.status
        else:
            return 'ok'

    @classmethod
    def attachment_class(cls):
        return ForumAttachment

    @property
    def email_address(self):
        return self.discussion.email_address

    def primary(self):
        return self

    def post(self, subject, text, message_id=None, parent_id=None, **kw):
        post = super(ForumThread, self).post(text,
                                             message_id=message_id,
                                             parent_id=parent_id,
                                             **kw)
        if not self.first_post_id:
            self.first_post_id = post._id
            self.num_replies = 1
        h.log_action(log, 'posted').info('')
        return post

    def set_forum(self, new_forum):
        self.post_class().query.update(
            dict(discussion_id=self.discussion_id, thread_id=self._id),
            {'$set': dict(discussion_id=new_forum._id)},
            multi=True)
        self.attachment_class().query.update(
            {
                'discussion_id': self.discussion_id,
                'thread_id': self._id
            }, {'$set': dict(discussion_id=new_forum._id)})
        self.discussion_id = new_forum._id
Example #4
0
class User(SproxTestClass):
    """Reasonably basic User definition. Probably would want additional
    attributes. (Relational-style)
    """
    class __mongometa__:
        name = 'tg_user_rs'
        unique_indexes = [('user_name', ), ('email_address', )]

    _id = FieldProperty(S.ObjectId)
    user_name = FieldProperty(str)  # unique 1
    email_address = FieldProperty(str)  # unique 2
    display_name = FieldProperty(str)
    display_name.sprox_meta = {'title': True}

    _password = FieldProperty(str)
    _password.sprox_meta = {'password': True}

    created = FieldProperty(datetime, if_missing=datetime.now)
    town_id = ForeignIdProperty(Town)
    town = RelationProperty(Town)

    groups = RelationProperty(Group)
    _groups = ForeignIdProperty(Group, uselist=True)

    @property
    def permissions(self):
        perms = set()
        for g in self.groups:
            perms = perms | set(g.permissions)
        return perms

    @classmethod
    def by_email_address(cls, email):
        """A class method that can be used to search users
        based on their email addresses since it is unique.
        """
        raise NotImplementedError

    @classmethod
    def by_user_name(cls, username):
        """A class method that permits to search users
        based on their user_name attribute.
        """
        raise NotImplementedError

    def _set_password(self, password):
        """encrypts password on the fly using the encryption
        algo defined in the configuration
        """
        #unfortunately, this causes coverage not to work
        #self._password = self._encrypt_password(algorithm, password)

    def _get_password(self):
        """returns password
        """
        return self._password

    password = property(_get_password, _set_password)
Example #5
0
class DocumentCategoryTagAssignment(SproxTestClass):
    class __mongometa__:
        name = 'document_category_tag_assignment_rs'

    _id = FieldProperty(S.ObjectId)
    document_category_id = ForeignIdProperty(DocumentCategory)
    document_category = RelationProperty(DocumentCategory)
    department_id = ForeignIdProperty(Department)
    department = RelationProperty(Department)
    document_category_tag_id = ForeignIdProperty("DocumentCategoryTag")
    document_category_tag = RelationProperty("DocumentCategoryTag")
class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
    class __mongometa__:
        name = 'ticket'
        history_class = TicketHistory
        indexes = [
            'ticket_num',
            'app_config_id',
            ('app_config_id', 'custom_fields._milestone'),
            'import_id',
        ]
        unique_indexes = [
            ('app_config_id', 'ticket_num'),
        ]

    type_s = 'Ticket'
    _id = FieldProperty(schema.ObjectId)
    created_date = FieldProperty(datetime, if_missing=datetime.utcnow)

    super_id = FieldProperty(schema.ObjectId, if_missing=None)
    sub_ids = FieldProperty([schema.ObjectId])
    ticket_num = FieldProperty(int, required=True, allow_none=False)
    summary = FieldProperty(str)
    description = FieldProperty(str, if_missing='')
    reported_by_id = ForeignIdProperty(User, if_missing=lambda: c.user._id)
    assigned_to_id = ForeignIdProperty(User, if_missing=None)
    milestone = FieldProperty(str, if_missing='')
    status = FieldProperty(str, if_missing='')
    custom_fields = FieldProperty({str: None})

    reported_by = RelationProperty(User, via='reported_by_id')

    @property
    def activity_name(self):
        return 'ticket #%s' % self.ticket_num

    @classmethod
    def new(cls):
        '''Create a new ticket, safely (ensuring a unique ticket_num'''
        while True:
            ticket_num = c.app.globals.next_ticket_num()
            ticket = cls(app_config_id=c.app.config._id,
                         custom_fields=dict(),
                         ticket_num=ticket_num)
            try:
                session(ticket).flush(ticket)
                h.log_action(log, 'opened').info('')
                return ticket
            except OperationFailure, err:
                if 'duplicate' in err.args[0]:
                    log.warning('Try to create duplicate ticket %s',
                                ticket.url())
                    session(ticket).expunge(ticket)
                    continue
                raise
Example #7
0
class OAuthRequestToken(OAuthToken):
    class __mongometa__:
        polymorphic_identity = 'request'

    type = FieldProperty(str, if_missing='request')
    consumer_token_id = ForeignIdProperty('OAuthConsumerToken')
    user_id = ForeignIdProperty('User', if_missing=lambda: c.user._id)
    callback = FieldProperty(str)
    validation_pin = FieldProperty(str)

    consumer_token = RelationProperty('OAuthConsumerToken')
Example #8
0
class AwardGrant(Artifact):

    "An :class:`Award <allura.model.artifact.Award>` can be bestowed upon a project by a neighborhood"

    class __mongometa__:
        session = main_orm_session
        name = 'grant'
        indexes = ['short']

    type_s = 'Generic Award Grant'

    _id = FieldProperty(S.ObjectId)
    award_id = ForeignIdProperty(Award, if_missing=None)
    award = RelationProperty(Award, via='award_id')
    granted_by_neighborhood_id = ForeignIdProperty('Neighborhood',
                                                   if_missing=None)
    granted_by_neighborhood = RelationProperty(
        'Neighborhood', via='granted_by_neighborhood_id')
    granted_to_project_id = ForeignIdProperty('Project', if_missing=None)
    granted_to_project = RelationProperty('Project',
                                          via='granted_to_project_id')
    award_url = FieldProperty(str, if_missing='')
    comment = FieldProperty(str, if_missing='')
    timestamp = FieldProperty(datetime, if_missing=datetime.utcnow)

    def index(self):
        result = Artifact.index(self)
        result.update(_id_s=self._id,
                      short_s=self.short,
                      timestamp_dt=self.timestamp,
                      full_s=self.full)
        if self.award:
            result['award_s'] = self.award.short
        return result

    @property
    def icon(self):
        return AwardFile.query.get(award_id=self.award_id)

    def url(self):
        slug = str(self.granted_to_project.shortname).replace('/', '_')
        return h.urlquote(slug)

    def longurl(self):
        slug = str(self.granted_to_project.shortname).replace('/', '_')
        slug = self.award.longurl() + '/' + slug
        return h.urlquote(slug)

    def shorthand_id(self):
        if self.award:
            return self.award.short
        else:
            return None
Example #9
0
class GroupPermission(SproxTestClass):
    """This is the association table for the many-to-many relationship between
    groups and permissions.
    """
    class __mongometa__:
        name = 'tg_group_permission_rs'
        unique_indexes = (('group_id', 'permission_id'), )

    _id = FieldProperty(S.ObjectId)
    group_id = ForeignIdProperty("Group")
    group = RelationProperty("Group")
    permission_id = ForeignIdProperty("Permission")
    permission = RelationProperty("Permission")
Example #10
0
class OAuthConsumerToken(OAuthToken):
    class __mongometa__:
        polymorphic_identity = 'consumer'
        name = 'oauth_consumer_token'
        unique_indexes = ['name']

    type = FieldProperty(str, if_missing='consumer')
    user_id = ForeignIdProperty('User', if_missing=lambda: c.user._id)
    name = FieldProperty(str)
    description = FieldProperty(str)

    user = RelationProperty('User')

    @property
    def description_html(self):
        return g.markdown.convert(self.description)

    @property
    def consumer(self):
        '''OAuth compatible consumer object'''
        return oauth.Consumer(self.api_key, self.secret_key)

    @classmethod
    def for_user(cls, user=None):
        if user is None: user = c.user
        return cls.query.find(dict(user_id=user._id)).all()
Example #11
0
class DocumentCategoryReference(SproxTestClass):
    class __mongometa__:
        name = 'document_category_reference_rs'

    _id = FieldProperty(S.ObjectId)
    document_category_id = ForeignIdProperty(DocumentCategory)
    category = RelationProperty(DocumentCategory)
Example #12
0
class ApiTicket(MappedClass, ApiAuthMixIn):
    class __mongometa__:
        name = 'api_ticket'
        session = main_orm_session

    PREFIX = 'tck'

    _id = FieldProperty(S.ObjectId)
    user_id = ForeignIdProperty('User')
    api_key = FieldProperty(str,
                            if_missing=lambda: ApiTicket.PREFIX + h.nonce(20))
    secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce)
    expires = FieldProperty(datetime, if_missing=None)
    capabilities = FieldProperty({str: None})
    mod_date = FieldProperty(datetime, if_missing=datetime.utcnow)

    user = RelationProperty('User')

    @classmethod
    def get(cls, api_ticket):
        if not api_ticket.startswith(cls.PREFIX):
            return None
        return cls.query.get(api_key=api_ticket)

    def authenticate_request(self, path, params):
        if self.expires and datetime.utcnow() > self.expires:
            return False
        return ApiAuthMixIn.authenticate_request(self, path, params)

    def get_capability(self, key):
        return self.capabilities.get(key)
Example #13
0
class OAuthAccessToken(OAuthToken):
    class __mongometa__:
        polymorphic_identity = 'access'

    type = FieldProperty(str, if_missing='access')
    consumer_token_id = ForeignIdProperty('OAuthConsumerToken')
    request_token_id = ForeignIdProperty('OAuthToken')
    user_id = ForeignIdProperty('User', if_missing=lambda: c.user._id)

    user = RelationProperty('User')
    consumer_token = RelationProperty('OAuthConsumerToken',
                                      via='consumer_token_id')
    request_token = RelationProperty('OAuthToken', via='request_token_id')

    @classmethod
    def for_user(cls, user=None):
        if user is None: user = c.user
        return cls.query.find(dict(user_id=user._id)).all()
Example #14
0
class MovedArtifact(Artifact):
    class __mongometa__:
        session = artifact_orm_session
        name='moved_artifact'

    _id = FieldProperty(S.ObjectId)
    app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
    app_config = RelationProperty('AppConfig')
    moved_to_url = FieldProperty(str, required=True, allow_none=False)
Example #15
0
class SpamCheckResult(MappedClass):
    class __mongometa__:
        session = main_orm_session
        name = 'spam_check_result'
        indexes = [
            ('project_id', 'result'),
            ('user_id', 'result'),
        ]

    _id = FieldProperty(S.ObjectId)
    ref_id = ForeignIdProperty('ArtifactReference')
    ref = RelationProperty('ArtifactReference', via='ref_id')
    project_id = ForeignIdProperty('Project')
    project = RelationProperty('Project', via='project_id')
    user_id = ForeignIdProperty('User')
    user = RelationProperty('User', via='user_id')
    timestamp = FieldProperty(datetime, if_missing=datetime.utcnow)
    result = FieldProperty(bool)
Example #16
0
class DocumentCategory(SproxTestClass):
    class __mongometa__:
        name = 'document_category_rs'

    _id = FieldProperty(int)
    document_category_id = FieldProperty(int)
    department_id = ForeignIdProperty(Department)
    department = RelationProperty(Department)
    name = FieldProperty(str)
Example #17
0
class ShortUrl(M.Artifact):
    class __mongometa__:
        name = 'short_urls'
        unique_indexes = [('short_name', 'app_config_id')]

    type_s = 'ShortUrl'
    full_url = FieldProperty(str)
    short_name = FieldProperty(str)
    description = FieldProperty(str)
    private = FieldProperty(bool)
    create_user = ForeignIdProperty(User)
    created = FieldProperty(datetime, if_missing=datetime.utcnow)
    last_updated = FieldProperty(datetime, if_missing=datetime.utcnow)

    @property
    def user(self):
        return User.query.get(_id=self.create_user)

    @classmethod
    def upsert(cls, shortname):
        u = cls.query.get(short_name=shortname, app_config_id=c.app.config._id)
        if u is not None:
            return u
        try:
            u = cls(short_name=shortname, app_config_id=c.app.config._id)
            session(u).flush(u)
        except pymongo.errors.DuplicateKeyError:
            session(u).expunge(u)
            u = cls.query.get(short_name=shortname,
                              app_config_id=c.app.config._id)
        return u

    def index(self):
        result = M.Artifact.index(self)
        result.update(full_url_s=self.full_url,
                      short_name_s=self.short_name,
                      description_s=self.description,
                      title='%s => %s' % (self.url(), self.full_url),
                      private_b=self.private,
                      type_s=self.type_s)
        return result

    def url(self):
        return self.app.url + self.short_name

    @classmethod
    def build_short_url(cls, app, short_name):
        return config['short_url.url_pattern'].format(
            base_url=config['base_url'],
            nbhd=app.project.neighborhood.url_prefix.strip('/'),
            project=app.project.shortname,
            mount_point=app.config.options.mount_point,
            short_name=short_name)

    def short_url(self):
        return self.build_short_url(self.app, self.short_name)
Example #18
0
class Globals(MappedClass):
    class __mongometa__:
        name = 'wiki-globals'
        session = project_orm_session
        indexes = ['app_config_id']

    type_s = 'WikiGlobals'
    _id = FieldProperty(schema.ObjectId)
    app_config_id = ForeignIdProperty(
        'AppConfig', if_missing=lambda: context.app.config._id)
    root = FieldProperty(str)
Example #19
0
class Globals(MappedClass):
    class __mongometa__:
        name = 'blog-globals'
        session = M.project_orm_session
        indexes = ['app_config_id']

    type_s = 'BlogGlobals'
    _id = FieldProperty(schema.ObjectId)
    app_config_id = ForeignIdProperty('AppConfig',
                                      if_missing=lambda: c.app.config._id)
    external_feeds = FieldProperty([str])
Example #20
0
class Document(SproxTestClass):
    class __mongometa__:
        name = 'document_rs'

    _id = FieldProperty(S.ObjectId)
    created = FieldProperty(datetime, if_missing=datetime.now)
    edited = FieldProperty(S.DateTime, if_missing=datetime.now)
    blob = FieldProperty(S.Binary)
    owner = ForeignIdProperty(User)
    url = FieldProperty(S.String)
    document_category_id = ForeignIdProperty(DocumentCategory)

    metadata = FieldProperty([{'name': S.String, 'value': S.String}])

    def _get_address(self):
        return self.url

    def _set_address(self, value):
        self.url = value

    category = RelationProperty(DocumentCategory)
Example #21
0
class ApiToken(MappedClass, ApiAuthMixIn):
    class __mongometa__:
        name = 'api_token'
        session = main_orm_session
        unique_indexes = ['user_id']

    _id = FieldProperty(S.ObjectId)
    user_id = ForeignIdProperty('User')
    api_key = FieldProperty(str, if_missing=lambda: str(uuid.uuid4()))
    secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce)

    user = RelationProperty('User')

    @classmethod
    def get(cls, api_key):
        return cls.query.get(api_key=api_key)
Example #22
0
class ShortUrl(M.Artifact):

    class __mongometa__:
        name = 'short_urls'
        unique_indexes = ['short_name']

    type_s = 'ShortUrl'
    full_url = FieldProperty(str)
    short_name = FieldProperty(str)
    description = FieldProperty(str)
    private = FieldProperty(bool)
    create_user = ForeignIdProperty(User)
    created = FieldProperty(datetime, if_missing=datetime.utcnow)
    last_updated = FieldProperty(datetime, if_missing=datetime.utcnow)

    @property
    def user(self):
        return User.query.get(_id=self.create_user)

    @classmethod
    def upsert(cls, shortname):
        u = cls.query.get(short_name=shortname, app_config_id=c.app.config._id)
        if u is not None:
            return u
        try:
            u = cls(short_name=shortname, app_config_id=c.app.config._id)
            session(u).flush(u)
        except pymongo.errors.DuplicateKeyError:
            session(u).expunge(u)
            u = cls.query.get(short_name=shortname,
                    app_config_id=c.app.config._id)
        return u

    def index(self):
        result = M.Artifact.index(self)
        result.update(
            full_url_s=self.full_url,
            short_name_s=self.short_name,
            description_s=self.description,
            title_s='%s => %s' % (self.url(), self.full_url),
            private_b=self.private,
            type_s=self.type_s)
        return result

    def url(self):
        return self.app.url + self.short_name
Example #23
0
class PortalConfig(Artifact):
    class __mongometa__:
        name='portal_config'
    type_s = 'Project Portal Configuration'

    _id = FieldProperty(schema.ObjectId)
    user_id = ForeignIdProperty('User')
    layout_class = FieldProperty(str)
    layout = FieldProperty([
            {'name':str,
             'content':[
                    {'mount_point':str,
                     'widget_name':str }
                    ]
             }])

    @classmethod
    def current(cls):
        result = cls.query.get(user_id=c.user._id)
        if result is None:
            result = cls(user_id=c.user._id,
                         layout_class='onecol',
                         layout=[dict(name='content',
                                      content=[dict(mount_point='home',
                                                    widget_name='welcome')
                                               ])])
        return result

    def rendered_layout(self):
        return [
            dict(name=div.name,
                 content=[ render_widget(**w)
                           for w in div.content]
                 )
            for div in self.layout ]
            

    def url(self):
        return self.app_config.script_name()

    def index(self):
        return None
Example #24
0
class Award(Artifact):
    class __mongometa__:
        session = main_orm_session
        name = 'award'
        indexes = ['short']

    type_s = 'Generic Award'

    from .project import Neighborhood
    _id = FieldProperty(S.ObjectId)
    created_by_neighborhood_id = ForeignIdProperty(Neighborhood,
                                                   if_missing=None)
    created_by_neighborhood = RelationProperty(
        Neighborhood, via='created_by_neighborhood_id')
    short = FieldProperty(str, if_missing=h.nonce)
    timestamp = FieldProperty(datetime, if_missing=datetime.utcnow)
    full = FieldProperty(str, if_missing='')

    def index(self):
        result = Artifact.index(self)
        result.update(_id_s=self._id,
                      short_s=self.short,
                      timestamp_dt=self.timestamp,
                      full_s=self.full)
        if self.created_by:
            result['created_by_s'] = self.created_by.name
        return result

    @property
    def icon(self):
        return AwardFile.query.get(award_id=self._id)

    def url(self):
        return str(self._id)

    def longurl(self):
        return self.created_by_neighborhood.url_prefix + "_admin/awards/" + self.url(
        )

    def shorthand_id(self):
        return self.short
Example #25
0
class Feedback(VersionedArtifact, ActivityObject):
    class __mongometa__:
        name = str('feedback')
        indexes = [
            ('project_id', 'reported_by_id'),
        ]

    type_s = 'Feedback'
    _id = FieldProperty(schema.ObjectId)
    created_date = FieldProperty(datetime, if_missing=datetime.utcnow)
    rating = FieldProperty(str, if_missing='')
    description = FieldProperty(str, if_missing='')
    reported_by_id = AlluraUserProperty(if_missing=lambda: c.user._id)
    project_id = ForeignIdProperty('Project', if_missing=lambda: c.project._id)
    reported_by = RelationProperty(User, via='reported_by_id')

    def index(self):
        result = VersionedArtifact.index(self)
        result.update(
            created_date_dt=self.created_date,
            reported_by_username_t=self.reported_by.username,
            text=self.description,
        )
        return result

    @property
    def activity_name(self):
        return 'a review comment'

    @property
    def activity_extras(self):
        d = ActivityObject.activity_extras.fget(self)
        d.update(summary=self.description)
        return d

    def url(self):
        return self.app_config.url()
Example #26
0
class Notification(MappedClass):
    '''
    Temporarily store notifications that will be emailed or displayed as a web flash.
    This does not contain any recipient information.
    '''

    class __mongometa__:
        session = main_orm_session
        name = 'notification'
        indexes = ['project_id']

    _id = FieldProperty(str, if_missing=h.gen_message_id)

    # Classify notifications
    neighborhood_id = ForeignIdProperty('Neighborhood', if_missing=lambda:c.project.neighborhood._id)
    project_id = ForeignIdProperty('Project', if_missing=lambda:c.project._id)
    app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
    tool_name = FieldProperty(str, if_missing=lambda:c.app.config.tool_name)
    ref_id = ForeignIdProperty('ArtifactReference')
    topic = FieldProperty(str)

    # Notification Content
    in_reply_to=FieldProperty(str)
    from_address=FieldProperty(str)
    reply_to_address=FieldProperty(str)
    subject=FieldProperty(str)
    text=FieldProperty(str)
    link=FieldProperty(str)
    author_id=ForeignIdProperty('User')
    feed_meta=FieldProperty(S.Deprecated)
    artifact_reference = FieldProperty(S.Deprecated)
    pubdate = FieldProperty(datetime, if_missing=datetime.utcnow)

    ref = RelationProperty('ArtifactReference')

    view = jinja2.Environment(
            loader=jinja2.PackageLoader('allura', 'templates'),
            auto_reload=asbool(config.get('auto_reload_templates', True)),
    )

    @classmethod
    def post(cls, artifact, topic, **kw):
        '''Create a notification and  send the notify message'''
        import allura.tasks.notification_tasks
        n = cls._make_notification(artifact, topic, **kw)
        if n:
            # make sure notification is flushed in time for task to process it
            session(n).flush(n)
            allura.tasks.notification_tasks.notify.post(
                n._id, artifact.index_id(), topic)
        return n

    @classmethod
    def post_user(cls, user, artifact, topic, **kw):
        '''Create a notification and deliver directly to a user's flash
    mailbox'''
        try:
            mbox = Mailbox(user_id=user._id, is_flash=True,
                           project_id=None,
                           app_config_id=None)
            session(mbox).flush(mbox)
        except pymongo.errors.DuplicateKeyError:
            session(mbox).expunge(mbox)
            mbox = Mailbox.query.get(user_id=user._id, is_flash=True)
        n = cls._make_notification(artifact, topic, **kw)
        if n:
            mbox.queue.append(n._id)
            mbox.queue_empty = False
        return n

    @classmethod
    def _make_notification(cls, artifact, topic, **kwargs):
        '''
        Create a Notification instance based on an artifact.  Special handling
        for comments when topic=='message'
        '''

        from allura.model import Project
        idx = artifact.index() if artifact else None
        subject_prefix = '[%s:%s] ' % (
            c.project.shortname, c.app.config.options.mount_point)
        post = ''
        if topic == 'message':
            post = kwargs.pop('post')
            text = post.text
            file_info = kwargs.pop('file_info', None)
            if file_info is not None:
                text = "%s\n\n\nAttachment:" % text
                if not isinstance(file_info, list):
                    file_info = [file_info]
                for attach in file_info:
                    attach.file.seek(0, 2)
                    bytecount = attach.file.tell()
                    attach.file.seek(0)
                    text = "%s %s (%s; %s) " % (text, attach.filename, h.do_filesizeformat(bytecount), attach.type)

            subject = post.subject or ''
            if post.parent_id and not subject.lower().startswith('re:'):
                subject = 'Re: ' + subject
            author = post.author()
            msg_id = artifact.url() + post._id
            parent_msg_id = artifact.url() + post.parent_id if post.parent_id else artifact.message_id()
            d = dict(
                _id=msg_id,
                from_address=str(author._id) if author != User.anonymous() else None,
                reply_to_address='"%s" <%s>' % (
                    subject_prefix, getattr(artifact, 'email_address', u'*****@*****.**')),
                subject=subject_prefix + subject,
                text=text,
                in_reply_to=parent_msg_id,
                author_id=author._id,
                pubdate=datetime.utcnow())
        elif topic == 'flash':
            n = cls(topic=topic,
                    text=kwargs['text'],
                    subject=kwargs.pop('subject', ''))
            return n
        else:
            subject = kwargs.pop('subject', '%s modified by %s' % (
                    h.get_first(idx, 'title'),c.user.get_pref('display_name')))
            reply_to = '"%s" <%s>' % (
                h.get_first(idx, 'title'),
                getattr(artifact, 'email_address', u'*****@*****.**'))
            d = dict(
                from_address=reply_to,
                reply_to_address=reply_to,
                subject=subject_prefix + subject,
                text=kwargs.pop('text', subject),
                author_id=c.user._id,
                pubdate=datetime.utcnow())
            if kwargs.get('message_id'):
                d['_id'] = kwargs['message_id']
            if c.user.get_pref('email_address'):
                d['from_address'] = '"%s" <%s>' % (
                    c.user.get_pref('display_name'),
                    c.user.get_pref('email_address'))
            elif c.user.email_addresses:
                d['from_address'] = '"%s" <%s>' % (
                    c.user.get_pref('display_name'),
                    c.user.email_addresses[0])
        if not d.get('text'):
            d['text'] = ''
        try:
            ''' Add addional text to the notification e-mail based on the artifact type '''
            template = cls.view.get_template('mail/' + artifact.type_s + '.txt')
            d['text'] += template.render(dict(c=c, g=g, config=config, data=artifact, post=post, h=h))
        except jinja2.TemplateNotFound:
            pass
        except:
            ''' Catch any errors loading or rendering the template,
            but the notification still gets sent if there is an error
            '''
            log.warn('Could not render notification template %s' % artifact.type_s, exc_info=True)

        assert d['reply_to_address'] is not None
        project = c.project
        if d.get('project_id', c.project._id) != c.project._id:
            project = Project.query.get(_id=d['project_id'])
        if project.notifications_disabled:
            log.info('Notifications disabled for project %s, not sending %s(%r)',
                     project.shortname, topic, artifact)
            return None
        n = cls(ref_id=artifact.index_id(),
                topic=topic,
                link=kwargs.pop('link', artifact.url()),
                **d)
        return n

    def footer(self, toaddr=''):
        return self.ref.artifact.get_mail_footer(self, toaddr)

    def _sender(self):
        from allura.model import AppConfig
        app_config = AppConfig.query.get(_id=self.app_config_id)
        app = app_config.project.app_instance(app_config)
        return app.email_address if app else None

    def send_simple(self, toaddr):
        allura.tasks.mail_tasks.sendsimplemail.post(
            toaddr=toaddr,
            fromaddr=self.from_address,
            reply_to=self.reply_to_address,
            subject=self.subject,
            sender=self._sender(),
            message_id=self._id,
            in_reply_to=self.in_reply_to,
            text=(self.text or '') + self.footer(toaddr))

    def send_direct(self, user_id):
        user = User.query.get(_id=ObjectId(user_id), disabled=False)
        artifact = self.ref.artifact
        log.debug('Sending direct notification %s to user %s', self._id, user_id)
        # Don't send if user disabled
        if not user:
            log.debug("Skipping notification - enabled user %s not found" % user_id)
            return
        # Don't send if user doesn't have read perms to the artifact
        if user and artifact and \
                not security.has_access(artifact, 'read', user)():
            log.debug("Skipping notification - User %s doesn't have read "
                      "access to artifact %s" % (user_id, str(self.ref_id)))
            log.debug("User roles [%s]; artifact ACL [%s]; PSC ACL [%s]",
                    ', '.join([str(r) for r in security.Credentials.get().user_roles(user_id=user_id, project_id=artifact.project._id).reaching_ids]),
                    ', '.join([str(a) for a in artifact.acl]),
                    ', '.join([str(a) for a in artifact.parent_security_context().acl]))
            return
        allura.tasks.mail_tasks.sendmail.post(
            destinations=[str(user_id)],
            fromaddr=self.from_address,
            reply_to=self.reply_to_address,
            subject=self.subject,
            message_id=self._id,
            in_reply_to=self.in_reply_to,
            sender=self._sender(),
            text=(self.text or '') + self.footer())

    @classmethod
    def send_digest(self, user_id, from_address, subject, notifications,
                    reply_to_address=None):
        if not notifications: return
        user = User.query.get(_id=ObjectId(user_id), disabled=False)
        if not user:
            log.debug("Skipping notification - enabled user %s not found " % user_id)
            return
        # Filter out notifications for which the user doesn't have read
        # permissions to the artifact.
        artifact = self.ref.artifact
        def perm_check(notification):
            return not (user and artifact) or \
                    security.has_access(artifact, 'read', user)()
        notifications = filter(perm_check, notifications)

        log.debug('Sending digest of notifications [%s] to user %s', ', '.join([n._id for n in notifications]), user_id)
        if reply_to_address is None:
            reply_to_address = from_address
        text = [ 'Digest of %s' % subject ]
        for n in notifications:
            text.append('From: %s' % n.from_address)
            text.append('Subject: %s' % (n.subject or '(no subject)'))
            text.append('Message-ID: %s' % n._id)
            text.append('')
            text.append(n.text or '-no text-')
        text.append(n.footer())
        text = '\n'.join(text)
        allura.tasks.mail_tasks.sendmail.post(
            destinations=[str(user_id)],
            fromaddr=from_address,
            reply_to=reply_to_address,
            subject=subject,
            message_id=h.gen_message_id(),
            text=text)

    @classmethod
    def send_summary(self, user_id, from_address, subject, notifications):
        if not notifications: return
        log.debug('Sending summary of notifications [%s] to user %s', ', '.join([n._id for n in notifications]), user_id)
        text = [ 'Digest of %s' % subject ]
        for n in notifications:
            text.append('From: %s' % n.from_address)
            text.append('Subject: %s' % (n.subject or '(no subject)'))
            text.append('Message-ID: %s' % n._id)
            text.append('')
            text.append(h.text.truncate(n.text or '-no text-', 128))
        text.append(n.footer())
        text = '\n'.join(text)
        allura.tasks.mail_tasks.sendmail.post(
            destinations=[str(user_id)],
            fromaddr=from_address,
            reply_to=from_address,
            subject=subject,
            message_id=h.gen_message_id(),
            text=text)
Example #27
0
class Mailbox(MappedClass):
    '''
    Holds a queue of notifications for an artifact, or a user (webflash messages)
    for a subscriber.
    FIXME: describe the Mailbox concept better.
    '''

    class __mongometa__:
        session = main_orm_session
        name = 'mailbox'
        unique_indexes = [
            ('user_id', 'project_id', 'app_config_id',
             'artifact_index_id', 'topic', 'is_flash'),
            ]
        indexes = [
            ('project_id', 'artifact_index_id'),
            ('is_flash', 'user_id'),
            ('type', 'next_scheduled'),  # for q_digest
            ('type', 'queue_empty'),  # for q_direct
            ('project_id', 'app_config_id', 'artifact_index_id', 'topic'), # for deliver()
        ]

    _id = FieldProperty(S.ObjectId)
    user_id = ForeignIdProperty('User', if_missing=lambda:c.user._id)
    project_id = ForeignIdProperty('Project', if_missing=lambda:c.project._id)
    app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)

    # Subscription filters
    artifact_title = FieldProperty(str)
    artifact_url = FieldProperty(str)
    artifact_index_id = FieldProperty(str)
    topic = FieldProperty(str)

    # Subscription type
    is_flash = FieldProperty(bool, if_missing=False)
    type = FieldProperty(S.OneOf('direct', 'digest', 'summary', 'flash'))
    frequency = FieldProperty(dict(
            n=int,unit=S.OneOf('day', 'week', 'month')))
    next_scheduled = FieldProperty(datetime, if_missing=datetime.utcnow)
    last_modified = FieldProperty(datetime, if_missing=datetime(2000,1,1))

    # a list of notification _id values
    queue = FieldProperty([str])
    queue_empty = FieldProperty(bool)

    project = RelationProperty('Project')
    app_config = RelationProperty('AppConfig')

    @classmethod
    def subscribe(
        cls,
        user_id=None, project_id=None, app_config_id=None,
        artifact=None, topic=None,
        type='direct', n=1, unit='day'):
        if user_id is None: user_id = c.user._id
        if project_id is None: project_id = c.project._id
        if app_config_id is None: app_config_id = c.app.config._id
        tool_already_subscribed = cls.query.get(user_id=user_id,
            project_id=project_id,
            app_config_id=app_config_id,
            artifact_index_id=None)
        if tool_already_subscribed:
            return
        if artifact is None:
            artifact_title = 'All artifacts'
            artifact_url = None
            artifact_index_id = None
        else:
            i = artifact.index()
            artifact_title = h.get_first(i, 'title')
            artifact_url = artifact.url()
            artifact_index_id = i['id']
            artifact_already_subscribed = cls.query.get(user_id=user_id,
                project_id=project_id,
                app_config_id=app_config_id,
                artifact_index_id=artifact_index_id)
            if artifact_already_subscribed:
                return
        d = dict(user_id=user_id, project_id=project_id, app_config_id=app_config_id,
                 artifact_index_id=artifact_index_id, topic=topic)
        sess = session(cls)
        try:
            mbox = cls(
                type=type, frequency=dict(n=n, unit=unit),
                artifact_title=artifact_title,
                artifact_url=artifact_url,
                **d)
            sess.flush(mbox)
        except pymongo.errors.DuplicateKeyError:
            sess.expunge(mbox)
            mbox = cls.query.get(**d)
            mbox.artifact_title = artifact_title
            mbox.artifact_url = artifact_url
            mbox.type = type
            mbox.frequency.n = n
            mbox.frequency.unit = unit
            sess.flush(mbox)
        if not artifact_index_id:
            # Unsubscribe from individual artifacts when subscribing to the tool
            for other_mbox in cls.query.find(dict(
                user_id=user_id, project_id=project_id, app_config_id=app_config_id)):
                if other_mbox is not mbox:
                    other_mbox.delete()

    @classmethod
    def unsubscribe(
        cls,
        user_id=None, project_id=None, app_config_id=None,
        artifact_index_id=None, topic=None):
        if user_id is None: user_id = c.user._id
        if project_id is None: project_id = c.project._id
        if app_config_id is None: app_config_id = c.app.config._id
        cls.query.remove(dict(
                user_id=user_id,
                project_id=project_id,
                app_config_id=app_config_id,
                artifact_index_id=artifact_index_id,
                topic=topic))

    @classmethod
    def subscribed(
        cls, user_id=None, project_id=None, app_config_id=None,
        artifact=None, topic=None):
        if user_id is None: user_id = c.user._id
        if project_id is None: project_id = c.project._id
        if app_config_id is None: app_config_id = c.app.config._id
        if artifact is None:
            artifact_index_id = None
        else:
            i = artifact.index()
            artifact_index_id = i['id']
        return cls.query.find(dict(
                user_id=user_id,
                project_id=project_id,
                app_config_id=app_config_id,
                artifact_index_id=artifact_index_id)).count() != 0

    @classmethod
    def deliver(cls, nid, artifact_index_id, topic):
        '''Called in the notification message handler to deliver notification IDs
        to the appropriate mailboxes.  Atomically appends the nids
        to the appropriate mailboxes.
        '''
        d = {
            'project_id':c.project._id,
            'app_config_id':c.app.config._id,
            'artifact_index_id':{'$in':[None, artifact_index_id]},
            'topic':{'$in':[None, topic]}
            }
        mboxes = cls.query.find(d).all()
        log.debug('Delivering notification %s to mailboxes [%s]', nid, ', '.join([str(m._id) for m in mboxes]))
        for mbox in mboxes:
            try:
                mbox.query.update(
                    {'$push':dict(queue=nid),
                     '$set':dict(last_modified=datetime.utcnow(),
                                 queue_empty=False),
                    })
                # Make sure the mbox doesn't stick around to be flush()ed
                session(mbox).expunge(mbox)
            except:
                # log error but try to keep processing, lest all the other eligible
                # mboxes for this notification get skipped and lost forever
                log.exception(
                    'Error adding notification: %s for artifact %s on project %s to user %s',
                    nid, artifact_index_id, c.project._id, mbox.user_id)

    @classmethod
    def fire_ready(cls):
        '''Fires all direct subscriptions with notifications as well as
        all summary & digest subscriptions with notifications that are ready.
        Clears the mailbox queue.
        '''
        now = datetime.utcnow()
        # Queries to find all matching subscription objects
        q_direct = dict(
            type='direct',
            queue_empty=False,
        )
        if MAILBOX_QUIESCENT:
            q_direct['last_modified']={'$lt':now - MAILBOX_QUIESCENT}
        q_digest = dict(
            type={'$in': ['digest', 'summary']},
            next_scheduled={'$lt':now})

        def find_and_modify_direct_mbox():
            return cls.query.find_and_modify(
                query=q_direct,
                update={'$set': dict(
                            queue=[],
                            queue_empty=True,
                        )},
                new=False)

        for mbox in take_while_true(find_and_modify_direct_mbox):
            try:
                mbox.fire(now)
            except:
                log.exception('Error firing mbox: %s with queue: [%s]', str(mbox._id), ', '.join(mbox.queue))
                raise  # re-raise so we don't keep (destructively) trying to process mboxes

        for mbox in cls.query.find(q_digest):
            next_scheduled = now
            if mbox.frequency.unit == 'day':
                next_scheduled += timedelta(days=mbox.frequency.n)
            elif mbox.frequency.unit == 'week':
                next_scheduled += timedelta(days=7 * mbox.frequency.n)
            elif mbox.frequency.unit == 'month':
                next_scheduled += timedelta(days=30 * mbox.frequency.n)
            mbox = cls.query.find_and_modify(
                query=dict(_id=mbox._id),
                update={'$set': dict(
                        next_scheduled=next_scheduled,
                        queue=[],
                        queue_empty=True,
                        )},
                new=False)
            mbox.fire(now)

    def fire(self, now):
        '''
        Send all notifications that this mailbox has enqueued.
        '''
        notifications = Notification.query.find(dict(_id={'$in':self.queue}))
        notifications = notifications.all()
        if len(notifications) != len(self.queue):
            log.error('Mailbox queue error: Mailbox %s queued [%s], found [%s]', str(self._id), ', '.join(self.queue), ', '.join([n._id for n in notifications]))
        else:
            log.debug('Firing mailbox %s notifications [%s], found [%s]', str(self._id), ', '.join(self.queue), ', '.join([n._id for n in notifications]))
        if self.type == 'direct':
            ngroups = defaultdict(list)
            for n in notifications:
                try:
                    if n.topic == 'message':
                        n.send_direct(self.user_id)
                        # Messages must be sent individually so they can be replied
                        # to individually
                    else:
                        key = (n.subject, n.from_address, n.reply_to_address, n.author_id)
                        ngroups[key].append(n)
                except:
                    # log error but keep trying to deliver other notifications,
                    # lest the other notifications (which have already been removed
                    # from the mobx's queue in mongo) be lost
                    log.exception(
                        'Error sending notification: %s to mbox %s (user %s)',
                        n._id, self._id, self.user_id)
            # Accumulate messages from same address with same subject
            for (subject, from_address, reply_to_address, author_id), ns in ngroups.iteritems():
                try:
                    if len(ns) == 1:
                        ns[0].send_direct(self.user_id)
                    else:
                        Notification.send_digest(
                            self.user_id, from_address, subject, ns, reply_to_address)
                except:
                    # log error but keep trying to deliver other notifications,
                    # lest the other notifications (which have already been removed
                    # from the mobx's queue in mongo) be lost
                    log.exception(
                        'Error sending notifications: [%s] to mbox %s (user %s)',
                        ', '.join([n._id for n in ns]), self._id, self.user_id)
        elif self.type == 'digest':
            Notification.send_digest(
                self.user_id, u'*****@*****.**', 'Digest Email',
                notifications)
        elif self.type == 'summary':
            Notification.send_summary(
                self.user_id, u'*****@*****.**', 'Digest Email',
                notifications)
Example #28
0
class Feed(MappedClass):
    """
    Used to generate rss/atom feeds.  This does not need to be extended;
    all feed items go into the same collection
    """
    class __mongometa__:
        session = project_orm_session
        name = 'artifact_feed'
        indexes = [
            'pubdate',
            ('artifact_ref.project_id', 'artifact_ref.mount_point'),
            (('ref_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
            (('project_id', pymongo.ASCENDING),
             ('app_config_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
            'author_link',  # used in ext/user_profile/user_main.py for user feeds
        ]

    _id = FieldProperty(S.ObjectId)
    ref_id = ForeignIdProperty('ArtifactReference')
    neighborhood_id = ForeignIdProperty('Neighborhood')
    project_id = ForeignIdProperty('Project')
    app_config_id = ForeignIdProperty('AppConfig')
    tool_name=FieldProperty(str)
    title=FieldProperty(str)
    link=FieldProperty(str)
    pubdate = FieldProperty(datetime, if_missing=datetime.utcnow)
    description = FieldProperty(str)
    unique_id = FieldProperty(str, if_missing=lambda:h.nonce(40))
    author_name = FieldProperty(str, if_missing=lambda:c.user.get_pref('display_name') if hasattr(c, 'user') else None)
    author_link = FieldProperty(str, if_missing=lambda:c.user.url() if hasattr(c, 'user') else None)
    artifact_reference = FieldProperty(S.Deprecated)


    @classmethod
    def post(cls, artifact, title=None, description=None, author=None, author_link=None, author_name=None, pubdate=None, link=None, **kw):
        """
        Create a Feed item.  Returns the item.
        But if anon doesn't have read access, create does not happen and None is returned
        """
        # TODO: fix security system so we can do this correctly and fast
        from allura import model as M
        anon = M.User.anonymous()
        if not security.has_access(artifact, 'read', user=anon):
            return
        if not security.has_access(c.project, 'read', user=anon):
            return
        idx = artifact.index()
        if author is None:
            author = c.user
        if author_name is None:
            author_name = author.get_pref('display_name')
        if title is None:
            title='%s modified by %s' % (h.get_first(idx, 'title'), author_name)
        if description is None: description = title
        if pubdate is None:
            pubdate = datetime.utcnow()
        if link is None:
            link=artifact.url()
        item = cls(
            ref_id=artifact.index_id(),
            neighborhood_id=artifact.app_config.project.neighborhood_id,
            project_id=artifact.app_config.project_id,
            app_config_id=artifact.app_config_id,
            tool_name=artifact.app_config.tool_name,
            title=title,
            description=g.markdown.convert(description),
            link=link,
            pubdate=pubdate,
            author_name=author_name,
            author_link=author_link or author.url())
        unique_id = kw.pop('unique_id', None)
        if unique_id:
            item.unique_id = unique_id
        return item

    @classmethod
    def feed(cls, q, feed_type, title, link, description,
             since=None, until=None, offset=None, limit=None):
        "Produces webhelper.feedgenerator Feed"
        d = dict(title=title, link=h.absurl(link), description=description, language=u'en')
        if feed_type == 'atom':
            feed = FG.Atom1Feed(**d)
        elif feed_type == 'rss':
            feed = FG.Rss201rev2Feed(**d)
        query = defaultdict(dict)
        query.update(q)
        if since is not None:
            query['pubdate']['$gte'] = since
        if until is not None:
            query['pubdate']['$lte'] = until
        cur = cls.query.find(query)
        cur = cur.sort('pubdate', pymongo.DESCENDING)
        if limit is None: limit = 10
        query = cur.limit(limit)
        if offset is not None: query = cur.offset(offset)
        for r in cur:
            feed.add_item(title=r.title,
                          link=h.absurl(r.link.encode('utf-8')),
                          pubdate=r.pubdate,
                          description=r.description,
                          unique_id=h.absurl(r.unique_id),
                          author_name=r.author_name,
                          author_link=h.absurl(r.author_link))
        return feed
Example #29
0
class Artifact(MappedClass):
    """
    The base class for anything you want to keep track of.

    It will automatically be added to solr (see index() method).  It also
    gains a discussion thread and can have files attached to it.

    :var tool_version: default's to the app's version
    :var acl: dict of permission name => [roles]
    :var labels: list of plain old strings
    :var references: list of outgoing references to other tickets
    :var backreferences: dict of incoming references to this artifact, mapped by solr id
    """

    class __mongometa__:
        session = artifact_orm_session
        name='artifact'
        indexes = [ 'app_config_id', ('labels', 'app_config_id') ]
        def before_save(data):
            if not getattr(artifact_orm_session._get(), 'skip_mod_date', False):
                data['mod_date'] = datetime.utcnow()
            else:
                log.debug('Not updating mod_date')
            if c.project:
                c.project.last_updated = datetime.utcnow()
    type_s = 'Generic Artifact'

    # Artifact base schema
    _id = FieldProperty(S.ObjectId)
    mod_date = FieldProperty(datetime, if_missing=datetime.utcnow)
    app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
    plugin_verson = FieldProperty(S.Deprecated)
    tool_version = FieldProperty(
        { str: str },
        if_missing=lambda:{c.app.config.tool_name:c.app.__version__})
    acl = FieldProperty(ACL)
    tags = FieldProperty(S.Deprecated)
    labels = FieldProperty([str])
    references = FieldProperty(S.Deprecated)
    backreferences = FieldProperty(S.Deprecated)
    app_config = RelationProperty('AppConfig')
    # Not null if artifact originated from external import, then API ticket id
    import_id = FieldProperty(str, if_missing=None)
    deleted=FieldProperty(bool, if_missing=False)

    def __json__(self):
        return dict(
            _id=str(self._id),
            mod_date=self.mod_date,
            labels=self.labels,
            related_artifacts=[a.url() for a in self.related_artifacts()],
            discussion_thread=self.discussion_thread,
            discussion_thread_url=h.absurl('/rest%s' % self.discussion_thread.url()),
        )

    def parent_security_context(self):
        '''ACL processing should continue at the  AppConfig object. This lets
        AppConfigs provide a 'default' ACL for all artifacts in the tool.'''
        return self.app_config

    @classmethod
    def attachment_class(cls):
        raise NotImplementedError, 'attachment_class'

    @classmethod
    def translate_query(cls, q, fields):
        for f in fields:
            if '_' in f:
                base, typ = f.rsplit('_', 1)
                q = q.replace(base + ':', f + ':')
        return q

    @LazyProperty
    def ref(self):
        return ArtifactReference.from_artifact(self)

    @LazyProperty
    def refs(self):
        return self.ref.references

    @LazyProperty
    def backrefs(self):
        q = ArtifactReference.query.find(dict(references=self.index_id()))
        return [ aref._id for aref in q ]

    def related_artifacts(self):
        related_artifacts = []
        for ref_id in self.refs+self.backrefs:
            ref = ArtifactReference.query.get(_id=ref_id)
            if ref is None: continue
            artifact = ref.artifact
            if artifact is None: continue
            artifact = artifact.primary()
            # don't link to artifacts in deleted tools
            if hasattr(artifact, 'app_config') and artifact.app_config is None:
                continue
            if artifact.type_s == 'Commit' and not artifact.repo:
                ac = AppConfig.query.get(
                        _id=ref.artifact_reference['app_config_id'])
                app = ac.project.app_instance(ac) if ac else None
                if app:
                    artifact.set_context(app.repo)
            if artifact not in related_artifacts and (getattr(artifact, 'deleted', False) == False):
                related_artifacts.append(artifact)
        return related_artifacts

    def subscribe(self, user=None, topic=None, type='direct', n=1, unit='day'):
        from allura.model import Mailbox
        if user is None: user = c.user
        Mailbox.subscribe(
            user_id=user._id,
            project_id=self.app_config.project_id,
            app_config_id=self.app_config._id,
            artifact=self, topic=topic,
            type=type, n=n, unit=unit)

    def unsubscribe(self, user=None):
        from allura.model import Mailbox
        if user is None: user = c.user
        Mailbox.unsubscribe(
            user_id=user._id,
            project_id=self.app_config.project_id,
            app_config_id=self.app_config._id,
            artifact_index_id=self.index_id())

    def primary(self):
        '''If an artifact is a "secondary" artifact (discussion of a ticket, for
        instance), return the artifact that is the "primary".
        '''
        return self

    @classmethod
    def artifacts_labeled_with(cls, label, app_config):
        """Return all artifacts of type `cls` that have the label `label` and
        are in the tool denoted by `app_config`.
        """
        return cls.query.find({'labels':label, 'app_config_id': app_config._id})

    def email_link(self, subject='artifact'):
        if subject:
            return 'mailto:%s?subject=[%s:%s:%s] Re: %s' % (
                self.email_address,
                self.app_config.project.shortname,
                self.app_config.options.mount_point,
                self.shorthand_id(),
                subject)
        else:
            return 'mailto:%s' % self.email_address

    @property
    def project(self):
        return self.app_config.project

    @property
    def project_id(self):
        return self.app_config.project_id

    @LazyProperty
    def app(self):
        if not self.app_config:
            return None
        if getattr(c, 'app', None) and c.app.config._id == self.app_config._id:
            return c.app
        else:
            return self.app_config.load()(self.project, self.app_config)

    def index_id(self):
        '''Globally unique artifact identifier.  Used for
        SOLR ID, shortlinks, and maybe elsewhere
        '''
        id = '%s.%s#%s' % (
            self.__class__.__module__,
            self.__class__.__name__,
            self._id)
        return id.replace('.', '/')

    def index(self):
        """
        Subclasses should override this, providing a dictionary of solr_field => value.
        These fields & values will be stored by solr.  Subclasses should call the
        super() index() and then extend it with more fields.

        The _s and _t suffixes, for example, follow solr dynamic field naming
        pattern.
        You probably want to override at least title and text to have
        meaningful search results and email senders.
        """

        project = self.project
        return dict(
            id=self.index_id(),
            mod_date_dt=self.mod_date,
            title='Artifact %s' % self._id,
            project_id_s=str(project._id),
            project_name_t=project.name,
            project_shortname_t=project.shortname,
            tool_name_s=self.app_config.tool_name,
            mount_point_s=self.app_config.options.mount_point,
            is_history_b=False,
            url_s=self.url(),
            type_s=self.type_s,
            labels_t=' '.join(l for l in self.labels),
            snippet_s='',
            deleted_b=self.deleted)

    def url(self):
        """
        Subclasses should implement this, providing the URL to the artifact
        """
        raise NotImplementedError, 'url' # pragma no cover

    def shorthand_id(self):
        '''How to refer to this artifact within the app instance context.

        For a wiki page, it might be the title.  For a ticket, it might be the
        ticket number.  For a discussion, it might be the message ID.  Generally
        this should have a strong correlation to the URL.
        '''
        return str(self._id) # pragma no cover

    def link_text(self):
        '''The link text that will be used when a shortlink to this artifact
        is expanded into an <a></a> tag.

        By default this method returns shorthand_id(). Subclasses should
        override this method to provide more descriptive link text.
        '''
        return self.shorthand_id()

    def get_discussion_thread(self, data=None):
        '''Return the discussion thread for this artifact (possibly made more
        specific by the message_data)'''
        from .discuss import Thread
        t = Thread.query.get(ref_id=self.index_id())
        if t is None:
            idx = self.index()
            t = Thread.new(
                discussion_id=self.app_config.discussion_id,
                ref_id=idx['id'],
                subject='%s discussion' % h.get_first(idx, 'title'))
        parent_id = None
        if data:
            in_reply_to = data.get('in_reply_to', [])
            if in_reply_to:
                parent_id = in_reply_to[0]
        return t, parent_id

    @LazyProperty
    def discussion_thread(self):
        return self.get_discussion_thread()[0]

    def attach(self, filename, fp, **kw):
        att = self.attachment_class().save_attachment(
            filename=filename,
            fp=fp, artifact_id=self._id, **kw)
        return att

    def delete(self):
        ArtifactReference.query.remove(dict(_id=self.index_id()))
        super(Artifact, self).delete()
Example #30
0
class ProjectRole(MappedClass):
    """
    Per-project roles, called "Groups" in the UI.
    This can be a proxy for a single user.  It can also inherit roles.

    :var user_id: used if this role is for a single user
    :var project_id:
    :var name:
    :var roles: a list of other :class:`ProjectRole` ``ObjectId`` values.  These roles are delegated through the
                current role.
    """

    class __mongometa__:
        session = main_orm_session
        name = str('project_role')
        unique_indexes = [('user_id', 'project_id', 'name')]
        indexes = [
            ('user_id',),
            ('project_id', 'name'),  # used in ProjectRole.by_name()
            ('roles',),
        ]

    _id = FieldProperty(S.ObjectId)
    user_id = AlluraUserProperty(if_missing=None)
    project_id = ForeignIdProperty('Project', if_missing=None)
    name = FieldProperty(str)
    roles = FieldProperty([S.ObjectId])

    user = RelationProperty('User')
    project = RelationProperty('Project')

    def __init__(self, **kw):
        assert 'project_id' in kw, 'Project roles must specify a project id'
        super(ProjectRole, self).__init__(**kw)

    def display(self):
        if self.name:
            return self.name
        if self.user_id:
            u = self.user
            if u.username:
                uname = u.username
            elif u.get_pref('display_name'):
                uname = u.get_pref('display_name')
            else:
                uname = u._id
            return '*user-%s' % uname
        return '**unknown name role: %s' % self._id  # pragma no cover

    @classmethod
    def by_user(cls, user, project=None, upsert=False):
        if project is None:
            project = c.project
        if user.is_anonymous():
            return cls.anonymous(project)
        if upsert:
            return cls.upsert(
                user_id=user._id,
                project_id=project.root_project._id,
            )
        else:
            return cls.query.get(
                user_id=user._id,
                project_id=project.root_project._id,
            )

    @classmethod
    def by_name(cls, name, project=None):
        if project is None:
            project = c.project
        if hasattr(project, 'root_project'):
            project = project.root_project
        if hasattr(project, '_id'):
            project_id = project._id
        else:
            project_id = project
        role = cls.query.get(
            name=name,
            project_id=project_id)
        return role

    @classmethod
    def anonymous(cls, project=None):
        return cls.by_name('*anonymous', project)

    @classmethod
    def authenticated(cls, project=None):
        return cls.by_name('*authenticated', project)

    @classmethod
    def upsert(cls, **kw):
        obj = cls.query.get(**kw)
        if obj is not None:
            return obj
        try:
            obj = cls(**kw)
            session(obj).insert_now(obj, state(obj))
        except pymongo.errors.DuplicateKeyError:
            session(obj).expunge(obj)
            obj = cls.query.get(**kw)
        return obj

    @property
    def special(self):
        if self.name:
            return '*' == self.name[0]
        if self.user_id:
            return True
        return False  # pragma no cover

    @property
    def user(self):
        if (self.user_id is None
            and self.name
            and self.name != '*anonymous'):
            return None
        return User.query.get(_id=self.user_id)

    @property
    def settings_href(self):
        if self.name in ('Admin', 'Developer', 'Member'):
            return None
        return self.project.url() + 'admin/groups/' + str(self._id) + '/'

    def parent_roles(self):
        return self.query.find({'roles': self._id}).all()

    def child_roles(self):
        to_check = [] + self.roles
        found_roles = []
        while to_check:
            checking = to_check.pop()
            for role in self.query.find({'_id': checking}).all():
                if role not in found_roles:
                    found_roles.append(role)
                    to_check = to_check + role.roles
        return found_roles

    def users_with_role(self, project=None):
        if not project:
            project = c.project
        return self.query.find(dict(project_id=project._id,
                                    user_id={'$ne': None}, roles=self._id)).all()