context_models.py 22KB

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