Beispiel #1
0
 def get_idea_ids_showing_post(cls, post_id):
     "Given a post, give the ID of the ideas that show this message"
     from sqlalchemy.sql.functions import func
     from .idea_content_link import IdeaContentPositiveLink
     from .post import Post
     (ancestry, discussion_id, idea_link_ids)  = cls.default_db.query(
         Post.ancestry, Post.discussion_id,
         func.idea_content_links_above_post(Post.id)
         ).filter(Post.id==post_id).first()
     post_path = "%s%d," % (ancestry, post_id)
     if not idea_link_ids:
         return []
     idea_link_ids = [int(id) for id in idea_link_ids.split(',') if id]
     # This could be combined with previous in postgres.
     root_ideas = cls.default_db.query(
             IdeaContentPositiveLink.idea_id.distinct()
         ).filter(
             IdeaContentPositiveLink.idea_id != None,
             IdeaContentPositiveLink.id.in_(idea_link_ids)).all()
     if not root_ideas:
         return []
     root_ideas = [x for (x,) in root_ideas]
     discussion_data = cls.get_discussion_data(discussion_id)
     counter = cls.prepare_counters(discussion_id)
     idea_contains = {}
     for root_idea_id in root_ideas:
         for idea_id in discussion_data.idea_ancestry(root_idea_id):
             if idea_id in idea_contains:
                 break
             idea_contains[idea_id] = counter.paths[idea_id].includes_post(post_path)
     ideas = [id for (id, incl) in idea_contains.iteritems() if incl]
     return ideas
Beispiel #2
0
 def get_idea_ids_showing_post(cls, post_id):
     "Given a post, give the ID of the ideas that show this message"
     from sqlalchemy.sql.functions import func
     from .idea_content_link import IdeaContentPositiveLink
     from .post import Post
     (ancestry, discussion_id, idea_link_ids)  = cls.default_db.query(
         Post.ancestry, Post.discussion_id,
         func.idea_content_links_above_post(Post.id)
         ).filter(Post.id==post_id).first()
     post_path = "%s%d," % (ancestry, post_id)
     if not idea_link_ids:
         return []
     idea_link_ids = [int(id) for id in idea_link_ids.split(',') if id]
     # This could be combined with previous in postgres.
     root_ideas = cls.default_db.query(
             IdeaContentPositiveLink.idea_id.distinct()
         ).filter(
             IdeaContentPositiveLink.idea_id != None,
             IdeaContentPositiveLink.id.in_(idea_link_ids)).all()
     if not root_ideas:
         return []
     root_ideas = [x for (x,) in root_ideas]
     discussion_data = cls.get_discussion_data(discussion_id)
     counter = cls.prepare_counters(discussion_id)
     idea_contains = {}
     for root_idea_id in root_ideas:
         for idea_id in discussion_data.idea_ancestry(root_idea_id):
             if idea_id in idea_contains:
                 break
             idea_contains[idea_id] = counter.paths[idea_id].includes_post(post_path)
     ideas = [id for (id, incl) in idea_contains.iteritems() if incl]
     return ideas
