data.py 16KB

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