serializers.py 41KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100
  1. # -*- coding: utf-8 -*-
  2. import cherrypy
  3. import types
  4. from babel.dates import format_timedelta
  5. from babel.dates import format_datetime
  6. from datetime import datetime
  7. import tg
  8. from tg.i18n import ugettext as _
  9. from tg.util import LazyString
  10. from depot.manager import DepotManager
  11. from tracim.lib.base import logger
  12. from tracim.lib.user import CurrentUserGetterApi
  13. from tracim.model.auth import Profile
  14. from tracim.model.auth import User
  15. from tracim.model.data import BreadcrumbItem, ActionDescription
  16. from tracim.model.data import ContentStatus
  17. from tracim.model.data import ContentRevisionRO
  18. from tracim.model.data import LinkItem
  19. from tracim.model.data import NodeTreeItem
  20. from tracim.model.data import Content
  21. from tracim.model.data import ContentType
  22. from tracim.model.data import RoleType
  23. from tracim.model.data import UserRoleInWorkspace
  24. from tracim.model.data import VirtualEvent
  25. from tracim.model.data import Workspace
  26. from tracim.model import data as pmd
  27. from tracim.lib import CST
  28. #############################################################"
  29. ##
  30. ## HERE COMES THE SERIALIZATION CLASSES
  31. ##
  32. ## The following code allow to define with high level
  33. ## of granularity the way models are serialized
  34. ##
  35. class pod_serializer(object):
  36. """
  37. This decorator allow to define a function being a converter of a Model in a Context
  38. """
  39. def __init__(self, model_class, context):
  40. """
  41. :param model_class: the model class to serialize. Should be a class defined in tracim.model.auth or tracim.model.data
  42. :param context: the Context string which should defined in CTX class
  43. :return:
  44. """
  45. assert hasattr(CTX, context)
  46. self.context = context
  47. self.model_class = model_class
  48. def __call__(self, func):
  49. Context.register_converter(self.context, self.model_class, func)
  50. return func
  51. class ContextConverterNotFoundException(Exception):
  52. def __init__(self, context_string, model_class):
  53. message = 'converter not found (context: {0} - model: {1})'.format(context_string, model_class.__name__)
  54. Exception.__init__(self, message)
  55. class CTX(object):
  56. """ constants that are used for serialization / dictification of models"""
  57. ADMIN_USER = 'ADMIN_USER'
  58. ADMIN_WORKSPACE = 'ADMIN_WORKSPACE'
  59. ADMIN_WORKSPACES = 'ADMIN_WORKSPACES'
  60. CONTENT_LIST = 'CONTENT_LIST'
  61. CONTENT_HISTORY = 'CONTENT_HISTORY'
  62. CURRENT_USER = 'CURRENT_USER'
  63. DEFAULT = 'DEFAULT' # default context. This will allow to define a serialization method to be used by default
  64. EMAIL_NOTIFICATION = 'EMAIL_NOTIFICATION'
  65. FILE = 'FILE'
  66. FILES = 'FILES'
  67. FOLDER = 'FOLDER'
  68. FOLDER_CONTENT_LIST = 'FOLDER_CONTENT_LIST'
  69. FOLDERS = 'FOLDERS'
  70. MENU_API = 'MENU_API'
  71. MENU_API_BUILD_FROM_TREE_ITEM = 'MENU_API_BUILD_FROM_TREE_ITEM'
  72. PAGE = 'PAGE'
  73. PAGES = 'PAGES'
  74. SEARCH = 'SEARCH'
  75. THREAD = 'THREAD'
  76. THREADS = 'THREADS'
  77. USER = 'USER'
  78. USERS = 'USERS'
  79. WORKSPACE = 'WORKSPACE'
  80. API_WORKSPACE = 'API_WORKSPACE'
  81. API_CALENDAR_WORKSPACE = 'API_CALENDAR_WORKSPACE'
  82. API_CALENDAR_USER = 'API_CALENDAR_USER'
  83. class DictLikeClass(dict):
  84. """
  85. This class allow to use dictionnary with property like access to values.
  86. eg.: user = { 'login': 'damien', 'password': 'bob'}
  87. will be accessible in the templates like following:
  88. ${user.login}
  89. ${user.password}
  90. """
  91. __getattr__ = dict.__getitem__
  92. __setattr__ = dict.__setitem__
  93. class Context(object):
  94. """
  95. Convert a series of mapped objects into ClassLikeDict (a dictionnary which can be accessed through properties)
  96. example: obj = ClassLikeDict({'foo': 'bar'}) allow to call obj.foo
  97. """
  98. _converters = dict()
  99. @classmethod
  100. def register_converter(cls, context_string, model_class, convert_function):
  101. """
  102. :param context_string:
  103. :param model_class:
  104. :param convert_function:
  105. :return:
  106. """
  107. if context_string not in Context._converters:
  108. Context._converters[context_string] = dict()
  109. if model_class not in Context._converters[context_string]:
  110. logger.info(Context, 'Registering Serialization feature: [ {2} | {1} | {0} ]'.format(
  111. convert_function.__name__,
  112. model_class.__name__,
  113. context_string))
  114. Context._converters[context_string][model_class] = convert_function
  115. @classmethod
  116. def get_converter(cls, context_string, model_class):
  117. """
  118. :param context_string:
  119. :param model_class:
  120. :return:
  121. """
  122. try:
  123. converter = Context._converters[context_string][model_class]
  124. return converter
  125. except KeyError:
  126. if CTX.DEFAULT in Context._converters:
  127. if model_class in Context._converters[CTX.DEFAULT]:
  128. return Context._converters[CTX.DEFAULT][model_class]
  129. raise ContextConverterNotFoundException(context_string, model_class)
  130. def __init__(self, context_string, base_url='', current_user=None):
  131. """
  132. """
  133. self.context_string = context_string
  134. self._current_user = current_user # Allow to define the current user if any
  135. if not current_user:
  136. self._current_user = CurrentUserGetterApi.get_current_user()
  137. self._base_url = base_url # real root url like http://mydomain.com:8080
  138. def url(self, base_url='/', params=None, qualified=False) -> str:
  139. # HACK (REF WSGIDAV.CONTEXT.TG.URL) This is a temporary hack who
  140. # permit to know we are in WSGIDAV context.
  141. if not hasattr(cherrypy.request, 'current_user_email'):
  142. url = tg.url(base_url, params)
  143. else:
  144. url = base_url
  145. if self._base_url:
  146. url = '{}{}'.format(self._base_url, url)
  147. return url
  148. def get_user(self):
  149. return self._current_user
  150. def toDict(self, serializableObject, key_value_for_a_list_object='', key_value_for_list_item_nb=''):
  151. """
  152. Converts a given object into a recursive dictionnary like structure.
  153. :param serializableObject: the structure to be serialized. this may be any type of object, or list
  154. :param key_value_for_a_list_object: if set, then the result will e a dictionnary with the given key.
  155. :param key_value_for_list_item_nb: in case of serializableObject being a list, then this key allow to add item number as property
  156. :return:
  157. """
  158. result = DictLikeClass()
  159. if serializableObject==None:
  160. return None
  161. if isinstance(serializableObject, (int, str, LazyString)):
  162. return serializableObject
  163. if isinstance(serializableObject, (list, tuple, types.GeneratorType)) and not isinstance(serializableObject, str):
  164. # Case of lists
  165. list_of_objects = list()
  166. for item in serializableObject:
  167. list_of_objects.append(self.toDict(item))
  168. if not key_value_for_a_list_object:
  169. return list_of_objects
  170. else:
  171. result[key_value_for_a_list_object] = list_of_objects
  172. if key_value_for_list_item_nb:
  173. result[key_value_for_list_item_nb] = len(serializableObject)
  174. return result
  175. if isinstance(serializableObject, dict):
  176. # Case of dictionnaries
  177. for key, value in serializableObject.items():
  178. result[key] = self.toDict(value)
  179. return result
  180. # Default case now
  181. if key_value_for_a_list_object:
  182. serialized_object = self.toDictSpecific(serializableObject)
  183. result[key_value_for_a_list_object] = serialized_object
  184. else:
  185. result = self.toDictSpecific(serializableObject)
  186. return result
  187. def toDictSpecific(self, serializableObject):
  188. """
  189. Convert to a dictonnary the specific classes. This code will search for the right convert function which
  190. must have been developped and registered.
  191. :param serializableObject: an object to be serialized
  192. :return: a ClassLikeDict instance
  193. """
  194. converter_function = Context.get_converter(self.context_string, serializableObject.__class__)
  195. result = converter_function(serializableObject, self) # process the object with the given serializer
  196. assert isinstance(result, DictLikeClass)
  197. return result
  198. ########################################################################################################################
  199. ## ActionDescription
  200. @pod_serializer(ActionDescription, CTX.DEFAULT)
  201. def serialize_breadcrumb_item(action: ActionDescription, context: Context):
  202. return DictLikeClass(
  203. id = action.id,
  204. icon = action.icon,
  205. label = action.label
  206. )
  207. ########################################################################################################################
  208. ## BREADCRUMB
  209. @pod_serializer(BreadcrumbItem, CTX.DEFAULT)
  210. def serialize_breadcrumb_item(item: BreadcrumbItem, context: Context):
  211. return DictLikeClass(
  212. icon = item.icon,
  213. label = item.label,
  214. url = item.url,
  215. is_active = item.is_active
  216. )
  217. ########################################################################################################################
  218. ## Content
  219. @pod_serializer(ContentRevisionRO, CTX.PAGE)
  220. @pod_serializer(ContentRevisionRO, CTX.FILE)
  221. def serialize_version_for_page_or_file(version: ContentRevisionRO, context: Context):
  222. return DictLikeClass(
  223. id = version.revision_id,
  224. label = version.label,
  225. owner = context.toDict(version.owner),
  226. created = version.created,
  227. action = context.toDict(version.get_last_action()),
  228. )
  229. @pod_serializer(Content, CTX.DEFAULT)
  230. def serialize_breadcrumb_item(content: Content, context: Context):
  231. return DictLikeClass(
  232. id = content.content_id,
  233. label = content.label,
  234. folder = context.toDict(DictLikeClass(id = content.parent.content_id if content.parent else None)),
  235. workspace = context.toDict(content.workspace)
  236. )
  237. @pod_serializer(Content, CTX.EMAIL_NOTIFICATION)
  238. def serialize_item(content: Content, context: Context):
  239. if ContentType.Comment==content.type:
  240. content = content.parent
  241. result = DictLikeClass(
  242. id = content.content_id,
  243. label = content.label,
  244. icon = ContentType.get_icon(content.type),
  245. status = context.toDict(content.get_status()),
  246. folder = context.toDict(DictLikeClass(id = content.parent.content_id if content.parent else None)),
  247. workspace = context.toDict(content.workspace),
  248. is_deleted = content.is_deleted,
  249. is_archived = content.is_archived,
  250. url = context.url('/workspaces/{wid}/folders/{fid}/{ctype}/{cid}'.format(wid = content.workspace_id, fid=content.parent_id, ctype=content.type+'s', cid=content.content_id)),
  251. last_action = context.toDict(content.get_last_action())
  252. )
  253. return result
  254. @pod_serializer(Content, CTX.MENU_API)
  255. def serialize_content_for_menu_api(content: Content, context: Context):
  256. content_id = content.content_id
  257. workspace_id = content.workspace_id
  258. has_children = False
  259. if content.type == ContentType.Folder:
  260. has_children = content.get_child_nb(ContentType.Any) > 0
  261. result = DictLikeClass(
  262. id = CST.TREEVIEW_MENU.ID_TEMPLATE__FULL.format(workspace_id, content_id),
  263. children = has_children,
  264. text = content.get_label(),
  265. a_attr = { 'href' : context.url(ContentType.fill_url(content))},
  266. li_attr = { 'title': content.get_label(), 'class': 'tracim-tree-item-is-a-folder' },
  267. type = content.type,
  268. state = { 'opened': True if ContentType.Folder!=content.type else False, 'selected': False }
  269. )
  270. return result
  271. @pod_serializer(Content, CTX.FILES)
  272. @pod_serializer(Content, CTX.PAGES)
  273. def serialize_node_for_page_list(content: Content, context: Context):
  274. if content.type==ContentType.Page:
  275. if not content.parent:
  276. folder = None
  277. else:
  278. folder = Context(CTX.DEFAULT).toDict(content.parent)
  279. result = DictLikeClass(
  280. id = content.content_id,
  281. label = content.label,
  282. status = context.toDict(content.get_status()),
  283. folder = folder
  284. )
  285. return result
  286. if content.type==ContentType.File:
  287. result = DictLikeClass(
  288. id = content.content_id,
  289. label = content.label,
  290. status = context.toDict(content.get_status()),
  291. folder = Context(CTX.DEFAULT).toDict(content.parent)
  292. )
  293. return result
  294. # TODO - DA - 2014-10-16 - THE FOLLOWING CODE SHOULD BE REMOVED
  295. #
  296. # if content.type==ContentType.Folder:
  297. # return DictLikeClass(
  298. # id = content.content_id,
  299. # label = content.label,
  300. # )
  301. raise NotImplementedError('node type / context not implemented: {} {}'. format(content.type, context.context_string))
  302. @pod_serializer(Content, CTX.PAGE)
  303. @pod_serializer(Content, CTX.FILE)
  304. def serialize_node_for_page(content: Content, context: Context):
  305. if content.type in (ContentType.Page, ContentType.File) :
  306. data_container = content
  307. # The following properties are overriden by revision values
  308. if content.revision_to_serialize>0:
  309. for revision in content.revisions:
  310. if revision.revision_id==content.revision_to_serialize:
  311. data_container = revision
  312. break
  313. result = DictLikeClass(
  314. id=content.content_id,
  315. parent=context.toDict(content.parent),
  316. workspace=context.toDict(content.workspace),
  317. type=content.type,
  318. is_new=content.has_new_information_for(context.get_user()),
  319. content=data_container.description,
  320. created=data_container.created,
  321. updated=content.last_revision.updated,
  322. label=data_container.label,
  323. icon=ContentType.get_icon(content.type),
  324. owner=context.toDict(content.first_revision.owner),
  325. last_modification_author=context.toDict(content.last_revision.owner),
  326. status=context.toDict(data_container.get_status()),
  327. links=[],
  328. revision_nb = len(content.revisions),
  329. selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
  330. history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history()),
  331. is_editable=content.is_editable,
  332. is_deleted=content.is_deleted,
  333. is_archived=content.is_archived,
  334. urls = context.toDict({
  335. 'mark_read': context.url(Content.format_path('/workspaces/{wid}/folders/{fid}/{ctype}s/{cid}/put_read', content)),
  336. 'mark_unread': context.url(Content.format_path('/workspaces/{wid}/folders/{fid}/{ctype}s/{cid}/put_unread', content))
  337. })
  338. )
  339. if content.type == ContentType.File:
  340. depot = DepotManager.get()
  341. depot_stored_file = depot.get(data_container.depot_file)
  342. result.label = content.label
  343. result['file'] = DictLikeClass(
  344. name=data_container.file_name,
  345. size=depot_stored_file.content_length,
  346. mimetype=data_container.file_mimetype)
  347. return result
  348. if content.type==ContentType.Folder:
  349. value = DictLikeClass(
  350. id=content.content_id,
  351. label=content.label,
  352. is_new=content.has_new_information_for(context.get_user()),
  353. )
  354. return value
  355. raise NotImplementedError
  356. @pod_serializer(VirtualEvent, CTX.CONTENT_HISTORY)
  357. def serialize_content_for_history(event: VirtualEvent, context: Context):
  358. urls = DictLikeClass({'delete': None})
  359. if ContentType.Comment == event.type.id:
  360. urls = context.toDict({
  361. 'delete': context.url('/workspaces/{wid}/folders/{fid}/{ctype}/{cid}/comments/{commentid}/put_delete'.format(
  362. wid = event.ref_object.workspace_id,
  363. fid=event.ref_object.parent.parent_id,
  364. ctype=event.ref_object.parent.type+'s',
  365. cid=event.ref_object.parent.content_id,
  366. commentid=event.ref_object.content_id))
  367. })
  368. return DictLikeClass(
  369. owner=context.toDict(event.owner),
  370. id=event.id,
  371. label=event.label,
  372. type=context.toDict(event.type),
  373. created=event.created,
  374. created_as_delta=event.created_as_delta(),
  375. content=event.content,
  376. is_new=event.ref_object.has_new_information_for(context.get_user()),
  377. urls = urls
  378. )
  379. @pod_serializer(Content, CTX.THREAD)
  380. def serialize_node_for_thread(item: Content, context: Context):
  381. if item.type==ContentType.Thread:
  382. return DictLikeClass(
  383. content = item.description,
  384. created = item.created,
  385. updated = item.last_revision.updated,
  386. revision_nb = len(item.revisions),
  387. icon = ContentType.get_icon(item.type),
  388. id = item.content_id,
  389. label = item.label,
  390. links=[],
  391. owner = context.toDict(item.owner),
  392. last_modification_author=context.toDict(item.last_revision.owner),
  393. parent = context.toDict(item.parent),
  394. selected_revision = 'latest',
  395. status = context.toDict(item.get_status()),
  396. type = item.type,
  397. workspace = context.toDict(item.workspace),
  398. comments = reversed(context.toDict(item.get_comments())),
  399. is_new=item.has_new_information_for(context.get_user()),
  400. history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history()),
  401. is_editable=item.is_editable,
  402. is_deleted=item.is_deleted,
  403. is_archived=item.is_archived,
  404. urls = context.toDict({
  405. 'mark_read': context.url(Content.format_path('/workspaces/{wid}/folders/{fid}/{ctype}s/{cid}/put_read', item)),
  406. 'mark_unread': context.url(Content.format_path('/workspaces/{wid}/folders/{fid}/{ctype}s/{cid}/put_unread', item))
  407. }),
  408. )
  409. if item.type==ContentType.Comment:
  410. return DictLikeClass(
  411. is_new=item.has_new_information_for(context.get_user()),
  412. content = item.description,
  413. created = item.created,
  414. created_as_delta = item.created_as_delta(),
  415. icon = ContentType.get_icon(item.type),
  416. id = item.content_id,
  417. label = item.label,
  418. owner = context.toDict(item.owner),
  419. # REMOVE parent = context.toDict(item.parent),
  420. type = item.type,
  421. urls = context.toDict({
  422. 'delete': context.url('/workspaces/{wid}/folders/{fid}/{ctype}/{cid}/comments/{commentid}/put_delete'.format(wid = item.workspace_id, fid=item.parent.parent_id, ctype=item.parent.type+'s', cid=item.parent.content_id, commentid=item.content_id))
  423. })
  424. )
  425. if item.type==ContentType.Folder:
  426. return Context(CTX.DEFAULT).toDict(item)
  427. @pod_serializer(Content, CTX.THREADS)
  428. def serialize_node_for_thread_list(content: Content, context: Context):
  429. if content.type==ContentType.Thread:
  430. return DictLikeClass(
  431. id = content.content_id,
  432. url=ContentType.fill_url(content),
  433. label=content.get_label(),
  434. status=context.toDict(content.get_status()),
  435. folder=context.toDict(content.parent),
  436. workspace=context.toDict(content.workspace) if content.workspace else None,
  437. comment_nb=len(content.get_comments())
  438. )
  439. if content.type==ContentType.Folder:
  440. return Context(CTX.DEFAULT).toDict(content)
  441. raise NotImplementedError
  442. @pod_serializer(Content, CTX.WORKSPACE)
  443. @pod_serializer(Content, CTX.FOLDERS)
  444. def serialize_content_for_workspace(content: Content, context: Context):
  445. thread_nb_all = content.get_child_nb(ContentType.Thread)
  446. thread_nb_open = content.get_child_nb(ContentType.Thread)
  447. file_nb_all = content.get_child_nb(ContentType.File)
  448. file_nb_open = content.get_child_nb(ContentType.File)
  449. folder_nb_all = content.get_child_nb(ContentType.Folder)
  450. folder_nb_open = content.get_child_nb(ContentType.Folder)
  451. page_nb_all = content.get_child_nb(ContentType.Page)
  452. page_nb_open = content.get_child_nb(ContentType.Page)
  453. content_nb_all = thread_nb_all +\
  454. thread_nb_open +\
  455. file_nb_all +\
  456. file_nb_open +\
  457. folder_nb_all +\
  458. folder_nb_open +\
  459. page_nb_all +\
  460. page_nb_open
  461. result = None
  462. if content.type==ContentType.Folder:
  463. result = DictLikeClass(
  464. id = content.content_id,
  465. label = content.label,
  466. thread_nb = DictLikeClass(
  467. all = thread_nb_all,
  468. open = thread_nb_open,
  469. ),
  470. file_nb = DictLikeClass(
  471. all = file_nb_all,
  472. open = file_nb_open,
  473. ),
  474. folder_nb = DictLikeClass(
  475. all = folder_nb_all,
  476. open = folder_nb_open,
  477. ),
  478. page_nb = DictLikeClass(
  479. all = page_nb_all,
  480. open = page_nb_open,
  481. ),
  482. content_nb = DictLikeClass(all = content_nb_all),
  483. is_editable=content.is_editable,
  484. )
  485. return result
  486. @pod_serializer(Content, CTX.FOLDER)
  487. def serialize_content_for_workspace_and_folder(content: Content, context: Context):
  488. thread_nb_all = content.get_child_nb(ContentType.Thread)
  489. thread_nb_open = content.get_child_nb(ContentType.Thread)
  490. file_nb_all = content.get_child_nb(ContentType.File)
  491. file_nb_open = content.get_child_nb(ContentType.File)
  492. folder_nb_all = content.get_child_nb(ContentType.Folder)
  493. folder_nb_open = content.get_child_nb(ContentType.Folder)
  494. page_nb_all = content.get_child_nb(ContentType.Page)
  495. page_nb_open = content.get_child_nb(ContentType.Page)
  496. content_nb_all = thread_nb_all +\
  497. thread_nb_open +\
  498. file_nb_all +\
  499. file_nb_open +\
  500. folder_nb_all +\
  501. folder_nb_open +\
  502. page_nb_all +\
  503. page_nb_open
  504. result = None
  505. if content.type==ContentType.Folder:
  506. allowed_content = DictLikeClass(content.properties['allowed_content']),
  507. result = DictLikeClass(
  508. id=content.content_id,
  509. label=content.label,
  510. created=content.created,
  511. updated=content.last_revision.updated,
  512. last_modification_author=context.toDict(content.last_revision.owner),
  513. revision_nb=len(content.revisions),
  514. workspace=context.toDict(content.workspace),
  515. allowed_content=DictLikeClass(content.properties['allowed_content']),
  516. allowed_content_types=context.toDict(content.get_allowed_content_types()),
  517. selected_revision='latest',
  518. status=context.toDict(content.get_status()),
  519. owner=context.toDict(content.owner),
  520. thread_nb=DictLikeClass(all=thread_nb_all,
  521. open=thread_nb_open),
  522. file_nb=DictLikeClass(all=file_nb_all,
  523. open=file_nb_open),
  524. folder_nb=DictLikeClass(all=folder_nb_all,
  525. open=folder_nb_open),
  526. page_nb=DictLikeClass(all=page_nb_all,
  527. open=page_nb_open),
  528. content_nb=DictLikeClass(all = content_nb_all),
  529. is_archived=content.is_archived,
  530. is_deleted=content.is_deleted,
  531. is_editable=content.is_editable,
  532. )
  533. elif content.type==ContentType.Page:
  534. result = DictLikeClass(
  535. id = content.content_id,
  536. label = content.label,
  537. created = content.created,
  538. workspace = context.toDict(content.workspace),
  539. owner = DictLikeClass(
  540. id = content.owner.user_id,
  541. name = content.owner.get_display_name()
  542. ),
  543. status = DictLikeClass(id='', label=''), #FIXME - EXPORT DATA
  544. )
  545. return result
  546. @pod_serializer(Content, CTX.CONTENT_LIST)
  547. def serialize_content_for_general_list(content: Content, context: Context):
  548. content_type = ContentType(content.type)
  549. last_activity_date = content.get_last_activity_date()
  550. last_activity_date_formatted = format_datetime(last_activity_date,
  551. locale=tg.i18n.get_lang()[0])
  552. last_activity_label = format_timedelta(
  553. datetime.utcnow() - last_activity_date,
  554. locale=tg.i18n.get_lang()[0],
  555. )
  556. last_activity_label = last_activity_label.replace(' ', '\u00A0') # espace insécable
  557. return DictLikeClass(
  558. id=content.content_id,
  559. folder = DictLikeClass({'id': content.parent_id}) if content.parent else None,
  560. workspace=context.toDict(content.workspace) if content.workspace else None,
  561. label=content.get_label(),
  562. url=ContentType.fill_url(content),
  563. type=DictLikeClass(content_type.toDict()),
  564. status=context.toDict(content.get_status()),
  565. is_deleted=content.is_deleted,
  566. is_archived=content.is_archived,
  567. is_editable=content.is_editable,
  568. last_activity = DictLikeClass({'date': last_activity_date,
  569. 'label': last_activity_date_formatted,
  570. 'delta': last_activity_label})
  571. )
  572. @pod_serializer(Content, CTX.FOLDER_CONTENT_LIST)
  573. def serialize_content_for_folder_content_list(content: Content, context: Context):
  574. content_type = ContentType(content.type)
  575. last_activity_date = content.get_last_activity_date()
  576. last_activity_date_formatted = format_datetime(last_activity_date,
  577. locale=tg.i18n.get_lang()[0])
  578. last_activity_label = format_timedelta(datetime.utcnow() - last_activity_date,
  579. locale=tg.i18n.get_lang()[0])
  580. last_activity_label = last_activity_label.replace(' ', '\u00A0') # espace insécable
  581. item = None
  582. if ContentType.Thread == content.type:
  583. item = Context(CTX.THREADS).toDict(content)
  584. item.type = context.toDict(content_type)
  585. item.folder = DictLikeClass({'id': content.parent_id}) if content.parent else None
  586. item.workspace = DictLikeClass({'id': content.workspace.workspace_id}) if content.workspace else None
  587. item.last_activity = DictLikeClass({'date': last_activity_date,
  588. 'label': last_activity_date_formatted,
  589. 'delta': last_activity_label})
  590. comments = content.get_comments()
  591. if len(comments)>1:
  592. item.notes = _('{nb} messages').format(nb=len(comments))
  593. else:
  594. item.notes = _('1 message')
  595. elif ContentType.File == content.type:
  596. item = Context(CTX.CONTENT_LIST).toDict(content)
  597. if len(content.revisions)>1:
  598. item.notes = _('{nb} revisions').format(nb=len(content.revisions))
  599. else:
  600. item.notes = _('1 revision')
  601. elif ContentType.Folder == content.type:
  602. item = Context(CTX.CONTENT_LIST).toDict(content)
  603. item.notes = ''
  604. folder_nb = content.get_child_nb(ContentType.Folder)
  605. if folder_nb == 1:
  606. item.notes += _('1 subfolder<br/>\n')
  607. elif folder_nb > 1:
  608. item.notes += _('{} subfolders<br/>').format(folder_nb)
  609. file_nb = content.get_child_nb(ContentType.File, ContentStatus.OPEN)
  610. if file_nb == 1:
  611. item.notes += _('1 open file<br/>\n')
  612. elif file_nb > 1:
  613. item.notes += _('{} open files<br/>').format(file_nb)
  614. thread_nb = content.get_child_nb(ContentType.Thread, ContentStatus.OPEN)
  615. if thread_nb == 1:
  616. item.notes += _('1 open thread<br/>\n')
  617. elif thread_nb > 1:
  618. item.notes += _('{} open threads<br/>').format(thread_nb)
  619. page_nb = content.get_child_nb(ContentType.Page, ContentStatus.OPEN)
  620. if page_nb == 1:
  621. item.notes += _('1 open page<br/>\n')
  622. elif page_nb > 1:
  623. item.notes += _('{} open pages<br/>').format(page_nb)
  624. else:
  625. item = Context(CTX.CONTENT_LIST).toDict(content)
  626. item.notes = ''
  627. item.is_deleted = content.is_deleted
  628. item.is_archived = content.is_archived
  629. item.is_editable = content.is_editable
  630. return item
  631. @pod_serializer(ContentType, CTX.DEFAULT)
  632. def serialize_breadcrumb_item(content_type: ContentType, context: Context):
  633. return DictLikeClass(content_type.toDict())
  634. @pod_serializer(Content, CTX.SEARCH)
  635. def serialize_content_for_search_result(content: Content, context: Context):
  636. def serialize_it():
  637. nonlocal content
  638. if content.type == ContentType.Comment:
  639. logger.info('serialize_content_for_search_result', 'Serializing parent class {} instead of {} [content #{}]'.format(content.parent.type, content.type, content.content_id))
  640. content = content.parent
  641. data_container = content
  642. if content.revision_to_serialize>0:
  643. for revision in content.revisions:
  644. if revision.revision_id==content.revision_to_serialize:
  645. data_container = revision
  646. break
  647. # FIXME - D.A. - 2015-02-23 - This import should not be there...
  648. from tracim.lib.content import ContentApi
  649. breadcrumbs = ContentApi(None).build_breadcrumb(data_container.workspace, data_container.content_id, skip_root=True)
  650. last_comment_datetime = data_container.updated
  651. comments = data_container.get_comments()
  652. if comments:
  653. last_comment_datetime = max(last_comment_datetime, max(comment.updated for comment in comments))
  654. content_type = ContentType(content.type)
  655. result = DictLikeClass(
  656. id = content.content_id,
  657. type = DictLikeClass(content_type.toDict()),
  658. parent = context.toDict(content.parent),
  659. workspace = context.toDict(content.workspace),
  660. content = data_container.description,
  661. content_raw = data_container.description_as_raw_text(),
  662. created = data_container.created,
  663. created_as_delta = data_container.created_as_delta(),
  664. label = data_container.label,
  665. icon = ContentType.get_icon(content.type),
  666. owner = context.toDict(data_container.owner),
  667. status = context.toDict(data_container.get_status()),
  668. breadcrumb = context.toDict(breadcrumbs),
  669. last_activity=last_comment_datetime,
  670. last_activity_as_delta=content.datetime_as_delta(last_comment_datetime)
  671. )
  672. if content.type==ContentType.File:
  673. result.label = content.label.__str__()
  674. if not result.label or ''==result.label:
  675. result.label = 'No title'
  676. return result
  677. return serialize_it()
  678. ########################################################################################################################
  679. # ContentStatus
  680. @pod_serializer(ContentStatus, CTX.DEFAULT)
  681. def serialize_content_status(status: ContentStatus, context: Context):
  682. return DictLikeClass(
  683. id = status.id,
  684. label = status.label,
  685. icon = status.icon,
  686. css = status.css
  687. )
  688. ########################################################################################################################
  689. # LinkItem
  690. @pod_serializer(LinkItem, CTX.DEFAULT)
  691. def serialize_content_status(link: LinkItem, context: Context):
  692. return DictLikeClass(
  693. href = link.href,
  694. label = link.href,
  695. )
  696. ########################################################################################################################
  697. # Profile
  698. @pod_serializer(Profile, CTX.DEFAULT)
  699. def serialize_user_list_default(profile: Profile, context: Context):
  700. return DictLikeClass(
  701. id = profile.id,
  702. name = profile.name,
  703. label = profile.label
  704. )
  705. ########################################################################################################################
  706. ## ROLE TYPE
  707. @pod_serializer(RoleType, CTX.ADMIN_WORKSPACE)
  708. @pod_serializer(RoleType, CTX.ADMIN_USER)
  709. def serialize_role_list_for_select_field_in_workspace(role_type: RoleType, context: Context):
  710. """
  711. Actually, roles are serialized as users (with minimal information)
  712. :param role:
  713. :param context:
  714. :return:
  715. """
  716. result = DictLikeClass()
  717. result['id'] = role_type.role_type_id
  718. result['icon'] = role_type.icon
  719. result['label'] = role_type.role_label
  720. result['style'] = role_type.css_style
  721. return result
  722. ########################################################################################################################
  723. ## USER
  724. @pod_serializer(User, CTX.DEFAULT)
  725. @pod_serializer(User, CTX.ADMIN_WORKSPACE)
  726. def serialize_user_default(user: User, context: Context):
  727. """
  728. Actually, roles are serialized as users (with minimal information)
  729. :param role:
  730. :param context:
  731. :return:
  732. """
  733. result = DictLikeClass()
  734. result['id'] = user.user_id
  735. result['name'] = user.get_display_name()
  736. return result
  737. @pod_serializer(User, CTX.USERS)
  738. @pod_serializer(User, CTX.ADMIN_WORKSPACE)
  739. def serialize_user_list_default(user: User, context: Context):
  740. """
  741. Actually, roles are serialized as users (with minimal information)
  742. :param role:
  743. :param context:
  744. :return:
  745. """
  746. result = DictLikeClass()
  747. result['id'] = user.user_id
  748. result['name'] = user.get_display_name()
  749. result['email'] = user.email
  750. result['enabled'] = user.is_active
  751. result['profile'] = user.profile
  752. result['has_password'] = user.password!=None
  753. result['timezone'] = user.timezone
  754. return result
  755. @pod_serializer(User, CTX.USER)
  756. @pod_serializer(User, CTX.ADMIN_USER)
  757. @pod_serializer(User, CTX.CURRENT_USER)
  758. def serialize_user_for_user(user: User, context: Context):
  759. """
  760. Actually, roles are serialized as users (with minimal information)
  761. :param role:
  762. :param context:
  763. :return:
  764. """
  765. result = DictLikeClass()
  766. result['id'] = user.user_id
  767. result['name'] = user.get_display_name()
  768. result['email'] = user.email
  769. result['roles'] = context.toDict(user.roles)
  770. result['enabled'] = user.is_active
  771. result['profile'] = user.profile
  772. result['calendar_url'] = user.calendar_url
  773. result['timezone'] = user.timezone
  774. return result
  775. ########################################################################################################################
  776. ## USER ROLE IN WORKSPACE
  777. @pod_serializer(UserRoleInWorkspace, CTX.ADMIN_WORKSPACE)
  778. @pod_serializer(UserRoleInWorkspace, CTX.WORKSPACE)
  779. def serialize_role_in_workspace(role: UserRoleInWorkspace, context: Context):
  780. """
  781. Actually, roles are serialized as users (with minimal information)
  782. :param role:
  783. :param context:
  784. :return:
  785. """
  786. result = DictLikeClass()
  787. result['id'] = role.user_id
  788. result['icon'] = role.icon
  789. result['name'] = role.user.display_name
  790. result['role'] = role.role
  791. result['style'] = role.style
  792. result['role_description'] = role.role_as_label()
  793. result['email'] = role.user.email
  794. result['user'] = context.toDict(role.user)
  795. result['notifications_subscribed'] = role.do_notify
  796. return result
  797. @pod_serializer(UserRoleInWorkspace, CTX.USER)
  798. @pod_serializer(UserRoleInWorkspace, CTX.CURRENT_USER)
  799. @pod_serializer(UserRoleInWorkspace, CTX.ADMIN_USER)
  800. def serialize_role_in_list_for_user(role: UserRoleInWorkspace, context: Context):
  801. """
  802. Actually, roles are serialized as users (with minimal information)
  803. :param role:
  804. :param context:
  805. :return:
  806. """
  807. result = DictLikeClass()
  808. result['id'] = role.role
  809. result['icon'] = role.icon
  810. result['label'] = role.role_as_label()
  811. result['style'] = RoleType(role.role).css_style
  812. result['workspace'] = context.toDict(role.workspace)
  813. result['user'] = Context(CTX.DEFAULT).toDict(role.user)
  814. result['notifications_subscribed'] = role.do_notify
  815. # result['workspace_name'] = role.workspace.label
  816. return result
  817. ########################################################################################################################
  818. ## WORKSPACE
  819. @pod_serializer(Workspace, CTX.DEFAULT)
  820. def serialize_workspace_default(workspace: Workspace, context: Context):
  821. result = DictLikeClass(
  822. id = workspace.workspace_id,
  823. label = workspace.label, # FIXME - 2015-08-20 - remove this property
  824. name = workspace.label, # use name instead of label
  825. is_deleted=workspace.is_deleted,
  826. url = context.url('/workspaces/{}'.format(workspace.workspace_id))
  827. )
  828. return result
  829. @pod_serializer(Workspace, CTX.USER)
  830. @pod_serializer(Workspace, CTX.CURRENT_USER)
  831. def serialize_workspace_in_list_for_one_user(workspace: Workspace, context: Context):
  832. """
  833. Actually, roles are serialized as users (with minimal information)
  834. :param role:
  835. :param context:
  836. :return:
  837. """
  838. result = DictLikeClass()
  839. result['id'] = workspace.workspace_id
  840. result['name'] = workspace.label
  841. return result
  842. @pod_serializer(Workspace, CTX.ADMIN_WORKSPACES)
  843. def serialize_workspace_in_list(workspace: pmd.Workspace, context: Context):
  844. result = DictLikeClass()
  845. result['id'] = workspace.workspace_id
  846. result['label'] = workspace.label
  847. result['description'] = workspace.description
  848. result['member_nb'] = len(workspace.roles)
  849. result['calendar_enabled'] = workspace.calendar_enabled
  850. result['calendar_url'] = workspace.calendar_url
  851. return result
  852. @pod_serializer(Workspace, CTX.ADMIN_WORKSPACE)
  853. @pod_serializer(Workspace, CTX.WORKSPACE)
  854. def serialize_workspace_complete(workspace: pmd.Workspace, context: Context):
  855. result = DictLikeClass()
  856. result['id'] = workspace.workspace_id
  857. result['label'] = workspace.label
  858. result['description'] = workspace.description
  859. result['created'] = workspace.created
  860. result['members'] = context.toDict(workspace.roles)
  861. result['member_nb'] = len(workspace.roles)
  862. result['allowed_content_types'] = context.toDict(workspace.get_allowed_content_types())
  863. result['calendar_enabled'] = workspace.calendar_enabled
  864. result['calendar_url'] = workspace.calendar_url
  865. return result
  866. @pod_serializer(Workspace, CTX.MENU_API)
  867. def serialize_workspace_for_menu_api(workspace: Workspace, context: Context):
  868. result = DictLikeClass(
  869. id = CST.TREEVIEW_MENU.ID_TEMPLATE__WORKSPACE_ONLY.format(workspace.workspace_id),
  870. children = True, # TODO: make this dynamic
  871. text = workspace.label,
  872. a_attr = { 'href' : context.url('/workspaces/{}'.format(workspace.workspace_id)) },
  873. li_attr = { 'title': workspace.label, 'class': 'tracim-tree-item-is-a-workspace' },
  874. type = 'workspace',
  875. state = { 'opened': False, 'selected': False }
  876. )
  877. return result
  878. @pod_serializer(NodeTreeItem, CTX.MENU_API_BUILD_FROM_TREE_ITEM)
  879. def serialize_node_tree_item_for_menu_api_tree(item: NodeTreeItem, context: Context):
  880. if isinstance(item.node, Content):
  881. ContentType.fill_url(item.node)
  882. return DictLikeClass(
  883. id=CST.TREEVIEW_MENU.ID_TEMPLATE__FULL.format(item.node.workspace_id, item.node.content_id),
  884. children=True if ContentType.Folder==item.node.type and len(item.children)<=0 else context.toDict(item.children),
  885. text=item.node.get_label(),
  886. a_attr={'href': context.url(ContentType.fill_url(item.node)) },
  887. li_attr={'title': item.node.get_label()},
  888. # type='folder',
  889. type=item.node.type,
  890. state={'opened': True if ContentType.Folder==item.node.type and len(item.children)>0 else False, 'selected': item.is_selected}
  891. )
  892. elif isinstance(item.node, Workspace):
  893. return DictLikeClass(
  894. id=CST.TREEVIEW_MENU.ID_TEMPLATE__WORKSPACE_ONLY.format(item.node.workspace_id),
  895. children=True if len(item.children)<=0 else context.toDict(item.children),
  896. text=item.node.label,
  897. a_attr={'href': context.url(ContentType.fill_url_for_workspace(item.node))},
  898. li_attr={'title': item.node.get_label()},
  899. type='workspace',
  900. state={'opened': True if len(item.children)>0 else False, 'selected': item.is_selected}
  901. )
  902. @pod_serializer(Workspace, CTX.API_WORKSPACE)
  903. def serialize_api_workspace(item: Workspace, context: Context):
  904. return DictLikeClass(
  905. id=item.workspace_id,
  906. label=item.label,
  907. description=item.description,
  908. has_calendar=item.calendar_enabled,
  909. )
  910. @pod_serializer(Workspace, CTX.API_CALENDAR_WORKSPACE)
  911. def serialize_api_calendar_workspace(item: Workspace, context: Context):
  912. return DictLikeClass(
  913. id=item.workspace_id,
  914. label=item.label,
  915. description=item.description,
  916. type='workspace',
  917. )
  918. @pod_serializer(User, CTX.API_CALENDAR_USER)
  919. def serialize_api_calendar_workspace(item: User, context: Context):
  920. from tracim.lib.calendar import CalendarManager # Cyclic import
  921. return DictLikeClass(
  922. id=item.user_id,
  923. label=item.display_name,
  924. description=CalendarManager.get_personal_calendar_description(),
  925. type='user',
  926. )