context_models.py 22KB

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