data.py 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. # -*- coding: utf-8 -*-
  2. from bs4 import BeautifulSoup
  3. import json
  4. import re
  5. import datetime as datetimeroot
  6. from datetime import datetime
  7. from hashlib import sha256
  8. from sqlalchemy import Table, ForeignKey, Column, Sequence
  9. import sqlalchemy as sqla
  10. from sqlalchemy.sql.sqltypes import Boolean
  11. from sqlalchemy.types import Unicode, Integer, DateTime, Text, LargeBinary
  12. import sqlalchemy.types as sqlat
  13. from sqlalchemy.ext.orderinglist import ordering_list
  14. from sqlalchemy.orm import relation, synonym, relationship
  15. from sqlalchemy.orm import backref
  16. import sqlalchemy.orm as sqlao
  17. import sqlalchemy.orm.query as sqlaoq
  18. from sqlalchemy import orm as sqlao
  19. from sqlalchemy.ext.hybrid import hybrid_property
  20. from tg.i18n import ugettext as _, lazy_ugettext as l_
  21. import tg
  22. from tracim.model import DeclarativeBase, metadata, DBSession
  23. # from tracim.model import auth as pma
  24. from tracim.model.auth import User
  25. from tracim.model.auth import Rights
  26. from tracim.model.auth import user_group_table
  27. from tracim.lib.base import current_user
  28. class BreadcrumbItem(object):
  29. def __init__(self, icon_string: str, label: str, url: str, is_active: bool = False):
  30. """
  31. A BreadcrumbItem contains minimal information required to build a breadcrumb
  32. icon_string: this is the Tango related id, eg 'places/remote-folder'
  33. """
  34. self.icon = icon_string
  35. self.label = label
  36. self.url = url
  37. self.is_active =is_active
  38. class Workspace(DeclarativeBase):
  39. __tablename__ = 'pod_workspaces'
  40. workspace_id = Column(Integer, Sequence('pod_workspaces__workspace_id__sequence'), primary_key=True)
  41. data_label = Column(Unicode(1024), unique=False, nullable=False, default='')
  42. data_comment = Column(Text(), unique=False, nullable=False, default='')
  43. created_at = Column(DateTime, unique=False, nullable=False)
  44. updated_at = Column(DateTime, unique=False, nullable=False)
  45. is_deleted = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  46. def get_user_role(self, user: User) -> int:
  47. for role in user.roles:
  48. if role.workspace.workspace_id==self.workspace_id:
  49. return role.role
  50. return UserRoleInWorkspace.NOT_APPLICABLE
  51. class UserRoleInWorkspace(DeclarativeBase):
  52. __tablename__ = 'pod_user_workspace'
  53. user_id = Column(Integer, ForeignKey('pod_user.user_id'), nullable=False, default=None, primary_key=True)
  54. workspace_id = Column(Integer, ForeignKey('pod_workspaces.workspace_id'), nullable=False, default=None, primary_key=True)
  55. role = Column(Integer, nullable=False, default=0, primary_key=False)
  56. workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
  57. user = relationship('User', remote_side=[User.user_id], backref='roles')
  58. NOT_APPLICABLE = 0
  59. READER = 1
  60. CONTRIBUTOR = 2
  61. CONTENT_MANAGER = 4
  62. WORKSPACE_MANAGER = 8
  63. LABEL = dict()
  64. LABEL[0] = l_('N/A')
  65. LABEL[1] = l_('Reader')
  66. LABEL[2] = l_('Contributor')
  67. LABEL[4] = l_('Content Manager')
  68. LABEL[8] = l_('Workspace Manager')
  69. STYLE = dict()
  70. STYLE[0] = ''
  71. STYLE[1] = 'color: #1fdb11;'
  72. STYLE[2] = 'color: #759ac5;'
  73. STYLE[4] = 'color: #ea983d;'
  74. STYLE[8] = 'color: #F00;'
  75. @property
  76. def style(self):
  77. return UserRoleInWorkspace.STYLE[self.role]
  78. def role_as_label(self):
  79. return UserRoleInWorkspace.LABEL[self.role]
  80. @classmethod
  81. def get_all_role_values(self):
  82. return [
  83. UserRoleInWorkspace.READER,
  84. UserRoleInWorkspace.CONTRIBUTOR,
  85. UserRoleInWorkspace.CONTENT_MANAGER,
  86. UserRoleInWorkspace.WORKSPACE_MANAGER
  87. ]
  88. class RoleType(object):
  89. def __init__(self, role_id):
  90. self.role_type_id = role_id
  91. self.role_label = UserRoleInWorkspace.LABEL[role_id]
  92. self.css_style = UserRoleInWorkspace.STYLE[role_id]
  93. class LinkItem(object):
  94. def __init__(self, href, label):
  95. self.href = href
  96. self.label = label
  97. class ActionDescription(object):
  98. """
  99. Allowed status are:
  100. - open
  101. - closed-validated
  102. - closed-invalidated
  103. - closed-deprecated
  104. """
  105. ARCHIVING = 'archiving'
  106. COMMENT = 'content-comment'
  107. CREATION = 'creation'
  108. DELETION = 'deletion'
  109. EDITION = 'edition' # Default action if unknow
  110. REVISION = 'revision'
  111. STATUS_UPDATE = 'status-update'
  112. UNARCHIVING = 'unarchiving'
  113. UNDELETION = 'undeletion'
  114. _ICONS = {
  115. 'archiving': 'mimetypes/package-x-generic',
  116. 'content-comment': 'apps/internet-group-chat',
  117. 'creation': 'apps/accessories-text-editor',
  118. 'deletion': 'status/user-trash-full',
  119. 'edition': 'apps/accessories-text-editor',
  120. 'revision': 'apps/accessories-text-editor',
  121. 'status-update': 'apps/utilities-system-monitor',
  122. 'unarchiving': 'mimetypes/package-x-generic',
  123. 'undeletion': 'places/user-trash'
  124. }
  125. _LABELS = {
  126. 'archiving': l_('Item archived'),
  127. 'content-comment': l_('Item commented'),
  128. 'creation': l_('Item created'),
  129. 'deletion': l_('Item deleted'),
  130. 'edition': l_('Item modified'),
  131. 'revision': l_('New revision'),
  132. 'status-update': l_('Status modified'),
  133. 'unarchiving': l_('Item un-archived'),
  134. 'undeletion': l_('Item undeleted'),
  135. }
  136. def __init__(self, id):
  137. assert id in ActionDescription.allowed_values()
  138. self.id = id
  139. self.label = ''
  140. self.icon = ActionDescription._ICONS[id]
  141. @classmethod
  142. def allowed_values(cls):
  143. return [cls.ARCHIVING,
  144. cls.COMMENT,
  145. cls.CREATION,
  146. cls.DELETION,
  147. cls.EDITION,
  148. cls.REVISION,
  149. cls.STATUS_UPDATE,
  150. cls.UNARCHIVING,
  151. cls.UNDELETION]
  152. class ContentStatus(object):
  153. """
  154. Allowed status are:
  155. - open
  156. - closed-validated
  157. - closed-invalidated
  158. - closed-deprecated
  159. """
  160. OPEN = 'open'
  161. CLOSED_VALIDATED = 'closed-validated'
  162. CLOSED_UNVALIDATED = 'closed-unvalidated'
  163. CLOSED_DEPRECATED = 'closed-deprecated'
  164. _LABELS = {'open': l_('work in progress'),
  165. 'closed-validated': l_('closed — validated'),
  166. 'closed-unvalidated': l_('closed — cancelled'),
  167. 'closed-deprecated': l_('deprecated')}
  168. _LABELS_THREAD = {'open': l_('subject in progress'),
  169. 'closed-validated': l_('subject closed — resolved'),
  170. 'closed-unvalidated': l_('subject closed — cancelled'),
  171. 'closed-deprecated': l_('deprecated')}
  172. _LABELS_FILE = {'open': l_('work in progress'),
  173. 'closed-validated': l_('closed — validated'),
  174. 'closed-unvalidated': l_('closed — cancelled'),
  175. 'closed-deprecated': l_('deprecated')}
  176. _ICONS = {
  177. 'open': 'status/status-open',
  178. 'closed-validated': 'emblems/emblem-checked',
  179. 'closed-unvalidated': 'emblems/emblem-unreadable',
  180. 'closed-deprecated': 'status/status-outdated',
  181. }
  182. _CSS = {
  183. 'open': 'tracim-status-open',
  184. 'closed-validated': 'tracim-status-closed-validated',
  185. 'closed-unvalidated': 'tracim-status-closed-unvalidated',
  186. 'closed-deprecated': 'tracim-status-closed-deprecated',
  187. }
  188. def __init__(self, id, node_type=''):
  189. self.id = id
  190. print('ID', id)
  191. self.icon = ContentStatus._ICONS[id]
  192. self.css = ContentStatus._CSS[id]
  193. if node_type==PBNodeType.Thread:
  194. self.label = ContentStatus._LABELS_THREAD[id]
  195. elif node_type==PBNodeType.File:
  196. self.label = ContentStatus._LABELS_FILE[id]
  197. else:
  198. self.label = ContentStatus._LABELS[id]
  199. @classmethod
  200. def all(cls, node_type='') -> ['ContentStatus']:
  201. all = []
  202. all.append(ContentStatus('open', node_type))
  203. all.append(ContentStatus('closed-validated', node_type))
  204. all.append(ContentStatus('closed-unvalidated', node_type))
  205. all.append(ContentStatus('closed-deprecated', node_type))
  206. return all
  207. @classmethod
  208. def allowed_values(cls):
  209. return ContentStatus._LABELS.keys()
  210. class PBNodeStatusItem(object):
  211. def __init__(self, psStatusId, psStatusLabel, psStatusFamily, psIconId, psCssClass): #, psBackgroundColor):
  212. self._sStatusId = psStatusId
  213. self._sStatusLabel = psStatusLabel
  214. self._sStatusFamily = psStatusFamily
  215. self._sIconId = psIconId
  216. self._sCssClass = psCssClass
  217. # self._sBackgroundColor = psBackgroundColor
  218. def getLabel(self):
  219. return self._sStatusLabel
  220. @property
  221. def status_family(self):
  222. return self._sStatusFamily
  223. @property
  224. def icon(self):
  225. return self._sIconId
  226. def getId(self):
  227. return self._sStatusId
  228. @property
  229. def css(self):
  230. return self._sCssClass
  231. @property
  232. def status_id(self):
  233. return self._sStatusId
  234. @property
  235. def icon_id(self):
  236. return self._sIconId
  237. @property
  238. def label(self):
  239. return self._sStatusLabel
  240. class PBNodeStatus(object):
  241. StatusList = dict()
  242. StatusList['information'] = PBNodeStatusItem('information', 'Information', 'normal', 'fa fa-info-circle', 'tracim-status-grey-light')
  243. StatusList['automatic'] = PBNodeStatusItem('automatic', 'Automatic', 'open', 'fa fa-flash', 'tracim-status-grey-light')
  244. StatusList['new'] = PBNodeStatusItem('new', 'New', 'open', 'fa fa-lightbulb-o fa-inverse', 'btn-success')
  245. StatusList['inprogress'] = PBNodeStatusItem('inprogress', 'In progress', 'open', 'fa fa-gears fa-inverse', 'btn-info')
  246. StatusList['standby'] = PBNodeStatusItem('standby', 'In standby', 'open', 'fa fa-spinner fa-inverse', 'btn-warning')
  247. StatusList['done'] = PBNodeStatusItem('done', 'Done', 'closed', 'fa fa-check-square-o', 'tracim-status-grey-light')
  248. StatusList['closed'] = PBNodeStatusItem('closed', 'Closed', 'closed', 'fa fa-lightbulb-o', 'tracim-status-grey-middle')
  249. StatusList['deleted'] = PBNodeStatusItem('deleted', 'Deleted', 'closed', 'fa fa-trash-o', 'tracim-status-grey-dark')
  250. @classmethod
  251. def getChoosableList(cls):
  252. return [
  253. PBNodeStatus.StatusList['information'],
  254. PBNodeStatus.StatusList['automatic'],
  255. PBNodeStatus.StatusList['new'],
  256. PBNodeStatus.StatusList['inprogress'],
  257. PBNodeStatus.StatusList['standby'],
  258. PBNodeStatus.StatusList['done'],
  259. PBNodeStatus.StatusList['closed'],
  260. ]
  261. @classmethod
  262. def getVisibleIdsList(cls):
  263. return ['information', 'automatic', 'new', 'inprogress', 'standby', 'done' ]
  264. @classmethod
  265. def getVisibleList(cls):
  266. return [
  267. PBNodeStatus.StatusList['information'],
  268. PBNodeStatus.StatusList['automatic'],
  269. PBNodeStatus.StatusList['new'],
  270. PBNodeStatus.StatusList['inprogress'],
  271. PBNodeStatus.StatusList['standby'],
  272. PBNodeStatus.StatusList['done'],
  273. ]
  274. @classmethod
  275. def getList(cls):
  276. return [
  277. PBNodeStatus.StatusList['information'],
  278. PBNodeStatus.StatusList['automatic'],
  279. PBNodeStatus.StatusList['new'],
  280. PBNodeStatus.StatusList['inprogress'],
  281. PBNodeStatus.StatusList['standby'],
  282. PBNodeStatus.StatusList['done'],
  283. PBNodeStatus.StatusList['closed'],
  284. PBNodeStatus.StatusList['deleted']
  285. ]
  286. @classmethod
  287. def getStatusItem(cls, psStatusId):
  288. return PBNodeStatus.StatusList[psStatusId]
  289. class PBNodeType(object):
  290. Any = 'any'
  291. Folder = 'folder'
  292. File = 'file'
  293. Comment = 'comment'
  294. Thread = 'thread'
  295. Page = 'page'
  296. # Obsolete ones - to be removed
  297. Node = 'node'
  298. Data = 'data'
  299. Event = 'event'
  300. Contact = 'contact'
  301. # Fake types, used for breadcrumb only
  302. FAKE_Dashboard = 'dashboard'
  303. FAKE_Workspace = 'workspace'
  304. _STRING_LIST_SEPARATOR = ','
  305. _ICONS = {
  306. 'dashboard': 'places/user-desktop',
  307. 'workspace': 'places/folder-remote',
  308. 'folder': 'places/jstree-folder',
  309. 'file': 'mimetypes/text-x-generic-template',
  310. 'page': 'mimetypes/text-html',
  311. 'thread': 'apps/internet-group-chat',
  312. 'comment': 'apps/internet-group-chat',
  313. }
  314. @classmethod
  315. def icon(cls, type: str):
  316. assert(type in PBNodeType._ICONS) # DYN_REMOVE
  317. return PBNodeType._ICONS[type]
  318. @classmethod
  319. def allowed_types(cls):
  320. return [cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page]
  321. @classmethod
  322. def allowed_types_from_str(cls, allowed_types_as_string: str):
  323. allowed_types = []
  324. # HACK - THIS
  325. for item in allowed_types_as_string.split(PBNodeType._STRING_LIST_SEPARATOR):
  326. if item and item in PBNodeType.allowed_types():
  327. allowed_types.append(item)
  328. return allowed_types
  329. MINIMUM_DATE = datetimeroot.date(datetimeroot.MINYEAR, 1, 1)
  330. class PBNode(DeclarativeBase):
  331. #def __init__(self):
  332. # self._lStaticChildList = []
  333. @sqlao.reconstructor
  334. def init_on_load(self):
  335. self._lStaticChildList = []
  336. def appendStaticChild(self, loNode):
  337. print("%s has child %s" % (self.node_id, loNode.node_id))
  338. self._lStaticChildList.append(loNode)
  339. def getStaticChildList(self):
  340. return self._lStaticChildList
  341. def getStaticChildNb(self):
  342. return len(self._lStaticChildList)
  343. __tablename__ = 'pod_nodes'
  344. revision_to_serialize = -0 # This flag allow to serialize a given revision if required by the user
  345. node_id = Column(Integer, Sequence('pod_nodes__node_id__sequence'), primary_key=True)
  346. parent_id = Column(Integer, ForeignKey('pod_nodes.node_id'), nullable=True, default=None)
  347. node_depth = Column(Integer, unique=False, nullable=False, default=0)
  348. parent_tree_path = Column(Unicode(255), unique=False, nullable=False, default='')
  349. owner_id = Column(Integer, ForeignKey('pod_user.user_id'), nullable=True, default=None)
  350. node_order = Column(Integer, nullable=True, default=1)
  351. node_type = Column(Unicode(32), unique=False, nullable=False)
  352. node_status = Column(Unicode(32), unique=False, nullable=False, default=ContentStatus.OPEN)
  353. created_at = Column(DateTime, unique=False, nullable=False)
  354. updated_at = Column(DateTime, unique=False, nullable=False)
  355. workspace_id = Column(Integer, ForeignKey('pod_workspaces.workspace_id'), unique=False, nullable=True)
  356. workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='contents')
  357. is_deleted = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  358. is_archived = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  359. is_shared = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  360. is_public = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  361. public_url_key = Column(Unicode(1024), unique=False, nullable=False, default='')
  362. data_label = Column(Unicode(1024), unique=False, nullable=False, default='')
  363. data_content = Column(Text(), unique=False, nullable=False, default='')
  364. _properties = Column('properties', Text(), unique=False, nullable=False, default='')
  365. data_datetime = Column(DateTime, unique=False, nullable=False)
  366. data_reminder_datetime = Column(DateTime, unique=False, nullable=True)
  367. data_file_name = Column(Unicode(255), unique=False, nullable=False, default='')
  368. data_file_mime_type = Column(Unicode(255), unique=False, nullable=False, default='')
  369. data_file_content = sqlao.deferred(Column(LargeBinary(), unique=False, nullable=False, default=None))
  370. last_action = Column(Unicode(32), unique=False, nullable=False, default='')
  371. _lRights = relationship('Rights', backref='_oNode', cascade = "all, delete-orphan")
  372. parent = relationship('PBNode', remote_side=[node_id], backref='children')
  373. owner = relationship('User', remote_side=[User.user_id], backref='_lAllNodes')
  374. @hybrid_property
  375. def _lAllChildren(self):
  376. # for backward compatibility method
  377. return self.children
  378. @property
  379. def _oOwner(self):
  380. # for backward compatibility method
  381. return self.owner
  382. @property
  383. def _oParent(self):
  384. # for backward compatibility method
  385. return self.parent
  386. @hybrid_property
  387. def properties(self):
  388. """ return a structure decoded from json content of _properties """
  389. if not self._properties:
  390. ContentChecker.reset_properties(self)
  391. return json.loads(self._properties)
  392. @properties.setter
  393. def properties(self, properties_struct):
  394. """ encode a given structure into json and store it in _properties attribute"""
  395. self._properties = json.dumps(properties_struct)
  396. ContentChecker.check_properties(self)
  397. def extract_links_from_content(self, other_content: str=None) -> [LinkItem]:
  398. """
  399. parse html content and extract links. By default, it works on the data_content property
  400. :param other_content: if not empty, then parse the given html content instead of data_content
  401. :return: a list of LinkItem
  402. """
  403. links = []
  404. soup = BeautifulSoup(self.data_content if not other_content else other_content)
  405. for link in soup.findAll('a'):
  406. href = link.get('href')
  407. print(href)
  408. label = link.contents
  409. links.append(LinkItem(href, label))
  410. links.sort(key=lambda link: link.href if link.href else '')
  411. sorted_links = sorted(links, key=lambda link: link.label if link.label else link.href, reverse=True)
  412. ## FIXME - Does this return a sorted list ???!
  413. return sorted_links
  414. def getChildrenOfType(self, plNodeTypeList, poKeySortingMethod=None, pbDoReverseSorting=False):
  415. """return all children nodes of type 'data' or 'node' or 'folder'"""
  416. llChildren = []
  417. user_id = current_user().user_id
  418. llChildren = DBSession.query(PBNode).outerjoin(Rights)\
  419. .outerjoin(user_group_table, Rights.group_id==user_group_table.columns['group_id'])\
  420. .filter(PBNode.parent_id==self.node_id)\
  421. .filter((PBNode.owner_id==user_id) | ((user_group_table.c.user_id==user_id) & (PBNode.is_shared == True)))\
  422. .filter(PBNode.node_type.in_(plNodeTypeList))\
  423. .all()
  424. if poKeySortingMethod!=None:
  425. llChildren = sorted(llChildren, key=poKeySortingMethod, reverse=pbDoReverseSorting)
  426. return llChildren
  427. def get_child_nb(self, content_type: PBNodeType, content_status = ''):
  428. # V2 method - to keep
  429. child_nb = 0
  430. for child in self._lAllChildren:
  431. if child.node_type==content_type:
  432. if not content_status:
  433. child_nb = child_nb+1
  434. elif content_status==child.node_status:
  435. child_nb = child_nb+1
  436. return child_nb
  437. def get_status(self) -> ContentStatus:
  438. return ContentStatus(self.node_status, self.node_type.__str__())
  439. def get_last_action(self) -> ActionDescription:
  440. return ActionDescription(self.last_action)
  441. def get_comments(self):
  442. children = []
  443. for child in self.children:
  444. if child.node_type==PBNodeType.Comment:
  445. children.append(child)
  446. return children
  447. def getChildNb(self):
  448. return self.getChildNbOfType([PBNodeType.Data])
  449. def getGroupsWithSomeAccess(self):
  450. llRights = []
  451. for loRight in self._lRights:
  452. if loRight.rights>0:
  453. llRights.append(loRight)
  454. return llRights
  455. def getChildren(self, pbIncludeDeleted=False):
  456. """return all children nodes of type 'data' or 'node' or 'folder'"""
  457. # return self.getChildrenOfType([PBNodeType.Node, PBNodeType.Folder, PBNodeType.Data])
  458. items = self.getChildrenOfType([PBNodeType.Node, PBNodeType.Folder, PBNodeType.Data])
  459. items2 = list()
  460. for item in items:
  461. if pbIncludeDeleted==True or item.node_status!='deleted':
  462. items2.append(item)
  463. return items2
  464. def getContacts(self):
  465. """return all children nodes of type 'data' or 'node' or 'folder'"""
  466. return self.getChildrenOfType([PBNodeType.Contact], PBNode.getSortingKeyForContact)
  467. def getContactNb(self):
  468. """return all children nodes of type 'data' or 'node' or 'folder'"""
  469. return self.getChildNbOfType([PBNodeType.Contact])
  470. @classmethod
  471. def getSortingKeyBasedOnDataDatetime(cls, poDataNode):
  472. return poDataNode.data_datetime or MINIMUM_DATE
  473. @classmethod
  474. def getSortingKeyForContact(cls, poDataNode):
  475. return poDataNode.data_label or ''
  476. @classmethod
  477. def getSortingKeyForComment(cls, poDataNode):
  478. return poDataNode.data_datetime or ''
  479. def getEvents(self):
  480. return self.getChildrenOfType([PBNodeType.Event], PBNode.getSortingKeyBasedOnDataDatetime, True)
  481. def getFiles(self):
  482. return self.getChildrenOfType([PBNodeType.File])
  483. def getIconClass(self):
  484. if self.node_type==PBNodeType.Data and self.getStaticChildNb()>0:
  485. return PBNode.getIconClassForNodeType('folder')
  486. else:
  487. return PBNode.getIconClassForNodeType(self.node_type)
  488. def getBreadCrumbNodes(self) -> list('PBNode'):
  489. loNodes = []
  490. if self._oParent!=None:
  491. loNodes = self._oParent.getBreadCrumbNodes()
  492. loNodes.append(self._oParent)
  493. return loNodes
  494. def getContentWithHighlightedKeywords(self, plKeywords, psPlainText):
  495. if len(plKeywords)<=0:
  496. return psPlainText
  497. lsPlainText = psPlainText
  498. for lsKeyword in plKeywords:
  499. lsPlainText = re.sub('(?i)(%s)' % lsKeyword, '<strong>\\1</strong>', lsPlainText)
  500. return lsPlainText
  501. @classmethod
  502. def getIconClassForNodeType(cls, psIconType):
  503. laIconClass = dict()
  504. laIconClass['node'] = 'fa fa-folder-open'
  505. laIconClass['folder'] = 'fa fa-folder-open'
  506. laIconClass['data'] = 'fa fa-file-text-o'
  507. laIconClass['file'] = 'fa fa-paperclip'
  508. laIconClass['event'] = 'fa fa-calendar'
  509. laIconClass['contact'] = 'fa fa-user'
  510. laIconClass['comment'] = 'fa fa-comments-o'
  511. return laIconClass[psIconType]
  512. def getUserFriendlyNodeType(self):
  513. laNodeTypesLng = dict()
  514. laNodeTypesLng['node'] = 'Document' # FIXME - D.A. - 2013-11-14 - Make text translatable
  515. laNodeTypesLng['folder'] = 'Document'
  516. laNodeTypesLng['data'] = 'Document'
  517. laNodeTypesLng['file'] = 'File'
  518. laNodeTypesLng['event'] = 'Event'
  519. laNodeTypesLng['contact'] = 'Contact'
  520. laNodeTypesLng['comment'] = 'Comment'
  521. if self.node_type==PBNodeType.Data and self.getStaticChildNb()>0:
  522. return laNodeTypesLng['folder']
  523. else:
  524. return laNodeTypesLng[self.node_type]
  525. def getFormattedDateTime(self, poDateTime, psDateTimeFormat = '%d/%m/%Y ~ %H:%M'):
  526. return poDateTime.strftime(psDateTimeFormat)
  527. def getFormattedDate(self, poDateTime, psDateTimeFormat = '%d/%m/%Y'):
  528. return poDateTime.strftime(psDateTimeFormat)
  529. def getFormattedTime(self, poDateTime, psDateTimeFormat = '%H:%M'):
  530. return poDateTime.strftime(psDateTimeFormat)
  531. def getStatus(self) -> PBNodeStatusItem:
  532. loStatus = PBNodeStatus.getStatusItem(self.node_status)
  533. if loStatus.status_id!='automatic':
  534. return loStatus
  535. # default case
  536. # Compute the status:
  537. # - if at least one child is 'new' or 'in progress' or 'in standby' => status is inprogress
  538. # - else if all status are 'done', 'closed' or 'deleted' => 'done'
  539. lsRealStatusId = 'done'
  540. for loChild in self.getChildren():
  541. if loChild.getStatus().status_id in ('new', 'inprogress', 'standby'):
  542. lsRealStatusId = 'inprogress'
  543. break
  544. return PBNodeStatus.getStatusItem(lsRealStatusId)
  545. def getTruncatedLabel(self, piCharNb: int):
  546. """
  547. return a truncated version of the data_label property.
  548. if piCharNb is not > 0, then the full data_label is returned
  549. note: if the node is a file and the data_label is empty, the file name is returned
  550. """
  551. lsTruncatedLabel = self.data_label
  552. # 2014-05-06 - D.A. - HACK
  553. # if the node is a file and label empty, then use the filename as data_label
  554. if self.node_type==PBNodeType.File and lsTruncatedLabel=='':
  555. lsTruncatedLabel = self.data_file_name
  556. liMaxLength = int(piCharNb)
  557. if liMaxLength>0 and len(lsTruncatedLabel)>liMaxLength:
  558. lsTruncatedLabel = lsTruncatedLabel[0:liMaxLength-1]+'…'
  559. if lsTruncatedLabel=='':
  560. lsTruncatedLabel = _('Titleless Document')
  561. return lsTruncatedLabel
  562. def getTruncatedContentAsText(self, piCharNb):
  563. lsPlainText = ''.join(BeautifulSoup(self.data_content).findAll(text=True))
  564. lsTruncatedContent = ''
  565. liMaxLength = int(piCharNb)
  566. if len(lsPlainText)>liMaxLength:
  567. lsTruncatedContent = lsPlainText[0:liMaxLength-1]+'…'
  568. else:
  569. lsTruncatedContent = lsPlainText
  570. return lsTruncatedContent
  571. def getTagList(self):
  572. loPattern = re.compile('(^|\s|@)@(\w+)')
  573. loResults = re.findall(loPattern, self.data_content)
  574. lsResultList = []
  575. for loResult in loResults:
  576. lsResultList.append(loResult[1].replace('@', '').replace('_', ' '))
  577. return lsResultList
  578. @classmethod
  579. def addTagReplacement(cls, matchobj):
  580. return " <span class='badge'>%s</span> " %(matchobj.group(0).replace('@', '').replace('_', ' '))
  581. @classmethod
  582. def addDocLinkReplacement(cls, matchobj):
  583. return " <a href='%s'>%s</a> " %(tg.url('/dashboard?node=%s')%(matchobj.group(1)), matchobj.group(0))
  584. def getContentWithTags(self):
  585. lsTemporaryResult = re.sub('(^|\s)@@(\w+)', '', self.data_content) # tags with @@ are explicitly removed from the body
  586. lsTemporaryResult = re.sub('#([0-9]*)', PBNode.addDocLinkReplacement, lsTemporaryResult) # tags with @@ are explicitly removed from the body
  587. return re.sub('(^|\s)@(\w+)', PBNode.addTagReplacement, lsTemporaryResult) # then, 'normal tags are transformed as labels'
  588. # FIXME - D.A. - 2013-09-12
  589. # Does not match @@ at end of content.
  590. def getHistory(self):
  591. return DBSession.execute("select node_id, version_id, created_at from pod_nodes_history where node_id = :node_id order by created_at desc", {"node_id":self.node_id}).fetchall()
  592. class ContentChecker(object):
  593. @classmethod
  594. def check_properties(cls, item: PBNode):
  595. if item.node_type==PBNodeType.Folder:
  596. properties = item.properties
  597. if 'allowed_content' not in properties.keys():
  598. return False
  599. if 'folders' not in properties['allowed_content']:
  600. return False
  601. if 'files' not in properties['allowed_content']:
  602. return False
  603. if 'pages' not in properties['allowed_content']:
  604. return False
  605. if 'threads' not in properties['allowed_content']:
  606. return False
  607. return True
  608. raise NotImplementedError
  609. @classmethod
  610. def reset_properties(cls, item: PBNode):
  611. if item.node_type==PBNodeType.Folder:
  612. item.properties = dict(
  613. allowed_content = dict (
  614. folder = True,
  615. file = True,
  616. page = True,
  617. thread = True
  618. )
  619. )
  620. return
  621. print('NODE TYPE', item.node_type)
  622. raise NotImplementedError
  623. class ContentRevisionRO(DeclarativeBase):
  624. __tablename__ = 'pod_nodes_history'
  625. version_id = Column(Integer, primary_key=True)
  626. node_id = Column(Integer, ForeignKey('pod_nodes.node_id'))
  627. # parent_id = Column(Integer, ForeignKey('pod_nodes.node_id'), nullable=True)
  628. owner_id = Column(Integer, ForeignKey('pod_user.user_id'), nullable=True)
  629. data_label = Column(Unicode(1024), unique=False, nullable=False)
  630. data_content = Column(Text(), unique=False, nullable=False, default='')
  631. data_file_name = Column(Unicode(255), unique=False, nullable=False, default='')
  632. data_file_mime_type = Column(Unicode(255), unique=False, nullable=False, default='')
  633. data_file_content = sqlao.deferred(Column(LargeBinary(), unique=False, nullable=False, default=None))
  634. node_status = Column(Unicode(32), unique=False, nullable=False)
  635. created_at = Column(DateTime, unique=False, nullable=False)
  636. updated_at = Column(DateTime, unique=False, nullable=False)
  637. is_deleted = Column(sqlat.Boolean, unique=False, nullable=False)
  638. is_archived = Column(sqlat.Boolean, unique=False, nullable=False)
  639. last_action = Column(Unicode(32), unique=False, nullable=False, default='')
  640. workspace_id = Column(Integer, ForeignKey('pod_workspaces.workspace_id'), unique=False, nullable=True)
  641. workspace = relationship('Workspace', remote_side=[Workspace.workspace_id])
  642. node = relationship('PBNode', remote_side=[PBNode.node_id], backref='revisions')
  643. owner = relationship('User', remote_side=[User.user_id])
  644. # parent = relationship('PBNode', remote_side=[PBNode.node_id])
  645. def get_status(self):
  646. return ContentStatus(self.node_status)
  647. def get_last_action(self) -> ActionDescription:
  648. return ActionDescription(self.last_action)
  649. class NodeTreeItem(object):
  650. """
  651. This class implements a model that allow to simply represents the left-panel menu items
  652. This model is used by dbapi but is not directly related to sqlalchemy and database
  653. """
  654. def __init__(self, node: PBNode, children: list('NodeTreeItem'), is_selected = False):
  655. self.node = node
  656. self.children = children
  657. self.is_selected = is_selected
  658. #####
  659. #
  660. # HACK - 2014-05-21 - D.A
  661. #
  662. # The following hack is a horrible piece of code that allow to map a raw SQL select to a mapped class
  663. #
  664. class DIRTY_GroupRightsOnNode(object):
  665. def hasSomeAccess(self):
  666. return self.rights >= Rights.READ_ACCESS
  667. def hasReadAccess(self):
  668. return self.rights & Rights.READ_ACCESS
  669. def hasWriteAccess(self):
  670. return self.rights & Rights.WRITE_ACCESS
  671. DIRTY_group_rights_on_node_query = Table('fake_table', metadata,
  672. Column('group_id', Integer, primary_key=True),
  673. Column('node_id', Integer, primary_key=True),
  674. Column('display_name', Unicode(255)),
  675. Column('personnal_group', Boolean),
  676. Column('rights', Integer, primary_key=True)
  677. )
  678. DIRTY_UserDedicatedGroupRightOnNodeSqlQuery = """
  679. SELECT
  680. COALESCE(NULLIF(pg.display_name, ''), pu.display_name) AS display_name,
  681. pg.personnal_group,
  682. pg.group_id,
  683. :node_id AS node_id,
  684. COALESCE(pgn.rights, 0) AS rights
  685. FROM
  686. pod_group AS pg
  687. LEFT JOIN
  688. pod_group_node AS pgn
  689. ON
  690. pg.group_id=pgn.group_id
  691. AND pgn.node_id=:node_id
  692. LEFT JOIN
  693. pod_user AS pu
  694. ON
  695. pu.user_id=-pg.group_id
  696. WHERE
  697. pg.personnal_group='t'
  698. ORDER BY
  699. display_name
  700. ;"""
  701. DIRTY_RealGroupRightOnNodeSqlQuery = """
  702. SELECT
  703. pg.display_name AS display_name,
  704. pg.personnal_group,
  705. pg.group_id,
  706. :node_id AS node_id,
  707. COALESCE(pgn.rights, 0) AS rights
  708. FROM
  709. pod_group AS pg
  710. LEFT JOIN
  711. pod_group_node AS pgn
  712. ON
  713. pg.group_id=pgn.group_id
  714. AND pgn.node_id=:node_id
  715. WHERE
  716. pg.personnal_group!='t'
  717. ORDER BY
  718. display_name
  719. ;"""
  720. sqlao.mapper(DIRTY_GroupRightsOnNode, DIRTY_group_rights_on_node_query)