context_models.py 21KB

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