Beispiel #3
0
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)

    # This is usually an email, but we won't enforce it because we get some
    # weird stuff from outside.
    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="")

    __table_args__ = (
         Index(
            'ix_%s_post_ancestry' % (Content.full_schema,),
            'ancestry', unique=False,
            postgresql_ops={'ancestry': 'varchar_pattern_ops'}),)

    parent_id = Column(Integer, ForeignKey(
        'post.id',
        ondelete='CASCADE',
        onupdate='SET NULL'), index=True)
    children = relationship(
        "Post",
        foreign_keys=[parent_id],
        backref=backref('parent', remote_side=[id]),
    )

    publication_state = Column(
        PublicationStates.db_type(),
        nullable=False,
        server_default=PublicationStates.PUBLISHED.name)

    moderator_id = Column(Integer, ForeignKey(
        'user.id',
        ondelete='SET NULL',
        onupdate='CASCADE'),
        nullable=True,)

    moderated_on = Column(DateTime)

    moderation_text = Column(UnicodeText)

    moderator_comment = Column(UnicodeText)  # For other moderators

    moderator = relationship(
        "User",
        foreign_keys=[moderator_id],
        backref=backref('posts_moderated'),
    )

    # All the idea content links of the ancestors of this post
    idea_content_links_above_post = column_property(
        func.idea_content_links_above_post(id),
        deferred=True, expire_on_flush=False)

    @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, index=True,
        info={'rdf': QuadMapPatternS(
            None, SIOC.has_creator, AgentProfile.agent_as_account_iri.apply(None))})
    creator = relationship(AgentProfile, foreign_keys=[creator_id], backref="posts_created")

    __mapper_args__ = {
        'polymorphic_identity': 'post',
        'with_polymorphic': '*'
    }

    def is_owner(self, user_id):
        return self.creator_id == user_id

    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)

    @staticmethod
    def shorten_text(text, target_len=120):
        if len(text) > target_len:
            text = text[:target_len].rsplit(' ', 1)[0].rstrip() + ' '
        return text

    @staticmethod
    def shorten_html_text(text, target_len=120):
        shortened = False
        html_len = 2 * target_len
        while True:
            pure_text = sanitize_text(text[:html_len])
            if html_len >= len(text) or len(pure_text) > target_len:
                shortened = html_len < len(text)
                text = pure_text
                break
            html_len += target_len
        text = Post.shorten_text(text)
        if shortened and text[-1] != ' ':
            text += ' '
        return text

    def get_body_preview(self):
        if self.publication_state in moderated_publication_states:
            # TODO: Handle multilingual moderation
            return LangString.create(
                self.moderation_text, self.discussion.main_locale)
        elif self.publication_state in deleted_publication_states:
            return LangString.EMPTY(self.db)
        body = self.get_body()
        is_html = self.get_body_mime_type() == 'text/html'
        ls = LangString()
        shortened = False
        for entry in body.entries:
            if not entry.value:
                short = entry.value
            elif is_html:
                short = self.shorten_html_text(entry.value)
            else:
                short = self.shorten_text(entry.value)
            if short != entry.value:
                shortened = True
            _ = LangStringEntry(
                value=short, locale_id=entry.locale_id, langstring=ls)
        if shortened or is_html:
            return ls
        else:
            return body

    def get_original_body_preview(self):
        if self.publication_state in moderated_publication_states:
            # TODO: Handle multilingual moderation
            return self.moderation_text
        elif self.publication_state in deleted_publication_states:
            return LangString.EMPTY(self.db)
        body = self.get_body().first_original().value
        is_html = self.get_body_mime_type() == 'text/html'
        shortened = False
        if not body:
            short = body
        elif is_html:
            short = self.shorten_html_text(body)
        else:
            short = self.shorten_text(body)
        if short != body:
            shortened = True
        if shortened or is_html:
            return short
        else:
            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

    @property
    def has_live_child(self):
        for child in self.children:
            if not child.is_tombstone:
                return True

    def delete_post(self, cause):
        """Set the publication state to a deleted state

        Includes an optimization whereby deleted posts without
        live descendents are tombstoned.
        Should be resilient to deletion order."""
        self.publication_state = cause
        if not self.has_live_child:
            self.is_tombstone = True
            # If ancestor is deleted without being tombstone, make it tombstone
            ancestor = self.parent
            while (ancestor and
                   ancestor.publication_state in deleted_publication_states and
                   not ancestor.is_tombstone and
                   not ancestor.has_live_child):
                ancestor.is_tombstone = True
                ancestor = ancestor.parent

    # As tombstones are an optimization in this case,
    # allow necromancy.
    can_be_resurrected = True

    def undelete_post(self):
        self.publication_state = PublicationStates.PUBLISHED
        ancestor = self
        while ancestor and ancestor.is_tombstone:
            ancestor.is_tombstone = False
            ancestor = ancestor.parent

    def get_subject(self):
        if self.publication_state in blocking_publication_states:
            #return None
            return LangString.EMPTY()
        if self.subject:
            return super(Post, self).get_subject()

    def get_body(self):
        if self.publication_state in blocking_publication_states:
            #return None
            return LangString.EMPTY()
        if self.body:
            return super(Post, self).get_body()

    def get_original_body_as_html(self):
        if self.publication_state in blocking_publication_states:
            return LangString.EMPTY(self.db)
        return super(Post, self).get_original_body_as_html()

    def get_body_as_text(self):
        if self.publication_state in blocking_publication_states:
            return LangString.EMPTY(self.db)
        return super(Post, self).get_body_as_text()

    def indirect_idea_content_links(self):
        from pyramid.threadlocal import get_current_request
        request = get_current_request()
        if request:
            return self.indirect_idea_content_links_with_cache()
        else:
            return self.indirect_idea_content_links_without_cache()

    def indirect_idea_content_links_without_cache(self):
        "Return all ideaContentLinks related to this post or its ancestors"
        from .idea_content_link import IdeaContentLink
        ancestors = filter(None, self.ancestry.split(","))
        ancestors = [int(x) for x in ancestors]
        ancestors.append(self.id)
        return self.db.query(IdeaContentLink).filter(
            IdeaContentLink.content_id.in_(ancestors)).all()

    def filter_idea_content_links_r(self, idea_content_links):
        """Exclude positive links if a negative link points from the same idea
        to the same post or a post below.

        Works on dict representations of IdeaContentLink, a version with instances is TODO."""
        from .idea_content_link import IdeaContentNegativeLink
        from collections import defaultdict
        icnl_polymap = {
            cls.external_typename()
            for cls in IdeaContentNegativeLink.get_subclasses()}

        neg_links = [icl for icl in idea_content_links
                     if icl["@type"] in icnl_polymap]
        if not neg_links:
            return idea_content_links
        pos_links = [icl for icl in idea_content_links
                     if icl["@type"] not in icnl_polymap]
        links = []
        ancestor_ids = self.ancestry.split(",")
        ancestor_ids = [int(x or 0) for x in ancestor_ids]
        ancestor_ids[-1] = self.id
        neg_link_post_ids = defaultdict(list)
        for icl in neg_links:
            neg_link_post_ids[icl["idIdea"]].append(
                self.get_database_id(icl["idPost"]))
        for link in pos_links:
            idea_id = link["idIdea"]
            if idea_id in neg_link_post_ids:
                pos_post_id = self.get_database_id(link["idPost"])
                for neg_post_id in neg_link_post_ids[idea_id]:
                    if (ancestor_ids.index(neg_post_id) >
                            ancestor_ids.index(pos_post_id)):
                        break
                else:
                    links.append(link)
            else:
                links.append(link)
        links.extend(neg_links)
        return links

    def indirect_idea_content_links_with_cache(
            self, links_above_post=None, filter=True):
        "Return all ideaContentLinks related to this post or its ancestors"
        # WIP: idea_content_links_above_post is still loaded separately
        # despite not being deferred. Deferring it hits a sqlalchemy bug.
        # Still appreciable performance gain using it instead of the orm,
        # and the ICL cache below.
        # TODO: move in path_utils?
        links_above_post = (self.idea_content_links_above_post
                            if links_above_post is None else links_above_post)
        if not links_above_post:
            return []
        from pyramid.threadlocal import get_current_request
        from .idea_content_link import IdeaContentLink
        from .idea import Idea
        icl_polymap = IdeaContentLink.__mapper__.polymorphic_map
        request = get_current_request()
        if getattr(request, "_idea_content_link_cache2", None) is None:
            if getattr(request, "_idea_content_link_cache1", None) is None:
                icl = with_polymorphic(IdeaContentLink, IdeaContentLink)
                co = with_polymorphic(Content, Content)
                request._idea_content_link_cache1 = {x[0]: x for x in self.db.query(
                    icl.id, icl.idea_id, icl.content_id, icl.creator_id, icl.type,
                    icl.creation_date).join(co).filter(
                    co.discussion_id == self.discussion_id)}
            request._idea_content_link_cache2 = {}

        def icl_representation(id):
            if id not in request._idea_content_link_cache2:
                data = request._idea_content_link_cache1.get(id, None)
                if data is None:
                    return None
                request._idea_content_link_cache2[id] = {
                    "@id": IdeaContentLink.uri_generic(data[0]),
                    "idIdea": Idea.uri_generic(data[1]),
                    "idPost": Content.uri_generic(data[2]),
                    "idCreator": AgentProfile.uri_generic(data[3]),
                    "@type": icl_polymap[data[4]].class_.external_typename(),
                    "created": data[5].isoformat() + "Z"
                }
            return request._idea_content_link_cache2[id]
        icls = [icl_representation(int(id)) for id in
                links_above_post.strip(',').split(',')]
        if filter:
            icls = self.filter_idea_content_links_r(icls)
        return icls

    def language_priors(self, translation_service):
        from .auth import User, UserLanguagePreferenceCollection
        priors = super(Post, self).language_priors(translation_service)
        creator = self.creator or AgentProfile.get(self.creator_id)
        if creator and isinstance(creator, User):
            # probably a language that the user knows
            try:
                prefs = UserLanguagePreferenceCollection(creator.id)
                known_languages = prefs.known_languages()
            except AssertionError:  # user without prefs
                from pyramid.threadlocal import get_current_request
                request = get_current_request()
                if request:
                    known_languages = [request.locale_name]
                else:
                    return priors
                known_languages = []
            known_languages = {translation_service.asKnownLocale(loc)
                               for loc in known_languages}
            priors = {k: v * (1 if k in known_languages else 0.7)
                      for (k, v) in priors.iteritems()}
            for lang in known_languages:
                if lang not in priors:
                    priors[lang] = 1
        return priors

    @classmethod
    def extra_collections(cls):
        from .idea_content_link import IdeaContentLink

        class IdeaContentLinkCollection(AbstractCollectionDefinition):
            def __init__(self, cls):
                super(IdeaContentLinkCollection, self
                      ).__init__(cls, IdeaContentLink)

            def decorate_query(
                    self, query, owner_alias, last_alias, parent_instance,
                    ctx):
                parent = owner_alias
                children = last_alias
                ancestors = filter(None, parent_instance.ancestry.split(","))
                ancestors = [int(x) for x in ancestors]
                ancestors.append(parent_instance.id)
                return query.join(
                    parent, children.content_id.in_(ancestors))

            def decorate_instance(
                    self, instance, parent_instance, assocs, user_id,
                    ctx, kwargs):
                pass

            def contains(self, parent_instance, instance):
                return instance.content_id == parent_instance.id or (
                    str(instance.content_id) in
                    parent_instance.ancestry.split(","))

        return {'indirect_idea_content_links': IdeaContentLinkCollection(cls)}
    @classmethod
    def restrict_to_owners(cls, query, user_id):
        "filter query according to object owners"
        return query.filter(cls.creator_id == user_id)