context_models.py 22KB

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