class ImportedPost(Post): """ A Post that originated outside of the Assembl system (was imported from elsewhere). """ __tablename__ = "imported_post" __table_args__ = (UniqueConstraint('source_post_id', 'source_id'), ) id = Column(Integer, ForeignKey('post.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True) import_date = Column(DateTime, nullable=False, default=datetime.utcnow) source_post_id = Column( CoerceUnicode(), nullable=False, doc= "The source-specific unique id of the imported post. A listener keeps the message_id in the post class in sync" ) source_id = Column('source_id', Integer, ForeignKey('post_source.id', ondelete='CASCADE'), nullable=False, info={'rdf': QuadMapPatternS(None, ASSEMBL.has_origin)}) source = relationship("PostSource", backref=backref('contents')) body_mime_type = Column( CoerceUnicode(), nullable=False, doc= "The mime type of the body of the imported content. See Content::get_body_mime_type() for allowed values." ) imported_blob = deferred(Column(Binary), group='raw_details') __mapper_args__ = { 'polymorphic_identity': 'imported_post', } def get_body_mime_type(self): return self.body_mime_type def unique_query(self): query, _ = super(ImportedPost, self).unique_query() source_id = self.source_id or self.source.id return query.filter_by(source_id=source_id, source_post_id=self.source_post_id), True
def upgrade(pyramid_env): with context.begin_transaction(): op.create_table( 'facebook_account', sa.Column('id', sa.Integer, sa.ForeignKey('idprovider_agent_account.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), sa.Column('app_id', sa.String(512))) op.create_table( 'facebook_source', sa.Column('id', sa.Integer, sa.ForeignKey('post_source.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), sa.Column('fb_source_id', sa.String(512), nullable=False), sa.Column('url_path', sa.String(1024)), sa.Column( 'creator_id', sa.Integer, sa.ForeignKey('facebook_account.id', onupdate='CASCADE', ondelete='CASCADE'))) op.create_table( 'facebook_post', sa.Column('id', sa.Integer, sa.ForeignKey('imported_post.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), sa.Column('attachment', sa.String(1024)), sa.Column('link_name', CoerceUnicode(1024)), sa.Column('post_type', sa.String(20))) op.create_table( 'facebook_access_token', sa.Column('id', sa.Integer, primary_key=True), sa.Column( 'fb_account_id', sa.Integer, sa.ForeignKey('facebook_account.id', onupdate='CASCADE', ondelete='CASCADE')), sa.Column('token', sa.String(512), unique=True), sa.Column('expiration', sa.DateTime), sa.Column('token_type', sa.String(50)), sa.Column('object_name', sa.String(512)), sa.Column('object_fb_id', sa.String(512))) # Do stuff with the app's models here. from assembl import models as m db = m.get_session_maker()() with transaction.manager: pass
class AbstractFilesystemMailbox(AbstractMailbox): """ A Mailbox refers to an Email inbox that is stored the server's filesystem. """ __tablename__ = "source_filesystemmailbox" id = Column(Integer, ForeignKey('mailbox.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True) filesystem_path = Column(CoerceUnicode(), nullable=False) __mapper_args__ = { 'polymorphic_identity': 'source_filesystemmailbox', }
class Attachment(DiscussionBoundBase): """ Represents a Document or file, local to the database or (more typically) a remote document """ __tablename__ = "attachment" id = Column(Integer, primary_key=True) creation_date = Column( DateTime, nullable=False, default=datetime.utcnow, info={'rdf': QuadMapPatternS(None, DCTERMS.created)}) discussion_id = Column( Integer, ForeignKey( 'discussion.id', ondelete='CASCADE', onupdate='CASCADE', ), nullable=False, ) discussion = relationship( "Discussion", backref=backref('attachments', cascade="all, delete-orphan"), ) document_id = Column( Integer, ForeignKey( 'document.id', ondelete='CASCADE', onupdate='CASCADE', ), nullable=False, ) document = relationship( Document, backref=backref('attachments'), ) creator_id = Column(Integer, ForeignKey('agent_profile.id'), nullable=False) creator = relationship(AgentProfile) title = Column(CoerceUnicode(1024), server_default="", info={'rdf': QuadMapPatternS(None, DCTERMS.title)}) description = Column( UnicodeText, info={'rdf': QuadMapPatternS(None, DCTERMS.description)}) attachmentPurpose = Column(CoerceUnicode(256), nullable=False, index=True) __mapper_args__ = { 'polymorphic_identity': 'attachment', 'with_polymorphic': '*' } def get_discussion_id(self): return self.discussion_id or self.discussion.id @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.id == discussion_id, )
class Document(DiscussionBoundBase): """ Represents a Document or file, local to the database or (more typically) a remote document """ __tablename__ = "document" id = Column(Integer, primary_key=True) """ The cannonical identifier of this document. If a URL, it's to be interpreted as a purl """ uri_id = Column( CoerceUnicode(514), unique=False, index=True ) ## MAP: Change to true once https://app.asana.com/0/51461630427071/52921943509398 is done creation_date = Column( DateTime, nullable=False, default=datetime.utcnow, info={'rdf': QuadMapPatternS(None, DCTERMS.created)}) discussion_id = Column( Integer, ForeignKey( 'discussion.id', ondelete='CASCADE', onupdate='CASCADE', ), nullable=False, ) discussion = relationship( "Discussion", backref=backref('documents', cascade="all, delete-orphan"), ) oembed_type = Column(CoerceUnicode(1024), server_default="") mime_type = Column(CoerceUnicode(1024), server_default="") #From metadata, not the user title = Column(CoerceUnicode(), server_default="", info={'rdf': QuadMapPatternS(None, DCTERMS.title)}) #From metadata, not the user description = Column( UnicodeText, info={'rdf': QuadMapPatternS(None, DCTERMS.description)}) #From metadata, not the user author_name = Column(UnicodeText) #From metadata, not the user author_url = Column(UnicodeText) #From metadata, not the user thumbnail_url = Column(UnicodeText) #From metadata, not the user site_name = Column(UnicodeText) __mapper_args__ = { 'polymorphic_identity': 'document', } def get_discussion_id(self): return self.discussion_id or self.discussion.id @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.id == discussion_id, )
class Content(DiscussionBoundBase): """ Content is a polymorphic class to describe what is imported from a Source. The body and subject properly belong to the Post but were moved here to optimize the most common case. """ __tablename__ = "content" # __table_cls__ = TableWithTextIndex rdf_class = SIOC.Post id = Column(Integer, primary_key=True, info={'rdf': QuadMapPatternS(None, ASSEMBL.db_id)}) type = Column(String(60), nullable=False) creation_date = Column( DateTime, nullable=False, default=datetime.utcnow, info={'rdf': QuadMapPatternS(None, DCTERMS.created)}) discussion_id = Column( Integer, ForeignKey( 'discussion.id', ondelete='CASCADE', onupdate='CASCADE', ), nullable=False, ) discussion = relationship( "Discussion", backref=backref('posts', order_by=creation_date, cascade="all, delete-orphan"), info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)}) subject = Column(CoerceUnicode(), server_default="", info={'rdf': QuadMapPatternS(None, DCTERMS.title)}) # TODO: check HTML or text? SIOC.content should be text. # Do not give it for now, privacy reasons body = Column(UnicodeText, server_default="") # info={'rdf': QuadMapPatternS(None, SIOC.content)}) hidden = Column(Boolean, server_default='0') # Another bloody virtuoso bug. Insert with large string fails. # body_text_index = TextIndex(body, clusters=[discussion_id]) __mapper_args__ = { 'polymorphic_identity': 'content', 'polymorphic_on': 'type', 'with_polymorphic': '*' } def get_body(self): return self.body.strip() def get_title(self): return self.subject def get_body_mime_type(self): """ Return the format of the body, so the frontend will know how to display it. Currently, only: text/plain (Understood as preformatted text) text/html (Undestood as some subste of html) """ return "text/plain" def get_body_as_html(self): if self.get_body_mime_type() == 'text/html': return self.body else: return '<span style="white-space: pre-wrap">%s</div>' % ( self.get_body_as_text(), ) def get_body_as_text(self): mimetype = self.get_body_mime_type() body = self.body or "" if mimetype == 'text/plain': return body.strip() elif mimetype == 'text/html': return BeautifulSoup(body).get_text().strip() else: log.error("What is this mimetype?" + mimetype) return body def send_to_changes(self, connection=None, operation=UPDATE_OP, discussion_id=None, view_def="changes"): super(Content, self).send_to_changes(connection, operation, discussion_id, view_def) watcher = get_model_watcher() if operation == INSERT_OP: watcher.processPostCreated(self.id) def get_discussion_id(self): return self.discussion_id or self.discussion.id @property def exported_to_sources(self): return [ ContentSource.uri_generic(s.source_id) for s in self.post_sink_associations ] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.discussion_id == discussion_id, ) @classmethod def special_quad_patterns(cls, alias_maker, discussion_id): discussion_alias = alias_maker.get_reln_alias(cls.discussion) return [ QuadMapPatternS( None, FOAF.homepage, PatternIriClass( QUADNAMES.post_external_link_iri, # TODO: Use discussion.get_base_url. # This should be computed outside the DB. get_global_base_url() + '/%s/posts/local:Content/%d', None, ('slug', Unicode, False), ('id', Integer, False)).apply(discussion_alias.slug, cls.id), name=QUADNAMES.post_external_link_map) ] widget_idea_links = relationship('IdeaContentWidgetLink') def widget_ideas(self): from .idea import Idea return [ Idea.uri_generic(wil.idea_id) for wil in self.widget_idea_links ] crud_permissions = CrudPermissions(P_ADD_POST, P_READ, P_EDIT_POST, P_ADMIN_DISC, P_EDIT_POST, P_ADMIN_DISC)
class Post(Content): """ A Post represents input into the broader discussion taking place on Assembl. It may be a response to another post, it may have responses, and its content may be of any type. """ __tablename__ = "post" id = Column(Integer, ForeignKey('content.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True) message_id = Column(CoerceUnicode(), nullable=False, index=True, doc="The email-compatible message-id for the post.", info={'rdf': QuadMapPatternS(None, SIOC.id)}) ancestry = Column(String, default="") parent_id = Column( Integer, ForeignKey('post.id', ondelete='CASCADE', onupdate='SET NULL')) children = relationship( "Post", foreign_keys=[parent_id], backref=backref('parent', remote_side=[id]), ) @classmethod def special_quad_patterns(cls, alias_maker, discussion_id): # Don't we need a recursive alias for this? It seems not. return [ QuadMapPatternS(Post.iri_class().apply(cls.id), SIOC.reply_of, cls.iri_class().apply(cls.parent_id), name=QUADNAMES.post_parent, conditions=(cls.parent_id != None, )), ] creator_id = Column(Integer, ForeignKey('agent_profile.id'), nullable=False, info={ 'rdf': QuadMapPatternS( None, SIOC.has_creator, AgentProfile.agent_as_account_iri.apply(None)) }) creator = relationship(AgentProfile, backref="posts_created") __mapper_args__ = {'polymorphic_identity': 'post', 'with_polymorphic': '*'} def get_descendants(self): assert self.id descendants = self.db.query(Post).filter( Post.parent_id == self.id).order_by(Content.creation_date) return descendants def is_read(self): # TODO: Make it user-specific. return self.views is not None def get_url(self): from assembl.lib.frontend_urls import FrontendUrls frontendUrls = FrontendUrls(self.discussion) return frontendUrls.get_post_url(self) def get_body_preview(self): body = self.get_body().strip() target_len = 120 shortened = False if self.get_body_mime_type() == 'text/html': html_len = 2 * target_len while True: text = BeautifulSoup(body[:html_len]).get_text().strip() if html_len >= len(body) or len(text) > target_len: shortened = html_len < len(body) body = text break html_len += target_len if len(body) > target_len: body = body[:target_len].rsplit(' ', 1)[0].rstrip() + ' ' elif shortened: body += ' ' return body def _set_ancestry(self, new_ancestry): self.ancestry = new_ancestry descendant_ancestry = "%s%d," % (self.ancestry, self.id) for descendant in self.get_descendants(): descendant._set_ancestry(descendant_ancestry) def set_parent(self, parent): self.parent = parent self.db.add(self) self.db.flush() self._set_ancestry("%s%d," % (parent.ancestry or '', parent.id)) def last_updated(self): ancestry_query_string = "%s%d,%%" % (self.ancestry or '', self.id) query = self.db.query(func.max( Content.creation_date)).select_from(Post).join(Content).filter( or_(Post.ancestry.like(ancestry_query_string), Post.id == self.id)) return query.scalar() def ancestor_ids(self): ancestor_ids = [ int(ancestor_id) \ for ancestor_id \ in self.ancestry.split(',') \ if ancestor_id ] return ancestor_ids def ancestors(self): ancestors = [ Post.get(ancestor_id) \ for ancestor_id \ in self.ancestor_ids ] return ancestors def prefetch_descendants(self): pass #TODO def visit_posts_depth_first(self, post_visitor): self.prefetch_descendants() self._visit_posts_depth_first(post_visitor, set()) def _visit_posts_depth_first(self, post_visitor, visited): if self in visited: # not necessary in a tree, but let's start to think graph. return False result = post_visitor.visit_post(self) visited.add(self) if result is not PostVisitor.CUT_VISIT: for child in self.children: child._visit_posts_depth_first(post_visitor, visited) def visit_posts_breadth_first(self, post_visitor): self.prefetch_descendants() result = post_visitor.visit_post(self) visited = {self} if result is not PostVisitor.CUT_VISIT: self._visit_posts_breadth_first(post_visitor, visited) def _visit_posts_breadth_first(self, post_visitor, visited): children = [] for child in self.children: if child in visited: continue result = post_visitor.visit_post(child) visited.add(child) if result != PostVisitor.CUT_VISIT: children.append(child) for child in children: child._visit_posts_breadth_first(post_visitor, visited) def has_next_sibling(self): if self.parent_id: return self != self.parent.children[-1] return False @classmethod def restrict_to_owners(cls, query, user_id): "filter query according to object owners" return query.filter(cls.creator_id == user_id)
class FacebookPost(ImportedPost): """ A facebook post, from any resource on the Open Graph API """ __tablename__ = 'facebook_post' id = Column(Integer, ForeignKey('imported_post.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) attachment = Column(String(1024)) link_name = Column(CoerceUnicode(1024)) post_type = Column(String(20)) __mapper_args__ = {'polymorphic_identity': 'facebook_post'} @classmethod def create(cls, source, post, user): import_date = datetime.utcnow() source_post_id = post.get('id') source = source creation_date = parse_datetime(post.get('created_time')) discussion = source.discussion creator_agent = user.profile blob = json.dumps(post) post_type = post.get('type', None) subject, body, attachment, link_name = (None, None, None, None) if not post_type: has_attach = post.get('link', None) if has_attach: attachment = has_attach body = post.get('message', "") + "\n" + post.get('link', "") \ if 'message' in post else post.get('link', "") else: post_type = 'comment' body = post.get('message') elif post_type is 'video' or 'photo': subject = post.get('story', None) body = post.get('message', "") + "\n" + post.get('link', "") \ if 'message' in post else post.get('link', "") attachment = post.get('link', None) link_name = post.get('caption', None) elif post_type is 'link': subject = post.get('story', None) body = post.get('message', "") attachment = post.get('link') link_name = post.get('caption', None) match_str = re.split(r"^\w+://", attachment)[1] if match_str not in body: body += "\n" + attachment elif post_type is 'status': if not post.get('message', None): # A type of post that does not have any links nor body content # It is useless, therefore it should never generate a post return None body = post.get('message') return cls(attachment=attachment, link_name=link_name, body_mime_type='text/plain', import_date=import_date, source_post_id=source_post_id, message_id=source_post_id, source=source, creation_date=creation_date, discussion=discussion, creator=creator_agent, post_type=post_type, imported_blob=blob, subject=subject, body=body)
def upgrade(pyramid_env): with context.begin_transaction(): op.create_table( 'document', sa.Column('id', sa.Integer, primary_key=True), sa.Column('uri_id', CoerceUnicode(1024), server_default="", unique=False, index=True), sa.Column('creation_date', sa.DateTime, nullable=False, default = datetime.utcnow), sa.Column('discussion_id', sa.Integer, sa.ForeignKey( 'discussion.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=False,), sa.Column('oembed_type', CoerceUnicode(1024), server_default=""), sa.Column('mime_type', CoerceUnicode(1024), server_default=""), sa.Column('title', CoerceUnicode(1024), server_default=""), sa.Column('description', sa.UnicodeText), sa.Column('author_name', sa.UnicodeText), sa.Column('author_url', sa.UnicodeText), sa.Column('thumbnail_url', sa.UnicodeText), sa.Column('site_name', sa.UnicodeText), ) op.create_table( 'attachment', sa.Column('id', sa.Integer, primary_key=True), sa.Column('creation_date', sa.DateTime, nullable=False, default = datetime.utcnow), sa.Column('discussion_id', sa.Integer, sa.ForeignKey( 'discussion.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=False,), sa.Column('document_id', sa.Integer, sa.ForeignKey( 'document.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False,), sa.Column('creator_id', sa.Integer, sa.ForeignKey( 'agent_profile.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=False,), sa.Column('title', CoerceUnicode(1024), server_default=""), sa.Column('description', sa.UnicodeText), sa.Column('attachmentPurpose', CoerceUnicode(256), server_default="", index=True,), ) op.create_table( 'post_attachment', sa.Column('id', sa.Integer, sa.ForeignKey( 'attachment.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True), sa.Column('post_id', sa.Integer, sa.ForeignKey( 'post.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True,), ) op.create_table( 'idea_attachment', sa.Column('id', sa.Integer, sa.ForeignKey( 'attachment.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True), sa.Column('idea_id', sa.Integer, sa.ForeignKey( 'idea.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True,), )
class Email(ImportedPost): """ An Email refers to an email message that was imported from an AbstractMailbox. """ __tablename__ = "email" id = Column(Integer, ForeignKey('imported_post.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True) # in virtuoso, varchar is 1024 bytes and sizeof(wchar)==4, so varchar is 256 chars recipients = Column(UnicodeText, nullable=False) sender = Column(CoerceUnicode(), nullable=False) in_reply_to = Column(CoerceUnicode()) __mapper_args__ = { 'polymorphic_identity': 'email', } def REWRITEMEreply(self, sender, response_body): """ Send a response to this email. `sender` is a user instance. `response` is a string. """ sent_from = ' '.join([ "%(sender_name)s on Assembl" % { "sender_name": sender.display_name() }, "<%(sender_email)s>" % { "sender_email": sender.get_preferred_email(), } ]) if type(response_body) == 'str': response_body = response_body.decode('utf-8') recipients = self.recipients message = MIMEMultipart('alternative') message['Subject'] = Header(self.subject, 'utf-8') message['From'] = sent_from message['To'] = self.recipients message.add_header('In-Reply-To', self.message_id) plain_text_body = response_body html_body = response_body # TODO: The plain text and html parts of the email should be different, # but we'll see what we can get from the front-end. plain_text_part = MIMEText(plain_text_body.encode('utf-8'), 'plain', 'utf-8') html_part = MIMEText(html_body.encode('utf-8'), 'html', 'utf-8') message.attach(plain_text_part) message.attach(html_part) smtp_connection = smtplib.SMTP( get_current_registry().settings['mail.host']) smtp_connection.sendmail(sent_from, recipients, message.as_string()) smtp_connection.quit() def __repr__(self): return "%s from %s to %s>" % (super( Email, self).__repr__(), self.sender.encode( 'ascii', 'ignore'), self.recipients.encode('ascii', 'ignore')) def get_title(self): return self.source.mangle_mail_subject(self.subject)
def normalize_to_type(self, value, dialect): return CoerceUnicode.process_bind_param(self, value, dialect)
class TimelineEvent(DiscussionBoundBase): __tablename__ = 'timeline_event' id = Column(Integer, primary_key=True, info={'rdf': QuadMapPatternS(None, ASSEMBL.db_id)}) discussion_id = Column(Integer, ForeignKey( 'discussion.id', ondelete='CASCADE', onupdate='CASCADE' ), nullable=False) type = Column(String(60), nullable=False) __mapper_args__ = { 'polymorphic_identity': 'timeline_event', 'polymorphic_on': type, 'with_polymorphic': '*' } title = Column(CoerceUnicode(), nullable=False, info={'rdf': QuadMapPatternS(None, DCTERMS.title)}) description = Column(UnicodeText, info={'rdf': QuadMapPatternS(None, DCTERMS.description)}) start = Column(DateTime, # Formally, TIME.hasBeginning o TIME.inXSDDateTime info={'rdf': QuadMapPatternS(None, TIME.hasBeginning)}) end = Column(DateTime, info={'rdf': QuadMapPatternS(None, TIME.hasEnd)}) # Since dates are optional, the previous event pointer allows # dateless events to form a linked list. # Ideally we could use a uniqueness constraint but # that disallows multiple NULLs. # Also, the linked list defines lanes. previous_event_id = Column(Integer, ForeignKey( 'timeline_event.id', ondelete="SET NULL"), nullable=True) previous_event = relationship( "TimelineEvent", remote_side=[id], post_update=True, uselist=False, backref=backref("next_event", uselist=False, remote_side=[previous_event_id])) def __init__(self, **kwargs): previous_event_id = None previous_event = None if 'previous_event' in kwargs: previous_event = kwargs['previous_event'] del kwargs['previous_event'] if 'previous_event_id' in kwargs: previous_event_id = kwargs['previous_event_id'] del kwargs['previous_event_id'] super(TimelineEvent, self).__init__(**kwargs) if previous_event is not None: self.set_previous_event(previous_event) elif previous_event_id is not None: self.set_previous_event_id(previous_event_id) discussion = relationship( Discussion, backref=backref( 'timeline_events', order_by=start, cascade="all, delete-orphan"), info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)} ) def set_previous_event(self, previous_event): # This allows setting the previous event as an insert. # this method may not be reliable with unflushed objects. self.set_previous_event_id(previous_event.id if previous_event is not None else None) self.previous_event = previous_event previous_event.next_event = self def set_previous_event_id(self, previous_event_id): if previous_event_id != self.previous_event_id: # TODO: Detect and avoid cycles if previous_event_id is not None: existing = self.__class__.get_by(previous_event_id=previous_event_id) if existing: existing.previous_event = self if inspect(self).persistent: self.db.expire(self, ['previous_event']) elif 'previous_event' in self.__dict__: del self.__dict__['previous_event'] self.previous_event_id = previous_event_id def get_discussion_id(self): return self.discussion_id @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.discussion_id == discussion_id,) crud_permissions = CrudPermissions(P_ADMIN_DISC)