Source code for assembl.models.idea_graph_view

"""Subsets of :py:class:`assembl.models.idea.Idea` and :py:class:`assembl.models.idea.IdeaLink`."""
from collections import defaultdict
from datetime import datetime
from abc import abstractmethod
from itertools import chain

from future.utils import as_native_str
from sqlalchemy.orm import (
    relationship, backref)
from sqlalchemy import (
    Column,
    Integer,
    String,
    UnicodeText,
    DateTime,
    Boolean,
    ForeignKey,
    UniqueConstraint,
)
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import join
import lxml.html as htmlt

from . import DiscussionBoundBase, OriginMixin
from .discussion import Discussion
from .langstrings import LangString
from ..semantic.virtuoso_mapping import QuadMapPatternS
from ..auth import (
    CrudPermissions, P_ADMIN_DISC, P_EDIT_SYNTHESIS)
from .idea import Idea, IdeaLink, RootIdea, IdeaVisitor
from ..semantic.namespaces import (
    SIOC, CATALYST, IDEA, ASSEMBL, DCTERMS, QUADNAMES)
from assembl.views.traversal import AbstractCollectionDefinition
from ..views.traversal import collection_creation_side_effects, InstanceContext


[docs]class defaultdictlist(defaultdict): def __init__(self): super(defaultdictlist, self).__init__(list)
[docs]class IdeaGraphView(DiscussionBoundBase, OriginMixin): """ A view on the graph of idea. """ __tablename__ = "idea_graph_view" __external_typename = "Map" rdf_class = CATALYST.Map type = Column(String(60), nullable=False) id = Column(Integer, primary_key=True, info={'rdf': QuadMapPatternS(None, ASSEMBL.db_id)}) 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, index=True, info={'rdf': QuadMapPatternS(None, SIOC.has_container)} ) discussion = relationship( Discussion, backref=backref("views", cascade="all, delete-orphan"), info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)}) __mapper_args__ = { 'polymorphic_identity': 'idea_graph_view', 'polymorphic_on': 'type', 'with_polymorphic': '*' } def copy(self, db=None): retval = self.__class__() retval.discussion = self.discussion return retval
[docs] def get_discussion_id(self): return self.discussion_id
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.discussion_id == discussion_id, )
crud_permissions = CrudPermissions(P_ADMIN_DISC) @abstractmethod def get_idea_links(self): pass @abstractmethod def get_ideas(self): pass
[docs]class SubGraphIdeaAssociation(DiscussionBoundBase): """Association table saying that an Idea is part of a ExplicitSubGraphView""" __tablename__ = 'sub_graph_idea_association' __table_args__ = ( UniqueConstraint("idea_id", "sub_graph_id"), ) id = Column(Integer, primary_key=True) sub_graph_id = Column(Integer, ForeignKey( 'explicit_sub_graph_view.id', ondelete="CASCADE", onupdate="CASCADE"), index=True, nullable=False) sub_graph = relationship( "ExplicitSubGraphView", backref=backref( "idea_assocs", cascade="all, delete-orphan")) idea_id = Column(Integer, ForeignKey( 'idea.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True) # reference to the "Idea" object for proxying idea = relationship("Idea", backref="in_subgraph_assoc") include_body = Column(Boolean, server_default='false')
[docs] @classmethod def special_quad_patterns(cls, alias_maker, discussion_id): idea_assoc = alias_maker.alias_from_class(cls) idea_alias = alias_maker.alias_from_relns(cls.idea) # Assume tombstone status of target is similar to source, for now. conditions = [(idea_assoc.idea_id == idea_alias.id), (idea_alias.tombstone_date == None)] if discussion_id: conditions.append((idea_alias.discussion_id == discussion_id)) return [ QuadMapPatternS( Idea.iri_class().apply(idea_assoc.idea_id), IDEA.inMap, IdeaGraphView.iri_class().apply(idea_assoc.sub_graph_id), conditions=conditions, name=QUADNAMES.sub_graph_idea_assoc_reln) ]
[docs] def get_discussion_id(self): sub_graph = self.sub_graph or IdeaGraphView.get(self.sub_graph_id) return sub_graph.get_discussion_id()
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return ((cls.sub_graph_id == ExplicitSubGraphView.id), (ExplicitSubGraphView.discussion_id == discussion_id))
discussion = relationship( Discussion, viewonly=True, uselist=False, secondary=Idea.__table__, info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)})
[docs] def unique_query(self): # documented in lib/sqla idea_id = self.idea_id or self.idea.id subgraph_id = self.sub_graph_id or self.sub_graph.id return self.db.query(self.__class__).filter_by( idea_id=idea_id, sub_graph_id=subgraph_id), True
# @classmethod # def special_quad_patterns(cls, alias_maker, discussion_id): # return [QuadMapPatternS( # Idea.iri_class().apply(cls.source_id), # IDEA.includes, # Idea.iri_class().apply(cls.target_id), # name=QUADNAMES.idea_inclusion_reln)] crud_permissions = CrudPermissions(P_ADMIN_DISC)
[docs]class SubGraphIdeaLinkAssociation(DiscussionBoundBase): """Association table saying that an IdeaLink is part of a ExplicitSubGraphView""" __tablename__ = 'sub_graph_idea_link_association' id = Column(Integer, primary_key=True) __table_args__ = ( UniqueConstraint("idea_link_id", "sub_graph_id"), ) sub_graph_id = Column(Integer, ForeignKey( 'explicit_sub_graph_view.id', ondelete="CASCADE", onupdate="CASCADE"), index=True, nullable=False) sub_graph = relationship( "ExplicitSubGraphView", backref=backref( "idealink_assocs", cascade="all, delete-orphan")) idea_link_id = Column(Integer, ForeignKey( 'idea_idea_link.id', ondelete="CASCADE", onupdate="CASCADE"), index=True, nullable=False) # reference to the "IdeaLink" object for proxying idea_link = relationship("IdeaLink", backref="in_subgraph_assoc")
[docs] @classmethod def special_quad_patterns(cls, alias_maker, discussion_id): idea_link_assoc = alias_maker.alias_from_class(cls) idea_link_alias = alias_maker.alias_from_relns(cls.idea_link) # Assume tombstone status of target is similar to source, for now. conditions = [(idea_link_assoc.idea_link_id == idea_link_alias.id), (idea_link_alias.tombstone_date == None)] if discussion_id: conditions.extend(cls.get_discussion_conditions( discussion_id, alias_maker)) return [ QuadMapPatternS( IdeaLink.iri_class().apply(idea_link_assoc.idea_link_id), IDEA.inMap, IdeaGraphView.iri_class().apply(idea_link_assoc.sub_graph_id), conditions=conditions, name=QUADNAMES.sub_graph_idea_link_assoc_reln) ]
[docs] def get_discussion_id(self): sub_graph = self.sub_graph or IdeaGraphView.get(self.sub_graph_id) return sub_graph.get_discussion_id()
[docs] def unique_query(self): # documented in lib/sqla idea_link_id = self.idea_link_id or self.idea_link.id subgraph_id = self.sub_graph_id or self.sub_graph.id return self.db.query(self.__class__).filter_by( idea_link_id=idea_link_id, sub_graph_id=subgraph_id), True
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): if alias_maker: subgraph_alias = alias_maker.alias_from_relns(cls.sub_graph) return ((subgraph_alias.discussion_id == discussion_id)) else: return ((cls.sub_graph_id == ExplicitSubGraphView.id), (ExplicitSubGraphView.discussion_id == discussion_id))
crud_permissions = CrudPermissions(P_ADMIN_DISC)
[docs]class ExplicitSubGraphView(IdeaGraphView): """ A view where the Ideas and/or ideaLinks have been explicitly selected. Note that ideaLinks may point to ideas that are not in the graph. They should be followed transitively (if their nature is compatible) to reach every idea in graph as if they were directly linked. """ __tablename__ = "explicit_sub_graph_view" id = Column(Integer, ForeignKey( 'idea_graph_view.id', ondelete='CASCADE', onupdate='CASCADE' ), primary_key=True) # proxy the 'idea' attribute from the 'idea_assocs' relationship # for direct access ideas = association_proxy('idea_assocs', 'idea', creator=lambda idea: SubGraphIdeaAssociation(idea=idea)) # proxy the 'idea_link' attribute from the 'idealink_assocs' # relationship for direct access idea_links = association_proxy('idealink_assocs', 'idea_link', creator=lambda idea_link: SubGraphIdeaLinkAssociation(idea_link=idea_link)) __mapper_args__ = { 'polymorphic_identity': 'explicit_sub_graph_view', } def copy(self, db=None): retval = IdeaGraphView.copy(self, db=db) # retval.ideas = self.ideas return retval def get_idea_links(self): # more efficient than the association_proxy return self.db.query(IdeaLink).join( SubGraphIdeaLinkAssociation ).filter_by(sub_graph_id=self.id).all() def get_idealink_assocs(self): return self.idealink_assocs def get_ideas(self): # more efficient than the association_proxy return self.db.query(Idea).join( SubGraphIdeaAssociation ).filter_by(sub_graph_id=self.id).all() def visit_ideas_depth_first(self, idea_visitor): # prefetch idea_assocs_by_idea_id = { link.idea_id: link for link in self.idea_assocs} children_links = defaultdict(list) with self.db.no_autoflush: idealink_assocs = self.get_idealink_assocs() for link_assoc in idealink_assocs: children_links[link_assoc.idea_link.source_id].append(link_assoc) for assocs in children_links.values(): assocs.sort(key=lambda l: l.idea_link.order) root = self.discussion.root_idea root_assoc = idea_assocs_by_idea_id.get(root.base_id, None) root_id = root_assoc.idea_id if root_assoc else root.id result = self._visit_ideas_depth_first( root_id, idea_assocs_by_idea_id, children_links, idea_visitor, set(), 0, None, None) # special case for autocreated links for link_assoc in idealink_assocs: self.db.expunge(link_assoc) return result def _visit_ideas_depth_first( self, idea_id, idea_assocs_by_idea_id, children_links, idea_visitor, visited, level, prev_result, parent_link_assoc): result = None if idea_id in visited: # not necessary in a tree, but let's start to think graph. return False assoc = idea_assocs_by_idea_id.get(idea_id, None) if assoc: result = idea_visitor.visit_idea( assoc, level, prev_result, parent_link_assoc) visited.add(idea_id) child_results = [] if result is not IdeaVisitor.CUT_VISIT: for link_assoc in children_links[idea_id]: child_id = link_assoc.idea_link.target_id r = self._visit_ideas_depth_first( child_id, idea_assocs_by_idea_id, children_links, idea_visitor, visited, level+1, result, link_assoc) if r: child_results.append((child_id, r)) return idea_visitor.end_visit(assoc, level, result, child_results, parent_link_assoc) @classmethod def extra_collections(cls): class GViewIdeaCollectionDefinition(AbstractCollectionDefinition): def __init__(self, cls): super(GViewIdeaCollectionDefinition, self).__init__( cls, 'ideas', Idea) def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx): return query.join(SubGraphIdeaAssociation, owner_alias) def contains(self, parent_instance, instance): return instance.db.query( SubGraphIdeaAssociation).filter_by( idea=instance, sub_graph=parent_instance ).count() > 0 @collection_creation_side_effects.register( inst_ctx=Idea, ctx='ExplicitSubGraphView.ideas') def add_graph_idea_assoc(inst_ctx, ctx): yield InstanceContext( inst_ctx['in_subgraph_assoc'], SubGraphIdeaAssociation( idea=inst_ctx._instance, sub_graph=ctx.parent_instance)) @collection_creation_side_effects.register( inst_ctx=IdeaLink, ctx='ExplicitSubGraphView.ideas') def add_graph_idea_link_assoc(inst_ctx, ctx): yield InstanceContext( inst_ctx['in_subgraph_assoc'], SubGraphIdeaLinkAssociation( idea_link=inst_ctx._instance, sub_graph=ctx.parent_instance)) class GViewIdeaLinkCollectionDefinition(AbstractCollectionDefinition): def __init__(self, cls): super(GViewIdeaLinkCollectionDefinition, self ).__init__(cls, 'idea_links', IdeaLink) def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx): return query.join(SubGraphIdeaLinkAssociation, owner_alias) def contains(self, parent_instance, instance): return instance.db.query( SubGraphIdeaLinkAssociation).filter_by( idea_link=instance, sub_graph=parent_instance ).count() > 0 @collection_creation_side_effects.register( inst_ctx=IdeaLink, ctx='ExplicitSubGraphView.idea_links') def add_graph_idea_link_assoc2(inst_ctx, ctx): yield InstanceContext( inst_ctx['in_subgraph_assoc'], SubGraphIdeaLinkAssociation( idea_link=inst_ctx._instance, sub_graph=ctx.parent_instance)) return (GViewIdeaCollectionDefinition(cls), GViewIdeaLinkCollectionDefinition(cls)) crud_permissions = CrudPermissions(P_ADMIN_DISC)
SubGraphIdeaLinkAssociation.discussion = relationship( Discussion, viewonly=True, uselist=False, secondary=join( ExplicitSubGraphView.__table__, IdeaGraphView.__table__, ExplicitSubGraphView.id == IdeaGraphView.id), info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)})
[docs]class TableOfContents(IdeaGraphView): """ Represents a Table of Ideas. A ToI in IdeaLoom is used to organize the core ideas of a discussion in a threaded hierarchy. """ __tablename__ = "table_of_contents" id = Column(Integer, ForeignKey( 'idea_graph_view.id', ondelete='CASCADE', onupdate='CASCADE' ), primary_key=True) __mapper_args__ = { 'polymorphic_identity': 'table_of_contents', } discussion = relationship( Discussion, backref=backref("table_of_contents", uselist=False))
[docs] def get_discussion_id(self): return self.discussion.id
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.discussion_id == discussion_id,)
def get_idea_links(self): return self.discussion.get_idea_links() def get_ideas(self): return self.discussion.ideas @as_native_str() def __repr__(self): r = super(TableOfContents, self).__repr__() return r[:-1] + self.discussion.slug + ">"
[docs]class Synthesis(ExplicitSubGraphView): """ A synthesis of the discussion. A selection of ideas, associated with comments, sent periodically to the discussion. A synthesis only has link's to ideas before publication (as it is edited) Once published, if freezes the links by copying tombstoned versions of each link in the discussion. """ __tablename__ = "synthesis" id = Column(Integer, ForeignKey( 'explicit_sub_graph_view.id', ondelete='CASCADE', onupdate='CASCADE' ), primary_key=True) subject_id = Column( Integer(), ForeignKey(LangString.id)) introduction_id = Column( Integer(), ForeignKey(LangString.id)) conclusion_id = Column( Integer(), ForeignKey(LangString.id)) subject = relationship( LangString, lazy="subquery", single_parent=True, primaryjoin=subject_id == LangString.id, backref=backref("synthesis_from_subject", lazy="dynamic"), cascade="all, delete-orphan") introduction = relationship( LangString, lazy="subquery", single_parent=True, primaryjoin=introduction_id == LangString.id, backref=backref("synthesis_from_introduction", lazy="dynamic"), cascade="all, delete-orphan") conclusion = relationship( LangString, lazy="subquery", single_parent=True, primaryjoin=conclusion_id == LangString.id, backref=backref("synthesis_from_conclusion", lazy="dynamic"), cascade="all, delete-orphan") __mapper_args__ = { 'polymorphic_identity': 'synthesis', } def copy(self): retval = ExplicitSubGraphView.copy(self) retval.subject = self.subject.clone() retval.introduction = self.introduction.clone() retval.conclusion = self.conclusion.clone() return retval
[docs] def publish(self): """ Publication is the end of a synthesis's lifecycle. It creates and returns a frozen copy of its state using tombstones for ideas and links.""" now = datetime.utcnow() frozen_synthesis = self.copy() self.db.add(frozen_synthesis) self.db.flush() # Copy tombstoned versions of all idea links and relevant ideas in the current synthesis links = Idea.get_all_idea_links(self.discussion_id) ideas = [idea for idea in self.ideas if not idea.is_tombstone] synthesis_idea_ids = {idea.id for idea in ideas} # Do not copy the root root = self.discussion.root_idea idea_copies = {root.id: root} # Also copies ideas between two synthesis ideas relevant_idea_ids = synthesis_idea_ids.copy() def add_ancestors_between(idea, path=None): if isinstance(idea, RootIdea): return path = path[:] if path else [] if idea.id in synthesis_idea_ids: relevant_idea_ids.update({i.id for i in path}) else: path.append(idea) for parent in idea.parents: add_ancestors_between(parent, path) for idea in ideas: for parent in idea.parents: add_ancestors_between(parent) for link in links: new_link = link.copy(tombstone=now) frozen_synthesis.idea_links.append(new_link) if link.source_id in relevant_idea_ids: if link.source_id not in idea_copies: new_idea = link.source_ts.copy(tombstone=now) idea_copies[link.source_id] = new_idea if link.source_id in synthesis_idea_ids: frozen_synthesis.ideas.append(new_idea) new_link.source_ts = idea_copies[link.source_id] if link.target_id in relevant_idea_ids: if link.target_id not in idea_copies: new_idea = link.target_ts.copy(tombstone=now) idea_copies[link.target_id] = new_idea if link.target_id in synthesis_idea_ids: frozen_synthesis.ideas.append(new_idea) new_link.target_ts = idea_copies[link.target_id] return frozen_synthesis
def as_html(self, jinja_env, lang_prefs): v = SynthesisHtmlizationVisitor(self, jinja_env, lang_prefs) self.visit_ideas_depth_first(v) return v.as_html() def get_idea_links(self): if self.is_next_synthesis: return Idea.get_all_idea_links(self.discussion_id) else: return super(Synthesis, self).get_idea_links() def get_idealink_assocs(self): if self.is_next_synthesis: return [ SubGraphIdeaLinkAssociation( idea_link=link) for link in Idea.get_all_idea_links(self.discussion_id)] else: return super(Synthesis, self).get_idealink_assocs() @property def is_next_synthesis(self): return self.discussion.get_next_synthesis() == self
[docs] def get_discussion_id(self): return self.discussion_id
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return (cls.discussion_id == discussion_id,)
@as_native_str() def __repr__(self): r = super(Synthesis, self).__repr__() subject = self.subject.first_original().value if self.subject else "" return r[:-1] + subject + ">" crud_permissions = CrudPermissions(P_EDIT_SYNTHESIS)
LangString.setup_ownership_load_event( Synthesis, ['subject', 'introduction', 'conclusion'])
[docs]class SynthesisHtmlizationVisitor(IdeaVisitor): def __init__(self, graph_view, jinja_env, lang_prefs): self.jinja_env = jinja_env self.lang_prefs = lang_prefs self.idea_template = jinja_env.get_template('idea_in_synthesis.jinja2') self.synthesis_template = jinja_env.get_template('synthesis.jinja2') self.graph_view = graph_view self.result = None def visit_idea(self, idea_assoc, level, prev_result, parent_link_assoc): return True def end_visit(self, idea_assoc, level, prev_result, child_results, parent_link_assoc): if prev_result is not True: idea_assoc = None if idea_assoc or child_results: results = [r for (c, r) in child_results] idea = idea_assoc.idea if idea_assoc else None self.result = self.idea_template.render( idea=idea, children=results, level=level, lang_prefs=self.lang_prefs, idea_assoc=idea_assoc, parent_link_assoc=parent_link_assoc) return self.result def as_html(self): inner = getattr(self, 'result', '') synthesis = self.graph_view subject = synthesis.subject.best_lang(self.lang_prefs).value introduction = synthesis.introduction.best_lang(self.lang_prefs).value conclusion = synthesis.conclusion.best_lang(self.lang_prefs).value return self.synthesis_template.render( synthesis=synthesis, content=inner, introduction=introduction, subject=subject, conclusion=conclusion)