context_models.py 22KB

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