123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521 |
- # -*- coding: utf-8 -*-
- import typing
- import datetime as datetime_root
- import json
- import os
- from datetime import datetime
-
- from babel.dates import format_timedelta
- from bs4 import BeautifulSoup
- from sqlalchemy import Column, inspect, Index
- from sqlalchemy import ForeignKey
- from sqlalchemy import Sequence
- from sqlalchemy.ext.associationproxy import association_proxy
- from sqlalchemy.ext.hybrid import hybrid_property
- from sqlalchemy.orm import backref
- from sqlalchemy.orm import relationship
- from sqlalchemy.orm.attributes import InstrumentedAttribute
- from sqlalchemy.orm.collections import attribute_mapped_collection
- from sqlalchemy.types import Boolean
- from sqlalchemy.types import DateTime
- from sqlalchemy.types import Integer
- from sqlalchemy.types import Text
- from sqlalchemy.types import Unicode
- from depot.fields.sqlalchemy import UploadedFileField
- from depot.fields.upload import UploadedFile
- from depot.io.utils import FileIntent
-
- from tracim_backend.lib.utils.translation import fake_translator as l_
- from tracim_backend.lib.utils.translation import get_locale
- from tracim_backend.exceptions import ContentRevisionUpdateError
- from tracim_backend.models.meta import DeclarativeBase
- from tracim_backend.models.auth import User
- from tracim_backend.models.roles import WorkspaceRoles
-
- DEFAULT_PROPERTIES = dict(
- allowed_content=dict(
- folder=True,
- file=True,
- page=True,
- thread=True,
- ),
- )
-
-
- class Workspace(DeclarativeBase):
-
- __tablename__ = 'workspaces'
-
- workspace_id = Column(Integer, Sequence('seq__workspaces__workspace_id'), autoincrement=True, primary_key=True)
-
- label = Column(Unicode(1024), unique=False, nullable=False, default='')
- description = Column(Text(), unique=False, nullable=False, default='')
- calendar_enabled = Column(Boolean, unique=False, nullable=False, default=False)
-
- # Default value datetime.utcnow,
- # see: http://stackoverflow.com/a/13370382/801924 (or http://pastebin.com/VLyWktUn)
- created = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
- # Default value datetime.utcnow,
- # see: http://stackoverflow.com/a/13370382/801924 (or http://pastebin.com/VLyWktUn)
- updated = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
-
- is_deleted = Column(Boolean, unique=False, nullable=False, default=False)
-
- revisions = relationship("ContentRevisionRO")
-
- @hybrid_property
- def contents(self) -> ['Content']:
- # Return a list of unique revisions parent content
- contents = []
- for revision in self.revisions:
- # TODO BS 20161209: This ``revision.node.workspace`` make a lot
- # of SQL queries !
- if revision.node.workspace == self and revision.node not in contents:
- contents.append(revision.node)
-
- return contents
-
- # TODO - G-M - 27-03-2018 - [Calendar] Replace this in context model object
- # @property
- # def calendar_url(self) -> str:
- # # TODO - 20160531 - Bastien: Cyclic import if import in top of file
- # from tracim.lib.calendar import CalendarManager
- # calendar_manager = CalendarManager(None)
- #
- # return calendar_manager.get_workspace_calendar_url(self.workspace_id)
-
- def get_user_role(self, user: User) -> int:
- for role in user.roles:
- if role.workspace.workspace_id==self.workspace_id:
- return role.role
- return UserRoleInWorkspace.NOT_APPLICABLE
-
- def get_label(self):
- """ this method is for interoperability with Content class"""
- return self.label
-
- def get_allowed_content_types(self):
- # @see Content.get_allowed_content_types()
- return CONTENT_TYPES.endpoint_allowed_types_slug()
-
- def get_valid_children(
- self,
- content_types: list=None,
- show_deleted: bool=False,
- show_archived: bool=False,
- ):
- for child in self.contents:
- # we search only direct children
- if not child.parent \
- and (show_deleted or not child.is_deleted) \
- and (show_archived or not child.is_archived):
- if not content_types or child.type in content_types:
- yield child
-
-
- class UserRoleInWorkspace(DeclarativeBase):
-
- __tablename__ = 'user_workspace'
-
- user_id = Column(Integer, ForeignKey('users.user_id'), nullable=False, default=None, primary_key=True)
- workspace_id = Column(Integer, ForeignKey('workspaces.workspace_id'), nullable=False, default=None, primary_key=True)
- role = Column(Integer, nullable=False, default=0, primary_key=False)
- do_notify = Column(Boolean, unique=False, nullable=False, default=False)
-
- workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
- user = relationship('User', remote_side=[User.user_id], backref='roles')
-
- NOT_APPLICABLE = WorkspaceRoles.NOT_APPLICABLE.level
- READER = WorkspaceRoles.READER.level
- CONTRIBUTOR = WorkspaceRoles.CONTRIBUTOR.level
- CONTENT_MANAGER = WorkspaceRoles.CONTENT_MANAGER.level
- WORKSPACE_MANAGER = WorkspaceRoles.WORKSPACE_MANAGER.level
-
- # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # SLUG = {
- # NOT_APPLICABLE: 'not-applicable',
- # READER: 'reader',
- # CONTRIBUTOR: 'contributor',
- # CONTENT_MANAGER: 'content-manager',
- # WORKSPACE_MANAGER: 'workspace-manager',
- # }
-
- # LABEL = dict()
- # LABEL[0] = l_('N/A')
- # LABEL[1] = l_('Reader')
- # LABEL[2] = l_('Contributor')
- # LABEL[4] = l_('Content Manager')
- # LABEL[8] = l_('Workspace Manager')
- # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- #
- # STYLE = dict()
- # STYLE[0] = ''
- # STYLE[1] = 'color: #1fdb11;'
- # STYLE[2] = 'color: #759ac5;'
- # STYLE[4] = 'color: #ea983d;'
- # STYLE[8] = 'color: #F00;'
- #
- # ICON = dict()
- # ICON[0] = ''
- # ICON[1] = 'fa-eye'
- # ICON[2] = 'fa-pencil'
- # ICON[4] = 'fa-graduation-cap'
- # ICON[8] = 'fa-legal'
- #
- #
- # @property
- # def fa_icon(self):
- # return UserRoleInWorkspace.ICON[self.role]
- #
- # @property
- # def style(self):
- # return UserRoleInWorkspace.STYLE[self.role]
- #
-
- def role_object(self):
- return WorkspaceRoles.get_role_from_level(level=self.role)
-
- def role_as_label(self):
- return self.role_object().label
-
- @classmethod
- def get_all_role_values(cls) -> typing.List[int]:
- """
- Return all valid role value
- """
- return [role.level for role in WorkspaceRoles.get_all_valid_role()]
-
- @classmethod
- def get_all_role_slug(cls) -> typing.List[str]:
- """
- Return all valid role slug
- """
- # INFO - G.M - 25-05-2018 - Be carefull, as long as this method
- # and get_all_role_values are both used for API, this method should
- # return item in the same order as get_all_role_values
- return [role.slug for role in WorkspaceRoles.get_all_valid_role()]
-
- # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # class RoleType(object):
- # def __init__(self, role_id):
- # self.role_type_id = role_id
- # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
- # self.role_label = UserRoleInWorkspace.LABEL[role_id]
- # self.css_style = UserRoleInWorkspace.STYLE[role_id]
-
-
- # TODO - G.M - 09-04-2018 [Cleanup] It this items really needed ?
- # class LinkItem(object):
- # def __init__(self, href, label):
- # self.href = href
- # self.label = label
-
-
- class ActionDescription(object):
- """
- Allowed status are:
- - open
- - closed-validated
- - closed-invalidated
- - closed-deprecated
- """
-
- COPY = 'copy'
- ARCHIVING = 'archiving'
- COMMENT = 'content-comment'
- CREATION = 'creation'
- DELETION = 'deletion'
- EDITION = 'edition' # Default action if unknow
- REVISION = 'revision'
- STATUS_UPDATE = 'status-update'
- UNARCHIVING = 'unarchiving'
- UNDELETION = 'undeletion'
- MOVE = 'move'
-
- # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- _ICONS = {
- 'archiving': 'archive',
- 'content-comment': 'comment-o',
- 'creation': 'magic',
- 'deletion': 'trash',
- 'edition': 'edit',
- 'revision': 'history',
- 'status-update': 'random',
- 'unarchiving': 'file-archive-o',
- 'undeletion': 'trash-o',
- 'move': 'arrows',
- 'copy': 'files-o',
- }
- #
- # _LABELS = {
- # 'archiving': l_('archive'),
- # 'content-comment': l_('Item commented'),
- # 'creation': l_('Item created'),
- # 'deletion': l_('Item deleted'),
- # 'edition': l_('item modified'),
- # 'revision': l_('New revision'),
- # 'status-update': l_('New status'),
- # 'unarchiving': l_('Item unarchived'),
- # 'undeletion': l_('Item undeleted'),
- # 'move': l_('Item moved'),
- # 'copy': l_('Item copied'),
- # }
-
- def __init__(self, id):
- assert id in ActionDescription.allowed_values()
- self.id = id
- # FIXME - G.M - 17-04-2018 - Label and fa_icon needed for webdav
- # design template,
- # find a way to not rely on this.
- self.label = self.id
- self.fa_icon = ActionDescription._ICONS[id]
- #self.icon = self.fa_icon
- # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # self.label = ActionDescription._LABELS[id]
-
- # self.css = ''
-
- @classmethod
- def allowed_values(cls):
- return [cls.ARCHIVING,
- cls.COMMENT,
- cls.CREATION,
- cls.DELETION,
- cls.EDITION,
- cls.REVISION,
- cls.STATUS_UPDATE,
- cls.UNARCHIVING,
- cls.UNDELETION,
- cls.MOVE,
- cls.COPY,
- ]
-
-
- from tracim_backend.models.contents import CONTENT_STATUS
- from tracim_backend.models.contents import ContentStatus
- from tracim_backend.models.contents import CONTENT_TYPES
- # TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
- # is lost .
-
-
- # class ContentStatus(object):
- # """
- # Allowed status are:
- # - open
- # - closed-validated
- # - closed-invalidated
- # - closed-deprecated
- # """
- #
- # OPEN = 'open'
- # CLOSED_VALIDATED = 'closed-validated'
- # CLOSED_UNVALIDATED = 'closed-unvalidated'
- # CLOSED_DEPRECATED = 'closed-deprecated'
- #
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # _LABELS = {'open': l_('work in progress'),
- # # 'closed-validated': l_('closed — validated'),
- # # 'closed-unvalidated': l_('closed — cancelled'),
- # # 'closed-deprecated': l_('deprecated')}
- # #
- # # _LABELS_THREAD = {'open': l_('subject in progress'),
- # # 'closed-validated': l_('subject closed — resolved'),
- # # 'closed-unvalidated': l_('subject closed — cancelled'),
- # # 'closed-deprecated': l_('deprecated')}
- # #
- # # _LABELS_FILE = {'open': l_('work in progress'),
- # # 'closed-validated': l_('closed — validated'),
- # # 'closed-unvalidated': l_('closed — cancelled'),
- # # 'closed-deprecated': l_('deprecated')}
- # #
- # # _ICONS = {
- # # 'open': 'fa fa-square-o',
- # # 'closed-validated': 'fa fa-check-square-o',
- # # 'closed-unvalidated': 'fa fa-close',
- # # 'closed-deprecated': 'fa fa-warning',
- # # }
- # #
- # # _CSS = {
- # # 'open': 'tracim-status-open',
- # # 'closed-validated': 'tracim-status-closed-validated',
- # # 'closed-unvalidated': 'tracim-status-closed-unvalidated',
- # # 'closed-deprecated': 'tracim-status-closed-deprecated',
- # # }
- #
- # def __init__(self,
- # id,
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # type=''
- # ):
- # self.id = id
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # self.icon = ContentStatus._ICONS[id]
- # # self.css = ContentStatus._CSS[id]
- # #
- # # if type==CONTENT_TYPES.Thread.slug:
- # # self.label = ContentStatus._LABELS_THREAD[id]
- # # elif type==CONTENT_TYPES.File.slug:
- # # self.label = ContentStatus._LABELS_FILE[id]
- # # else:
- # # self.label = ContentStatus._LABELS[id]
- #
- #
- # @classmethod
- # def all(cls, type='') -> ['ContentStatus']:
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # all = []
- # # all.append(ContentStatus('open', type))
- # # all.append(ContentStatus('closed-validated', type))
- # # all.append(ContentStatus('closed-unvalidated', type))
- # # all.append(ContentStatus('closed-deprecated', type))
- # # return all
- # status_list = list()
- # for elem in cls.allowed_values():
- # status_list.append(ContentStatus(elem))
- # return status_list
- #
- # @classmethod
- # def allowed_values(cls):
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # return ContentStatus._LABELS.keys()
- # return [
- # ContentStatus.OPEN,
- # ContentStatus.CLOSED_UNVALIDATED,
- # ContentStatus.CLOSED_VALIDATED,
- # ContentStatus.CLOSED_DEPRECATED
- # ]
-
-
- # class ContentType(object):
- # Any = 'any'
- #
- # Folder = 'folder'
- # File = 'file'
- # Comment = 'comment'
- # Thread = 'thread'
- # Page = 'page'
- # Event = 'event'
- #
- # # TODO - G.M - 10-04-2018 - [Cleanup] Do we really need this ?
- # # _STRING_LIST_SEPARATOR = ','
- #
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # _ICONS = { # Deprecated
- # # 'dashboard': 'fa-home',
- # # 'workspace': 'fa-bank',
- # # 'folder': 'fa fa-folder-open-o',
- # # 'file': 'fa fa-paperclip',
- # # 'page': 'fa fa-file-text-o',
- # # 'thread': 'fa fa-comments-o',
- # # 'comment': 'fa fa-comment-o',
- # # 'event': 'fa fa-calendar-o',
- # # }
- # #
- # # _CSS_ICONS = {
- # # 'dashboard': 'fa fa-home',
- # # 'workspace': 'fa fa-bank',
- # # 'folder': 'fa fa-folder-open-o',
- # # 'file': 'fa fa-paperclip',
- # # 'page': 'fa fa-file-text-o',
- # # 'thread': 'fa fa-comments-o',
- # # 'comment': 'fa fa-comment-o',
- # # 'event': 'fa fa-calendar-o',
- # # }
- # #
- # # _CSS_COLORS = {
- # # 'dashboard': 't-dashboard-color',
- # # 'workspace': 't-less-visible',
- # # 'folder': 't-folder-color',
- # # 'file': 't-file-color',
- # # 'page': 't-page-color',
- # # 'thread': 't-thread-color',
- # # 'comment': 't-thread-color',
- # # 'event': 't-event-color',
- # # }
- #
- # _ORDER_WEIGHT = {
- # 'folder': 0,
- # 'page': 1,
- # 'thread': 2,
- # 'file': 3,
- # 'comment': 4,
- # 'event': 5,
- # }
- #
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # _LABEL = {
- # # 'dashboard': '',
- # # 'workspace': l_('workspace'),
- # # 'folder': l_('folder'),
- # # 'file': l_('file'),
- # # 'page': l_('page'),
- # # 'thread': l_('thread'),
- # # 'comment': l_('comment'),
- # # 'event': l_('event'),
- # # }
- # #
- # # _DELETE_LABEL = {
- # # 'dashboard': '',
- # # 'workspace': l_('Delete this workspace'),
- # # 'folder': l_('Delete this folder'),
- # # 'file': l_('Delete this file'),
- # # 'page': l_('Delete this page'),
- # # 'thread': l_('Delete this thread'),
- # # 'comment': l_('Delete this comment'),
- # # 'event': l_('Delete this event'),
- # # }
- # #
- # # @classmethod
- # # def get_icon(cls, type: str):
- # # assert(type in ContentType._ICONS) # DYN_REMOVE
- # # return ContentType._ICONS[type]
- #
- # @classmethod
- # def all(cls):
- # return cls.allowed_types()
- #
- # @classmethod
- # def allowed_types(cls):
- # return [cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page,
- # cls.Event]
- #
- # @classmethod
- # def allowed_types_for_folding(cls):
- # # This method is used for showing only "main"
- # # types in the left-side treeview
- # return [cls.Folder, cls.File, cls.Thread, cls.Page]
- #
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # @classmethod
- # # def allowed_types_from_str(cls, allowed_types_as_string: str):
- # # allowed_types = []
- # # # HACK - THIS
- # # for item in allowed_types_as_string.split(ContentType._STRING_LIST_SEPARATOR):
- # # if item and item in ContentType.allowed_types_for_folding():
- # # allowed_types.append(item)
- # # return allowed_types
- # #
- # # @classmethod
- # # def fill_url(cls, content: 'Content'):
- # # # TODO - DYNDATATYPE - D.A. - 2014-12-02
- # # # Make this code dynamic loading data types
- # #
- # # if content.type==CONTENT_TYPES.Folder.slug:
- # # return '/workspaces/{}/folders/{}'.format(content.workspace_id, content.content_id)
- # # elif content.type==CONTENT_TYPES.File.slug:
- # # return '/workspaces/{}/folders/{}/files/{}'.format(content.workspace_id, content.parent_id, content.content_id)
- # # elif content.type==CONTENT_TYPES.Thread.slug:
- # # return '/workspaces/{}/folders/{}/threads/{}'.format(content.workspace_id, content.parent_id, content.content_id)
- # # elif content.type==CONTENT_TYPES.Page.slug:
- # # return '/workspaces/{}/folders/{}/pages/{}'.format(content.workspace_id, content.parent_id, content.content_id)
- # #
- # # @classmethod
- # # def fill_url_for_workspace(cls, workspace: Workspace):
- # # # TODO - DYNDATATYPE - D.A. - 2014-12-02
- # # # Make this code dynamic loading data types
- # # return '/workspaces/{}'.format(workspace.workspace_id)
- #
- # @classmethod
- # def sorted(cls, types: ['ContentType']) -> ['ContentType']:
- # return sorted(types, key=lambda content_type: content_type.priority)
- #
- # @property
- # def type(self):
- # return self.id
- #
- # def __init__(self, type):
- # self.id = type
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # self.icon = ContentType._CSS_ICONS[type]
- # # self.color = ContentType._CSS_COLORS[type] # deprecated
- # # self.css = ContentType._CSS_COLORS[type]
- # # self.label = ContentType._LABEL[type]
- # self.priority = ContentType._ORDER_WEIGHT[type]
- #
- # def toDict(self):
- # return dict(id=self.type,
- # type=self.type,
- # # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # # icon=self.icon,
- # # color=self.color,
- # # label=self.label,
- # priority=self.priority)
-
- class ContentChecker(object):
-
- @classmethod
- def check_properties(cls, item):
- if item.type == CONTENT_TYPES.Folder.slug:
- properties = item.properties
- if 'allowed_content' not in properties.keys():
- return False
- if 'folders' not in properties['allowed_content']:
- return False
- if 'files' not in properties['allowed_content']:
- return False
- if 'pages' not in properties['allowed_content']:
- return False
- if 'threads' not in properties['allowed_content']:
- return False
- return True
-
- if item.type == CONTENT_TYPES.Event.slug:
- properties = item.properties
- if 'name' not in properties.keys():
- return False
- if 'raw' not in properties.keys():
- return False
- if 'start' not in properties.keys():
- return False
- if 'end' not in properties.keys():
- return False
- return True
-
- # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
- # Only content who can be copied need this
- if item.type == CONTENT_TYPES.Any_SLUG:
- properties = item.properties
- if 'origin' in properties.keys():
- return True
- raise NotImplementedError
-
- @classmethod
- def reset_properties(cls, item):
- if item.type == CONTENT_TYPES.Folder.slug:
- item.properties = DEFAULT_PROPERTIES
- return
-
- raise NotImplementedError
-
-
- class ContentRevisionRO(DeclarativeBase):
- """
- Revision of Content. It's immutable, update or delete an existing ContentRevisionRO will throw
- ContentRevisionUpdateError errors.
- """
-
- __tablename__ = 'content_revisions'
-
- revision_id = Column(Integer, primary_key=True)
- content_id = Column(Integer, ForeignKey('content.id'), nullable=False)
- # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
- owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
-
- label = Column(Unicode(1024), unique=False, nullable=False)
- description = Column(Text(), unique=False, nullable=False, default='')
- file_extension = Column(
- Unicode(255),
- unique=False,
- nullable=False,
- server_default='',
- )
- file_mimetype = Column(Unicode(255), unique=False, nullable=False, default='')
- # INFO - A.P - 2017-07-03 - Depot Doc
- # http://depot.readthedocs.io/en/latest/#attaching-files-to-models
- # http://depot.readthedocs.io/en/latest/api.html#module-depot.fields
- depot_file = Column(UploadedFileField, unique=False, nullable=True)
- properties = Column('properties', Text(), unique=False, nullable=False, default='')
-
- type = Column(Unicode(32), unique=False, nullable=False)
- status = Column(Unicode(32), unique=False, nullable=False, default=str(CONTENT_STATUS.get_default_status().slug))
- created = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
- updated = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
- is_deleted = Column(Boolean, unique=False, nullable=False, default=False)
- is_archived = Column(Boolean, unique=False, nullable=False, default=False)
- is_temporary = Column(Boolean, unique=False, nullable=False, default=False)
- revision_type = Column(Unicode(32), unique=False, nullable=False, default='')
-
- workspace_id = Column(Integer, ForeignKey('workspaces.workspace_id'), unique=False, nullable=True)
- workspace = relationship('Workspace', remote_side=[Workspace.workspace_id])
-
- parent_id = Column(Integer, ForeignKey('content.id'), nullable=True, default=None)
- parent = relationship("Content", foreign_keys=[parent_id], back_populates="children_revisions")
-
- node = relationship("Content", foreign_keys=[content_id], back_populates="revisions")
- # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
- owner = relationship('User', remote_side=[User.user_id])
-
- """ List of column copied when make a new revision from another """
- _cloned_columns = (
- 'content_id',
- 'created',
- 'description',
- 'file_mimetype',
- 'file_extension',
- 'is_archived',
- 'is_deleted',
- 'label',
- 'owner',
- 'owner_id',
- 'parent',
- 'parent_id',
- 'properties',
- 'revision_type',
- 'status',
- 'type',
- 'updated',
- 'workspace',
- 'workspace_id',
- 'is_temporary',
- )
-
- # Read by must be used like this:
- # read_datetime = revision.ready_by[<User instance>]
- # if user did not read the content, then a key error is raised
- read_by = association_proxy(
- 'revision_read_statuses', # name of the attribute
- 'view_datetime', # attribute the value is taken from
- creator=lambda k, v: \
- RevisionReadStatus(user=k, view_datetime=v)
- )
-
- @property
- def file_name(self):
- return '{0}{1}'.format(
- self.label,
- self.file_extension,
- )
-
- @classmethod
- def new_from(cls, revision: 'ContentRevisionRO') -> 'ContentRevisionRO':
- """
-
- Return new instance of ContentRevisionRO where properties are copied from revision parameter.
- Look at ContentRevisionRO._cloned_columns to see what columns are copieds.
-
- :param revision: revision to copy
- :type revision: ContentRevisionRO
- :return: new revision from revision parameter
- :rtype: ContentRevisionRO
- """
- new_rev = cls()
-
- for column_name in cls._cloned_columns:
- column_value = getattr(revision, column_name)
- setattr(new_rev, column_name, column_value)
-
- new_rev.updated = datetime.utcnow()
- if revision.depot_file:
- new_rev.depot_file = FileIntent(
- revision.depot_file.file.read(),
- revision.file_name,
- revision.file_mimetype,
- )
-
- return new_rev
-
- @classmethod
- def copy(
- cls,
- revision: 'ContentRevisionRO',
- parent: 'Content'
- ) -> 'ContentRevisionRO':
-
- copy_rev = cls()
- import copy
- copy_columns = cls._cloned_columns
- for column_name in copy_columns:
- # INFO - G-M - 15-03-2018 - set correct parent
- if column_name == 'parent_id':
- column_value = copy.copy(parent.id)
- elif column_name == 'parent':
- column_value = copy.copy(parent)
- else:
- column_value = copy.copy(getattr(revision, column_name))
- setattr(copy_rev, column_name, column_value)
-
- # copy attached_file
- if revision.depot_file:
- copy_rev.depot_file = FileIntent(
- revision.depot_file.file.read(),
- revision.file_name,
- revision.file_mimetype,
- )
- return copy_rev
-
- def __setattr__(self, key: str, value: 'mixed'):
- """
- ContentRevisionUpdateError is raised if tried to update column and revision own identity
- :param key: attribute name
- :param value: attribute value
- :return:
- """
- if key in ('_sa_instance_state', ): # Prevent infinite loop from SQLAlchemy code and altered set
- return super().__setattr__(key, value)
-
- # FIXME - G.M - 28-03-2018 - Cycling Import
- from tracim_backend.models.revision_protection import RevisionsIntegrity
- if inspect(self).has_identity \
- and key in self._cloned_columns \
- and not RevisionsIntegrity.is_updatable(self):
- raise ContentRevisionUpdateError(
- "Can't modify revision. To work on new revision use tracim.model.new_revision " +
- "context manager.")
-
- super().__setattr__(key, value)
-
- def get_status(self) -> ContentStatus:
- return CONTENT_STATUS.get_one_by_slug(self.status)
-
- def get_label(self) -> str:
- return self.label or self.file_name or ''
-
- def get_last_action(self) -> ActionDescription:
- return ActionDescription(self.revision_type)
-
- def has_new_information_for(self, user: User) -> bool:
- """
- :param user: the _session current user
- :return: bool, True if there is new information for given user else False
- False if the user is None
- """
- if not user:
- return False
-
- if user not in self.read_by.keys():
- return True
-
- return False
-
- def get_label_as_file(self):
- file_extension = self.file_extension or ''
-
- if self.type == CONTENT_TYPES.Thread.slug:
- file_extension = '.html'
- elif self.type == CONTENT_TYPES.Page.slug:
- file_extension = '.html'
-
- return '{0}{1}'.format(
- self.label,
- file_extension,
- )
-
- # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
- Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
- Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
-
-
- class Content(DeclarativeBase):
- """
- Content is used as a virtual representation of ContentRevisionRO.
- content.PROPERTY (except for content.id, content.revisions, content.children_revisions) will return
- value of most recent revision of content.
-
- # UPDATE A CONTENT
-
- To update an existing Content, you must use tracim.model.new_revision context manager:
- content = my_sontent_getter_method()
- with new_revision(content):
- content.description = 'foo bar baz'
- DBSession.flush()
-
- # QUERY CONTENTS
-
- To query contents you will need to join your content query with ContentRevisionRO. Join
- condition is available at tracim.lib.content.ContentApi#_get_revision_join:
-
- content = DBSession.query(Content).join(ContentRevisionRO, ContentApi._get_revision_join())
- .filter(Content.label == 'foo')
- .one()
-
- ContentApi provide also prepared Content at tracim.lib.content.ContentApi#get_canonical_query:
-
- content = ContentApi.get_canonical_query()
- .filter(Content.label == 'foo')
- .one()
- """
-
- __tablename__ = 'content'
-
- revision_to_serialize = -0 # This flag allow to serialize a given revision if required by the user
-
- id = Column(Integer, primary_key=True)
- # TODO - A.P - 2017-09-05 - revisions default sorting
- # The only sorting that makes sens is ordering by "updated" field. But:
- # - its content will soon replace the one of "created",
- # - this "updated" field will then be dropped.
- # So for now, we order by "revision_id" explicitly, but remember to switch
- # to "created" once "updated" removed.
- # https://github.com/tracim/tracim/issues/336
- revisions = relationship("ContentRevisionRO",
- foreign_keys=[ContentRevisionRO.content_id],
- back_populates="node",
- order_by="ContentRevisionRO.revision_id")
- children_revisions = relationship("ContentRevisionRO",
- foreign_keys=[ContentRevisionRO.parent_id],
- back_populates="parent")
-
- @hybrid_property
- def content_id(self) -> int:
- return self.revision.content_id
-
- @content_id.setter
- def content_id(self, value: int) -> None:
- self.revision.content_id = value
-
- @content_id.expression
- def content_id(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.content_id
-
- @hybrid_property
- def revision_id(self) -> int:
- return self.revision.revision_id
-
- @revision_id.setter
- def revision_id(self, value: int):
- self.revision.revision_id = value
-
- @revision_id.expression
- def revision_id(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.revision_id
-
- # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
- # and should be author of first revision.
- @hybrid_property
- def owner_id(self) -> int:
- return self.revision.owner_id
-
- @owner_id.setter
- def owner_id(self, value: int) -> None:
- self.revision.owner_id = value
-
- @owner_id.expression
- def owner_id(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.owner_id
-
- @hybrid_property
- def label(self) -> str:
- return self.revision.label
-
- @label.setter
- def label(self, value: str) -> None:
- self.revision.label = value
-
- @label.expression
- def label(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.label
-
- @hybrid_property
- def description(self) -> str:
- return self.revision.description
-
- @description.setter
- def description(self, value: str) -> None:
- self.revision.description = value
-
- @description.expression
- def description(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.description
-
- @hybrid_property
- def file_name(self) -> str:
- return '{0}{1}'.format(
- self.revision.label,
- self.revision.file_extension,
- )
-
- @file_name.setter
- def file_name(self, value: str) -> None:
- file_name, file_extension = os.path.splitext(value)
- if not self.revision.label:
- self.revision.label = file_name
- self.revision.file_extension = file_extension
-
- @file_name.expression
- def file_name(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.file_name + ContentRevisionRO.file_extension
-
- @hybrid_property
- def file_extension(self) -> str:
- return self.revision.file_extension
-
- @file_extension.setter
- def file_extension(self, value: str) -> None:
- self.revision.file_extension = value
-
- @file_extension.expression
- def file_extension(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.file_extension
-
- @hybrid_property
- def file_mimetype(self) -> str:
- return self.revision.file_mimetype
-
- @file_mimetype.setter
- def file_mimetype(self, value: str) -> None:
- self.revision.file_mimetype = value
-
- @file_mimetype.expression
- def file_mimetype(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.file_mimetype
-
- @hybrid_property
- def _properties(self) -> str:
- return self.revision.properties
-
- @_properties.setter
- def _properties(self, value: str) -> None:
- self.revision.properties = value
-
- @_properties.expression
- def _properties(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.properties
-
- @hybrid_property
- def type(self) -> str:
- return self.revision.type
-
- @type.setter
- def type(self, value: str) -> None:
- self.revision.type = value
-
- @type.expression
- def type(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.type
-
- @hybrid_property
- def status(self) -> str:
- return self.revision.status
-
- @status.setter
- def status(self, value: str) -> None:
- self.revision.status = value
-
- @status.expression
- def status(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.status
-
- @hybrid_property
- def created(self) -> datetime:
- return self.revision.created
-
- @created.setter
- def created(self, value: datetime) -> None:
- self.revision.created = value
-
- @created.expression
- def created(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.created
-
- @hybrid_property
- def updated(self) -> datetime:
- return self.revision.updated
-
- @updated.setter
- def updated(self, value: datetime) -> None:
- self.revision.updated = value
-
- @updated.expression
- def updated(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.updated
-
- @hybrid_property
- def is_deleted(self) -> bool:
- return self.revision.is_deleted
-
- @is_deleted.setter
- def is_deleted(self, value: bool) -> None:
- self.revision.is_deleted = value
-
- @is_deleted.expression
- def is_deleted(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.is_deleted
-
- @hybrid_property
- def is_archived(self) -> bool:
- return self.revision.is_archived
-
- @is_archived.setter
- def is_archived(self, value: bool) -> None:
- self.revision.is_archived = value
-
- @is_archived.expression
- def is_archived(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.is_archived
-
- @hybrid_property
- def is_temporary(self) -> bool:
- return self.revision.is_temporary
-
- @is_temporary.setter
- def is_temporary(self, value: bool) -> None:
- self.revision.is_temporary = value
-
- @is_temporary.expression
- def is_temporary(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.is_temporary
-
- @hybrid_property
- def revision_type(self) -> str:
- return self.revision.revision_type
-
- @revision_type.setter
- def revision_type(self, value: str) -> None:
- self.revision.revision_type = value
-
- @revision_type.expression
- def revision_type(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.revision_type
-
- @hybrid_property
- def workspace_id(self) -> int:
- return self.revision.workspace_id
-
- @workspace_id.setter
- def workspace_id(self, value: int) -> None:
- self.revision.workspace_id = value
-
- @workspace_id.expression
- def workspace_id(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.workspace_id
-
- @hybrid_property
- def workspace(self) -> Workspace:
- return self.revision.workspace
-
- @workspace.setter
- def workspace(self, value: Workspace) -> None:
- self.revision.workspace = value
-
- @workspace.expression
- def workspace(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.workspace
-
- @hybrid_property
- def parent_id(self) -> int:
- return self.revision.parent_id
-
- @parent_id.setter
- def parent_id(self, value: int) -> None:
- self.revision.parent_id = value
-
- @parent_id.expression
- def parent_id(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.parent_id
-
- @hybrid_property
- def parent(self) -> 'Content':
- return self.revision.parent
-
- @parent.setter
- def parent(self, value: 'Content') -> None:
- self.revision.parent = value
-
- @parent.expression
- def parent(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.parent
-
- @hybrid_property
- def node(self) -> 'Content':
- return self.revision.node
-
- @node.setter
- def node(self, value: 'Content') -> None:
- self.revision.node = value
-
- @node.expression
- def node(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.node
-
- # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
- # and should be author of first revision.
- @hybrid_property
- def owner(self) -> User:
- return self.revision.owner
-
- @owner.setter
- def owner(self, value: User) -> None:
- self.revision.owner = value
-
- @owner.expression
- def owner(cls) -> InstrumentedAttribute:
- return ContentRevisionRO.owner
-
- @hybrid_property
- def children(self) -> ['Content']:
- """
- :return: list of children Content
- :rtype Content
- """
- # Return a list of unique revisions parent content
- return list(set([revision.node for revision in self.children_revisions]))
-
- @property
- def revision(self) -> ContentRevisionRO:
- return self.get_current_revision()
-
- @property
- def first_revision(self) -> ContentRevisionRO:
- return self.revisions[0] # FIXME
-
- @property
- def last_revision(self) -> ContentRevisionRO:
- return self.revisions[-1]
-
- @property
- def is_editable(self) -> bool:
- return not self.is_archived and not self.is_deleted
-
- @property
- def is_active(self) -> bool:
- return self.is_editable
-
- @property
- def depot_file(self) -> UploadedFile:
- return self.revision.depot_file
-
- @depot_file.setter
- def depot_file(self, value):
- self.revision.depot_file = value
-
- def get_current_revision(self) -> ContentRevisionRO:
- if not self.revisions:
- return self.new_revision()
-
- # If last revisions revision don't have revision_id, return it we just add it.
- if self.revisions[-1].revision_id is None:
- return self.revisions[-1]
-
- # Revisions should be ordred by revision_id but we ensure that here
- revisions = sorted(self.revisions, key=lambda revision: revision.revision_id)
- return revisions[-1]
-
- def new_revision(self) -> ContentRevisionRO:
- """
- Return and assign to this content a new revision.
- If it's a new content, revision is totally new.
- If this content already own revision, revision is build from last revision.
- :return:
- """
- if not self.revisions:
- self.revisions.append(ContentRevisionRO())
- return self.revisions[0]
-
- new_rev = ContentRevisionRO.new_from(self.get_current_revision())
- self.revisions.append(new_rev)
- return new_rev
-
- def get_valid_children(self, content_types: list=None) -> ['Content']:
- for child in self.children:
- if not child.is_deleted and not child.is_archived:
- if not content_types or child.type in content_types:
- yield child.node
-
- @hybrid_property
- def properties(self) -> dict:
- """ return a structure decoded from json content of _properties """
- if not self._properties:
- return DEFAULT_PROPERTIES
- return json.loads(self._properties)
-
- @properties.setter
- def properties(self, properties_struct: dict) -> None:
- """ encode a given structure into json and store it in _properties attribute"""
- self._properties = json.dumps(properties_struct)
- ContentChecker.check_properties(self)
-
- def created_as_delta(self, delta_from_datetime:datetime=None):
- if not delta_from_datetime:
- delta_from_datetime = datetime.utcnow()
-
- return format_timedelta(delta_from_datetime - self.created,
- locale=get_locale())
-
- def datetime_as_delta(self, datetime_object,
- delta_from_datetime:datetime=None):
- if not delta_from_datetime:
- delta_from_datetime = datetime.utcnow()
- return format_timedelta(delta_from_datetime - datetime_object,
- locale=get_locale())
-
- def get_child_nb(self, content_type: str, content_status = ''):
- child_nb = 0
- for child in self.get_valid_children():
- if child.type == content_type or content_type.slug == CONTENT_TYPES.Any_SLUG:
- if not content_status:
- child_nb = child_nb+1
- elif content_status==child.status:
- child_nb = child_nb+1
- return child_nb
-
- def get_label(self):
- return self.label or self.file_name or ''
-
- def get_label_as_file(self) -> str:
- """
- :return: Return content label in file representation context
- """
- return self.revision.get_label_as_file()
-
- def get_status(self) -> ContentStatus:
- return CONTENT_STATUS.get_one_by_slug(
- self.status,
- )
-
- def get_last_action(self) -> ActionDescription:
- return ActionDescription(self.revision_type)
-
- def get_simple_last_activity_date(self) -> datetime_root.datetime:
- """
- Get last activity_date, comments_included. Do not search recursively
- in revision or children.
- :return:
- """
- last_revision_date = self.updated
- for comment in self.get_comments():
- if comment.updated > last_revision_date:
- last_revision_date = comment.updated
- return last_revision_date
-
- def get_last_activity_date(self) -> datetime_root.datetime:
- """
- Get last activity date with complete recursive search
- :return:
- """
- last_revision_date = self.updated
- for revision in self.revisions:
- if revision.updated > last_revision_date:
- last_revision_date = revision.updated
-
- for child in self.children:
- if child.updated > last_revision_date:
- last_revision_date = child.updated
- return last_revision_date
-
- def has_new_information_for(self, user: User) -> bool:
- """
- :param user: the _session current user
- :return: bool, True if there is new information for given user else False
- False if the user is None
- """
- revision = self.get_current_revision()
-
- if not user:
- return False
-
- if user not in revision.read_by.keys():
- # The user did not read this item, so yes!
- return True
-
- for child in self.get_valid_children():
- if child.has_new_information_for(user):
- return True
-
- return False
-
- def get_comments(self):
- children = []
- for child in self.children:
- if CONTENT_TYPES.Comment.slug == child.type and not child.is_deleted and not child.is_archived:
- children.append(child.node)
- return children
-
- def get_last_comment_from(self, user: User) -> 'Content':
- # TODO - Make this more efficient
- last_comment_updated = None
- last_comment = None
- for comment in self.get_comments():
- if user.user_id == comment.owner.user_id:
- if not last_comment or last_comment_updated<comment.updated:
- # take only the latest comment !
- last_comment = comment
- last_comment_updated = comment.updated
-
- return last_comment
-
- def get_previous_revision(self) -> 'ContentRevisionRO':
- rev_ids = [revision.revision_id for revision in self.revisions]
- rev_ids.sort()
-
- if len(rev_ids)>=2:
- revision_rev_id = rev_ids[-2]
-
- for revision in self.revisions:
- if revision.revision_id == revision_rev_id:
- return revision
-
- return None
-
- def description_as_raw_text(self):
- # 'html.parser' fixes a hanging bug
- # see http://stackoverflow.com/questions/12618567/problems-running-beautifulsoup4-within-apache-mod-python-django
- return BeautifulSoup(self.description, 'html.parser').text
-
- def get_allowed_content_types(self):
- types = []
- try:
- allowed_types = self.properties['allowed_content']
- for type_label, is_allowed in allowed_types.items():
- if is_allowed:
- types.append(CONTENT_TYPES.get_one_by_slug(type_label))
- except Exception as e:
- print(e.__str__())
- print('----- /*\ *****')
- raise ValueError('Not allowed content property')
-
- return types
-
- def get_history(self, drop_empty_revision=False) -> '[VirtualEvent]':
- events = []
- for comment in self.get_comments():
- events.append(VirtualEvent.create_from_content(comment))
-
- revisions = sorted(self.revisions, key=lambda rev: rev.revision_id)
- for revision in revisions:
- # INFO - G.M - 09-03-2018 - Do not show file revision with empty
- # file to have a more clear view of revision.
- # Some webdav client create empty file before uploading, we must
- # have possibility to not show the related revision
- if drop_empty_revision:
- if revision.depot_file and revision.depot_file.file.content_length == 0: # nopep8
- # INFO - G.M - 12-03-2018 -Always show the last and
- # first revision.
- if revision != revisions[-1] and revision != revisions[0]:
- continue
-
- events.append(VirtualEvent.create_from_content_revision(revision))
-
- sorted_events = sorted(events,
- key=lambda event: event.created, reverse=True)
- return sorted_events
-
- @classmethod
- def format_path(cls, url_template: str, content: 'Content') -> str:
- wid = content.workspace.workspace_id
- fid = content.parent_id # May be None if no parent
- ctype = content.type
- cid = content.content_id
- return url_template.format(wid=wid, fid=fid, ctype=ctype, cid=cid)
-
- def copy(self, parent):
- cpy_content = Content()
- for rev in self.revisions:
- cpy_rev = ContentRevisionRO.copy(rev, parent)
- cpy_content.revisions.append(cpy_rev)
- return cpy_content
-
-
- class RevisionReadStatus(DeclarativeBase):
-
- __tablename__ = 'revision_read_status'
-
- revision_id = Column(Integer, ForeignKey('content_revisions.revision_id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True)
- user_id = Column(Integer, ForeignKey('users.user_id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True)
- # Default value datetime.utcnow, see: http://stackoverflow.com/a/13370382/801924 (or http://pastebin.com/VLyWktUn)
- view_datetime = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
-
- content_revision = relationship(
- 'ContentRevisionRO',
- backref=backref(
- 'revision_read_statuses',
- collection_class=attribute_mapped_collection('user'),
- cascade='all, delete-orphan'
- ))
-
- user = relationship('User', backref=backref(
- 'revision_readers',
- collection_class=attribute_mapped_collection('view_datetime'),
- cascade='all, delete-orphan'
- ))
-
-
- class NodeTreeItem(object):
- """
- This class implements a model that allow to simply represents
- the left-panel menu items. This model is used by dbapi but
- is not directly related to sqlalchemy and database
- """
- def __init__(
- self,
- node: Content,
- children: typing.List['NodeTreeItem'],
- is_selected: bool = False,
- ):
- self.node = node
- self.children = children
- self.is_selected = is_selected
-
-
- class VirtualEvent(object):
- @classmethod
- def create_from(cls, object):
- if Content == object.__class__:
- return cls.create_from_content(object)
- elif ContentRevisionRO == object.__class__:
- return cls.create_from_content_revision(object)
-
- @classmethod
- def create_from_content(cls, content: Content):
-
- label = content.get_label()
- if content.type == CONTENT_TYPES.Comment.slug:
- # TODO - G.M - 10-04-2018 - [Cleanup] Remove label param
- # from this object ?
- label = l_('<strong>{}</strong> wrote:').format(content.owner.get_display_name())
-
- return VirtualEvent(id=content.content_id,
- created=content.created,
- owner=content.owner,
- type=ActionDescription(content.revision_type),
- label=label,
- content=content.description,
- ref_object=content)
-
- @classmethod
- def create_from_content_revision(cls, revision: ContentRevisionRO):
- action_description = ActionDescription(revision.revision_type)
-
- return VirtualEvent(id=revision.revision_id,
- created=revision.updated,
- owner=revision.owner,
- type=action_description,
- label=action_description.label,
- content='',
- ref_object=revision)
-
- def __init__(self, id, created, owner, type, label, content, ref_object):
- self.id = id
- self.created = created
- self.owner = owner
- self.type = type
- self.label = label
- self.content = content
- self.ref_object = ref_object
-
- assert hasattr(type, 'id')
- # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
- # assert hasattr(type, 'css')
- # assert hasattr(type, 'fa_icon')
- # assert hasattr(type, 'label')
-
- def created_as_delta(self, delta_from_datetime:datetime=None):
- if not delta_from_datetime:
- delta_from_datetime = datetime.utcnow()
- return format_timedelta(delta_from_datetime - self.created,
- locale=get_locale())
-
- def create_readable_date(self, delta_from_datetime:datetime=None):
- aff = ''
-
- if not delta_from_datetime:
- delta_from_datetime = datetime.utcnow()
-
- delta = delta_from_datetime - self.created
-
- if delta.days > 0:
- if delta.days >= 365:
- aff = '%d year%s ago' % (delta.days/365, 's' if delta.days/365>=2 else '')
- elif delta.days >= 30:
- aff = '%d month%s ago' % (delta.days/30, 's' if delta.days/30>=2 else '')
- else:
- aff = '%d day%s ago' % (delta.days, 's' if delta.days>=2 else '')
- else:
- if delta.seconds < 60:
- aff = '%d second%s ago' % (delta.seconds, 's' if delta.seconds>1 else '')
- elif delta.seconds/60 < 60:
- aff = '%d minute%s ago' % (delta.seconds/60, 's' if delta.seconds/60>=2 else '')
- else:
- aff = '%d hour%s ago' % (delta.seconds/3600, 's' if delta.seconds/3600>=2 else '')
-
- return aff
|