# -*- coding: utf-8 -*-
import os
import re
import datetime as datetimeroot
from datetime import datetime
from hashlib import sha256
import bs4
from sqlalchemy import Table, ForeignKey, Column, Sequence
import sqlalchemy as sqla
from sqlalchemy.sql.sqltypes import Boolean
from sqlalchemy.types import Unicode, Integer, DateTime, Text, LargeBinary
import sqlalchemy.types as sqlat
from sqlalchemy.orm import relation, synonym, relationship
from sqlalchemy.orm import backref
import sqlalchemy.orm as sqlao
import sqlalchemy.orm.query as sqlaoq
from sqlalchemy import orm as sqlao
from tg.i18n import ugettext as _, lazy_ugettext as l_
import tg
from pod.model import DeclarativeBase, metadata, DBSession
from pod.model import auth as pma
from pod.lib.base import current_user
class PBNodeStatusItem(object):
def __init__(self, psStatusId, psStatusLabel, psStatusFamily, psIconId, psCssClass): #, psBackgroundColor):
self._sStatusId = psStatusId
self._sStatusLabel = psStatusLabel
self._sStatusFamily = psStatusFamily
self._sIconId = psIconId
self._sCssClass = psCssClass
# self._sBackgroundColor = psBackgroundColor
def getLabel(self):
return self._sStatusLabel
@property
def status_family(self):
return self._sStatusFamily
@property
def icon(self):
return self._sIconId
def getId(self):
return self._sStatusId
@property
def css(self):
return self._sCssClass
@property
def status_id(self):
return self._sStatusId
@property
def icon_id(self):
return self._sIconId
@property
def label(self):
return self._sStatusLabel
class PBNodeStatus(object):
StatusList = dict()
StatusList['information'] = PBNodeStatusItem('information', 'Information', 'normal', 'fa fa-info-circle', 'pod-status-grey-light')
StatusList['automatic'] = PBNodeStatusItem('automatic', 'Automatic', 'open', 'fa fa-flash', 'pod-status-grey-light')
StatusList['new'] = PBNodeStatusItem('new', 'New', 'open', 'fa fa-lightbulb-o fa-inverse', 'btn-success')
StatusList['inprogress'] = PBNodeStatusItem('inprogress', 'In progress', 'open', 'fa fa-gears fa-inverse', 'btn-info')
StatusList['standby'] = PBNodeStatusItem('standby', 'In standby', 'open', 'fa fa-spinner fa-inverse', 'btn-warning')
StatusList['done'] = PBNodeStatusItem('done', 'Done', 'closed', 'fa fa-check-square-o', 'pod-status-grey-light')
StatusList['closed'] = PBNodeStatusItem('closed', 'Closed', 'closed', 'fa fa-lightbulb-o', 'pod-status-grey-middle')
StatusList['deleted'] = PBNodeStatusItem('deleted', 'Deleted', 'closed', 'fa fa-trash-o', 'pod-status-grey-dark')
@classmethod
def getChoosableList(cls):
return [
PBNodeStatus.StatusList['information'],
PBNodeStatus.StatusList['automatic'],
PBNodeStatus.StatusList['new'],
PBNodeStatus.StatusList['inprogress'],
PBNodeStatus.StatusList['standby'],
PBNodeStatus.StatusList['done'],
PBNodeStatus.StatusList['closed'],
]
@classmethod
def getVisibleIdsList(cls):
return ['information', 'automatic', 'new', 'inprogress', 'standby', 'done' ]
@classmethod
def getVisibleList(cls):
return [
PBNodeStatus.StatusList['information'],
PBNodeStatus.StatusList['automatic'],
PBNodeStatus.StatusList['new'],
PBNodeStatus.StatusList['inprogress'],
PBNodeStatus.StatusList['standby'],
PBNodeStatus.StatusList['done'],
]
@classmethod
def getList(cls):
return [
PBNodeStatus.StatusList['information'],
PBNodeStatus.StatusList['automatic'],
PBNodeStatus.StatusList['new'],
PBNodeStatus.StatusList['inprogress'],
PBNodeStatus.StatusList['standby'],
PBNodeStatus.StatusList['done'],
PBNodeStatus.StatusList['closed'],
PBNodeStatus.StatusList['deleted']
]
@classmethod
def getStatusItem(cls, psStatusId):
return PBNodeStatus.StatusList[psStatusId]
class PBNodeType(object):
Node = 'node'
Folder = 'folder'
Data = 'data'
File = 'file'
Event = 'event'
Contact = 'contact'
Comment = 'comment'
MINIMUM_DATE = datetimeroot.date(datetimeroot.MINYEAR, 1, 1)
class PBNode(DeclarativeBase):
#def __init__(self):
# self._lStaticChildList = []
@sqlao.reconstructor
def init_on_load(self):
self._lStaticChildList = []
def appendStaticChild(self, loNode):
print("%s has child %s" % (self.node_id, loNode.node_id))
self._lStaticChildList.append(loNode)
def getStaticChildList(self):
return self._lStaticChildList
def getStaticChildNb(self):
return len(self._lStaticChildList)
__tablename__ = 'pod_nodes'
node_id = Column(Integer, Sequence('pod_nodes__node_id__sequence'), primary_key=True)
parent_id = Column(Integer, ForeignKey('pod_nodes.node_id'), nullable=True, default=None)
node_depth = Column(Integer, unique=False, nullable=False, default=0)
parent_tree_path = Column(Unicode(255), unique=False, nullable=False, default='')
owner_id = Column(Integer, ForeignKey('pod_user.user_id'), nullable=True, default=None)
node_order = Column(Integer, nullable=True, default=1)
node_type = Column(Unicode(16), unique=False, nullable=False, default='data')
node_status = Column(Unicode(16), unique=False, nullable=False, default='new')
created_at = Column(DateTime, unique=False, nullable=False)
updated_at = Column(DateTime, unique=False, nullable=False)
"""
if 1, the document is available for other users logged into pod.
default is 0 (private document)
"""
is_shared = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
"""
if 1, the document is available through a public - but obfuscated, url
default is 0 (document not publicly available)
"""
is_public = Column(sqlat.Boolean, unique=False, nullable=False, default=False)
"""
here is the hash allowing to get the document publicly
"""
public_url_key = Column(Unicode(1024), unique=False, nullable=False, default='')
data_label = Column(Unicode(1024), unique=False, nullable=False, default='')
data_content = Column(Text(), unique=False, nullable=False, default='')
data_datetime = Column(DateTime, unique=False, nullable=False)
data_reminder_datetime = Column(DateTime, unique=False, nullable=True)
data_file_name = Column(Unicode(255), unique=False, nullable=False, default='')
data_file_mime_type = Column(Unicode(255), unique=False, nullable=False, default='')
data_file_content = sqlao.deferred(Column(LargeBinary(), unique=False, nullable=False, default=None))
_lRights = relationship('Rights', backref='_oNode', cascade = "all, delete-orphan")
_oParent = relationship('PBNode', remote_side=[node_id], backref='_lAllChildren')
_oOwner = relationship('User', remote_side=[pma.User.user_id], backref='_lAllNodes')
def getChildrenOfType(self, plNodeTypeList, poKeySortingMethod=None, pbDoReverseSorting=False):
"""return all children nodes of type 'data' or 'node' or 'folder'"""
llChildren = []
user_id = current_user().user_id
llChildren = DBSession.query(PBNode).outerjoin(pma.Rights)\
.outerjoin(pma.user_group_table, pma.Rights.group_id==pma.user_group_table.columns['group_id'])\
.filter(PBNode.parent_id==self.node_id)\
.filter((PBNode.owner_id==user_id) | ((pma.user_group_table.c.user_id==user_id) & (PBNode.is_shared == True)))\
.filter(PBNode.node_type.in_(plNodeTypeList))\
.all()
if poKeySortingMethod!=None:
llChildren = sorted(llChildren, key=poKeySortingMethod, reverse=pbDoReverseSorting)
return llChildren
def getChildNbOfType(self, plNodeTypeList):
"""return all children nodes of type 'data' or 'node' or 'folder'"""
liChildNb = 0
for child in self._lAllChildren:
if child.node_type in plNodeTypeList:
liChildNb = liChildNb+1
return liChildNb
# return DBSession.query(PBNode).filter(PBNode.parent_id==self.node_id).filter(PBNode.node_type.in_(plNodeTypeList)).order_by(plSortingCriteria).all()
def getChildNb(self):
return self.getChildNbOfType([PBNodeType.Data])
def getGroupsWithSomeAccess(self):
llRights = []
for loRight in self._lRights:
if loRight.rights>0:
llRights.append(loRight)
return llRights
def getChildren(self, pbIncludeDeleted=False):
"""return all children nodes of type 'data' or 'node' or 'folder'"""
# return self.getChildrenOfType([PBNodeType.Node, PBNodeType.Folder, PBNodeType.Data])
items = self.getChildrenOfType([PBNodeType.Node, PBNodeType.Folder, PBNodeType.Data])
items2 = list()
for item in items:
if pbIncludeDeleted==True or item.node_status!='deleted':
items2.append(item)
return items2
def getContacts(self):
"""return all children nodes of type 'data' or 'node' or 'folder'"""
return self.getChildrenOfType([PBNodeType.Contact], PBNode.getSortingKeyForContact)
def getContactNb(self):
"""return all children nodes of type 'data' or 'node' or 'folder'"""
return self.getChildNbOfType([PBNodeType.Contact])
@classmethod
def getSortingKeyBasedOnDataDatetime(cls, poDataNode):
return poDataNode.data_datetime or MINIMUM_DATE
@classmethod
def getSortingKeyForContact(cls, poDataNode):
return poDataNode.data_label or ''
@classmethod
def getSortingKeyForComment(cls, poDataNode):
return poDataNode.data_datetime or ''
def getEvents(self):
return self.getChildrenOfType([PBNodeType.Event], PBNode.getSortingKeyBasedOnDataDatetime, True)
def getFiles(self):
return self.getChildrenOfType([PBNodeType.File])
def getComments(self):
return self.getChildrenOfType([PBNodeType.Comment], PBNode.getSortingKeyBasedOnDataDatetime, True)
def getIconClass(self):
if self.node_type==PBNodeType.Data and self.getStaticChildNb()>0:
return PBNode.getIconClassForNodeType('folder')
else:
return PBNode.getIconClassForNodeType(self.node_type)
def getBreadCrumbNodes(self) -> list('PBNode'):
loNodes = []
if self._oParent!=None:
loNodes = self._oParent.getBreadCrumbNodes()
loNodes.append(self._oParent)
return loNodes
def getContentWithHighlightedKeywords(self, plKeywords, psPlainText):
if len(plKeywords)<=0:
return psPlainText
lsPlainText = psPlainText
for lsKeyword in plKeywords:
lsPlainText = re.sub('(?i)(%s)' % lsKeyword, '\\1', lsPlainText)
return lsPlainText
@classmethod
def getIconClassForNodeType(cls, psIconType):
laIconClass = dict()
laIconClass['node'] = 'fa fa-folder-open'
laIconClass['folder'] = 'fa fa-folder-open'
laIconClass['data'] = 'fa fa-file-text-o'
laIconClass['file'] = 'fa fa-paperclip'
laIconClass['event'] = 'fa fa-calendar'
laIconClass['contact'] = 'fa fa-user'
laIconClass['comment'] = 'fa fa-comments-o'
return laIconClass[psIconType]
def getUserFriendlyNodeType(self):
laNodeTypesLng = dict()
laNodeTypesLng['node'] = 'Document' # FIXME - D.A. - 2013-11-14 - Make text translatable
laNodeTypesLng['folder'] = 'Document'
laNodeTypesLng['data'] = 'Document'
laNodeTypesLng['file'] = 'File'
laNodeTypesLng['event'] = 'Event'
laNodeTypesLng['contact'] = 'Contact'
laNodeTypesLng['comment'] = 'Comment'
if self.node_type==PBNodeType.Data and self.getStaticChildNb()>0:
return laNodeTypesLng['folder']
else:
return laNodeTypesLng[self.node_type]
def getFormattedDateTime(self, poDateTime, psDateTimeFormat = '%d/%m/%Y ~ %H:%M'):
return poDateTime.strftime(psDateTimeFormat)
def getFormattedDate(self, poDateTime, psDateTimeFormat = '%d/%m/%Y'):
return poDateTime.strftime(psDateTimeFormat)
def getFormattedTime(self, poDateTime, psDateTimeFormat = '%H:%M'):
return poDateTime.strftime(psDateTimeFormat)
def getStatus(self) -> PBNodeStatusItem:
loStatus = PBNodeStatus.getStatusItem(self.node_status)
if loStatus.status_id!='automatic':
return loStatus
else:
# Compute the status:
# - if at least one child is 'new' or 'in progress' or 'in standby' => status is inprogress
# - else if all status are 'done', 'closed' or 'deleted' => 'done'
lsRealStatusId = 'done'
for loChild in self.getChildren():
if loChild.getStatus().status_id in ('new', 'inprogress', 'standby'):
lsRealStatusId = 'inprogress'
break
return PBNodeStatus.getStatusItem(lsRealStatusId)
def getTruncatedLabel(self, piCharNb: int):
"""
return a truncated version of the data_label property.
if piCharNb is not > 0, then the full data_label is returned
note: if the node is a file and the data_label is empty, the file name is returned
"""
lsTruncatedLabel = self.data_label
# 2014-05-06 - D.A. - HACK
# if the node is a file and label empty, then use the filename as data_label
if self.node_type==PBNodeType.File and lsTruncatedLabel=='':
lsTruncatedLabel = self.data_file_name
liMaxLength = int(piCharNb)
if liMaxLength>0 and len(lsTruncatedLabel)>liMaxLength:
lsTruncatedLabel = lsTruncatedLabel[0:liMaxLength-1]+'…'
if lsTruncatedLabel=='':
lsTruncatedLabel = _('Titleless Document')
return lsTruncatedLabel
def getTruncatedContentAsText(self, piCharNb):
lsPlainText = ''.join(bs4.BeautifulSoup(self.data_content).findAll(text=True))
lsTruncatedContent = ''
liMaxLength = int(piCharNb)
if len(lsPlainText)>liMaxLength:
lsTruncatedContent = lsPlainText[0:liMaxLength-1]+'…'
else:
lsTruncatedContent = lsPlainText
return lsTruncatedContent
def getTagList(self):
loPattern = re.compile('(^|\s|@)@(\w+)')
loResults = re.findall(loPattern, self.data_content)
lsResultList = []
for loResult in loResults:
lsResultList.append(loResult[1].replace('@', '').replace('_', ' '))
return lsResultList
@classmethod
def addTagReplacement(cls, matchobj):
return " %s " %(matchobj.group(0).replace('@', '').replace('_', ' '))
@classmethod
def addDocLinkReplacement(cls, matchobj):
return " %s " %(tg.url('/dashboard?node=%s')%(matchobj.group(1)), matchobj.group(0))
def getContentWithTags(self):
lsTemporaryResult = re.sub('(^|\s)@@(\w+)', '', self.data_content) # tags with @@ are explicitly removed from the body
lsTemporaryResult = re.sub('#([0-9]*)', PBNode.addDocLinkReplacement, lsTemporaryResult) # tags with @@ are explicitly removed from the body
return re.sub('(^|\s)@(\w+)', PBNode.addTagReplacement, lsTemporaryResult) # then, 'normal tags are transformed as labels'
# FIXME - D.A. - 2013-09-12
# Does not match @@ at end of content.
def getHistory(self):
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()
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: PBNode, children: list('NodeTreeItem')):
self.node = node
self.children = children
#####
#
# HACK - 2014-05-21 - D.A
#
# The following hack is a horrible piece of code that allow to map a raw SQL select to a mapped class
#
class DIRTY_GroupRightsOnNode(object):
def hasSomeAccess(self):
return self.rights >= pma.Rights.READ_ACCESS
def hasReadAccess(self):
return self.rights & pma.Rights.READ_ACCESS
def hasWriteAccess(self):
return self.rights & pma.Rights.WRITE_ACCESS
DIRTY_group_rights_on_node_query = Table('fake_table', metadata,
Column('group_id', Integer, primary_key=True),
Column('node_id', Integer, primary_key=True),
Column('display_name', Unicode(255)),
Column('personnal_group', Boolean),
Column('rights', Integer, primary_key=True)
)
DIRTY_UserDedicatedGroupRightOnNodeSqlQuery = """
SELECT
COALESCE(NULLIF(pg.display_name, ''), pu.display_name) AS display_name,
pg.personnal_group,
pg.group_id,
:node_id AS node_id,
COALESCE(pgn.rights, 0) AS rights
FROM
pod_group AS pg
LEFT JOIN
pod_group_node AS pgn
ON
pg.group_id=pgn.group_id
AND pgn.node_id=:node_id
LEFT JOIN
pod_user AS pu
ON
pu.user_id=-pg.group_id
WHERE
pg.personnal_group='t'
ORDER BY
display_name
;"""
DIRTY_RealGroupRightOnNodeSqlQuery = """
SELECT
pg.display_name AS display_name,
pg.personnal_group,
pg.group_id,
:node_id AS node_id,
COALESCE(pgn.rights, 0) AS rights
FROM
pod_group AS pg
LEFT JOIN
pod_group_node AS pgn
ON
pg.group_id=pgn.group_id
AND pgn.node_id=:node_id
WHERE
pg.personnal_group!='t'
ORDER BY
display_name
;"""
sqlao.mapper(DIRTY_GroupRightsOnNode, DIRTY_group_rights_on_node_query)