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
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
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
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)
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
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')
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
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")
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()
class DocumentCategoryReference(SproxTestClass): class __mongometa__: name = 'document_category_reference_rs' _id = FieldProperty(S.ObjectId) document_category_id = ForeignIdProperty(DocumentCategory) category = RelationProperty(DocumentCategory)
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)
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()
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)
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)
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)
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)
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)
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])
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)
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)
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
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
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
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()
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)
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)
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
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()
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()