workspace_controller.py 22KB

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