context_models.py 21KB

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