# -*- coding: utf-8 -*-
"""Defining the idea and links between ideas."""
from builtins import str
from builtins import object
from itertools import chain, groupby
from collections import defaultdict
from abc import ABCMeta, abstractmethod
from datetime import datetime
import threading
from future.utils import as_native_str
from rdflib import URIRef
from sqlalchemy.orm import (
relationship, backref, aliased, contains_eager, joinedload, deferred,
column_property, with_polymorphic, remote, foreign)
from sqlalchemy.orm.attributes import NO_VALUE
from sqlalchemy.sql import text, column
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.sql.expression import union, bindparam, literal_column
from sqlalchemy import (
Column,
Boolean,
Integer,
String,
Unicode,
Float,
UnicodeText,
DateTime,
ForeignKey,
Index,
inspect,
select,
event,
func,
)
from sqlalchemy.ext.associationproxy import association_proxy
from sqla_rdfbridge.mapping import IriClass, PatternIriClass
from pyramid.i18n import TranslationStringFactory
from pyramid.httpexceptions import HTTPBadRequest, HTTPUnauthorized
from sqlalchemy.types import TIMESTAMP as Timestamp
from ..lib.clean_input import sanitize_text
from ..lib.utils import get_global_base_url
from ..nlp.wordcounter import WordCounter
from . import (
DiscussionBoundBase, HistoryMixinWithOrigin, TimestampedMixin)
from .discussion import Discussion
from .uriref import URIRefDb
from ..semantic.virtuoso_mapping import QuadMapPatternS
from ..auth import (
CrudPermissions, P_READ, P_ADMIN_DISC, P_EDIT_IDEA, P_SYSADMIN,
P_ADD_IDEA, P_ASSOCIATE_IDEA, P_READ_IDEA, R_OWNER, MAYBE, Everyone)
from .permissions import (
AbstractLocalUserRole, Role, Permission)
from .langstrings import LangString, LangStringEntry
from ..semantic.namespaces import (
SIOC, IDEA, ASSEMBL, QUADNAMES, FOAF, RDF, VirtRDF)
from ..lib.sqla import CrudOperation
from ..lib.model_watcher import get_model_watcher
from .auth import AgentProfile
from .publication_states import PublicationState, PublicationTransition
from .import_records import ImportRecord
from assembl.views.traversal import (
AbstractCollectionDefinition, RelationCollectionDefinition,
collection_creation_side_effects, InstanceContext)
from future.utils import with_metaclass
_ = TranslationStringFactory('assembl')
[docs]class defaultdictlist(defaultdict):
"""A defaultdict of lists."""
def __init__(self):
super(defaultdictlist, self).__init__(list)
[docs]class IdeaVisitor(with_metaclass(ABCMeta, object)):
"""A Visitor_ for the structure of :py:class:`Idea`
The visit is started by :py:meth:`Idea.visit_ideas_depth_first`,
:py:meth:`Idea.visit_ideas_breadth_first` or
:py:meth:`Idea.visit_idea_ids_depth_first`
.. _Visitor: https://sourcemaking.com/design_patterns/visitor
"""
CUT_VISIT = object()
@abstractmethod
def visit_idea(self, idea, level, prev_result):
pass
def end_visit(self, idea, level, result, child_results):
return result
[docs]class IdeaLinkVisitor(with_metaclass(ABCMeta, object)):
"""A Visitor for the structure of :py:class:`IdeaLink`"""
CUT_VISIT = object()
@abstractmethod
def visit_link(self, link):
pass
[docs]class AppendingVisitor(IdeaVisitor):
"""A Visitor that appends visit results to a list"""
def __init__(self):
self.ideas = []
def visit_idea(self, idea, level, prev_result):
self.ideas.append(idea)
return self.ideas
[docs]class WordCountVisitor(IdeaVisitor):
"""A Visitor that counts words related to an idea"""
def __init__(self, langs, count_posts=True):
self.counter = WordCounter(langs)
self.count_posts = True
def cleantext(self, text):
return sanitize_text(text)
def visit_idea(self, idea, level, prev_result):
if idea.short_title:
self.counter.add_text(self.cleantext(idea.short_title), 2)
if idea.long_title:
self.counter.add_text(self.cleantext(idea.long_title))
if idea.definition:
self.counter.add_text(self.cleantext(idea.definition))
if self.count_posts and level == 0:
from .generic import Content
query = idea.db.query(Content)
related = idea.get_related_posts_query(True)
query = query.join(related, Content.id == related.c.post_id
).filter(Content.hidden==False,
Content.tombstone_condition()).options(
Content.subqueryload_options())
titles = set()
# TODO maparent: Group langstrings by language.
for content in query:
body = content.body.safe_best_entry_in_request().value
self.counter.add_text(self.cleantext(body), 0.5)
title = content.subject.safe_best_entry_in_request().value
title = self.cleantext(title)
if title not in titles:
self.counter.add_text(title)
titles.add(title)
def best(self, num=8):
return self.counter.best(num)
[docs]class Idea(HistoryMixinWithOrigin, TimestampedMixin, DiscussionBoundBase):
"""
An idea (or concept) distilled from the conversation flux.
"""
__tablename__ = "idea"
__external_typename = "GenericIdeaNode"
ORPHAN_POSTS_IDEA_ID = 'orphan_posts'
sqla_type = Column(String(60), nullable=False)
rdf_type_id = Column(
Integer, ForeignKey(URIRefDb.id),
server_default=str(URIRefDb.index_of(IDEA.GenericIdeaNode)))
title_id = Column(
Integer(), ForeignKey(LangString.id))
synthesis_title_id = Column(
Integer(), ForeignKey(LangString.id))
description_id = Column(
Integer(), ForeignKey(LangString.id))
title = relationship(
LangString,
lazy="joined", single_parent=True,
primaryjoin=title_id == LangString.id,
backref=backref("idea_from_title", lazy="dynamic"),
cascade="all, delete-orphan")
synthesis_title = relationship(
LangString,
lazy="joined", single_parent=True,
primaryjoin=synthesis_title_id == LangString.id,
backref=backref("idea_from_synthesis_title", lazy="dynamic"),
cascade="all, delete-orphan")
description = relationship(
LangString,
lazy="joined", single_parent=True,
primaryjoin=description_id == LangString.id,
backref=backref("idea_from_description", lazy="dynamic"),
cascade="all, delete-orphan")
hidden = Column(Boolean, server_default='0')
creator_id = Column(Integer, ForeignKey(
AgentProfile.id, ondelete="SET NULL", onupdate="CASCADE"))
pub_state_id = Column(Integer, ForeignKey(
PublicationState.id, ondelete="SET NULL", onupdate="CASCADE"))
@declared_attr
def import_record(cls):
return relationship(
ImportRecord, uselist=False,
primaryjoin=(remote(ImportRecord.target_id)==foreign(cls.id)) &
(ImportRecord.target_table == cls.__tablename__))
@property
def is_imported(self):
return self.db.query(ImportRecord.records_query(self).exists()).scalar()
# temporary placeholders
@property
def definition(self):
if self.description_id:
return self.description.safe_best_entry_in_request().value
return ""
@property
def text_definition(self):
return sanitize_text(self.definition)
@property
def long_title(self):
if self.synthesis_title_id:
return self.synthesis_title.safe_best_entry_in_request().value
return ""
@property
def short_title(self):
if self.title_id:
return self.title.safe_best_entry_in_request().value
return ""
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(
'ideas', order_by="Idea.creation_date",
primaryjoin="and_(Idea.discussion_id==Discussion.id, "
"Idea.tombstone_date == None)"),
info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)}
)
discussion_ts = relationship(
Discussion,
backref=backref(
'ideas_ts', order_by="Idea.creation_date",
cascade="all, delete-orphan")
)
title_entries = relationship(
LangStringEntry, viewonly=True, uselist=True,
primaryjoin=foreign(title_id)==remote(LangStringEntry.langstring_id))
description_entries = relationship(
LangStringEntry, viewonly=True, uselist=True,
primaryjoin=foreign(description_id)==remote(
LangStringEntry.langstring_id))
links = relationship(
"IdeaLink", viewonly=True, uselist=True,
primaryjoin="""(IdeaLink.tombstone_date == None) & (
(IdeaLink.source_id==Idea.id)|(IdeaLink.target_id==Idea.id))""")
creator = relationship(AgentProfile, backref="created_ideas")
pub_state = relationship(PublicationState)
#widget_id = deferred(Column(Integer, ForeignKey('widget.id')))
#widget = relationship("Widget", backref=backref('ideas', order_by=creation_date))
__mapper_args__ = {
'polymorphic_identity': 'idea',
'polymorphic_on': sqla_type,
# Not worth it for now, as the only other class is RootIdea, and there
# is only one per discussion - benoitg 2013-12-23
#'with_polymorphic': '*'
}
[docs] def populate_from_context(self, context):
if not(self.discussion or self.discussion_id):
self.discussion = context.get_instance_of_class(Discussion)
super(Idea, self).populate_from_context(context)
[docs] @classmethod
def special_quad_patterns(cls, alias_maker, discussion_id):
discussion_alias = alias_maker.get_reln_alias(cls.discussion)
return [
QuadMapPatternS(
None, RDF.type,
IriClass(VirtRDF.QNAME_ID).apply(Idea.rdf_type_db.val),
name=QUADNAMES.class_Idea_class),
QuadMapPatternS(
None, FOAF.homepage,
PatternIriClass(
QUADNAMES.idea_external_link_iri,
# TODO: Use discussion.get_base_url.
# This should be computed outside the DB.
get_global_base_url() + '/%s/idea/local:' +
cls.external_typename_with_inheritance() + '/%d', None,
('slug', Unicode, False), ('id', Integer, False)).apply(
discussion_alias.slug, cls.id),
name=QUADNAMES.idea_external_link_map)
]
parents = association_proxy(
'source_links', 'source',
creator=lambda idea: IdeaLink(source=idea))
parents_ts = association_proxy(
'source_links_ts', 'source_ts',
creator=lambda idea: IdeaLink(source=idea))
children = association_proxy(
'target_links', 'target',
creator=lambda idea: IdeaLink(target=idea))
rdf_type_db = relationship(URIRefDb)
@property
def rdf_type_url(self):
return self.rdf_type_db.val
@rdf_type_url.setter
def rdf_type_url(self, val):
val = URIRefDb.get_or_create(val, self.db)
if val != self.rdf_type_db:
self.rdf_type_db = val
self.applyTypeRules()
@property
def rdf_type_curie(self):
return self.rdf_type_db.as_curie
@rdf_type_curie.setter
def rdf_type_curie(self, val):
val = URIRefDb.get_or_create_from_curie(val, self.db)
if val != self.rdf_type_db:
self.rdf_type_db = val
self.applyTypeRules()
@property
def rdf_type(self):
return self.rdf_type_db.as_context
@rdf_type.setter
def rdf_type(self, val):
val = URIRefDb.get_or_create_from_ctx(val, self.db)
if val != self.rdf_type_db:
self.rdf_type_db = val
self.applyTypeRules()
def get_children(self):
return self.db.query(Idea).join(
IdeaLink, (IdeaLink.target_id == Idea.id)
& (IdeaLink.tombstone_date == None)).filter(
(IdeaLink.source_id == self.id)
& (Idea.tombstone_date == None)
).order_by(IdeaLink.order).all()
def get_parents(self):
return self.db.query(Idea).join(
IdeaLink, (IdeaLink.source_id == Idea.id)
& (IdeaLink.tombstone_date == None)).filter(
(IdeaLink.target_id == self.id)
& (Idea.tombstone_date == None)).all()
def safe_title(self, user_prefs, localizer=None):
if self.title:
entry = self.title.best_lang(user_prefs)
if entry:
return entry.value
# absurd fallback
text = _("Idea")
if localizer:
text = localizer.translate(text)
return " ".join((text, str(self.id)))
@property
def parent_uris(self):
return [Idea.uri_generic(l.source_id) for l in self.source_links]
@property
def children_uris(self):
return [Idea.uri_generic(l.target_id) for l in self.target_links]
[docs] def is_owner(self, user_id):
return user_id == self.creator_id
[docs] @classmethod
def restrict_to_owners_condition(cls, query, user_id, alias=None, alias_maker=None):
if not alias:
if alias_maker:
alias = alias_maker.alias_from_class(cls)
else:
alias = cls
return (query, alias.creator_id == user_id)
@classmethod
def pubflowid_from_discussion(cls, discussion):
return discussion.idea_pubflow_id
@classmethod
def local_role_class_and_fkey(cls):
return (IdeaLocalUserRole, 'idea_id')
@classmethod
def query_filter_with_permission_req(
cls, request, permission=P_READ_IDEA, query=None, clsAlias=None):
return cls.query_filter_with_permission(
request.discussion, request.authenticated_userid, permission,
query, request.base_permissions, request.roles, clsAlias)
@property
def widget_add_post_endpoint(self):
# Only for api v2
from pyramid.threadlocal import get_current_request
from .widgets import Widget
req = get_current_request() or {}
ctx = getattr(req, 'context', {})
if getattr(ctx, 'get_instance_of_class', None):
# optional optimization
widget = ctx.get_instance_of_class(Widget)
if widget:
if getattr(widget, 'get_add_post_endpoint', None):
return {widget.uri(): widget.get_add_post_endpoint(self)}
else:
return self.widget_ancestor_endpoints(self)
def widget_ancestor_endpoints(self, target_idea=None):
# HACK. Review consequences after test.
target_idea = target_idea or self
inherited = dict()
for p in self.get_parents():
inherited.update(p.widget_ancestor_endpoints(target_idea))
inherited.update({
widget.uri(): widget.get_add_post_endpoint(target_idea)
for widget in self.widgets
if getattr(widget, 'get_add_post_endpoint', None)
})
return inherited
[docs] def copy(self, tombstone=None, db=None, **kwargs):
if tombstone is True:
tombstone = datetime.utcnow()
kwargs.update(
tombstone=tombstone,
hidden=self.hidden,
creation_date=self.creation_date,
discussion=self.discussion,
pub_state_id=self.pub_state_id,
title=self.title.clone(db=db) if self.title else None,
synthesis_title=self.synthesis_title.clone(db=db) if self.synthesis_title else None,
description=self.description.clone(db=db) if self.description else None)
# TODO: Clone local roles?
return super(Idea, self).copy(db=db, **kwargs)
@classmethod
def get_ancestors_query_cls(
cls, target_id=bindparam('root_id', type_=Integer),
inclusive=True, tombstone_date=None):
if isinstance(target_id, list):
root_condition = IdeaLink.target_id.in_(target_id)
else:
root_condition = (IdeaLink.target_id == target_id)
link = select(
[IdeaLink.source_id, IdeaLink.target_id]
).select_from(
IdeaLink
).where(
(IdeaLink.tombstone_date == tombstone_date) &
(root_condition)
).cte(recursive=True)
target_alias = aliased(link)
sources_alias = aliased(IdeaLink)
parent_link = sources_alias.target_id == target_alias.c.source_id
parents = select(
[sources_alias.source_id, sources_alias.target_id]
).select_from(sources_alias).where(parent_link
& (sources_alias.tombstone_date == tombstone_date))
with_parents = link.union(parents)
select_exp = select([with_parents.c.source_id.label('id')]
).select_from(with_parents)
if inclusive:
if isinstance(target_id, int):
target_id = literal_column(str(target_id), Integer)
elif isinstance(target_id, list):
raise NotImplementedError()
# postgres: select * from unnest(ARRAY[1,6,7]) as id
else:
select_exp = select_exp.union(
select([target_id.label('id')]))
return select_exp.alias('ancestors')
def get_ancestors_query(
self, inclusive=True, tombstone_date=None, subquery=True):
select_exp = self.get_ancestors_query_cls(
self.id, inclusive=inclusive, tombstone_date=tombstone_date)
if subquery:
select_exp = self.db.query(select_exp).subquery()
return select_exp
def get_all_ancestors(self, id_only=False):
query = self.get_ancestors_query(
tombstone_date=self.tombstone_date, subquery=not id_only)
if id_only:
return list((id for (id,) in self.db.query(query)))
else:
return self.db.query(Idea).filter(Idea.id.in_(query)).all()
def get_applicable_announcement(self):
from .announcement import IdeaAnnouncement
if self.announcement:
return self.announcement
aq = self.get_ancestors_query()
announcements = self.db.query(IdeaAnnouncement
).filter(IdeaAnnouncement.idea_id.in_(aq),
IdeaAnnouncement.should_propagate_down==True
).all()
# assume order is preserved from aq...
if announcements:
return announcements[-1]
@classmethod
def get_descendants_query_cls(
cls, root_idea_id=bindparam('root_idea_id', type_=Integer),
inclusive=True):
link = select(
[IdeaLink.source_id, IdeaLink.target_id]
).select_from(
IdeaLink
).where(
(IdeaLink.tombstone_date == None) &
(IdeaLink.source_id == root_idea_id)
).cte(recursive=True)
source_alias = aliased(link)
targets_alias = aliased(IdeaLink)
parent_link = targets_alias.source_id == source_alias.c.target_id
children = select(
[targets_alias.source_id, targets_alias.target_id]
).select_from(targets_alias).where(parent_link
& (targets_alias.tombstone_date == None))
with_children = link.union(children)
select_exp = select([with_children.c.target_id.label('id')]
).select_from(with_children)
if inclusive:
if isinstance(root_idea_id, int):
root_idea_id = literal_column(str(root_idea_id), Integer)
select_exp = select_exp.union(
select([root_idea_id.label('id')]))
return select_exp.alias('descendants')
def get_descendants_query(
self, inclusive=True, subquery=True):
select_exp = self.get_descendants_query_cls(self.id, inclusive=inclusive)
if subquery:
select_exp = self.db.query(select_exp).subquery()
return select_exp
def get_all_descendants(self, id_only=False, inclusive=True):
query = self.get_descendants_query(
inclusive=inclusive, subquery=not id_only)
if id_only:
return list((id for (id,) in self.db.query(query)))
else:
return self.db.query(Idea).filter(Idea.id.in_(query)).all()
def get_order_from_first_parent(self):
return self.source_links[0].order if self.source_links else None
def get_order_from_first_parent_ts(self):
return self.source_links_ts[0].order if self.source_links_ts else None
def get_first_parent_uri(self):
data = self.get_discussion_data(self.discussion_id, False)
if data is not None and self.id in data.parent_dict:
return Idea.uri_generic(data.parent_dict[self.id])
for link in self.source_links:
return Idea.uri_generic(link.source_id)
def get_first_parent_uri_ts(self):
return Idea.uri_generic(
self.source_links_ts[0].source_id
) if self.source_links_ts else None
@classmethod
def get_related_posts_query_c(
cls, discussion_id, root_idea_id, partial=False,
include_deleted=False):
from .generic import Content
counters = cls.prepare_counters(discussion_id)
if partial:
return counters.paths[root_idea_id].as_clause_base(
cls.default_db(), discussion_id, include_deleted=include_deleted)
else:
return counters.paths[root_idea_id].as_clause(
cls.default_db(), discussion_id, counters.user_id, Content,
include_deleted=include_deleted)
@classmethod
def get_discussion_data(cls, discussion_id, create=True):
from pyramid.threadlocal import get_current_request
from .path_utils import DiscussionGlobalData
from pyramid.security import authenticated_userid
req = get_current_request()
discussion_data = None
if req:
discussion_data = getattr(req, "discussion_data", None)
if create and not discussion_data:
discussion_data = DiscussionGlobalData(
cls.default_db(), discussion_id,
authenticated_userid(req) if req else None)
if req:
req.discussion_data = discussion_data
return discussion_data
@classmethod
def prepare_counters(cls, discussion_id, calc_all=False):
discussion_data = cls.get_discussion_data(discussion_id)
return discussion_data.post_path_counter(
discussion_data.user_id, calc_all)
def get_related_posts_query(self, partial=False, include_deleted=False):
return self.get_related_posts_query_c(
self.discussion_id, self.id, partial,
include_deleted=include_deleted)
@classmethod
def _get_orphan_posts_statement(
cls, discussion_id, get_read_status=False, content_alias=None,
include_deleted=False):
""" Requires discussion_id bind parameters
Excludes synthesis posts """
counters = cls.prepare_counters(discussion_id)
return counters.orphan_clause(
counters.user_id if get_read_status else None,
content_alias, include_deleted=include_deleted)
@property
def num_posts(self):
counters = self.prepare_counters(self.discussion_id)
return counters.get_counts(self.id)[0]
@property
def num_contributors(self):
counters = self.prepare_counters(self.discussion_id)
return counters.get_counts(self.id)[1]
@property
def num_read_posts(self):
counters = self.prepare_counters(self.discussion_id)
return counters.get_counts(self.id)[2]
@property
def num_total_and_read_posts(self):
counters = self.prepare_counters(self.discussion_id)
return counters.get_counts(self.id)
def prefetch_descendants(self):
# TODO maparent: descendants only. Let's just prefetch all ideas.
ideas = self.db.query(Idea).filter_by(
discussion_id=self.discussion_id, tombstone_date=None).all()
ideas_by_id = {idea.id: idea for idea in ideas}
children_id_dict = self.children_dict(self.discussion_id)
return {
id: [ideas_by_id[child_id] for child_id in child_ids]
for (id, child_ids) in children_id_dict.items()
}
def visit_ideas_depth_first(self, idea_visitor):
children_dict = self.prefetch_descendants()
return self._visit_ideas_depth_first(idea_visitor, set(), 0, None, children_dict)
def _visit_ideas_depth_first(
self, idea_visitor, visited, level, prev_result, children_dict):
if self in visited:
# not necessary in a tree, but let's start to think graph.
return False
result = idea_visitor.visit_idea(self, level, prev_result)
visited.add(self)
child_results = []
if result is not IdeaVisitor.CUT_VISIT:
for child in children_dict.get(self.id, ()):
r = child._visit_ideas_depth_first(
idea_visitor, visited, level+1, result, children_dict)
if r:
child_results.append((child, r))
return idea_visitor.end_visit(self, level, result, child_results)
@classmethod
def children_dict(cls, discussion_id):
# We do not want a subclass
cls = [c for c in cls.mro() if c.__name__=="Idea"][0]
source = aliased(cls, name="source")
target = aliased(cls, name="target")
link_info = list(cls.default_db.query(
IdeaLink.target_id, IdeaLink.source_id
).join(source, source.id == IdeaLink.source_id
).join(target, target.id == IdeaLink.target_id
).filter(
source.discussion_id == discussion_id,
IdeaLink.tombstone_date == None,
source.tombstone_date == None,
target.tombstone_date == None,
target.discussion_id == discussion_id
).order_by(IdeaLink.order))
if not link_info:
(root_id,) = cls.default_db.query(
RootIdea.id).filter_by(discussion_id=discussion_id).first()
return {None: (root_id,), root_id: ()}
child_nodes = {child for (child, parent) in link_info}
children_of = defaultdict(list)
for (child, parent) in link_info:
children_of[parent].append(child)
root = set(children_of.keys()) - child_nodes
assert len(root) == 1
children_of[None] = [root.pop()]
return children_of
@classmethod
def visit_idea_ids_depth_first(
cls, idea_visitor, discussion_id, children_dict=None):
# Lightweight descent
if children_dict is None:
children_dict = cls.children_dict(discussion_id)
root_id = children_dict[None][0]
return cls._visit_idea_ids_depth_first(
root_id, idea_visitor, children_dict, set(), 0, None)
@classmethod
def _visit_idea_ids_depth_first(
cls, idea_id, idea_visitor, children_dict, visited,
level, prev_result):
if idea_id in visited:
# not necessary in a tree, but let's start to think graph.
return False
result = idea_visitor.visit_idea(idea_id, level, prev_result)
visited.add(idea_id)
child_results = []
if result is not IdeaVisitor.CUT_VISIT:
for child_id in children_dict[idea_id]:
r = cls._visit_idea_ids_depth_first(
child_id, idea_visitor, children_dict, visited, level+1, result)
if r:
child_results.append((child_id, r))
return idea_visitor.end_visit(idea_id, level, result, child_results)
def visit_ideas_breadth_first(self, idea_visitor):
self.prefetch_descendants()
result = idea_visitor.visit_idea(self, 0, None)
visited = {self}
if result is not IdeaVisitor.CUT_VISIT:
return self._visit_ideas_breadth_first(
idea_visitor, visited, 1, result)
def _visit_ideas_breadth_first(
self, idea_visitor, visited, level, prev_result):
children = []
result = True
child_results = []
for child in self.get_children():
if child in visited:
continue
result = idea_visitor.visit_idea(child, level, prev_result)
visited.add(child)
if result != IdeaVisitor.CUT_VISIT:
children.append(child)
if result:
child_results.append((child, r))
for child in children:
child._visit_ideas_breadth_first(
idea_visitor, visited, level+1, result)
return idea_visitor.end_visit(self, level, prev_result, child_results)
def most_common_words(self, lang=None, num=8):
if lang:
langs = (lang,)
else:
langs = self.discussion.discussion_locales
word_counter = WordCountVisitor(langs)
self.visit_ideas_depth_first(word_counter)
return word_counter.best(num)
@property
def most_common_words_prop(self):
return self.most_common_words()
def get_siblings_of_type(self, cls):
# TODO: optimize
siblings = set(chain(*(p.children for p in self.get_parents())))
if siblings:
siblings.remove(self)
return [c for c in siblings if isinstance(c, cls)]
def get_synthesis_contributors(self, id_only=True):
# author of important extracts
from .idea_content_link import Extract, IdeaExtractLink
from .post import Post
from .generic import Content
from sqlalchemy.sql.functions import count
subquery = self.get_descendants_query()
query = self.db.query(
Post.creator_id
).join(IdeaExtractLink
).join(Extract
).join(subquery, IdeaExtractLink.idea_id == subquery.c.id
).filter(Extract.important == True
).group_by(Post.creator_id
).order_by(count(Extract.id).desc())
if id_only:
return [AgentProfile.uri_generic(a) for (a,) in query]
else:
ids = [x for (x,) in query]
if not ids:
return []
agents = {a.id: a for a in self.db.query(AgentProfile).filter(
AgentProfile.id.in_(ids))}
return [agents[id] for id in ids]
def get_contributors(self):
from .generic import Content
from .post import Post
from sqlalchemy.sql.functions import count
related = self.get_related_posts_query(True)
content = with_polymorphic(
Content, [], Content.__table__,
aliased=False, flat=True)
post = with_polymorphic(
Post, [], Post.__table__,
aliased=False, flat=True)
query = self.db.query(post.creator_id
).join(content, post.id == content.id
).join(related, content.id == related.c.post_id
).filter(content.hidden == False,
content.discussion_id == self.discussion_id
).group_by(
post.creator_id
).order_by(
count(post.id.distinct()).desc())
return ['local:Agent/' + str(i) for (i,) in query]
def applyTypeRules(self):
from ..semantic.inference import get_inference_store
ontology = get_inference_store()
typology = self.discussion.idea_typology
rules = typology.get('ideas', {}).get(self.rdf_type, {}).get('rules', {})
for child_link in self.target_links:
link_type = child_link.rdf_type
child = child_link.target
child_type = child.rdf_type
if child_type != 'GenericIdeaNode':
child_types = (
set(ontology.getSuperClassesCtx(child_type)) -
set(ontology.getSuperClassesCtx('GenericIdeaNode')))
else:
child_types = set((child_type,))
if link_type != 'InclusionRelation':
link_types = (
set(ontology.getSuperClassesCtx(link_type)) -
set(ontology.getSuperClassesCtx('InclusionRelation')))
else:
link_types = set((link_type,))
if link_type not in rules:
# TODO: no real guarantee of mro ordering in that function
if link_type not in rules:
for supertype in link_types:
if supertype in rules:
break
else:
supertype = 'InclusionRelation'
child_link.rdf_type = link_type = supertype
node_rules = rules.get(link_type, ())
if child_type not in node_rules:
# Ideally keep child type stable. In order:
# 0: Supertype of link and same child_type
# 1: Any link with same child_type
# 2: Original link with child supertype
# 3: Supertype of link with child supertype
# 4: Original link with generic child type
potential_link_types = []
for r_link_type, r_child_types in rules.items():
r_child_types = set(r_child_types)
if child_type in r_child_types:
potential_link_types.append((
0 if r_link_type in link_types else 1,
r_link_type, child_type))
else:
inter = child_types.intersection(r_child_types)
if inter:
potential_link_types.append((
2 if r_link_type == link_type else 3,
r_link_type, inter.pop()))
if 'GenericIdeaNode' in node_rules:
potential_link_types.append((4, link_type, 'GenericIdeaNode'))
else:
potential_link_types.append((5, 'InclusionRelation', 'GenericIdeaNode'))
potential_link_types.sort()
_, child_link.rdf_type, new_child_type = potential_link_types[0]
if new_child_type != child_type:
child.rdf_type = new_child_type
child.applyTypeRules()
[docs] def get_discussion_id(self):
return self.discussion_id or self.discussion.id
def get_definition_preview(self):
body = self.definition.strip()
target_len = 120
shortened = False
html_len = 2 * target_len
while True:
text = sanitize_text(body[:html_len])
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 get_url(self):
from assembl.lib.frontend_urls import FrontendUrls
frontendUrls = FrontendUrls(self.discussion)
return frontendUrls.get_idea_url(self)
def local_mind_map(self):
import pygraphviz
from colour import Color
from datetime import datetime
from assembl.models import Idea, IdeaLink, RootIdea
G = pygraphviz.AGraph(directed=True, overlap=False)
# G.graph_attr['overlap']='prism'
G.node_attr['penwidth']=0
G.node_attr['shape']='rect'
G.node_attr['style']='filled'
G.node_attr['fillcolor'] = '#efefef'
root_time = self.creation_date
class MindMapVisitor(IdeaVisitor):
def __init__(self, G):
self.G = G
self.min_time = None
self.max_time = None
def visit_idea(self, idea, level, prev_result):
age = ((idea.last_modified or idea.creation_date)-root_time).total_seconds() # may be negative
color = Color(hsl=(180-(135.0 * age), 0.15, 0.85))
kwargs = {}
if idea.description_id:
kwargs['tooltip'] = idea.text_definition
G.add_node(idea.id,
label=idea.short_title or "",
fontsize = 18 - (1.5 * level),
height=(20-(1.5*level))/72.0,
# fillcolor=color.hex,
target="idealoom",
URL=idea.get_url(),
**kwargs)
if prev_result:
links = [l for l in idea.source_links if l.source_id == prev_result.id]
if links:
link = links[0]
G.add_edge(link.source_id, link.target_id)
return idea
visitor = MindMapVisitor(G)
self.visit_ideas_depth_first(visitor)
G.layout(prog='twopi')
return G
[docs] @classmethod
def get_discussion_conditions(cls, discussion_id, alias_maker=None):
return (cls.discussion_id == discussion_id,)
[docs] def send_to_changes(self, connection=None, operation=CrudOperation.UPDATE,
discussion_id=None, view_def="changes"):
"""invoke the modelWatcher on creation"""
connection = connection or self.db.connection()
if self.is_tombstone:
self.tombstone().send_to_changes(
connection, CrudOperation.DELETE, discussion_id, view_def)
else:
super(Idea, self).send_to_changes(
connection, operation, discussion_id, view_def)
watcher = get_model_watcher()
if operation == CrudOperation.UPDATE:
watcher.processIdeaModified(self.id, 0) # no versions yet.
elif operation == CrudOperation.DELETE:
watcher.processIdeaDeleted(self.id)
elif operation == CrudOperation.CREATE:
watcher.processIdeaCreated(self.id)
@as_native_str()
def __repr__(self):
r = super(Idea, self).__repr__()
title = self.short_title or ""
return r[:-1] + title + ">"
@classmethod
def invalidate_ideas(cls, discussion_id, post_id):
raise NotImplementedError()
[docs] @classmethod
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.items() if incl]
return ideas
[docs] @classmethod
def idea_read_counts(cls, discussion_id, post_id, user_id):
"""Given a post and a user, give the total and read count
of posts for each affected idea"""
idea_ids = cls.get_idea_ids_showing_post(post_id)
if not idea_ids:
return []
ideas = cls.default_db.query(cls).filter(cls.id.in_(idea_ids))
return [(idea.id, idea.num_read_posts)
for idea in ideas]
def get_widget_creation_urls(self):
from .widgets import GeneratedIdeaWidgetLink
return [wl.context_url for wl in self.widget_links
if isinstance(wl, GeneratedIdeaWidgetLink)]
@property
def pub_state_name(self):
return self.pub_state.label if self.pub_state else None
@pub_state_name.setter
def pub_state_name(self, name):
flow = self.discussion.idea_publication_flow
state = PublicationState.getByName(name, parent_object=flow)
# assert?
if state:
old = self.pub_state
self.pub_state = state
return old
def can_apply_transition(self, transition, user_id, base_permissions):
if transition.req_permission_name in base_permissions:
return True
return transition.req_permission_name in self.extra_permissions_for(user_id)
def apply_transition(self, name, user_id, permissions=None):
flow = self.discussion.idea_publication_flow
transition = PublicationTransition.getByName(name, parent_object=flow)
assert transition, "Cannot find transition " + name
if permissions is None:
permissions = self.all_permissions_for(user_id)
if transition.req_permission_name not in permissions:
raise HTTPUnauthorized("You need permission %s to apply transition %s" % (
transition.req_permission_name, transition.label))
if transition.source_id and transition.source_id != self.pub_state_id:
raise HTTPBadRequest("Transition %s applies to state %s, not %s" %(
transition.label, transition.source_label, self.pub_state_name))
self.pub_state_id = transition.target_id
def safe_set_pub_state(self, state_label, user_id=None, request=None):
pub_state = (self.pub_state or
self.discussion.preferences['default_idea_pub_state'])
if state_label == pub_state.label:
return True
assert self.discussion.idea_publication_flow
if request is None:
from pyramid.threadlocal import get_current_request
request = get_current_request()
assert user_id or request, "Please call with a request or user_id"
if not user_id:
user_id = request.user.id
permissions = self.all_permissions_for(user_id, request)
if P_SYSADMIN in permissions or P_ADMIN_DISC in permissions:
state = self.discussion.idea_publication_flow.state_by_label(state_label)
assert state, "No such state"
self.pub_state = state
return True
# look for a transition chain that can lead you to target state.
new_states = {pub_state}
known_states = set()
while new_states:
state = new_states.pop()
for transition in state.transitions_to:
if transition.req_permission_name in permissions:
if transition.target.label == state_label:
self.pub_state = transition.target
return True
new_states.add(transition.target)
known_states.add(state)
new_states -= known_states
return False
def all_permissions_for(self, user_id, request=None):
if request is None:
from pyramid.threadlocal import get_current_request
request = get_current_request()
if request:
if request.main_target == self:
return request.permissions
else:
return list(set(request.base_permissions) | set(self.extra_permissions_for(user_id)))
else:
from assembl.auth.util import get_permissions
return get_permissions(user_id, self.discussion_id, self)
def extra_permissions_for(self, user_id):
from assembl.auth.util import get_permissions, permissions_for_state
from pyramid.threadlocal import get_current_request
request = get_current_request()
if request.unauthenticated_userid != user_id:
request = None
base_permissions = request.base_permissions if request else get_permissions(user_id, self.discussion_id)
if request and request.main_target is self:
permissions = request.permissions
elif not self.local_user_roles:
if not self.pub_state_id:
return []
if request:
permissions = request.permissions_for_states[self.pub_state.label]
else:
permissions = permissions_for_state(self.discussion_id, self.pub_state_id, user_id)
else:
permissions = get_permissions(user_id, self.discussion_id, self)
return list(set(permissions) - set(base_permissions))
def extra_permissions(self):
from pyramid.threadlocal import get_current_request
request = get_current_request()
assert request, "Only use from a request"
# authenticated hits database.
user_id = request.unauthenticated_userid
return self.extra_permissions_for(user_id)
def principals_with_read_permission(self):
from ..auth.util import roles_with_permission
from .publication_states import StateDiscussionPermission
from .auth import User
# TODO: CACHE!!!
base = set(roles_with_permission(self.get_discussion(), P_READ_IDEA))
q = self.db.query(Role.name).join(StateDiscussionPermission
).filter_by(discussion_id=self.discussion_id,
pub_state_id=self.pub_state_id
).join(Permission).filter_by(name=P_READ_IDEA).all()
base.update((x for (x,) in q))
# stop caching here
base.update((local_role.get_user_uri()
for local_role in self.local_user_roles
if local_role.role_name in base))
creator_id = self.creator_id
if creator_id:
base.add(User.uri_generic(creator_id))
return list(base)
# def get_notifications(self):
# # Dead code?
# from .widgets import BaseIdeaWidgetLink
# for widget_link in self.widget_links:
# if not isinstance(self, BaseIdeaWidgetLink):
# continue
# for n in widget_link.widget.has_notification():
# yield n
@classmethod
def get_all_idea_links(cls, discussion_id):
target = aliased(cls)
source = aliased(cls)
return cls.default_db.query(
IdeaLink).join(
source, source.id == IdeaLink.source_id).join(
target, target.id == IdeaLink.target_id).filter(
target.discussion_id == discussion_id).filter(
source.discussion_id == discussion_id).filter(
IdeaLink.tombstone_date == None).all()
@classmethod
def extra_collections(cls):
from .widgets import Widget
from .idea_content_link import (
IdeaRelatedPostLink, IdeaContentWidgetLink)
from .generic import Content
from .post import Post, WidgetPost
from ..views.traversal import NsDictCollection
class ChildIdeaCollectionDefinition(AbstractCollectionDefinition):
def __init__(self, cls):
super(ChildIdeaCollectionDefinition, self).__init__(
cls, 'children', Idea)
def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx):
parent = owner_alias
children = last_alias
return query.join(
IdeaLink, IdeaLink.target_id == children.id).join(
parent, IdeaLink.source_id == parent.id).filter(
IdeaLink.source_id == parent_instance.id,
IdeaLink.tombstone_date == None,
children.tombstone_date == None)
def contains(self, parent_instance, instance):
return instance.db.query(
IdeaLink).filter_by(
source=parent_instance, target=instance
).count() > 0
@collection_creation_side_effects.register(
inst_ctx=Idea, ctx='Idea.children')
def add_child_link(inst_ctx, ctx):
yield InstanceContext(
inst_ctx['target_links'],
IdeaLink(
source=ctx.parent_instance, target=inst_ctx._instance))
class LinkedPostCollectionDefinition(AbstractCollectionDefinition):
# used by inspiration widget
def __init__(self, cls):
super(LinkedPostCollectionDefinition, self).__init__(
cls, 'linkedposts', Content)
def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx):
return query.join(IdeaRelatedPostLink, owner_alias)
def contains(self, parent_instance, instance):
return instance.db.query(
IdeaRelatedPostLink).filter_by(
content=instance, idea=parent_instance
).count() > 0
@collection_creation_side_effects.register(
inst_ctx=Post, ctx='Idea.linkedposts')
def add_related_post_link(inst_ctx, ctx):
post = inst_ctx._instance
idea = ctx.parent_instance
link = IdeaRelatedPostLink(
content=post, idea=idea,
creator=post.creator)
yield InstanceContext(
inst_ctx['idea_links_of_content'], link)
@collection_creation_side_effects.register(
inst_ctx=WidgetPost, ctx='Idea.linkedposts')
def add_youtube_attachment(inst_ctx, ctx):
from .attachment import Document, PostAttachment
for subctx in add_related_post_link(inst_ctx, ctx):
yield subctx
post = inst_ctx._instance
insp_url = post.metadata_json.get('inspiration_url', '')
if insp_url.startswith("https://www.youtube.com/"):
# TODO: detect all video/image inspirations.
# Handle duplicates in docs!
# Check whether we already have such an attachment?
doc = Document(
discussion=post.discussion,
uri_id=insp_url)
doc = doc.handle_duplication()
attachment_ctx = InstanceContext(
inst_ctx['attachments'],
PostAttachment(
discussion=post.discussion,
creator=post.creator,
document=doc,
attachmentPurpose='EMBED_ATTACHMENT',
post=post))
yield attachment_ctx
yield InstanceContext(
attachment_ctx['document'], doc)
class WidgetPostCollectionDefinition(AbstractCollectionDefinition):
# used by creativity widget
def __init__(self, cls):
super(WidgetPostCollectionDefinition, self).__init__(
cls, 'widgetposts', Content)
def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx):
from .post import IdeaProposalPost
idea = owner_alias
query = query.join(IdeaContentWidgetLink).join(
idea,
IdeaContentWidgetLink.idea_id == parent_instance.id)
if Content in chain(*(
mapper.entities for mapper in query._entities)):
query = query.options(
contains_eager(Content.widget_idea_links))
# contains_eager(Content.extracts) seems to slow things down instead
# Filter out idea proposal posts
query = query.filter(last_alias.type.notin_(
IdeaProposalPost.polymorphic_identities()))
return query
def contains(self, parent_instance, instance):
return instance.db.query(
IdeaContentWidgetLink).filter_by(
content=instance, idea=parent_instance
).count() > 0
@collection_creation_side_effects.register(
inst_ctx=Post, ctx='Idea.widgetposts')
def add_content_widget_link(inst_ctx, ctx):
obj = inst_ctx._instance
if ctx.parent_instance.proposed_in_post:
obj.set_parent(ctx.parent_instance.proposed_in_post)
obj.hidden = True
yield InstanceContext(
inst_ctx['idea_links_of_content'],
IdeaContentWidgetLink(
content=obj, idea=ctx.parent_instance,
creator=obj.creator))
class ActiveShowingWidgetsCollection(RelationCollectionDefinition):
def __init__(self, cls):
super(ActiveShowingWidgetsCollection, self).__init__(
cls, cls.active_showing_widget_links)
def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx):
from .widgets import IdeaShowingWidgetLink
idea = owner_alias
widget_idea_link = last_alias
query = query.join(
idea, widget_idea_link.idea).join(
Widget, widget_idea_link.widget).filter(
Widget.test_active(),
widget_idea_link.type.in_(
IdeaShowingWidgetLink.polymorphic_identities()),
idea.id == parent_instance.id)
return query
return (ChildIdeaCollectionDefinition(cls),
LinkedPostCollectionDefinition(cls),
WidgetPostCollectionDefinition(cls),
NsDictCollection(cls),
ActiveShowingWidgetsCollection(cls))
def widget_link_signatures(self):
from .widgets import Widget
return [
{'widget': Widget.uri_generic(l.widget_id),
'@type': l.external_typename()}
for l in self.widget_links]
def active_widget_uris(self):
from .widgets import Widget
return [Widget.uri_generic(l.widget_id)
for l in self.active_showing_widget_links]
crud_permissions = CrudPermissions(
P_ADD_IDEA, P_READ_IDEA, P_EDIT_IDEA, P_ADMIN_DISC, variable=MAYBE)
LangString.setup_ownership_load_event(Idea,
['title', 'description', 'synthesis_title'])
[docs]class RootIdea(Idea):
"""
The root idea. It represents the discussion.
It has implicit links to all content and posts in the discussion.
"""
root_for_discussion = relationship(
Discussion,
backref=backref('root_idea', uselist=False),
)
__mapper_args__ = {
'polymorphic_identity': 'root_idea',
}
def __init__(self, *args, **kwargs):
kwargs['rdf_type_id'] = 3
super(RootIdea, self).__init__(*args, **kwargs)
@property
def num_posts(self):
""" In the root idea, num_posts is the count of all non-deleted mesages in the discussion """
from .post import Post
result = self.db.query(Post).filter(
Post.discussion_id == self.discussion_id,
Post.hidden==False,
Post.tombstone_condition()
).count()
return int(result)
@property
def num_read_posts(self):
""" In the root idea, num_posts is the count of all non-deleted read mesages in the discussion """
from .post import Post
from .action import ViewPost
discussion_data = self.get_discussion_data(self.discussion_id)
result = self.db.query(Post).filter(
Post.discussion_id == self.discussion_id,
Post.hidden==False,
Post.tombstone_condition()
).join(
ViewPost,
(ViewPost.post_id == Post.id)
& (ViewPost.tombstone_date == None)
& (ViewPost.actor_id == discussion_data.user_id)
).count()
return int(result)
@property
def num_contributors(self):
""" In the root idea, num_posts is the count of contributors to
all non-deleted mesages in the discussion """
from .post import Post
result = self.db.query(Post.creator_id).filter(
Post.discussion_id == self.discussion_id,
Post.hidden==False,
Post.tombstone_condition()
).distinct().count()
return int(result)
@property
def num_total_and_read_posts(self):
return (self.num_posts, self.num_contributors, self.num_read_posts)
@property
def num_orphan_posts(self):
"The number of posts unrelated to any idea in the current discussion"
counters = self.prepare_counters(self.discussion_id)
return counters.get_orphan_counts()[0]
@property
def num_synthesis_posts(self):
""" In the root idea, this is the count of all published and non-deleted SynthesisPost of the discussion """
return self.discussion.get_all_syntheses_query(False).count()
def discussion_topic(self):
return self.discussion.topic
crud_permissions = CrudPermissions(P_ADMIN_DISC)
[docs]class IdeaLink(HistoryMixinWithOrigin, DiscussionBoundBase):
"""
A generic link between two ideas
If a parent-child relation, the parent is the source, the child the target.
Note: it's reversed in the RDF model.
"""
__tablename__ = 'idea_idea_link'
__external_typename = "DirectedIdeaRelation"
rdf_class = IDEA.DirectedIdeaRelation
rdf_type_id = Column(
Integer, ForeignKey('uriref.id'),
server_default=str(URIRefDb.index_of(IDEA.InclusionRelation)))
source_id = Column(
Integer, ForeignKey(
'idea.id', ondelete="CASCADE", onupdate="CASCADE"),
nullable=False, index=True)
#info={'rdf': QuadMapPatternS(None, IDEA.target_idea)})
target_id = Column(Integer, ForeignKey(
'idea.id', ondelete="CASCADE", onupdate="CASCADE"),
nullable=False, index=True)
source = relationship(
'Idea',
primaryjoin="and_(Idea.id==IdeaLink.source_id, "
"Idea.tombstone_date == None)",
backref=backref(
'target_links',
primaryjoin="and_(Idea.id==IdeaLink.source_id, "
"IdeaLink.tombstone_date == None)",
cascade="all, delete-orphan"),
foreign_keys=(source_id))
target = relationship(
'Idea',
primaryjoin="and_(Idea.id==IdeaLink.target_id, "
"Idea.tombstone_date == None)",
backref=backref(
'source_links',
primaryjoin="and_(Idea.id==IdeaLink.target_id, "
"IdeaLink.tombstone_date == None)",
cascade="all, delete-orphan"),
foreign_keys=(target_id))
source_ts = relationship(
'Idea',
backref=backref('target_links_ts', cascade="all, delete-orphan"),
foreign_keys=(source_id))
target_ts = relationship(
'Idea',
backref=backref('source_links_ts', cascade="all, delete-orphan"),
foreign_keys=(target_id))
order = Column(
Float, nullable=False, default=0.0,
info={'rdf': QuadMapPatternS(None, ASSEMBL.link_order)})
@declared_attr
def import_record(cls):
return relationship(
ImportRecord, uselist=False,
primaryjoin=(remote(ImportRecord.target_id)==foreign(cls.id)) &
(ImportRecord.target_table == cls.__tablename__))
rdf_type_db = relationship(URIRefDb)
@property
def rdf_type_url(self):
return self.rdf_type_db.val
@rdf_type_url.setter
def rdf_type_url(self, val):
self.rdf_type_db = URIRefDb.get_or_create(val, self.db)
@property
def rdf_type_curie(self):
return self.rdf_type_db.as_curie
@rdf_type_curie.setter
def rdf_type_curie(self, val):
self.rdf_type_db = URIRefDb.get_or_create_from_curie(val, self.db)
@property
def rdf_type(self):
return self.rdf_type_db.as_context
@rdf_type.setter
def rdf_type(self, val):
self.rdf_type_db = URIRefDb.get_or_create_from_ctx(val, self.db)
[docs] def populate_from_context(self, context):
if not(self.source or self.source_ts or self.source_id):
self.source = context.get_instance_of_class(Idea)
super(IdeaLink, self).populate_from_context(context)
[docs] @classmethod
def base_conditions(cls, alias=None, alias_maker=None):
if alias_maker is None:
idea_link = alias or cls
source_idea = Idea
else:
idea_link = alias or alias_maker.alias_from_class(cls)
source_idea = alias_maker.alias_from_relns(idea_link.source)
# Assume tombstone status of target is similar to source, for now.
return ((idea_link.tombstone_date == None),
(idea_link.source_id == source_idea.id),
(source_idea.tombstone_date == None))
[docs] @classmethod
def special_quad_patterns(cls, alias_maker, discussion_id):
idea_link = alias_maker.alias_from_class(cls)
target_alias = alias_maker.alias_from_relns(cls.target)
source_alias = alias_maker.alias_from_relns(cls.source)
# Assume tombstone status of target is similar to source, for now.
conditions = [(idea_link.target_id == target_alias.id),
(target_alias.tombstone_date == None)]
if discussion_id:
conditions.append((target_alias.discussion_id == discussion_id))
return [
QuadMapPatternS(
Idea.iri_class().apply(idea_link.source_id),
IDEA.includes,
Idea.iri_class().apply(idea_link.target_id),
conditions=conditions,
name=QUADNAMES.idea_inclusion_reln),
QuadMapPatternS(
cls.iri_class().apply(idea_link.id),
IDEA.source_idea, # Note that RDF is inverted
Idea.iri_class().apply(idea_link.target_id),
conditions=conditions,
name=QUADNAMES.col_pattern_IdeaLink_target_id
#exclude_base_condition=True
),
QuadMapPatternS(
cls.iri_class().apply(idea_link.id),
IDEA.target_idea,
Idea.iri_class().apply(idea_link.source_id),
name=QUADNAMES.col_pattern_IdeaLink_source_id
),
QuadMapPatternS(
None, RDF.type, IriClass(VirtRDF.QNAME_ID).apply(IdeaLink.rdf_type_db.val),
name=QUADNAMES.class_IdeaLink_class),
]
def derived_sub_properties(self):
from ..semantic.inference import get_inference_store
from ..semantic.namespaces import RDFS, IDEA
ontology = get_inference_store()
my_type = self.rdf_type_url
result = {}
for atype in chain([my_type], ontology.getDirectSuperClasses(my_type)):
props = ontology.ontology.subjects(RDFS.domain, atype)
for prop in props:
# bug: why doesn't to_symbol work?
name = ontology.context._prefixes[str(prop)]
for superp in ontology.getDirectSuperProperties(prop):
if superp == IDEA.source_idea:
result[name] = Idea.uri_generic(self.target_id)
break
elif superp == IDEA.target_idea:
result[name] = Idea.uri_generic(self.source_id)
break
return result
[docs] def copy(self, tombstone=None, db=None, **kwargs):
kwargs.update(
tombstone=tombstone,
order=self.order,
creation_date=self.creation_date,
source_id=self.source_id,
target_id=self.target_id)
return super(IdeaLink, self).copy(db=db, **kwargs)
[docs] def get_discussion_id(self):
source = self.source_ts or self.source or Idea.get(self.source_id)
return source.get_discussion_id()
[docs] def send_to_changes(self, connection=None, operation=CrudOperation.UPDATE,
discussion_id=None, view_def="changes"):
connection = connection or self.db.connection()
if self.is_tombstone:
self.tombstone().send_to_changes(
connection, CrudOperation.DELETE, discussion_id, view_def)
else:
super(IdeaLink, self).send_to_changes(
connection, operation, discussion_id, view_def)
[docs] @classmethod
def get_discussion_conditions(cls, discussion_id, alias_maker=None):
if alias_maker is None:
idea_link = cls
source_idea = Idea
else:
idea_link = alias_maker.alias_from_class(cls)
source_idea = alias_maker.alias_from_relns(idea_link.source)
return ((idea_link.source_id == source_idea.id),
(source_idea.discussion_id == discussion_id))
[docs] def user_can(self, user_id, operation, permissions):
result = super(IdeaLink, self).user_can(user_id, operation, permissions)
if operation != CrudOperation.CREATE and not result:
user_id = user_id or Everyone
perm, owner_perm = self.crud_permissions.crud_permissions(operation)
local_perms = self.target.local_permissions(user_id)
if perm in local_perms:
return perm
is_owner = self.target.is_owner(user_id)
if is_owner and owner_perm in local_perms:
return owner_perm
return False
return result
crud_permissions = CrudPermissions(
P_ADD_IDEA, P_READ, P_ASSOCIATE_IDEA, P_ASSOCIATE_IDEA)
# discussion = relationship(
# Discussion, viewonly=True, uselist=False, backref="idea_links",
# secondary=Idea.__table__, primaryjoin=(source_id == Idea.id),
# info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)})
discussion = relationship(
Discussion,
viewonly=True,
uselist=False,
secondary=Idea.__table__,
primaryjoin=(source_id == Idea.id),
# secondaryjoin=(Idea.discussion_id == Discussion.id),
# backref is Discussion.idea_links below
info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation),
'backref': 'Discussion.idea_links'}
)
discussion_ts = relationship(
Discussion,
viewonly=True,
uselist=False,
secondary=Idea.__table__,
primaryjoin=(source_id == Idea.id),
# backref is Discussion.idea_links_ts below
info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation),
'backref': 'Discussion.idea_links_ts'}
)
# explicit backref to IdeaLink.discussion
Discussion.idea_links = relationship(
IdeaLink,
viewonly=True,
secondary=Idea.__table__,
primaryjoin=(Idea.discussion_id == Discussion.id),
secondaryjoin="""and_(IdeaLink.source_id==Idea.id,
Idea.tombstone_date == None,
IdeaLink.tombstone_date == None)""",
info={'backref': IdeaLink.discussion})
# explicit backref to IdeaLink.discussion_ts
Discussion.idea_links_ts = relationship(
IdeaLink,
viewonly=True,
secondary=Idea.__table__,
secondaryjoin=(IdeaLink.source_id == Idea.id),
info={'backref': IdeaLink.discussion_ts}
)
_it = Idea.__table__
_ilt = IdeaLink.__table__
Idea.num_children = column_property(
select([func.count(_ilt.c.id)]).where(
(_ilt.c.source_id == _it.c.id)
& (_ilt.c.tombstone_date == None)
& (_it.c.tombstone_date == None)
).correlate_except(_ilt),
deferred=True)
[docs]class IdeaLocalUserRole(AbstractLocalUserRole):
"""The role that a user has in the context of a discussion"""
__tablename__ = 'idea_user_role'
user = relationship(AgentProfile, backref=backref("local_idea_roles", cascade="all, delete-orphan"))
idea_id = Column(Integer, ForeignKey(
Idea.id, ondelete='CASCADE', onupdate='CASCADE'), nullable=False, index=True)
idea = relationship(
Idea, backref=backref(
"local_user_roles", cascade="all, delete-orphan", lazy="subquery"))
__table_args__ = (
Index('ix_idea_local_user_role_user_idea',
'profile_id', 'idea_id'),)
@classmethod
def filter_on_instance(cls, instance, query):
assert isinstance(instance, Idea)
return query.filter_by(idea_id=instance.id)
[docs] def get_discussion_id(self):
return self.idea.discussion_id
[docs] def container_url(self):
return "/data/Discussion/%d/ideas/%d/local_user_roles" % (
self.discussion_id, self.idea_id)
def get_default_parent_context(self, request=None, user_id=None):
return self.idea.get_collection_context('local_user_roles', request=request, user_id=user_id)
[docs] @classmethod
def get_discussion_conditions(cls, discussion_id, alias_maker=None):
if alias_maker is None:
idea_local_role = cls
idea = Idea
else:
idea_local_role = alias_maker.alias_from_class(cls)
idea = alias_maker.alias_from_relns(idea_local_role.idea)
return ((idea_local_role.idea_id == idea.id),
(idea.discussion_id == discussion_id))
[docs] def unique_query(self):
query, _ = super(IdeaLocalUserRole, self).unique_query()
profile_id = self.profile_id or self.user.id
role_id = self.role_id or self.role.id
idea_id = self.idea_id or self.idea.id
return query.filter_by(
profile_id=profile_id, role_id=role_id, idea_id=idea_id), True
def _do_update_from_json(
self, json, parse_def, ctx,
duplicate_handling=None, object_importer=None):
user_id = ctx.get_user_id()
json_user_id = json.get('user', None)
if json_user_id is None:
json_user_id = user_id
else:
json_user_id = AgentProfile.get_database_id(json_user_id)
# Do not allow changing user
if self.profile_id is not None and json_user_id != self.profile_id:
raise HTTPBadRequest()
self.profile_id = json_user_id
role_name = json.get("role", None)
if not (role_name or self.role_id):
role_name = R_PARTICIPANT
if role_name:
role = Role.getByName(role_name)
if not role:
raise HTTPBadRequest("Invalid role name:"+role_name)
self.role = role
json_discussion_id = json.get('discussion', None)
if json_discussion_id:
from .discussion import Discussion
json_discussion_id = Discussion.get_database_id(json_discussion_id)
# Do not allow change of discussion
if self.discussion_id is not None \
and json_discussion_id != self.discussion_id:
raise HTTPBadRequest()
self.discussion_id = json_discussion_id
else:
if not self.discussion_id:
raise HTTPBadRequest()
return self
crud_permissions = CrudPermissions(P_ADMIN_DISC, P_READ)
@event.listens_for(IdeaLocalUserRole, 'after_delete', propagate=True)
@event.listens_for(IdeaLocalUserRole, 'after_insert', propagate=True)
def send_user_to_socket_for_idea_local_user_role(
mapper, connection, target):
user = target.user
if not target.user:
user = User.get(target.profile_id)
user.send_to_changes(connection, CrudOperation.UPDATE, target.get_discussion_id())
user.send_to_changes(
connection, CrudOperation.UPDATE, target.get_discussion_id(), "private")