123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- from pyramid.config import Configurator
- from tracim_backend.lib.core.userworkspace import RoleApi
- from tracim_backend.lib.utils.utils import password_generator
-
- try: # Python 3.5+
- from http import HTTPStatus
- except ImportError:
- from http import client as HTTPStatus
-
- from tracim_backend import hapic
- from tracim_backend.lib.utils.request import TracimRequest
- from tracim_backend.models import Group
- from tracim_backend.lib.core.group import GroupApi
- from tracim_backend.lib.core.user import UserApi
- from tracim_backend.lib.core.workspace import WorkspaceApi
- from tracim_backend.lib.core.content import ContentApi
- from tracim_backend.views.controllers import Controller
- from tracim_backend.lib.utils.authorization import require_same_user_or_profile
- from tracim_backend.lib.utils.authorization import require_profile
- from tracim_backend.exceptions import WrongUserPassword
- from tracim_backend.exceptions import EmailAlreadyExistInDb
- from tracim_backend.exceptions import PasswordDoNotMatch
- from tracim_backend.views.core_api.schemas import UserSchema
- from tracim_backend.views.core_api.schemas import AutocompleteQuerySchema
- from tracim_backend.views.core_api.schemas import UserDigestSchema
- from tracim_backend.views.core_api.schemas import SetEmailSchema
- from tracim_backend.views.core_api.schemas import SetPasswordSchema
- from tracim_backend.views.core_api.schemas import UserInfosSchema
- from tracim_backend.views.core_api.schemas import UserCreationSchema
- from tracim_backend.views.core_api.schemas import UserProfileSchema
- from tracim_backend.views.core_api.schemas import UserIdPathSchema
- from tracim_backend.views.core_api.schemas import ReadStatusSchema
- from tracim_backend.views.core_api.schemas import ContentIdsQuerySchema
- from tracim_backend.views.core_api.schemas import NoContentSchema
- from tracim_backend.views.core_api.schemas import UserWorkspaceIdPathSchema
- from tracim_backend.views.core_api.schemas import UserWorkspaceAndContentIdPathSchema
- from tracim_backend.views.core_api.schemas import ContentDigestSchema
- from tracim_backend.views.core_api.schemas import ActiveContentFilterQuerySchema
- from tracim_backend.views.core_api.schemas import WorkspaceDigestSchema
- from tracim_backend.app_models.contents import CONTENT_TYPES
-
- SWAGGER_TAG__USER_ENDPOINTS = 'Users'
-
-
- class UserController(Controller):
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(WorkspaceDigestSchema(many=True),)
- def user_workspace(self, context, request: TracimRequest, hapic_data=None):
- """
- Get list of user workspaces
- """
- app_config = request.registry.settings['CFG']
- wapi = WorkspaceApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
-
- workspaces = wapi.get_all_for_user(request.candidate_user)
- return [
- wapi.get_workspace_with_context(workspace)
- for workspace in workspaces
- ]
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(UserSchema())
- def user(self, context, request: TracimRequest, hapic_data=None):
- """
- Get user infos.
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- return uapi.get_user_with_context(request.candidate_user)
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_profile(Group.TIM_ADMIN)
- @hapic.output_body(UserDigestSchema(many=True))
- def users(self, context, request: TracimRequest, hapic_data=None):
- """
- Get all users
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- users = uapi.get_all()
- context_users = [
- uapi.get_user_with_context(user) for user in users
- ]
- return context_users
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_MANAGER)
- @hapic.input_path(UserIdPathSchema())
- @hapic.input_query(AutocompleteQuerySchema())
- @hapic.output_body(UserDigestSchema(many=True))
- def known_members(self, context, request: TracimRequest, hapic_data=None):
- """
- Get known users list
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- users = uapi.get_known_user(acp=hapic_data.query.acp)
- context_users = [
- uapi.get_user_with_context(user) for user in users
- ]
- return context_users
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
- @hapic.handle_exception(EmailAlreadyExistInDb, HTTPStatus.BAD_REQUEST)
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_body(SetEmailSchema())
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(UserSchema())
- def set_user_email(self, context, request: TracimRequest, hapic_data=None):
- """
- Set user Email
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- user = uapi.set_email(
- request.candidate_user,
- hapic_data.body.loggedin_user_password,
- hapic_data.body.email,
- do_save=True
- )
- return uapi.get_user_with_context(user)
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
- @hapic.handle_exception(PasswordDoNotMatch, HTTPStatus.BAD_REQUEST)
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_body(SetPasswordSchema())
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def set_user_password(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- Set user password
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- uapi.set_password(
- request.candidate_user,
- hapic_data.body.loggedin_user_password,
- hapic_data.body.new_password,
- hapic_data.body.new_password2,
- do_save=True
- )
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_body(UserInfosSchema())
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(UserSchema())
- def set_user_infos(self, context, request: TracimRequest, hapic_data=None):
- """
- Set user info data
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- user = uapi.update(
- request.candidate_user,
- name=hapic_data.body.public_name,
- timezone=hapic_data.body.timezone,
- lang=hapic_data.body.lang,
- do_save=True
- )
- return uapi.get_user_with_context(user)
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @hapic.handle_exception(EmailAlreadyExistInDb, HTTPStatus.BAD_REQUEST)
- @require_profile(Group.TIM_ADMIN)
- @hapic.input_body(UserCreationSchema())
- @hapic.output_body(UserSchema())
- def create_user(self, context, request: TracimRequest, hapic_data=None):
- """
- Create new user
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- gapi = GroupApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- groups = [gapi.get_one_with_name(hapic_data.body.profile)]
- user = uapi.create_user(
- email=hapic_data.body.email,
- password=hapic_data.body.password,
- timezone=hapic_data.body.timezone,
- lang=hapic_data.body.lang,
- name=hapic_data.body.public_name,
- do_notify=hapic_data.body.email_notification,
- groups=groups,
- do_save=True
- )
- return uapi.get_user_with_context(user)
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def enable_user(self, context, request: TracimRequest, hapic_data=None):
- """
- enable user
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- uapi.enable(user=request.candidate_user, do_save=True)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def delete_user(self, context, request: TracimRequest, hapic_data=None):
- """
- delete user
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- uapi.delete(user=request.candidate_user, do_save=True)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def undelete_user(self, context, request: TracimRequest, hapic_data=None):
- """
- undelete user
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- show_deleted=True,
- )
- uapi.undelete(user=request.candidate_user, do_save=True)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def disable_user(self, context, request: TracimRequest, hapic_data=None):
- """
- disable user
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- uapi.disable(user=request.candidate_user, do_save=True)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserIdPathSchema())
- @hapic.input_body(UserProfileSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def set_profile(self, context, request: TracimRequest, hapic_data=None):
- """
- set user profile
- """
- app_config = request.registry.settings['CFG']
- uapi = UserApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- gapi = GroupApi(
- current_user=request.current_user, # User
- session=request.dbsession,
- config=app_config,
- )
- groups = [gapi.get_one_with_name(hapic_data.body.profile)]
- uapi.update(
- user=request.candidate_user,
- groups=groups,
- do_save=True,
- )
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceIdPathSchema())
- @hapic.input_query(ActiveContentFilterQuerySchema())
- @hapic.output_body(ContentDigestSchema(many=True))
- def last_active_content(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- Get last_active_content for user
- """
- app_config = request.registry.settings['CFG']
- content_filter = hapic_data.query
- api = ContentApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- wapi = WorkspaceApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- workspace = None
- if hapic_data.path.workspace_id:
- workspace = wapi.get_one(hapic_data.path.workspace_id)
- before_content = None
- if content_filter.before_content_id:
- before_content = api.get_one(
- content_id=content_filter.before_content_id,
- workspace=workspace,
- content_type=CONTENT_TYPES.Any_SLUG
- )
- last_actives = api.get_last_active(
- workspace=workspace,
- limit=content_filter.limit or None,
- before_content=before_content,
- )
- return [
- api.get_content_in_context(content)
- for content in last_actives
- ]
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceIdPathSchema())
- @hapic.input_query(ContentIdsQuerySchema(), as_list=['contents_ids'])
- @hapic.output_body(ReadStatusSchema(many=True)) # nopep8
- def contents_read_status(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- get user_read status of contents
- """
- app_config = request.registry.settings['CFG']
- content_filter = hapic_data.query
- api = ContentApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- wapi = WorkspaceApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- workspace = None
- if hapic_data.path.workspace_id:
- workspace = wapi.get_one(hapic_data.path.workspace_id)
- last_actives = api.get_last_active(
- workspace=workspace,
- limit=None,
- before_content=None,
- content_ids=hapic_data.query.contents_ids or None
- )
- return [
- api.get_content_in_context(content)
- for content in last_actives
- ]
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceAndContentIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def set_content_as_read(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- set user_read status of content to read
- """
- app_config = request.registry.settings['CFG']
- api = ContentApi(
- show_archived=True,
- show_deleted=True,
- current_user=request.candidate_user,
- session=request.dbsession,
- config=app_config,
- )
- api.mark_read(request.current_content, do_flush=True)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceAndContentIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def set_content_as_unread(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- set user_read status of content to unread
- """
- app_config = request.registry.settings['CFG']
- api = ContentApi(
- show_archived=True,
- show_deleted=True,
- current_user=request.candidate_user,
- session=request.dbsession,
- config=app_config,
- )
- api.mark_unread(request.current_content, do_flush=True)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def set_workspace_as_read(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- set user_read status of all content of workspace to read
- """
- app_config = request.registry.settings['CFG']
- api = ContentApi(
- show_archived=True,
- show_deleted=True,
- current_user=request.candidate_user,
- session=request.dbsession,
- config=app_config,
- )
- api.mark_read__workspace(request.current_workspace)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def enable_workspace_notification(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- enable workspace notification
- """
- app_config = request.registry.settings['CFG']
- api = ContentApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- wapi = WorkspaceApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- workspace = wapi.get_one(hapic_data.path.workspace_id)
- wapi.enable_notifications(request.candidate_user, workspace)
- rapi = RoleApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- role = rapi.get_one(request.candidate_user.user_id, workspace.workspace_id)
- wapi.save(workspace)
- return
-
- @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
- @require_same_user_or_profile(Group.TIM_ADMIN)
- @hapic.input_path(UserWorkspaceIdPathSchema())
- @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT) # nopep8
- def disable_workspace_notification(self, context, request: TracimRequest, hapic_data=None): # nopep8
- """
- disable workspace notification
- """
- app_config = request.registry.settings['CFG']
- api = ContentApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- wapi = WorkspaceApi(
- current_user=request.candidate_user, # User
- session=request.dbsession,
- config=app_config,
- )
- workspace = wapi.get_one(hapic_data.path.workspace_id)
- wapi.disable_notifications(request.candidate_user, workspace)
- wapi.save(workspace)
- return
-
- def bind(self, configurator: Configurator) -> None:
- """
- Create all routes and views using pyramid configurator
- for this controller
- """
-
- # user workspace
- configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET') # nopep8
- configurator.add_view(self.user_workspace, route_name='user_workspace')
-
- # user info
- configurator.add_route('user', '/users/{user_id}', request_method='GET') # nopep8
- configurator.add_view(self.user, route_name='user')
-
- # users lists
- configurator.add_route('users', '/users', request_method='GET') # nopep8
- configurator.add_view(self.users, route_name='users')
-
- # known members lists
- configurator.add_route('known_members', '/users/{user_id}/known_members', request_method='GET') # nopep8
- configurator.add_view(self.known_members, route_name='known_members')
-
- # set user email
- configurator.add_route('set_user_email', '/users/{user_id}/email', request_method='PUT') # nopep8
- configurator.add_view(self.set_user_email, route_name='set_user_email')
-
- # set user password
- configurator.add_route('set_user_password', '/users/{user_id}/password', request_method='PUT') # nopep8
- configurator.add_view(self.set_user_password, route_name='set_user_password') # nopep8
-
- # set user_info
- configurator.add_route('set_user_info', '/users/{user_id}', request_method='PUT') # nopep8
- configurator.add_view(self.set_user_infos, route_name='set_user_info')
-
- # create user
- configurator.add_route('create_user', '/users', request_method='POST')
- configurator.add_view(self.create_user, route_name='create_user')
-
- # enable user
- configurator.add_route('enable_user', '/users/{user_id}/enable', request_method='PUT') # nopep8
- configurator.add_view(self.enable_user, route_name='enable_user')
-
- # disable user
- configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT') # nopep8
- configurator.add_view(self.disable_user, route_name='disable_user')
-
- # delete user
- configurator.add_route('delete_user', '/users/{user_id}/delete', request_method='PUT') # nopep8
- configurator.add_view(self.delete_user, route_name='delete_user')
-
- # undelete user
- configurator.add_route('undelete_user', '/users/{user_id}/undelete', request_method='PUT') # nopep8
- configurator.add_view(self.undelete_user, route_name='undelete_user')
-
- # set user profile
- configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT') # nopep8
- configurator.add_view(self.set_profile, route_name='set_user_profile')
-
- # user content
- configurator.add_route('contents_read_status', '/users/{user_id}/workspaces/{workspace_id}/contents/read_status', request_method='GET') # nopep8
- configurator.add_view(self.contents_read_status, route_name='contents_read_status') # nopep8
- # last active content for user
- configurator.add_route('last_active_content', '/users/{user_id}/workspaces/{workspace_id}/contents/recently_active', request_method='GET') # nopep8
- configurator.add_view(self.last_active_content, route_name='last_active_content') # nopep8
-
- # set content as read/unread
- configurator.add_route('read_content', '/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/read', request_method='PUT') # nopep8
- configurator.add_view(self.set_content_as_read, route_name='read_content') # nopep8
- configurator.add_route('unread_content', '/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/unread', request_method='PUT') # nopep8
- configurator.add_view(self.set_content_as_unread, route_name='unread_content') # nopep8
-
- # set workspace as read
- configurator.add_route('read_workspace', '/users/{user_id}/workspaces/{workspace_id}/read', request_method='PUT') # nopep8
- configurator.add_view(self.set_workspace_as_read, route_name='read_workspace') # nopep8
-
- # enable workspace notification
- configurator.add_route('enable_workspace_notification', '/users/{user_id}/workspaces/{workspace_id}/notify', request_method='PUT') # nopep8
- configurator.add_view(self.enable_workspace_notification, route_name='enable_workspace_notification') # nopep8
-
- # enable workspace notification
- configurator.add_route('disable_workspace_notification', '/users/{user_id}/workspaces/{workspace_id}/unnotify', request_method='PUT') # nopep8
- configurator.add_view(self.disable_workspace_notification, route_name='disable_workspace_notification') # nopep8
|