data.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. # -*- coding: utf-8 -*-
  2. import os
  3. import re
  4. import datetime as datetimeroot
  5. from datetime import datetime
  6. from hashlib import sha256
  7. import bs4
  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.orm import relation, synonym, relationship
  14. from sqlalchemy.orm import backref
  15. import sqlalchemy.orm as sqlao
  16. import sqlalchemy.orm.query as sqlaoq
  17. from sqlalchemy import orm as sqlao
  18. from tg.i18n import ugettext as _, lazy_ugettext as l_
  19. import tg
  20. from pod.model import DeclarativeBase, metadata, DBSession
  21. from pod.model import auth as pma
  22. from pod.lib.base import current_user
  23. class PBNodeStatusItem(object):
  24. def __init__(self, psStatusId, psStatusLabel, psStatusFamily, psIconId, psCssClass): #, psBackgroundColor):
  25. self._sStatusId = psStatusId
  26. self._sStatusLabel = psStatusLabel
  27. self._sStatusFamily = psStatusFamily
  28. self._sIconId = psIconId
  29. self._sCssClass = psCssClass
  30. # self._sBackgroundColor = psBackgroundColor
  31. def getLabel(self):
  32. return self._sStatusLabel
  33. @property
  34. def status_family(self):
  35. return self._sStatusFamily
  36. @property
  37. def icon(self):
  38. return self._sIconId
  39. def getId(self):
  40. return self._sStatusId
  41. @property
  42. def css(self):
  43. return self._sCssClass
  44. @property
  45. def status_id(self):
  46. return self._sStatusId
  47. @property
  48. def icon_id(self):
  49. return self._sIconId
  50. @property
  51. def label(self):
  52. return self._sStatusLabel
  53. class PBNodeStatus(object):
  54. StatusList = dict()
  55. StatusList['information'] = PBNodeStatusItem('information', 'Information', 'normal', 'fa fa-info-circle', 'pod-status-grey-light')
  56. StatusList['automatic'] = PBNodeStatusItem('automatic', 'Automatic', 'open', 'fa fa-flash', 'pod-status-grey-light')
  57. StatusList['new'] = PBNodeStatusItem('new', 'New', 'open', 'fa fa-lightbulb-o fa-inverse', 'btn-success')
  58. StatusList['inprogress'] = PBNodeStatusItem('inprogress', 'In progress', 'open', 'fa fa-gears fa-inverse', 'btn-info')
  59. StatusList['standby'] = PBNodeStatusItem('standby', 'In standby', 'open', 'fa fa-spinner fa-inverse', 'btn-warning')
  60. StatusList['done'] = PBNodeStatusItem('done', 'Done', 'closed', 'fa fa-check-square-o', 'pod-status-grey-light')
  61. StatusList['closed'] = PBNodeStatusItem('closed', 'Closed', 'closed', 'fa fa-lightbulb-o', 'pod-status-grey-middle')
  62. StatusList['deleted'] = PBNodeStatusItem('deleted', 'Deleted', 'closed', 'fa fa-trash-o', 'pod-status-grey-dark')
  63. @classmethod
  64. def getChoosableList(cls):
  65. return [
  66. PBNodeStatus.StatusList['information'],
  67. PBNodeStatus.StatusList['automatic'],
  68. PBNodeStatus.StatusList['new'],
  69. PBNodeStatus.StatusList['inprogress'],
  70. PBNodeStatus.StatusList['standby'],
  71. PBNodeStatus.StatusList['done'],
  72. PBNodeStatus.StatusList['closed'],
  73. ]
  74. @classmethod
  75. def getVisibleIdsList(cls):
  76. return ['information', 'automatic', 'new', 'inprogress', 'standby', 'done' ]
  77. @classmethod
  78. def getVisibleList(cls):
  79. return [
  80. PBNodeStatus.StatusList['information'],
  81. PBNodeStatus.StatusList['automatic'],
  82. PBNodeStatus.StatusList['new'],
  83. PBNodeStatus.StatusList['inprogress'],
  84. PBNodeStatus.StatusList['standby'],
  85. PBNodeStatus.StatusList['done'],
  86. ]
  87. @classmethod
  88. def getList(cls):
  89. return [
  90. PBNodeStatus.StatusList['information'],
  91. PBNodeStatus.StatusList['automatic'],
  92. PBNodeStatus.StatusList['new'],
  93. PBNodeStatus.StatusList['inprogress'],
  94. PBNodeStatus.StatusList['standby'],
  95. PBNodeStatus.StatusList['done'],
  96. PBNodeStatus.StatusList['closed'],
  97. PBNodeStatus.StatusList['deleted']
  98. ]
  99. @classmethod
  100. def getStatusItem(cls, psStatusId):
  101. return PBNodeStatus.StatusList[psStatusId]
  102. class PBNodeType(object):
  103. Node = 'node'
  104. Folder = 'folder'
  105. Data = 'data'
  106. File = 'file'
  107. Event = 'event'
  108. Contact = 'contact'
  109. Comment = 'comment'
  110. MINIMUM_DATE = datetimeroot.date(datetimeroot.MINYEAR, 1, 1)
  111. class PBNode(DeclarativeBase):
  112. #def __init__(self):
  113. # self._lStaticChildList = []
  114. @sqlao.reconstructor
  115. def init_on_load(self):
  116. self._lStaticChildList = []
  117. def appendStaticChild(self, loNode):
  118. print("%s has child %s" % (self.node_id, loNode.node_id))
  119. self._lStaticChildList.append(loNode)
  120. def getStaticChildList(self):
  121. return self._lStaticChildList
  122. def getStaticChildNb(self):
  123. return len(self._lStaticChildList)
  124. __tablename__ = 'pod_nodes'
  125. node_id = Column(Integer, Sequence('pod_nodes__node_id__sequence'), primary_key=True)
  126. parent_id = Column(Integer, ForeignKey('pod_nodes.node_id'), nullable=True, default=None)
  127. node_depth = Column(Integer, unique=False, nullable=False, default=0)
  128. parent_tree_path = Column(Unicode(255), unique=False, nullable=False, default='')
  129. owner_id = Column(Integer, ForeignKey('pod_user.user_id'), nullable=True, default=None)
  130. node_order = Column(Integer, nullable=True, default=1)
  131. node_type = Column(Unicode(16), unique=False, nullable=False, default='data')
  132. node_status = Column(Unicode(16), unique=False, nullable=False, default='new')
  133. created_at = Column(DateTime, unique=False, nullable=False)
  134. updated_at = Column(DateTime, unique=False, nullable=False)
  135. """
  136. if 1, the document is available for other users logged into pod.
  137. default is 0 (private document)
  138. """
  139. is_shared = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  140. """
  141. if 1, the document is available through a public - but obfuscated, url
  142. default is 0 (document not publicly available)
  143. """
  144. is_public = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
  145. """
  146. here is the hash allowing to get the document publicly
  147. """
  148. public_url_key = Column(Unicode(1024), unique=False, nullable=False, default='')
  149. data_label = Column(Unicode(1024), unique=False, nullable=False, default='')
  150. data_content = Column(Text(), unique=False, nullable=False, default='')
  151. data_datetime = Column(DateTime, unique=False, nullable=False)
  152. data_reminder_datetime = Column(DateTime, unique=False, nullable=True)
  153. data_file_name = Column(Unicode(255), unique=False, nullable=False, default='')
  154. data_file_mime_type = Column(Unicode(255), unique=False, nullable=False, default='')
  155. data_file_content = sqlao.deferred(Column(LargeBinary(), unique=False, nullable=False, default=None))
  156. _lRights = relationship('Rights', backref='_oNode', cascade = "all, delete-orphan")
  157. _oParent = relationship('PBNode', remote_side=[node_id], backref='_lAllChildren')
  158. _oOwner = relationship('User', remote_side=[pma.User.user_id], backref='_lAllNodes')
  159. def getChildrenOfType(self, plNodeTypeList, poKeySortingMethod=None, pbDoReverseSorting=False):
  160. """return all children nodes of type 'data' or 'node' or 'folder'"""
  161. llChildren = []
  162. user_id = current_user().user_id
  163. llChildren = DBSession.query(PBNode).outerjoin(pma.Rights)\
  164. .outerjoin(pma.user_group_table, pma.Rights.group_id==pma.user_group_table.columns['group_id'])\
  165. .filter(PBNode.parent_id==self.node_id)\
  166. .filter((PBNode.owner_id==user_id) | ((pma.user_group_table.c.user_id==user_id) & (PBNode.is_shared == True)))\
  167. .filter(PBNode.node_type.in_(plNodeTypeList))\
  168. .all()
  169. if poKeySortingMethod!=None:
  170. llChildren = sorted(llChildren, key=poKeySortingMethod, reverse=pbDoReverseSorting)
  171. return llChildren
  172. def getChildNbOfType(self, plNodeTypeList):
  173. """return all children nodes of type 'data' or 'node' or 'folder'"""
  174. liChildNb = 0
  175. for child in self._lAllChildren:
  176. if child.node_type in plNodeTypeList:
  177. liChildNb = liChildNb+1
  178. return liChildNb
  179. # return DBSession.query(PBNode).filter(PBNode.parent_id==self.node_id).filter(PBNode.node_type.in_(plNodeTypeList)).order_by(plSortingCriteria).all()
  180. def getChildNb(self):
  181. return self.getChildNbOfType([PBNodeType.Data])
  182. def getGroupsWithSomeAccess(self):
  183. llRights = []
  184. for loRight in self._lRights:
  185. if loRight.rights>0:
  186. llRights.append(loRight)
  187. return llRights
  188. def getChildren(self, pbIncludeDeleted=False):
  189. """return all children nodes of type 'data' or 'node' or 'folder'"""
  190. # return self.getChildrenOfType([PBNodeType.Node, PBNodeType.Folder, PBNodeType.Data])
  191. items = self.getChildrenOfType([PBNodeType.Node, PBNodeType.Folder, PBNodeType.Data])
  192. items2 = list()
  193. for item in items:
  194. if pbIncludeDeleted==True or item.node_status!='deleted':
  195. items2.append(item)
  196. return items2
  197. def getContacts(self):
  198. """return all children nodes of type 'data' or 'node' or 'folder'"""
  199. return self.getChildrenOfType([PBNodeType.Contact], PBNode.getSortingKeyForContact)
  200. def getContactNb(self):
  201. """return all children nodes of type 'data' or 'node' or 'folder'"""
  202. return self.getChildNbOfType([PBNodeType.Contact])
  203. @classmethod
  204. def getSortingKeyBasedOnDataDatetime(cls, poDataNode):
  205. return poDataNode.data_datetime or MINIMUM_DATE
  206. @classmethod
  207. def getSortingKeyForContact(cls, poDataNode):
  208. return poDataNode.data_label or ''
  209. @classmethod
  210. def getSortingKeyForComment(cls, poDataNode):
  211. return poDataNode.data_datetime or ''
  212. def getEvents(self):
  213. return self.getChildrenOfType([PBNodeType.Event], PBNode.getSortingKeyBasedOnDataDatetime, True)
  214. def getFiles(self):
  215. return self.getChildrenOfType([PBNodeType.File])
  216. def getComments(self):
  217. return self.getChildrenOfType([PBNodeType.Comment], PBNode.getSortingKeyBasedOnDataDatetime, True)
  218. def getIconClass(self):
  219. if self.node_type==PBNodeType.Data and self.getStaticChildNb()>0:
  220. return PBNode.getIconClassForNodeType('folder')
  221. else:
  222. return PBNode.getIconClassForNodeType(self.node_type)
  223. def getBreadCrumbNodes(self) -> list('PBNode'):
  224. loNodes = []
  225. if self._oParent!=None:
  226. loNodes = self._oParent.getBreadCrumbNodes()
  227. loNodes.append(self._oParent)
  228. return loNodes
  229. def getContentWithHighlightedKeywords(self, plKeywords, psPlainText):
  230. if len(plKeywords)<=0:
  231. return psPlainText
  232. lsPlainText = psPlainText
  233. for lsKeyword in plKeywords:
  234. lsPlainText = re.sub('(?i)(%s)' % lsKeyword, '<strong>\\1</strong>', lsPlainText)
  235. return lsPlainText
  236. @classmethod
  237. def getIconClassForNodeType(cls, psIconType):
  238. laIconClass = dict()
  239. laIconClass['node'] = 'fa fa-folder-open'
  240. laIconClass['folder'] = 'fa fa-folder-open'
  241. laIconClass['data'] = 'fa fa-file-text-o'
  242. laIconClass['file'] = 'fa fa-paperclip'
  243. laIconClass['event'] = 'fa fa-calendar'
  244. laIconClass['contact'] = 'fa fa-user'
  245. laIconClass['comment'] = 'fa fa-comments-o'
  246. return laIconClass[psIconType]
  247. def getUserFriendlyNodeType(self):
  248. laNodeTypesLng = dict()
  249. laNodeTypesLng['node'] = 'Document' # FIXME - D.A. - 2013-11-14 - Make text translatable
  250. laNodeTypesLng['folder'] = 'Document'
  251. laNodeTypesLng['data'] = 'Document'
  252. laNodeTypesLng['file'] = 'File'
  253. laNodeTypesLng['event'] = 'Event'
  254. laNodeTypesLng['contact'] = 'Contact'
  255. laNodeTypesLng['comment'] = 'Comment'
  256. if self.node_type==PBNodeType.Data and self.getStaticChildNb()>0:
  257. return laNodeTypesLng['folder']
  258. else:
  259. return laNodeTypesLng[self.node_type]
  260. def getFormattedDateTime(self, poDateTime, psDateTimeFormat = '%d/%m/%Y ~ %H:%M'):
  261. return poDateTime.strftime(psDateTimeFormat)
  262. def getFormattedDate(self, poDateTime, psDateTimeFormat = '%d/%m/%Y'):
  263. return poDateTime.strftime(psDateTimeFormat)
  264. def getFormattedTime(self, poDateTime, psDateTimeFormat = '%H:%M'):
  265. return poDateTime.strftime(psDateTimeFormat)
  266. def getStatus(self) -> PBNodeStatusItem:
  267. loStatus = PBNodeStatus.getStatusItem(self.node_status)
  268. if loStatus.status_id!='automatic':
  269. return loStatus
  270. else:
  271. # Compute the status:
  272. # - if at least one child is 'new' or 'in progress' or 'in standby' => status is inprogress
  273. # - else if all status are 'done', 'closed' or 'deleted' => 'done'
  274. lsRealStatusId = 'done'
  275. for loChild in self.getChildren():
  276. if loChild.getStatus().status_id in ('new', 'inprogress', 'standby'):
  277. lsRealStatusId = 'inprogress'
  278. break
  279. return PBNodeStatus.getStatusItem(lsRealStatusId)
  280. def getTruncatedLabel(self, piCharNb: int):
  281. """
  282. return a truncated version of the data_label property.
  283. if piCharNb is not > 0, then the full data_label is returned
  284. note: if the node is a file and the data_label is empty, the file name is returned
  285. """
  286. lsTruncatedLabel = self.data_label
  287. # 2014-05-06 - D.A. - HACK
  288. # if the node is a file and label empty, then use the filename as data_label
  289. if self.node_type==PBNodeType.File and lsTruncatedLabel=='':
  290. lsTruncatedLabel = self.data_file_name
  291. liMaxLength = int(piCharNb)
  292. if liMaxLength>0 and len(lsTruncatedLabel)>liMaxLength:
  293. lsTruncatedLabel = lsTruncatedLabel[0:liMaxLength-1]+'…'
  294. if lsTruncatedLabel=='':
  295. lsTruncatedLabel = _('Titleless Document')
  296. return lsTruncatedLabel
  297. def getTruncatedContentAsText(self, piCharNb):
  298. lsPlainText = ''.join(bs4.BeautifulSoup(self.data_content).findAll(text=True))
  299. lsTruncatedContent = ''
  300. liMaxLength = int(piCharNb)
  301. if len(lsPlainText)>liMaxLength:
  302. lsTruncatedContent = lsPlainText[0:liMaxLength-1]+'…'
  303. else:
  304. lsTruncatedContent = lsPlainText
  305. return lsTruncatedContent
  306. def getTagList(self):
  307. loPattern = re.compile('(^|\s|@)@(\w+)')
  308. loResults = re.findall(loPattern, self.data_content)
  309. lsResultList = []
  310. for loResult in loResults:
  311. lsResultList.append(loResult[1].replace('@', '').replace('_', ' '))
  312. return lsResultList
  313. @classmethod
  314. def addTagReplacement(cls, matchobj):
  315. return " <span class='badge'>%s</span> " %(matchobj.group(0).replace('@', '').replace('_', ' '))
  316. @classmethod
  317. def addDocLinkReplacement(cls, matchobj):
  318. return " <a href='%s'>%s</a> " %(tg.url('/dashboard?node=%s')%(matchobj.group(1)), matchobj.group(0))
  319. def getContentWithTags(self):
  320. lsTemporaryResult = re.sub('(^|\s)@@(\w+)', '', self.data_content) # tags with @@ are explicitly removed from the body
  321. lsTemporaryResult = re.sub('#([0-9]*)', PBNode.addDocLinkReplacement, lsTemporaryResult) # tags with @@ are explicitly removed from the body
  322. return re.sub('(^|\s)@(\w+)', PBNode.addTagReplacement, lsTemporaryResult) # then, 'normal tags are transformed as labels'
  323. # FIXME - D.A. - 2013-09-12
  324. # Does not match @@ at end of content.
  325. def getHistory(self):
  326. 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()
  327. class NodeTreeItem(object):
  328. """
  329. This class implements a model that allow to simply represents the left-panel menu items
  330. This model is used by dbapi but is not directly related to sqlalchemy and database
  331. """
  332. def __init__(self, node: PBNode, children: list('NodeTreeItem')):
  333. self.node = node
  334. self.children = children
  335. #####
  336. #
  337. # HACK - 2014-05-21 - D.A
  338. #
  339. # The following hack is a horrible piece of code that allow to map a raw SQL select to a mapped class
  340. #
  341. class DIRTY_GroupRightsOnNode(object):
  342. def hasSomeAccess(self):
  343. return self.rights >= pma.Rights.READ_ACCESS
  344. def hasReadAccess(self):
  345. return self.rights & pma.Rights.READ_ACCESS
  346. def hasWriteAccess(self):
  347. return self.rights & pma.Rights.WRITE_ACCESS
  348. DIRTY_group_rights_on_node_query = Table('fake_table', metadata,
  349. Column('group_id', Integer, primary_key=True),
  350. Column('node_id', Integer, primary_key=True),
  351. Column('display_name', Unicode(255)),
  352. Column('personnal_group', Boolean),
  353. Column('rights', Integer, primary_key=True)
  354. )
  355. DIRTY_UserDedicatedGroupRightOnNodeSqlQuery = """
  356. SELECT
  357. COALESCE(NULLIF(pg.display_name, ''), pu.display_name) AS display_name,
  358. pg.personnal_group,
  359. pg.group_id,
  360. :node_id AS node_id,
  361. COALESCE(pgn.rights, 0) AS rights
  362. FROM
  363. pod_group AS pg
  364. LEFT JOIN
  365. pod_group_node AS pgn
  366. ON
  367. pg.group_id=pgn.group_id
  368. AND pgn.node_id=:node_id
  369. LEFT JOIN
  370. pod_user AS pu
  371. ON
  372. pu.user_id=-pg.group_id
  373. WHERE
  374. pg.personnal_group='t'
  375. ORDER BY
  376. display_name
  377. ;"""
  378. DIRTY_RealGroupRightOnNodeSqlQuery = """
  379. SELECT
  380. pg.display_name AS display_name,
  381. pg.personnal_group,
  382. pg.group_id,
  383. :node_id AS node_id,
  384. COALESCE(pgn.rights, 0) AS rights
  385. FROM
  386. pod_group AS pg
  387. LEFT JOIN
  388. pod_group_node AS pgn
  389. ON
  390. pg.group_id=pgn.group_id
  391. AND pgn.node_id=:node_id
  392. WHERE
  393. pg.personnal_group!='t'
  394. ORDER BY
  395. display_name
  396. ;"""
  397. sqlao.mapper(DIRTY_GroupRightsOnNode, DIRTY_group_rights_on_node_query)