context_models.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. # coding=utf-8
  2. import typing
  3. from datetime import datetime
  4. from enum import Enum
  5. from slugify import slugify
  6. from sqlalchemy.orm import Session
  7. from tracim import CFG
  8. from tracim.models import User
  9. from tracim.models.auth import Profile
  10. from tracim.models.data import Content
  11. from tracim.models.data import ContentRevisionRO
  12. from tracim.models.data import Workspace
  13. from tracim.models.data import UserRoleInWorkspace
  14. from tracim.models.roles import WorkspaceRoles
  15. from tracim.models.workspace_menu_entries import default_workspace_menu_entry
  16. from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
  17. from tracim.models.contents import ContentTypeLegacy as ContentType
  18. class MoveParams(object):
  19. """
  20. Json body params for move action model
  21. """
  22. def __init__(self, new_parent_id: str, new_workspace_id: str = None) -> None: # nopep8
  23. self.new_parent_id = new_parent_id
  24. self.new_workspace_id = new_workspace_id
  25. class LoginCredentials(object):
  26. """
  27. Login credentials model for login model
  28. """
  29. def __init__(self, email: str, password: str) -> None:
  30. self.email = email
  31. self.password = password
  32. class WorkspaceAndContentPath(object):
  33. """
  34. Paths params with workspace id and content_id model
  35. """
  36. def __init__(self, workspace_id: int, content_id: int) -> None:
  37. self.content_id = content_id
  38. self.workspace_id = workspace_id
  39. class WorkspaceAndUserPath(object):
  40. """
  41. Paths params with workspace id and user_id
  42. """
  43. def __init__(self, workspace_id: int, user_id: int):
  44. self.workspace_id = workspace_id
  45. self.user_id = workspace_id
  46. class UserWorkspaceAndContentPath(object):
  47. """
  48. Paths params with user_id, workspace id and content_id model
  49. """
  50. def __init__(self, user_id: int, workspace_id: int, content_id: int) -> None: # nopep8
  51. self.content_id = content_id
  52. self.workspace_id = workspace_id
  53. self.user_id = user_id
  54. class CommentPath(object):
  55. """
  56. Paths params with workspace id and content_id and comment_id model
  57. """
  58. def __init__(
  59. self,
  60. workspace_id: int,
  61. content_id: int,
  62. comment_id: int
  63. ) -> None:
  64. self.content_id = content_id
  65. self.workspace_id = workspace_id
  66. self.comment_id = comment_id
  67. class ContentFilter(object):
  68. """
  69. Content filter model
  70. """
  71. def __init__(
  72. self,
  73. workspace_id: int = None,
  74. parent_id: int = None,
  75. show_archived: int = 0,
  76. show_deleted: int = 0,
  77. show_active: int = 1,
  78. content_type: str = None,
  79. offset: int = None,
  80. limit: int = None,
  81. ) -> None:
  82. self.parent_id = parent_id
  83. self.workspace_id = workspace_id
  84. self.show_archived = bool(show_archived)
  85. self.show_deleted = bool(show_deleted)
  86. self.show_active = bool(show_active)
  87. self.limit = limit
  88. self.offset = offset
  89. self.content_type = content_type
  90. class ActiveContentFilter(object):
  91. def __init__(
  92. self,
  93. limit: int = None,
  94. before_datetime: datetime = None,
  95. ):
  96. self.limit = limit
  97. self.before_datetime = before_datetime
  98. class ContentIdsQuery(object):
  99. def __init__(
  100. self,
  101. contents_ids: typing.List[int] = None,
  102. ):
  103. self.contents_ids = contents_ids
  104. class RoleUpdate(object):
  105. """
  106. Update role
  107. """
  108. def __init__(
  109. self,
  110. role: str,
  111. ):
  112. self.role = role
  113. class WorkspaceMemberInvitation(object):
  114. """
  115. Workspace Member Invitation
  116. """
  117. def __init__(
  118. self,
  119. user_id: int,
  120. user_email_or_public_name: str,
  121. role: str,
  122. ):
  123. self.role = role
  124. self.user_email_or_public_name = user_email_or_public_name
  125. self.user_id = user_id
  126. class WorkspaceUpdate(object):
  127. """
  128. Update workspace
  129. """
  130. def __init__(
  131. self,
  132. label: str,
  133. description: str,
  134. ):
  135. self.label = label
  136. self.description = description
  137. class ContentCreation(object):
  138. """
  139. Content creation model
  140. """
  141. def __init__(
  142. self,
  143. label: str,
  144. content_type: str,
  145. parent_id: typing.Optional[int] = None,
  146. ) -> None:
  147. self.label = label
  148. self.content_type = content_type
  149. self.parent_id = parent_id
  150. class CommentCreation(object):
  151. """
  152. Comment creation model
  153. """
  154. def __init__(
  155. self,
  156. raw_content: str,
  157. ) -> None:
  158. self.raw_content = raw_content
  159. class SetContentStatus(object):
  160. """
  161. Set content status
  162. """
  163. def __init__(
  164. self,
  165. status: str,
  166. ) -> None:
  167. self.status = status
  168. class TextBasedContentUpdate(object):
  169. """
  170. TextBasedContent update model
  171. """
  172. def __init__(
  173. self,
  174. label: str,
  175. raw_content: str,
  176. ) -> None:
  177. self.label = label
  178. self.raw_content = raw_content
  179. class TypeUser(Enum):
  180. """Params used to find user"""
  181. USER_ID = 'found_id'
  182. EMAIL = 'found_email'
  183. PUBLIC_NAME = 'found_public_name'
  184. class UserInContext(object):
  185. """
  186. Interface to get User data and User data related to context.
  187. """
  188. def __init__(self, user: User, dbsession: Session, config: CFG):
  189. self.user = user
  190. self.dbsession = dbsession
  191. self.config = config
  192. # Default
  193. @property
  194. def email(self) -> str:
  195. return self.user.email
  196. @property
  197. def user_id(self) -> int:
  198. return self.user.user_id
  199. @property
  200. def public_name(self) -> str:
  201. return self.display_name
  202. @property
  203. def display_name(self) -> str:
  204. return self.user.display_name
  205. @property
  206. def created(self) -> datetime:
  207. return self.user.created
  208. @property
  209. def is_active(self) -> bool:
  210. return self.user.is_active
  211. @property
  212. def timezone(self) -> str:
  213. return self.user.timezone
  214. @property
  215. def profile(self) -> Profile:
  216. return self.user.profile.name
  217. # Context related
  218. @property
  219. def calendar_url(self) -> typing.Optional[str]:
  220. # TODO - G-M - 20-04-2018 - [Calendar] Replace calendar code to get
  221. # url calendar url.
  222. #
  223. # from tracim.lib.calendar import CalendarManager
  224. # calendar_manager = CalendarManager(None)
  225. # return calendar_manager.get_workspace_calendar_url(self.workspace_id)
  226. return None
  227. @property
  228. def avatar_url(self) -> typing.Optional[str]:
  229. # TODO - G-M - 20-04-2018 - [Avatar] Add user avatar feature
  230. return None
  231. class WorkspaceInContext(object):
  232. """
  233. Interface to get Workspace data and Workspace data related to context.
  234. """
  235. def __init__(self, workspace: Workspace, dbsession: Session, config: CFG):
  236. self.workspace = workspace
  237. self.dbsession = dbsession
  238. self.config = config
  239. @property
  240. def workspace_id(self) -> int:
  241. """
  242. numeric id of the workspace.
  243. """
  244. return self.workspace.workspace_id
  245. @property
  246. def id(self) -> int:
  247. """
  248. alias of workspace_id
  249. """
  250. return self.workspace_id
  251. @property
  252. def label(self) -> str:
  253. """
  254. get workspace label
  255. """
  256. return self.workspace.label
  257. @property
  258. def description(self) -> str:
  259. """
  260. get workspace description
  261. """
  262. return self.workspace.description
  263. @property
  264. def slug(self) -> str:
  265. """
  266. get workspace slug
  267. """
  268. return slugify(self.workspace.label)
  269. @property
  270. def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
  271. """
  272. get sidebar entries, those depends on activated apps.
  273. """
  274. # TODO - G.M - 22-05-2018 - Rework on this in
  275. # order to not use hardcoded list
  276. # list should be able to change (depending on activated/disabled
  277. # apps)
  278. return default_workspace_menu_entry(self.workspace)
  279. class UserRoleWorkspaceInContext(object):
  280. """
  281. Interface to get UserRoleInWorkspace data and related content
  282. """
  283. def __init__(
  284. self,
  285. user_role: UserRoleInWorkspace,
  286. dbsession: Session,
  287. config: CFG,
  288. # Extended params
  289. newly_created: bool = None,
  290. email_sent: bool = None
  291. )-> None:
  292. self.user_role = user_role
  293. self.dbsession = dbsession
  294. self.config = config
  295. # Extended params
  296. self.newly_created = newly_created
  297. self.email_sent = email_sent
  298. @property
  299. def user_id(self) -> int:
  300. """
  301. User who has the role has this id
  302. :return: user id as integer
  303. """
  304. return self.user_role.user_id
  305. @property
  306. def workspace_id(self) -> int:
  307. """
  308. This role apply only on the workspace with this workspace_id
  309. :return: workspace id as integer
  310. """
  311. return self.user_role.workspace_id
  312. # TODO - G.M - 23-05-2018 - Check the API spec for this this !
  313. @property
  314. def role_id(self) -> int:
  315. """
  316. role as int id, each value refer to a different role.
  317. """
  318. return self.user_role.role
  319. @property
  320. def role(self) -> str:
  321. return self.role_slug
  322. @property
  323. def role_slug(self) -> str:
  324. """
  325. simple name of the role of the user.
  326. can be anything from UserRoleInWorkspace SLUG, like
  327. 'not_applicable', 'reader',
  328. 'contributor', 'content-manager', 'workspace-manager'
  329. :return: user workspace role as slug.
  330. """
  331. return WorkspaceRoles.get_role_from_level(self.user_role.role).slug
  332. @property
  333. def is_active(self) -> bool:
  334. return self.user.is_active
  335. @property
  336. def user(self) -> UserInContext:
  337. """
  338. User who has this role, with context data
  339. :return: UserInContext object
  340. """
  341. return UserInContext(
  342. self.user_role.user,
  343. self.dbsession,
  344. self.config
  345. )
  346. @property
  347. def workspace(self) -> WorkspaceInContext:
  348. """
  349. Workspace related to this role, with his context data
  350. :return: WorkspaceInContext object
  351. """
  352. return WorkspaceInContext(
  353. self.user_role.workspace,
  354. self.dbsession,
  355. self.config
  356. )
  357. class ContentInContext(object):
  358. """
  359. Interface to get Content data and Content data related to context.
  360. """
  361. def __init__(self, content: Content, dbsession: Session, config: CFG, user: User=None): # nopep8
  362. self.content = content
  363. self.dbsession = dbsession
  364. self.config = config
  365. self._user = user
  366. # Default
  367. @property
  368. def content_id(self) -> int:
  369. return self.content.content_id
  370. @property
  371. def parent_id(self) -> int:
  372. """
  373. Return parent_id of the content
  374. """
  375. return self.content.parent_id
  376. @property
  377. def workspace_id(self) -> int:
  378. return self.content.workspace_id
  379. @property
  380. def label(self) -> str:
  381. return self.content.label
  382. @property
  383. def content_type(self) -> str:
  384. content_type = ContentType(self.content.type)
  385. return content_type.slug
  386. @property
  387. def sub_content_types(self) -> typing.List[str]:
  388. return [_type.slug for _type in self.content.get_allowed_content_types()] # nopep8
  389. @property
  390. def status(self) -> str:
  391. return self.content.status
  392. @property
  393. def is_archived(self):
  394. return self.content.is_archived
  395. @property
  396. def is_deleted(self):
  397. return self.content.is_deleted
  398. @property
  399. def raw_content(self):
  400. return self.content.description
  401. @property
  402. def author(self):
  403. return UserInContext(
  404. dbsession=self.dbsession,
  405. config=self.config,
  406. user=self.content.first_revision.owner
  407. )
  408. @property
  409. def current_revision_id(self):
  410. return self.content.revision_id
  411. @property
  412. def created(self):
  413. return self.content.created
  414. @property
  415. def modified(self):
  416. return self.updated
  417. @property
  418. def updated(self):
  419. return self.content.updated
  420. @property
  421. def last_modifier(self):
  422. return UserInContext(
  423. dbsession=self.dbsession,
  424. config=self.config,
  425. user=self.content.last_revision.owner
  426. )
  427. # Context-related
  428. @property
  429. def show_in_ui(self):
  430. # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
  431. # if false, then do not show content in the treeview.
  432. # This may his maybe used for specific contents or for sub-contents.
  433. # Default is True.
  434. # In first version of the API, this field is always True
  435. return True
  436. @property
  437. def slug(self):
  438. return slugify(self.content.label)
  439. @property
  440. def read_by_user(self):
  441. assert self._user
  442. return not self.content.has_new_information_for(self._user)
  443. class RevisionInContext(object):
  444. """
  445. Interface to get Content data and Content data related to context.
  446. """
  447. def __init__(self, content_revision: ContentRevisionRO, dbsession: Session, config: CFG):
  448. assert content_revision is not None
  449. self.revision = content_revision
  450. self.dbsession = dbsession
  451. self.config = config
  452. # Default
  453. @property
  454. def content_id(self) -> int:
  455. return self.revision.content_id
  456. @property
  457. def parent_id(self) -> int:
  458. """
  459. Return parent_id of the content
  460. """
  461. return self.revision.parent_id
  462. @property
  463. def workspace_id(self) -> int:
  464. return self.revision.workspace_id
  465. @property
  466. def label(self) -> str:
  467. return self.revision.label
  468. @property
  469. def revision_type(self) -> str:
  470. return self.revision.revision_type
  471. @property
  472. def content_type(self) -> str:
  473. content_type = ContentType(self.revision.type)
  474. if content_type:
  475. return content_type.slug
  476. else:
  477. return None
  478. @property
  479. def sub_content_types(self) -> typing.List[str]:
  480. return [_type.slug for _type
  481. in self.revision.node.get_allowed_content_types()]
  482. @property
  483. def status(self) -> str:
  484. return self.revision.status
  485. @property
  486. def is_archived(self) -> bool:
  487. return self.revision.is_archived
  488. @property
  489. def is_deleted(self) -> bool:
  490. return self.revision.is_deleted
  491. @property
  492. def raw_content(self) -> str:
  493. return self.revision.description
  494. @property
  495. def author(self) -> UserInContext:
  496. return UserInContext(
  497. dbsession=self.dbsession,
  498. config=self.config,
  499. user=self.revision.owner
  500. )
  501. @property
  502. def revision_id(self) -> int:
  503. return self.revision.revision_id
  504. @property
  505. def created(self) -> datetime:
  506. return self.updated
  507. @property
  508. def modified(self) -> datetime:
  509. return self.updated
  510. @property
  511. def updated(self) -> datetime:
  512. return self.revision.updated
  513. @property
  514. def next_revision(self) -> typing.Optional[ContentRevisionRO]:
  515. """
  516. Get next revision (later revision)
  517. :return: next_revision
  518. """
  519. next_revision = None
  520. revisions = self.revision.node.revisions
  521. # INFO - G.M - 2018-06-177 - Get revisions more recent that
  522. # current one
  523. next_revisions = [
  524. revision for revision in revisions
  525. if revision.revision_id > self.revision.revision_id
  526. ]
  527. if next_revisions:
  528. # INFO - G.M - 2018-06-177 -sort revisions by date
  529. sorted_next_revisions = sorted(
  530. next_revisions,
  531. key=lambda revision: revision.updated
  532. )
  533. # INFO - G.M - 2018-06-177 - return only next revision
  534. return sorted_next_revisions[0]
  535. else:
  536. return None
  537. @property
  538. def comment_ids(self) -> typing.List[int]:
  539. """
  540. Get list of ids of all current revision related comments
  541. :return: list of comments ids
  542. """
  543. comments = self.revision.node.get_comments()
  544. # INFO - G.M - 2018-06-177 - Get comments more recent than revision.
  545. revision_comments = [
  546. comment for comment in comments
  547. if comment.created > self.revision.updated
  548. ]
  549. if self.next_revision:
  550. # INFO - G.M - 2018-06-177 - if there is a revision more recent
  551. # than current remove comments from theses rev (comments older
  552. # than next_revision.)
  553. revision_comments = [
  554. comment for comment in revision_comments
  555. if comment.created < self.next_revision.updated
  556. ]
  557. sorted_revision_comments = sorted(
  558. revision_comments,
  559. key=lambda revision: revision.created
  560. )
  561. comment_ids = []
  562. for comment in sorted_revision_comments:
  563. comment_ids.append(comment.content_id)
  564. return comment_ids
  565. # Context-related
  566. @property
  567. def show_in_ui(self) -> bool:
  568. # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
  569. # if false, then do not show content in the treeview.
  570. # This may his maybe used for specific contents or for sub-contents.
  571. # Default is True.
  572. # In first version of the API, this field is always True
  573. return True
  574. @property
  575. def slug(self) -> str:
  576. return slugify(self.revision.label)