Source code for assembl.models.action

"""Records of actions taken by the platform users.

"""

from datetime import datetime

from future.utils import as_native_str
from sqlalchemy import (
    Boolean,
    Column,
    String,
    ForeignKey,
    Integer,
    Unicode,
    DateTime,
    desc,
    select,
    func,
    event,
)
from sqlalchemy.inspection import inspect
from sqlalchemy.orm import relationship, backref, column_property
from sqla_rdfbridge.mapping import IriClass

from . import (
    DiscussionBoundBase, DiscussionBoundTombstone, TombstonableMixin,
    OriginMixin, Post)
from ..semantic.namespaces import (
    ASSEMBL, QUADNAMES, VERSION, RDF, VirtRDF)
from ..semantic.virtuoso_mapping import QuadMapPatternS
from .auth import User, AgentProfile
from .generic import Content
from .discussion import Discussion
from .idea import Idea
from ..auth import P_READ, P_SYSADMIN, CrudPermissions


[docs]class Action(TombstonableMixin, OriginMixin, DiscussionBoundBase): """ An action that can be taken by an actor (a :py:class:`.auth.User`). Most actions are expressed in terms of actor-verb-target-time, with verbs including but not restricted to CRUD operations. """ __tablename__ = 'action' __external_typename = "Update" id = Column(Integer, primary_key=True) type = Column(String(255), nullable=False) __mapper_args__ = { 'polymorphic_identity': 'action', 'polymorphic_on': type, 'with_polymorphic': '*' } actor_id = Column( Integer, ForeignKey('user.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False, index=True, info={'rdf': QuadMapPatternS( None, VERSION.who, AgentProfile.agent_as_account_iri.apply(None))} ) actor = relationship( User, backref=backref('actions', order_by="Action.creation_date", cascade="all, delete-orphan") ) verb = 'did something to' # Because abstract, do in concrete subclasses # @classmethod # def special_quad_patterns(cls, alias_maker, discussion_id): # return [QuadMapPatternS(None, # RDF.type, IriClass(VirtRDF.QNAME_ID).apply(Action.type), # name=QUADNAMES.class_Action_class)]
[docs] def populate_from_context(self, context): if not(self.actor or self.actor_id): from .auth import User self.actor = context.get_instance_of_class(User) super(Action, self).populate_from_context(context)
@as_native_str() def __repr__(self): return "%s %s %s %s>" % ( super(Action, self).__repr__()[:-1], self.actor.display_name() if self.actor else 'nobody', self.verb, self.object_type)
[docs] def is_owner(self, user_id): return self.actor_id == user_id
[docs] def container_url(self): return "/data/Discussion/%d/all_users/%d/actions" % ( self.get_discussion_id(), self.actor_id)
def get_default_parent_context(self, request=None, user_id=None): return self.actor.get_collection_context( 'actions', request=request, user_id=user_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.actor_id == user_id)
crud_permissions = CrudPermissions( P_READ, P_SYSADMIN, P_SYSADMIN, P_SYSADMIN, P_READ, P_READ, P_READ)
Action.creation_date.info['rdf'] = QuadMapPatternS(None, VERSION.when)
[docs]class ActionOnPost(Action): """ An action whose target is a post. (Mixin) """ __tablename__ = 'action_on_post' id = Column( Integer, ForeignKey(Action.id, ondelete="CASCADE", onupdate='CASCADE'), primary_key=True ) post_id = Column( Integer, ForeignKey('content.id', ondelete="CASCADE", onupdate='CASCADE'), nullable=False, index=True, info={'rdf': QuadMapPatternS(None, VERSION.what)} ) post_ts = relationship( Content, foreign_keys=(post_id,), backref=backref("actions_ts", cascade="all, delete-orphan")) post = relationship( Content, primaryjoin="and_(Content.id == ActionOnPost.post_id," "Content.tombstone_date == None)", foreign_keys=(post_id,), backref=backref( 'actions', primaryjoin="and_(Content.id == ActionOnPost.post_id," "ActionOnPost.tombstone_date == None)", cascade="all, delete-orphan")) object_type = 'post'
[docs] def populate_from_context(self, context): if not(self.post or self.post_id): self.post = context.get_instance_of_class(Content) super(ActionOnPost, self).populate_from_context(context)
[docs] def get_discussion_id(self): post = self.post or Post.get(self.post_id) return post.get_discussion_id()
[docs] @classmethod def special_quad_patterns(cls, alias_maker, discussion_id): return [QuadMapPatternS(None, RDF.type, IriClass(VirtRDF.QNAME_ID_SUFFIX).apply(Action.type), name=QUADNAMES.class_ActionOnPost_class)]
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): from .generic import Content return ((cls.id == Action.id), (cls.post_id == Content.id), (Content.discussion_id == discussion_id))
discussion = relationship( Discussion, viewonly=True, secondary=Content.__table__, uselist=False, info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)})
[docs]class UniqueActionOnPost(ActionOnPost): "An action that should be unique of its subclass for a post, user pair"
[docs] def unique_query(self): # inheritance leads in trouble query = self.db.query(self.__class__) actor_id = self.actor_id or self.actor.id post_id = self.post_id or self.post.id return query.filter_by( actor_id=actor_id, type=self.type, post_id=post_id, tombstone_date=self.tombstone_date), True
[docs]class ViewPost(UniqueActionOnPost): """ A view action on a post. """ __mapper_args__ = { 'polymorphic_identity': 'version:ReadStatusChange_P' } __external_typename = "ReadStatusChange"
[docs] def tombstone(self): from .generic import Content return DiscussionBoundTombstone( self, post=Content.uri_generic(self.post_id), actor=User.uri_generic(self.actor_id))
post_from_view = relationship( 'Content', backref=backref('views'), ) verb = 'viewed'
[docs]class LikedPost(UniqueActionOnPost): """ A like action on a post. """ __mapper_args__ = { 'polymorphic_identity': 'vote:BinaryVote_P' }
[docs] def tombstone(self): from .generic import Content return DiscussionBoundTombstone( self, post=Content.uri_generic(self.post_id), actor=User.uri_generic(self.actor_id))
post_from_like = relationship( 'Content', primaryjoin="and_(Content.id == ActionOnPost.post_id," "Content.tombstone_date == None)", foreign_keys=(ActionOnPost.post_id,), backref=backref( 'was_liked', primaryjoin="and_(Content.id == ActionOnPost.post_id," "ActionOnPost.tombstone_date == None)")) verb = 'liked' crud_permissions = CrudPermissions( P_READ, P_READ, P_SYSADMIN, P_SYSADMIN, P_READ, P_READ, P_READ)
@event.listens_for(LikedPost, 'after_insert', propagate=True) def send_post_to_socket(mapper, connection, target): target.post.send_to_changes() @event.listens_for(LikedPost, 'after_update', propagate=True) def send_post_to_socket_ts(mapper, connection, target): if not inspect(target).unmodified_intersection(('tombstone_date')): target.db.expire(target.post, ['like_count']) target.post.send_to_changes() _lpt = LikedPost.__table__ _actt = Action.__table__ Content.like_count = column_property( select([func.count(_actt.c.id)]).where( (_lpt.c.id == _actt.c.id) & (_lpt.c.post_id == Content.__table__.c.id) & (_actt.c.type == LikedPost.__mapper_args__['polymorphic_identity']) & (_actt.c.tombstone_date == None) ).correlate_except(_actt, _lpt))
[docs]class ExpandPost(UniqueActionOnPost): """ An expansion action on a post. """ __mapper_args__ = { 'polymorphic_identity': 'version:ExpandPost_P' } verb = 'expanded'
[docs]class CollapsePost(UniqueActionOnPost): """ A collapse action on a post. """ __mapper_args__ = { 'polymorphic_identity': 'version:CollapsePost_P' } verb = 'collapsed'
[docs]class ActionOnIdea(Action): """ An action that is taken on an idea. (Mixin) """ __tablename__ = 'action_on_idea' id = Column( Integer, ForeignKey(Action.id, ondelete="CASCADE", onupdate='CASCADE'), primary_key=True ) idea_id = Column( Integer, ForeignKey(Idea.id, ondelete="CASCADE", onupdate='CASCADE'), nullable=False, index=True, info={'rdf': QuadMapPatternS(None, VERSION.what)} ) idea_ts = relationship( Idea, foreign_keys=(idea_id,), backref=backref("actions_ts", cascade="all, delete-orphan")) idea = relationship( Idea, primaryjoin="and_(Idea.id == ActionOnIdea.idea_id," "Idea.tombstone_date == None)", foreign_keys=(idea_id,), backref=backref( 'actions', primaryjoin="and_(Idea.id == ActionOnIdea.idea_id," "ActionOnIdea.tombstone_date == None)")) # TODO: cascade="all, delete-orphan" object_type = 'idea'
[docs] def populate_from_context(self, context): if not(self.idea or self.idea_id): self.idea = context.get_instance_of_class(Idea) super(ActionOnIdea, self).populate_from_context(context)
# This should not be necessary, but is.
[docs] @classmethod def special_quad_patterns(cls, alias_maker, discussion_id): return [QuadMapPatternS(None, RDF.type, IriClass(VirtRDF.QNAME_ID_SUFFIX).apply(Action.type), name=QUADNAMES.class_ActionOnIdea_class)]
[docs] def get_discussion_id(self): idea = self.idea or Idea.get(self.idea_id) return idea.get_discussion_id()
[docs] @classmethod def get_discussion_conditions(cls, discussion_id, alias_maker=None): return ((cls.id == Action.id), (cls.idea_id == Idea.id), (Idea.discussion_id == discussion_id))
discussion = relationship( Discussion, viewonly=True, secondary=Idea.__table__, uselist=False, info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)})
[docs]class UniqueActionOnIdea(ActionOnIdea): "An action that should be unique of its subclass for an idea, user pair"
[docs] def unique_query(self): # inheritance leads in trouble query = self.db.query(self.__class__) actor_id = self.actor_id or self.actor.id idea_id = self.idea_id or self.idea.id return query.filter_by( actor_id=actor_id, type=self.type, idea_id=idea_id, tombstone_date=self.tombstone_date), True
[docs]class ViewIdea(ActionOnIdea): """ A view action on an idea. (Not a status) """ __mapper_args__ = { 'polymorphic_identity': 'version:ReadStatusChange_I' }
[docs] def tombstone(self): from .generic import Content return DiscussionBoundTombstone( self, idea=Content.uri_generic(self.idea_id), actor=User.uri_generic(self.actor_id))
verb = 'viewed'