workspace_controller.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import typing
  2. import transaction
  3. from pyramid.config import Configurator
  4. from tracim_backend.lib.core.user import UserApi
  5. from tracim_backend.models.roles import WorkspaceRoles
  6. try: # Python 3.5+
  7. from http import HTTPStatus
  8. except ImportError:
  9. from http import client as HTTPStatus
  10. from tracim_backend import hapic
  11. from tracim_backend import TracimRequest
  12. from tracim_backend.lib.core.workspace import WorkspaceApi
  13. from tracim_backend.lib.core.content import ContentApi
  14. from tracim_backend.lib.core.userworkspace import RoleApi
  15. from tracim_backend.lib.utils.authorization import require_workspace_role
  16. from tracim_backend.lib.utils.authorization import require_profile
  17. from tracim_backend.models import Group
  18. from tracim_backend.lib.utils.authorization import require_candidate_workspace_role
  19. from tracim_backend.models.data import UserRoleInWorkspace
  20. from tracim_backend.models.data import ActionDescription
  21. from tracim_backend.models.context_models import UserRoleWorkspaceInContext
  22. from tracim_backend.models.context_models import ContentInContext
  23. from tracim_backend.exceptions import EmptyLabelNotAllowed
  24. from tracim_backend.exceptions import EmailValidationFailed
  25. from tracim_backend.exceptions import UserCreationFailed
  26. from tracim_backend.exceptions import UserDoesNotExist
  27. from tracim_backend.exceptions import ContentNotFound
  28. from tracim_backend.exceptions import WorkspacesDoNotMatch
  29. from tracim_backend.exceptions import ParentNotFound
  30. from tracim_backend.views.controllers import Controller
  31. from tracim_backend.views.core_api.schemas import FilterContentQuerySchema
  32. from tracim_backend.views.core_api.schemas import WorkspaceMemberCreationSchema
  33. from tracim_backend.views.core_api.schemas import WorkspaceMemberInviteSchema
  34. from tracim_backend.views.core_api.schemas import RoleUpdateSchema
  35. from tracim_backend.views.core_api.schemas import WorkspaceCreationSchema
  36. from tracim_backend.views.core_api.schemas import WorkspaceModifySchema
  37. from tracim_backend.views.core_api.schemas import WorkspaceAndUserIdPathSchema
  38. from tracim_backend.views.core_api.schemas import ContentMoveSchema
  39. from tracim_backend.views.core_api.schemas import NoContentSchema
  40. from tracim_backend.views.core_api.schemas import ContentCreationSchema
  41. from tracim_backend.views.core_api.schemas import WorkspaceAndContentIdPathSchema
  42. from tracim_backend.views.core_api.schemas import ContentDigestSchema
  43. from tracim_backend.views.core_api.schemas import WorkspaceSchema
  44. from tracim_backend.views.core_api.schemas import WorkspaceIdPathSchema
  45. from tracim_backend.views.core_api.schemas import WorkspaceMemberSchema
  46. from tracim_backend.models.contents import ContentTypeLegacy as ContentType
  47. from tracim_backend.models.revision_protection import new_revision
  48. SWAGGER_TAG_WORKSPACE_ENDPOINTS = 'Workspaces'
  49. class WorkspaceController(Controller):
  50. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  51. @require_workspace_role(UserRoleInWorkspace.READER)
  52. @hapic.input_path(WorkspaceIdPathSchema())
  53. @hapic.output_body(WorkspaceSchema())
  54. def workspace(self, context, request: TracimRequest, hapic_data=None):
  55. """
  56. Get workspace informations
  57. """
  58. app_config = request.registry.settings['CFG']
  59. wapi = WorkspaceApi(
  60. current_user=request.current_user, # User
  61. session=request.dbsession,
  62. config=app_config,
  63. )
  64. return wapi.get_workspace_with_context(request.current_workspace)
  65. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  66. @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
  67. @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
  68. @hapic.input_path(WorkspaceIdPathSchema())
  69. @hapic.input_body(WorkspaceModifySchema())
  70. @hapic.output_body(WorkspaceSchema())
  71. def update_workspace(self, context, request: TracimRequest, hapic_data=None): # nopep8
  72. """
  73. Update workspace informations
  74. """
  75. app_config = request.registry.settings['CFG']
  76. wapi = WorkspaceApi(
  77. current_user=request.current_user, # User
  78. session=request.dbsession,
  79. config=app_config,
  80. )
  81. wapi.update_workspace(
  82. request.current_workspace,
  83. label=hapic_data.body.label,
  84. description=hapic_data.body.description,
  85. save_now=True,
  86. )
  87. return wapi.get_workspace_with_context(request.current_workspace)
  88. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  89. @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
  90. @require_profile(Group.TIM_MANAGER)
  91. @hapic.input_body(WorkspaceCreationSchema())
  92. @hapic.output_body(WorkspaceSchema())
  93. def create_workspace(self, context, request: TracimRequest, hapic_data=None): # nopep8
  94. """
  95. create workspace
  96. """
  97. app_config = request.registry.settings['CFG']
  98. wapi = WorkspaceApi(
  99. current_user=request.current_user, # User
  100. session=request.dbsession,
  101. config=app_config,
  102. )
  103. workspace = wapi.create_workspace(
  104. label=hapic_data.body.label,
  105. description=hapic_data.body.description,
  106. save_now=True,
  107. )
  108. return wapi.get_workspace_with_context(workspace)
  109. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  110. @require_workspace_role(UserRoleInWorkspace.READER)
  111. @hapic.input_path(WorkspaceIdPathSchema())
  112. @hapic.output_body(WorkspaceMemberSchema(many=True))
  113. def workspaces_members(
  114. self,
  115. context,
  116. request: TracimRequest,
  117. hapic_data=None
  118. ) -> typing.List[UserRoleWorkspaceInContext]:
  119. """
  120. Get Members of this workspace
  121. """
  122. app_config = request.registry.settings['CFG']
  123. rapi = RoleApi(
  124. current_user=request.current_user,
  125. session=request.dbsession,
  126. config=app_config,
  127. )
  128. roles = rapi.get_all_for_workspace(request.current_workspace)
  129. return [
  130. rapi.get_user_role_workspace_with_context(user_role)
  131. for user_role in roles
  132. ]
  133. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  134. @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
  135. @hapic.input_path(WorkspaceAndUserIdPathSchema())
  136. @hapic.input_body(RoleUpdateSchema())
  137. @hapic.output_body(WorkspaceMemberSchema())
  138. def update_workspaces_members_role(
  139. self,
  140. context,
  141. request: TracimRequest,
  142. hapic_data=None
  143. ) -> UserRoleWorkspaceInContext:
  144. """
  145. Update Members to this workspace
  146. """
  147. app_config = request.registry.settings['CFG']
  148. rapi = RoleApi(
  149. current_user=request.current_user,
  150. session=request.dbsession,
  151. config=app_config,
  152. )
  153. role = rapi.get_one(
  154. user_id=hapic_data.path.user_id,
  155. workspace_id=hapic_data.path.workspace_id,
  156. )
  157. workspace_role = WorkspaceRoles.get_role_from_slug(hapic_data.body.role)
  158. role = rapi.update_role(
  159. role,
  160. role_level=workspace_role.level
  161. )
  162. return rapi.get_user_role_workspace_with_context(role)
  163. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  164. @hapic.handle_exception(UserCreationFailed, HTTPStatus.BAD_REQUEST)
  165. @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
  166. @hapic.input_path(WorkspaceIdPathSchema())
  167. @hapic.input_body(WorkspaceMemberInviteSchema())
  168. @hapic.output_body(WorkspaceMemberCreationSchema())
  169. def create_workspaces_members_role(
  170. self,
  171. context,
  172. request: TracimRequest,
  173. hapic_data=None
  174. ) -> UserRoleWorkspaceInContext:
  175. """
  176. Add Members to this workspace
  177. """
  178. newly_created = False
  179. email_sent = False
  180. app_config = request.registry.settings['CFG']
  181. rapi = RoleApi(
  182. current_user=request.current_user,
  183. session=request.dbsession,
  184. config=app_config,
  185. )
  186. uapi = UserApi(
  187. current_user=request.current_user,
  188. session=request.dbsession,
  189. config=app_config,
  190. )
  191. try:
  192. _, user = uapi.find(
  193. user_id=hapic_data.body.user_id,
  194. email=hapic_data.body.user_email_or_public_name,
  195. public_name=hapic_data.body.user_email_or_public_name
  196. )
  197. except UserDoesNotExist:
  198. try:
  199. # TODO - G.M - 2018-07-05 - [UserCreation] Reenable email
  200. # notification for creation
  201. user = uapi.create_user(
  202. hapic_data.body.user_email_or_public_name,
  203. do_notify=False
  204. ) # nopep8
  205. newly_created = True
  206. except EmailValidationFailed:
  207. raise UserCreationFailed('no valid mail given')
  208. role = rapi.create_one(
  209. user=user,
  210. workspace=request.current_workspace,
  211. role_level=WorkspaceRoles.get_role_from_slug(hapic_data.body.role).level, # nopep8
  212. with_notif=False,
  213. flush=True,
  214. )
  215. return rapi.get_user_role_workspace_with_context(
  216. role,
  217. newly_created=newly_created,
  218. email_sent=email_sent,
  219. )
  220. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  221. @require_workspace_role(UserRoleInWorkspace.READER)
  222. @hapic.input_path(WorkspaceIdPathSchema())
  223. @hapic.input_query(FilterContentQuerySchema())
  224. @hapic.output_body(ContentDigestSchema(many=True))
  225. def workspace_content(
  226. self,
  227. context,
  228. request: TracimRequest,
  229. hapic_data=None,
  230. ) -> typing.List[ContentInContext]:
  231. """
  232. return list of contents found in the workspace
  233. """
  234. app_config = request.registry.settings['CFG']
  235. content_filter = hapic_data.query
  236. api = ContentApi(
  237. current_user=request.current_user,
  238. session=request.dbsession,
  239. config=app_config,
  240. show_archived=content_filter.show_archived,
  241. show_deleted=content_filter.show_deleted,
  242. show_active=content_filter.show_active,
  243. )
  244. contents = api.get_all(
  245. parent_id=content_filter.parent_id,
  246. workspace=request.current_workspace,
  247. content_type=content_filter.content_type or ContentType.Any,
  248. )
  249. contents = [
  250. api.get_content_in_context(content) for content in contents
  251. ]
  252. return contents
  253. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  254. @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
  255. @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
  256. @hapic.input_path(WorkspaceIdPathSchema())
  257. @hapic.input_body(ContentCreationSchema())
  258. @hapic.output_body(ContentDigestSchema())
  259. def create_generic_empty_content(
  260. self,
  261. context,
  262. request: TracimRequest,
  263. hapic_data=None,
  264. ) -> ContentInContext:
  265. """
  266. create a generic empty content
  267. """
  268. app_config = request.registry.settings['CFG']
  269. creation_data = hapic_data.body
  270. api = ContentApi(
  271. current_user=request.current_user,
  272. session=request.dbsession,
  273. config=app_config
  274. )
  275. parent = None
  276. if creation_data.parent_id:
  277. try:
  278. parent = api.get_one(content_id=creation_data.parent_id, content_type=ContentType.Any) # nopep8
  279. except ContentNotFound as exc:
  280. raise ParentNotFound(
  281. 'Parent with content_id {} not found'.format(creation_data.parent_id)
  282. ) from exc
  283. content = api.create(
  284. label=creation_data.label,
  285. content_type=creation_data.content_type,
  286. workspace=request.current_workspace,
  287. parent=parent,
  288. )
  289. api.save(content, ActionDescription.CREATION)
  290. content = api.get_content_in_context(content)
  291. return content
  292. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  293. @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
  294. @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
  295. @require_candidate_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
  296. @hapic.input_path(WorkspaceAndContentIdPathSchema())
  297. @hapic.input_body(ContentMoveSchema())
  298. @hapic.output_body(ContentDigestSchema())
  299. def move_content(
  300. self,
  301. context,
  302. request: TracimRequest,
  303. hapic_data=None,
  304. ) -> ContentInContext:
  305. """
  306. move a content
  307. """
  308. app_config = request.registry.settings['CFG']
  309. path_data = hapic_data.path
  310. move_data = hapic_data.body
  311. api = ContentApi(
  312. current_user=request.current_user,
  313. session=request.dbsession,
  314. config=app_config,
  315. )
  316. content = api.get_one(
  317. path_data.content_id,
  318. content_type=ContentType.Any
  319. )
  320. new_parent = api.get_one(
  321. move_data.new_parent_id, content_type=ContentType.Any
  322. )
  323. new_workspace = request.candidate_workspace
  324. with new_revision(
  325. session=request.dbsession,
  326. tm=transaction.manager,
  327. content=content
  328. ):
  329. api.move(
  330. content,
  331. new_parent=new_parent,
  332. new_workspace=new_workspace,
  333. must_stay_in_same_workspace=False,
  334. )
  335. updated_content = api.get_one(
  336. path_data.content_id,
  337. content_type=ContentType.Any
  338. )
  339. return api.get_content_in_context(updated_content)
  340. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  341. @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
  342. @hapic.input_path(WorkspaceAndContentIdPathSchema())
  343. @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
  344. def delete_content(
  345. self,
  346. context,
  347. request: TracimRequest,
  348. hapic_data=None,
  349. ) -> None:
  350. """
  351. delete a content
  352. """
  353. app_config = request.registry.settings['CFG']
  354. path_data = hapic_data.path
  355. api = ContentApi(
  356. current_user=request.current_user,
  357. session=request.dbsession,
  358. config=app_config,
  359. )
  360. content = api.get_one(
  361. path_data.content_id,
  362. content_type=ContentType.Any
  363. )
  364. with new_revision(
  365. session=request.dbsession,
  366. tm=transaction.manager,
  367. content=content
  368. ):
  369. api.delete(content)
  370. return
  371. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  372. @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
  373. @hapic.input_path(WorkspaceAndContentIdPathSchema())
  374. @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
  375. def undelete_content(
  376. self,
  377. context,
  378. request: TracimRequest,
  379. hapic_data=None,
  380. ) -> None:
  381. """
  382. undelete a content
  383. """
  384. app_config = request.registry.settings['CFG']
  385. path_data = hapic_data.path
  386. api = ContentApi(
  387. current_user=request.current_user,
  388. session=request.dbsession,
  389. config=app_config,
  390. show_deleted=True,
  391. )
  392. content = api.get_one(
  393. path_data.content_id,
  394. content_type=ContentType.Any
  395. )
  396. with new_revision(
  397. session=request.dbsession,
  398. tm=transaction.manager,
  399. content=content
  400. ):
  401. api.undelete(content)
  402. return
  403. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  404. @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
  405. @hapic.input_path(WorkspaceAndContentIdPathSchema())
  406. @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
  407. def archive_content(
  408. self,
  409. context,
  410. request: TracimRequest,
  411. hapic_data=None,
  412. ) -> None:
  413. """
  414. archive a content
  415. """
  416. app_config = request.registry.settings['CFG']
  417. path_data = hapic_data.path
  418. api = ContentApi(
  419. current_user=request.current_user,
  420. session=request.dbsession,
  421. config=app_config,
  422. )
  423. content = api.get_one(path_data.content_id, content_type=ContentType.Any) # nopep8
  424. with new_revision(
  425. session=request.dbsession,
  426. tm=transaction.manager,
  427. content=content
  428. ):
  429. api.archive(content)
  430. return
  431. @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
  432. @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
  433. @hapic.input_path(WorkspaceAndContentIdPathSchema())
  434. @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
  435. def unarchive_content(
  436. self,
  437. context,
  438. request: TracimRequest,
  439. hapic_data=None,
  440. ) -> None:
  441. """
  442. unarchive a content
  443. """
  444. app_config = request.registry.settings['CFG']
  445. path_data = hapic_data.path
  446. api = ContentApi(
  447. current_user=request.current_user,
  448. session=request.dbsession,
  449. config=app_config,
  450. show_archived=True,
  451. )
  452. content = api.get_one(
  453. path_data.content_id,
  454. content_type=ContentType.Any
  455. )
  456. with new_revision(
  457. session=request.dbsession,
  458. tm=transaction.manager,
  459. content=content
  460. ):
  461. api.unarchive(content)
  462. return
  463. def bind(self, configurator: Configurator) -> None:
  464. """
  465. Create all routes and views using
  466. pyramid configurator for this controller
  467. """
  468. # Workspace
  469. configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET') # nopep8
  470. configurator.add_view(self.workspace, route_name='workspace')
  471. # Create workspace
  472. configurator.add_route('create_workspace', '/workspaces', request_method='POST') # nopep8
  473. configurator.add_view(self.create_workspace, route_name='create_workspace') # nopep8
  474. # Update Workspace
  475. configurator.add_route('update_workspace', '/workspaces/{workspace_id}', request_method='PUT') # nopep8
  476. configurator.add_view(self.update_workspace, route_name='update_workspace') # nopep8
  477. # Workspace Members (Roles)
  478. configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET') # nopep8
  479. configurator.add_view(self.workspaces_members, route_name='workspace_members') # nopep8
  480. # Update Workspace Members roles
  481. configurator.add_route('update_workspace_member', '/workspaces/{workspace_id}/members/{user_id}', request_method='PUT') # nopep8
  482. configurator.add_view(self.update_workspaces_members_role, route_name='update_workspace_member') # nopep8
  483. # Create Workspace Members roles
  484. configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST') # nopep8
  485. configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member') # nopep8
  486. # Workspace Content
  487. configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET') # nopep8
  488. configurator.add_view(self.workspace_content, route_name='workspace_content') # nopep8
  489. # Create Generic Content
  490. configurator.add_route('create_generic_content', '/workspaces/{workspace_id}/contents', request_method='POST') # nopep8
  491. configurator.add_view(self.create_generic_empty_content, route_name='create_generic_content') # nopep8
  492. # Move Content
  493. configurator.add_route('move_content', '/workspaces/{workspace_id}/contents/{content_id}/move', request_method='PUT') # nopep8
  494. configurator.add_view(self.move_content, route_name='move_content') # nopep8
  495. # Delete/Undelete Content
  496. configurator.add_route('delete_content', '/workspaces/{workspace_id}/contents/{content_id}/delete', request_method='PUT') # nopep8
  497. configurator.add_view(self.delete_content, route_name='delete_content') # nopep8
  498. configurator.add_route('undelete_content', '/workspaces/{workspace_id}/contents/{content_id}/undelete', request_method='PUT') # nopep8
  499. configurator.add_view(self.undelete_content, route_name='undelete_content') # nopep8
  500. # # Archive/Unarchive Content
  501. configurator.add_route('archive_content', '/workspaces/{workspace_id}/contents/{content_id}/archive', request_method='PUT') # nopep8
  502. configurator.add_view(self.archive_content, route_name='archive_content') # nopep8
  503. configurator.add_route('unarchive_content', '/workspaces/{workspace_id}/contents/{content_id}/unarchive', request_method='PUT') # nopep8
  504. configurator.add_view(self.unarchive_content, route_name='unarchive_content') # nopep8