Browse Source

fix import issue with merging

Guénaël Muller 6 years ago
parent
commit
e0c2ef6edf
72 changed files with 3654 additions and 494 deletions
  1. 1 0
      .travis.yml
  2. 57 7
      backend/README.md
  3. 23 0
      backend/doc/known_issues.md
  4. 4 0
      backend/doc/migration.md
  5. 4 2
      backend/doc/roles.md
  6. 4 0
      backend/tracim_backend/__init__.py
  7. 2 0
      backend/tracim_backend/exceptions.py
  8. 78 15
      backend/tracim_backend/lib/core/content.py
  9. 16 1
      backend/tracim_backend/lib/core/user.py
  10. 13 12
      backend/tracim_backend/lib/core/workspace.py
  11. 34 0
      backend/tracim_backend/lib/utils/authorization.py
  12. 5 3
      backend/tracim_backend/lib/utils/request.py
  13. 0 8
      backend/tracim_backend/lib/webdav/resources.py
  14. 26 0
      backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py
  15. 2 0
      backend/tracim_backend/models/auth.py
  16. 22 0
      backend/tracim_backend/models/contents.py
  17. 37 11
      backend/tracim_backend/models/context_models.py
  18. 24 43
      backend/tracim_backend/models/data.py
  19. 761 0
      backend/tracim_backend/tests/functional/test_contents.py
  20. 52 2
      backend/tracim_backend/tests/functional/test_user.py
  21. 816 22
      backend/tracim_backend/tests/functional/test_workspaces.py
  22. 401 0
      backend/tracim_backend/tests/library/test_content_api.py
  23. 198 0
      backend/tracim_backend/views/contents_api/folder_controller.py
  24. 24 1
      backend/tracim_backend/views/core_api/schemas.py
  25. 43 0
      backend/tracim_backend/views/core_api/user_controller.py
  26. 146 0
      backend/tracim_backend/views/core_api/workspace_controller.py
  27. 52 0
      backend_lib.sh
  28. 32 0
      frontend/src/action-creator.async.js
  29. 5 0
      frontend/src/action-creator.sync.js
  30. 3 3
      frontend/src/component/Account/Password.jsx
  31. 3 3
      frontend/src/component/Account/PersonalData.jsx
  32. 2 2
      frontend/src/component/Account/UserInfo.jsx
  33. 1 5
      frontend/src/component/Dashboard/ContentTypeBtn.styl
  34. 3 2
      frontend/src/component/Dashboard/MemberList.jsx
  35. 18 4
      frontend/src/component/Dashboard/MemberList.styl
  36. 1 1
      frontend/src/component/Dashboard/MoreInfo.jsx
  37. 19 3
      frontend/src/component/Dashboard/RecentActivity.styl
  38. 2 2
      frontend/src/component/Dashboard/UserStatus.jsx
  39. 41 2
      frontend/src/component/Dashboard/UserStatus.styl
  40. 29 0
      frontend/src/component/Header/MenuActionListItem/AdminLink.jsx
  41. 26 23
      frontend/src/component/Header/MenuActionListItem/MenuProfil.jsx
  42. 1 1
      frontend/src/component/Header/MenuActionListItem/Search.jsx
  43. 1 1
      frontend/src/container/Account.jsx
  44. 158 163
      frontend/src/container/AdminWorkspacePage.jsx
  45. 9 5
      frontend/src/container/AppFullscreenRouter.jsx
  46. 2 2
      frontend/src/container/Dashboard.jsx
  47. 6 1
      frontend/src/container/Header.jsx
  48. 29 21
      frontend/src/container/Login.jsx
  49. 1 1
      frontend/src/container/ProgressBar.jsx
  50. 1 0
      frontend/src/container/Sidebar.jsx
  51. 4 5
      frontend/src/container/Tracim.jsx
  52. 30 8
      frontend/src/container/WorkspaceContent.jsx
  53. 6 5
      frontend/src/css/AccountPage.styl
  54. 0 21
      frontend/src/css/AdminWorkspacePage.styl
  55. 22 12
      frontend/src/css/Dashboard.styl
  56. 2 2
      frontend/src/css/Generic.styl
  57. 14 3
      frontend/src/css/Header.styl
  58. 8 12
      frontend/src/css/Login.styl
  59. 0 1
      frontend/src/css/ProgressBar.styl
  60. 0 2
      frontend/src/css/index.styl
  61. 6 1
      frontend/src/helper.js
  62. 30 14
      frontend/src/reducer/workspaceContentList.js
  63. 194 0
      frontend_app_admin_workspace_user/src/component/AdminWorkspace.jsx
  64. 17 17
      frontend_app_admin_workspace_user/src/container/AdminWorkspaceUser.jsx
  65. 20 0
      frontend_app_admin_workspace_user/src/css/index.styl
  66. 10 0
      frontend_app_html-document/src/action.async.js
  67. 5 1
      frontend_app_html-document/src/container/HtmlDocument.jsx
  68. 10 0
      frontend_app_thread/src/action.async.js
  69. 20 15
      frontend_app_thread/src/container/Thread.jsx
  70. 0 2
      frontend_lib/src/component/Input/Checkbox.jsx
  71. 1 1
      frontend_lib/src/component/Timeline/Timeline.jsx
  72. 17 0
      setup_default_backend.sh

+ 1 - 0
.travis.yml View File

23
         - cd backend
23
         - cd backend
24
       install:
24
       install:
25
         - pip install --upgrade pip setuptools
25
         - pip install --upgrade pip setuptools
26
+        - pip install -e .
26
         - pip install -e ".[testing]"
27
         - pip install -e ".[testing]"
27
         - pip install pytest-cov
28
         - pip install pytest-cov
28
         - pip install python-coveralls
29
         - pip install python-coveralls

+ 57 - 7
backend/README.md View File

77
 
77
 
78
     tracimcli db init
78
     tracimcli db init
79
 
79
 
80
+Stamp current version of database to last (useful for migration):
81
+
82
+    alembic -c development.ini stamp head
83
+
80
 create wsgidav configuration file for webdav:
84
 create wsgidav configuration file for webdav:
81
 
85
 
82
     cp wsgidav.conf.sample wsgidav.conf
86
     cp wsgidav.conf.sample wsgidav.conf
83
 
87
 
84
-## Run Tracim_backend ##
88
+## Run Tracim_backend With Uwsgi : great for production ##
85
 
89
 
86
-### With Uwsgi ###
87
 
90
 
88
-Run all services with uwsgi
91
+#### Install Uwsgi
92
+
93
+You can either install uwsgi with pip or with you distrib package manager:
89
 
94
 
90
     # install uwsgi with pip ( unneeded if you already have uwsgi with python3 plugin enabled)
95
     # install uwsgi with pip ( unneeded if you already have uwsgi with python3 plugin enabled)
91
     sudo pip3 install uwsgi
96
     sudo pip3 install uwsgi
97
+
98
+or on debian 9 :
99
+
100
+    # install uwsgi on debian 9
101
+    sudo apt install uwsgi uwsgi-plugin-python3
102
+
103
+### All in terminal way ###
104
+
105
+
106
+Run all services with uwsgi
107
+
92
     # set tracim_conf_file path
108
     # set tracim_conf_file path
93
     export TRACIM_CONF_PATH="$(pwd)/development.ini"
109
     export TRACIM_CONF_PATH="$(pwd)/development.ini"
94
     export TRACIM_WEBDAV_CONF_PATH="$(pwd)/wsgidav.conf"
110
     export TRACIM_WEBDAV_CONF_PATH="$(pwd)/wsgidav.conf"
95
     # pyramid webserver
111
     # pyramid webserver
96
-    uwsgi -d /tmp/tracim_web.log --http-socket :6543 --wsgi-file wsgi/web.py -H env --pidfile /tmp/tracim_web.pid
112
+    uwsgi -d /tmp/tracim_web.log --http-socket :6543 --plugin python3 --wsgi-file wsgi/web.py -H env --pidfile /tmp/tracim_web.pid
97
     # webdav wsgidav server
113
     # webdav wsgidav server
98
-    uwsgi -d /tmp/tracim_webdav.log --http-socket :3030 --wsgi-file wsgi/webdav.py -H env --pidfile /tmp/tracim_webdav.pid
114
+    uwsgi -d /tmp/tracim_webdav.log --http-socket :3030 --plugin python3 --wsgi-file wsgi/webdav.py -H env --pidfile /tmp/tracim_webdav.pid
99
 
115
 
100
 to stop them:
116
 to stop them:
101
 
117
 
104
     # webdav wsgidav server
120
     # webdav wsgidav server
105
     uwsgi --stop /tmp/tracim_webdav.pid
121
     uwsgi --stop /tmp/tracim_webdav.pid
106
 
122
 
107
-### With Waitress (legacy way, usefull for debug) ###
123
+## With Uwsgi ini script file ##
124
+
125
+You can also preset uwsgi config for tracim, this way, creating this kind of .ini file:
126
+
127
+    # You need to replace <PATH> with correct absolute path
128
+    [uwsgi]
129
+    plugins = python3
130
+    chdir = <PATH>/tracim_v2/backend/
131
+    module = wsgi.web:application
132
+    home = <PATH>/tracim_v2/backend/env/
133
+    env = TRACIM_CONF_PATH=<PATH>/tracim_v2/backend/development.ini
134
+
135
+and :
136
+
137
+    # You need to replace <PATH> with correct absolute path
138
+    [uwsgi]
139
+    plugins = python3
140
+    chdir = <PATH>/tracim_v2/backend/
141
+    module = wsgi.webdav:application
142
+    home = <PATH>/tracim_v2/backend/env/
143
+    env = TRACIM_CONF_PATH=<PATH>/tracim_v2/backend/development.ini
144
+    env = TRACIM_WEBDAV_CONF_PATH=<PATH>/tracim_v2/backend/wsgidav.conf
145
+
146
+You can then run the process this way :
147
+
148
+    # You need to replace <WSGI_CONF_WEB> with correct path
149
+    uwsgi --ini <WSGI_CONF_WEB>.ini --http-socket :6543
150
+    # You need to replace <WSGI_CONF_WEBDAV> with correct path
151
+    uwsgi --ini <WSGI_CONF_WEBDAV>.ini --http-socket :3030
152
+
153
+### Run Tracim_Backend with Waitress : legacy way, usefull for debug and dev ###
108
 
154
 
109
 run tracim_backend web api:
155
 run tracim_backend web api:
110
 
156
 
159
 
205
 
160
 In Tracim, only some user can access to some informations, this is also true in
206
 In Tracim, only some user can access to some informations, this is also true in
161
 Tracim REST API. you can check the [roles documentation](doc/roles.md) to check
207
 Tracim REST API. you can check the [roles documentation](doc/roles.md) to check
162
-what a specific user can do.
208
+what a specific user can do.
209
+
210
+# Known issues
211
+
212
+see [here](doc/known_issues.md)

+ 23 - 0
backend/doc/known_issues.md View File

1
+# Known issue with Tracim Backend
2
+
3
+## Uwsgi
4
+
5
+### plaster.exceptions.LoaderNotFound
6
+
7
+If you obtain error with :
8
+
9
+
10
+    plaster.exceptions.LoaderNotFound: Could not find a matching loader for the scheme "file+ini"".
11
+
12
+
13
+you most probably don't set correctly TRACIM_CONF_PATH or TRACIM_WEBDAV_CONF_PATH.
14
+You have to set a correct absolute path.
15
+be careful for uwsgi ini file :
16
+
17
+
18
+    # incorrect
19
+    env = TRACIM_CONF_PATH="/home/me/tracim_v2/backend/development.ini"
20
+
21
+    # correct
22
+    env = TRACIM_CONF_PATH=/home/me/tracim_v2/backend/development.ini
23
+

+ 4 - 0
backend/doc/migration.md View File

28
 
28
 
29
     alembic -c development.ini current
29
     alembic -c development.ini current
30
 
30
 
31
+## Set Alembic stamp to last version (first time use) ##
32
+
33
+    alembic -c development.ini stamp head
34
+
31
 ### Creating new schema migration ###
35
 ### Creating new schema migration ###
32
 
36
 
33
 This creates a new auto-generated python migration file 
37
 This creates a new auto-generated python migration file 

+ 4 - 2
backend/doc/roles.md View File

9
 
9
 
10
 |                               | Normal User | Managers    | Admin          |
10
 |                               | Normal User | Managers    | Admin          |
11
 |-------------------------------|-------------|-------------|----------------|
11
 |-------------------------------|-------------|-------------|----------------|
12
-| slug                            | users       | managers    | administrators |
12
+| slug                          | users       | managers    | administrators |
13
 |-------------------------------|-------------|-------------|---------|
13
 |-------------------------------|-------------|-------------|---------|
14
 
14
 
15
 
15
 
20
 |-------------------------------|-------------|-------------|---------|
20
 |-------------------------------|-------------|-------------|---------|
21
 | create workspace              |  no         | yes         | yes     |
21
 | create workspace              |  no         | yes         | yes     |
22
 | invite user to tracim         |  no         | yes, if manager of a given workspace         | yes     |
22
 | invite user to tracim         |  no         | yes, if manager of a given workspace         | yes     |
23
+| delete workspace              |  no         | yes, if manager of a given workspace         | yes     |
23
 |-------------------------------|-------------|-------------|---------|
24
 |-------------------------------|-------------|-------------|---------|
24
 | set user global profile rights|  no         | no          | yes     |
25
 | set user global profile rights|  no         | no          | yes     |
25
-| deactivate user               |  no         | no          | yes     |
26
+| activate/deactivate user      |  no         | no          | yes     |
27
+| delete user/ undelete user    |  no         | no          | yes     |
26
 |-------------------------------|-------------|-------------|---------|
28
 |-------------------------------|-------------|-------------|---------|
27
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
29
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
28
 
30
 

+ 4 - 0
backend/tracim_backend/__init__.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+
2
 try:  # Python 3.5+
3
 try:  # Python 3.5+
3
     from http import HTTPStatus
4
     from http import HTTPStatus
4
 except ImportError:
5
 except ImportError:
27
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
28
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
28
 from tracim_backend.views.contents_api.comment_controller import CommentController
29
 from tracim_backend.views.contents_api.comment_controller import CommentController
29
 from tracim_backend.views.contents_api.file_controller import FileController
30
 from tracim_backend.views.contents_api.file_controller import FileController
31
+from tracim_backend.views.contents_api.folder_controller import FolderController
30
 from tracim_backend.views.frontend import FrontendController
32
 from tracim_backend.views.frontend import FrontendController
31
 from tracim_backend.views.errors import ErrorSchema
33
 from tracim_backend.views.errors import ErrorSchema
32
 from tracim_backend.exceptions import NotAuthenticated
34
 from tracim_backend.exceptions import NotAuthenticated
115
     html_document_controller = HTMLDocumentController()
117
     html_document_controller = HTMLDocumentController()
116
     thread_controller = ThreadController()
118
     thread_controller = ThreadController()
117
     file_controller = FileController()
119
     file_controller = FileController()
120
+    folder_controller = FolderController()
118
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
121
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
119
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
122
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
120
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
123
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
123
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
126
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
124
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
127
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
125
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
128
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
129
+    configurator.include(folder_controller.bind, route_prefix=BASE_API_V2)
126
 
130
 
127
     if app_config.FRONTEND_SERVE:
131
     if app_config.FRONTEND_SERVE:
128
         configurator.include('pyramid_mako')
132
         configurator.include('pyramid_mako')

+ 2 - 0
backend/tracim_backend/exceptions.py View File

204
 class PreviewDimNotAllowed(TracimException):
204
 class PreviewDimNotAllowed(TracimException):
205
     pass
205
     pass
206
 
206
 
207
+class UnallowedSubContent(TracimException):
208
+    pass
207
 
209
 
208
 class TooShortAutocompleteString(TracimException):
210
 class TooShortAutocompleteString(TracimException):
209
     pass
211
     pass

+ 78 - 15
backend/tracim_backend/lib/core/content.py View File

26
 from tracim_backend.lib.utils.utils import cmp_to_key
26
 from tracim_backend.lib.utils.utils import cmp_to_key
27
 from tracim_backend.lib.core.notifications import NotifierFactory
27
 from tracim_backend.lib.core.notifications import NotifierFactory
28
 from tracim_backend.exceptions import SameValueError
28
 from tracim_backend.exceptions import SameValueError
29
+from tracim_backend.exceptions import UnallowedSubContent
30
+from tracim_backend.exceptions import ContentTypeNotExist
29
 from tracim_backend.exceptions import PageOfPreviewNotFound
31
 from tracim_backend.exceptions import PageOfPreviewNotFound
30
 from tracim_backend.exceptions import PreviewDimNotAllowed
32
 from tracim_backend.exceptions import PreviewDimNotAllowed
31
 from tracim_backend.exceptions import RevisionDoesNotMatchThisContent
33
 from tracim_backend.exceptions import RevisionDoesNotMatchThisContent
408
 
410
 
409
     def create(self, content_type_slug: str, workspace: Workspace, parent: Content=None, label: str = '', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
411
     def create(self, content_type_slug: str, workspace: Workspace, parent: Content=None, label: str = '', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
410
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
412
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
411
-        assert content_type_slug in CONTENT_TYPES.query_allowed_types_slugs()
412
         assert content_type_slug != CONTENT_TYPES.Any_SLUG
413
         assert content_type_slug != CONTENT_TYPES.Any_SLUG
413
         assert not (label and filename)
414
         assert not (label and filename)
414
 
415
 
415
         if content_type_slug == CONTENT_TYPES.Folder.slug and not label:
416
         if content_type_slug == CONTENT_TYPES.Folder.slug and not label:
416
             label = self.generate_folder_label(workspace, parent)
417
             label = self.generate_folder_label(workspace, parent)
417
 
418
 
419
+        # TODO BS 2018-08-13: Despite that workspace is required, create_comment
420
+        # can call here with None. Must update create_comment tu require the
421
+        # workspace.
422
+        if not workspace:
423
+            workspace = parent.workspace
424
+
425
+        content_type = CONTENT_TYPES.get_one_by_slug(content_type_slug)
426
+        if parent and parent.properties and 'allowed_content' in parent.properties:
427
+            if content_type.slug not in parent.properties['allowed_content'] or not parent.properties['allowed_content'][content_type.slug]:
428
+                raise UnallowedSubContent(' SubContent of type {subcontent_type}  not allowed in content {content_id}'.format(  # nopep8
429
+                    subcontent_type=content_type.slug,
430
+                    content_id=parent.content_id,
431
+                ))
432
+        if not workspace and parent:
433
+            workspace = parent.workspace
434
+
435
+        if workspace:
436
+            if content_type.slug not in workspace.get_allowed_content_types():
437
+                raise UnallowedSubContent(
438
+                    ' SubContent of type {subcontent_type}  not allowed in workspace {content_id}'.format(  # nopep8
439
+                        subcontent_type=content_type.slug,
440
+                        content_id=workspace.workspace_id,
441
+                    )
442
+                )
443
+
418
         content = Content()
444
         content = Content()
419
 
445
 
420
         if filename:
446
         if filename:
433
 
459
 
434
         content.owner = self._user
460
         content.owner = self._user
435
         content.parent = parent
461
         content.parent = parent
462
+
436
         content.workspace = workspace
463
         content.workspace = workspace
437
-        content.type = content_type_slug
464
+        content.type = content_type.slug
438
         content.is_temporary = is_temporary
465
         content.is_temporary = is_temporary
439
         content.revision_type = ActionDescription.CREATION
466
         content.revision_type = ActionDescription.CREATION
440
 
467
 
450
         return content
477
         return content
451
 
478
 
452
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
479
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
480
+        # TODO: check parent allowed_type and workspace allowed_ type
453
         assert parent and parent.type != CONTENT_TYPES.Folder.slug
481
         assert parent and parent.type != CONTENT_TYPES.Folder.slug
454
         if not content:
482
         if not content:
455
             raise EmptyCommentContentNotAllowed()
483
             raise EmptyCommentContentNotAllowed()
456
-        item = Content()
457
-        item.owner = self._user
458
-        item.parent = parent
459
-        if not workspace:
460
-            workspace = item.parent.workspace
461
-        item.workspace = workspace
462
-        item.type = CONTENT_TYPES.Comment.slug
484
+
485
+        item = self.create(
486
+            content_type_slug=CONTENT_TYPES.Comment.slug,
487
+            workspace=workspace,
488
+            parent=parent,
489
+            do_notify=False,
490
+            do_save=False,
491
+            label='',
492
+        )
463
         item.description = content
493
         item.description = content
464
-        item.label = ''
465
         item.revision_type = ActionDescription.COMMENT
494
         item.revision_type = ActionDescription.COMMENT
466
 
495
 
467
         if do_save:
496
         if do_save:
1001
             else:
1030
             else:
1002
                 related_active_content = content
1031
                 related_active_content = content
1003
 
1032
 
1033
+            # INFO - G.M - 2018-08-10 - re-apply general filters here to avoid
1034
+            # issue with comments
1035
+            if not self._show_deleted and related_active_content.is_deleted:
1036
+                continue
1037
+            if not self._show_archived and related_active_content.is_archived:
1038
+                continue
1039
+
1004
             if related_active_content not in active_contents and related_active_content not in too_recent_content:  # nopep8
1040
             if related_active_content not in active_contents and related_active_content not in too_recent_content:  # nopep8
1005
 
1041
 
1006
                 if not before_content or before_content_find:
1042
                 if not before_content or before_content_find:
1076
     #
1112
     #
1077
     #     return result
1113
     #     return result
1078
 
1114
 
1079
-    def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
1115
+    def _set_allowed_content(self, content: Content, allowed_content_dict: dict) -> None:  # nopep8
1080
         """
1116
         """
1081
-        :param folder: the given folder instance
1117
+        :param content: the given content instance
1082
         :param allowed_content_dict: must be something like this:
1118
         :param allowed_content_dict: must be something like this:
1083
             dict(
1119
             dict(
1084
                 folder = True
1120
                 folder = True
1086
                 file = False,
1122
                 file = False,
1087
                 page = True
1123
                 page = True
1088
             )
1124
             )
1089
-        :return:
1125
+        :return: nothing
1126
+        """
1127
+        properties = content.properties.copy()
1128
+        properties['allowed_content'] = allowed_content_dict
1129
+        content.properties = properties
1130
+
1131
+    def set_allowed_content(self, content: Content, allowed_content_type_slug_list: typing.List[str]) -> None:  # nopep8
1132
+        """
1133
+        :param content: the given content instance
1134
+        :param allowed_content_type_slug_list: list of content_type_slug to
1135
+        accept as subcontent.
1136
+        :return: nothing
1137
+        """
1138
+        allowed_content_dict = {}
1139
+        for allowed_content_type_slug in allowed_content_type_slug_list:
1140
+            if allowed_content_type_slug not in CONTENT_TYPES.extended_endpoint_allowed_types_slug():
1141
+                raise ContentTypeNotExist('Content_type {} does not exist'.format(allowed_content_type_slug))  # nopep8
1142
+            allowed_content_dict[allowed_content_type_slug] = True
1143
+
1144
+        self._set_allowed_content(content, allowed_content_dict)
1145
+
1146
+    def restore_content_default_allowed_content(self, content: Content) -> None:
1147
+        """
1148
+        Return to default allowed_content_types
1149
+        :param content: the given content instance
1150
+        :return: nothing
1090
         """
1151
         """
1091
-        properties = dict(allowed_content = allowed_content_dict)
1092
-        folder.properties = properties
1152
+        if content._properties and 'allowed_content' in content._properties:
1153
+            properties = content.properties.copy()
1154
+            del properties['allowed_content']
1155
+            content.properties = properties
1093
 
1156
 
1094
     def set_status(self, content: Content, new_status: str):
1157
     def set_status(self, content: Content, new_status: str):
1095
         if new_status in CONTENT_STATUS.get_all_slugs_values():
1158
         if new_status in CONTENT_STATUS.get_all_slugs_values():

+ 16 - 1
backend/tracim_backend/lib/core/user.py View File

35
             current_user: typing.Optional[User],
35
             current_user: typing.Optional[User],
36
             session: Session,
36
             session: Session,
37
             config: CFG,
37
             config: CFG,
38
+            show_deleted: bool = False,
38
     ) -> None:
39
     ) -> None:
39
         self._session = session
40
         self._session = session
40
         self._user = current_user
41
         self._user = current_user
41
         self._config = config
42
         self._config = config
43
+        self._show_deleted = show_deleted
42
 
44
 
43
     def _base_query(self):
45
     def _base_query(self):
44
-        return self._session.query(User)
46
+        query = self._session.query(User)
47
+        if not self._show_deleted:
48
+            query = query.filter(User.is_deleted == False)
49
+        return query
45
 
50
 
46
     def get_user_with_context(self, user: User) -> UserInContext:
51
     def get_user_with_context(self, user: User) -> UserInContext:
47
         """
52
         """
404
         if do_save:
409
         if do_save:
405
             self.save(user)
410
             self.save(user)
406
 
411
 
412
+    def delete(self, user: User, do_save=False):
413
+        user.is_deleted = True
414
+        if do_save:
415
+            self.save(user)
416
+
417
+    def undelete(self, user: User, do_save=False):
418
+        user.is_deleted = False
419
+        if do_save:
420
+            self.save(user)
421
+
407
     def save(self, user: User):
422
     def save(self, user: User):
408
         self._session.flush()
423
         self._session.flush()
409
 
424
 

+ 13 - 12
backend/tracim_backend/lib/core/workspace.py View File

27
             session: Session,
27
             session: Session,
28
             current_user: User,
28
             current_user: User,
29
             config: CFG,
29
             config: CFG,
30
-            force_role: bool=False
30
+            force_role: bool=False,
31
+            show_deleted: bool=False,
31
     ):
32
     ):
32
         """
33
         """
33
         :param current_user: Current user of context
34
         :param current_user: Current user of context
37
         self._user = current_user
38
         self._user = current_user
38
         self._config = config
39
         self._config = config
39
         self._force_role = force_role
40
         self._force_role = force_role
41
+        self.show_deleted = show_deleted
40
 
42
 
41
     def _base_query_without_roles(self):
43
     def _base_query_without_roles(self):
42
-        return self._session.query(Workspace).filter(Workspace.is_deleted == False)
44
+        query = self._session.query(Workspace)
45
+        if not self.show_deleted:
46
+            query = query.filter(Workspace.is_deleted == False)
47
+        return query
43
 
48
 
44
     def _base_query(self):
49
     def _base_query(self):
45
         if not self._force_role and self._user.profile.id>=Group.TIM_ADMIN:
50
         if not self._force_role and self._user.profile.id>=Group.TIM_ADMIN:
46
             return self._base_query_without_roles()
51
             return self._base_query_without_roles()
47
 
52
 
48
-        return self._session.query(Workspace).\
49
-            join(Workspace.roles).\
50
-            filter(UserRoleInWorkspace.user_id == self._user.user_id).\
51
-            filter(Workspace.is_deleted == False)
53
+        query = self._base_query_without_roles()
54
+        query = query.join(Workspace.roles).\
55
+            filter(UserRoleInWorkspace.user_id == self._user.user_id)
56
+        return query
52
 
57
 
53
     def get_workspace_with_context(
58
     def get_workspace_with_context(
54
             self,
59
             self,
207
     def save(self, workspace: Workspace):
212
     def save(self, workspace: Workspace):
208
         self._session.flush()
213
         self._session.flush()
209
 
214
 
210
-    def delete_one(self, workspace_id, flush=True):
211
-        workspace = self.get_one(workspace_id)
215
+    def delete(self, workspace: Workspace, flush=True):
212
         workspace.is_deleted = True
216
         workspace.is_deleted = True
213
 
217
 
214
         if flush:
218
         if flush:
215
             self._session.flush()
219
             self._session.flush()
216
 
220
 
217
-    def restore_one(self, workspace_id, flush=True):
218
-        workspace = self._session.query(Workspace)\
219
-            .filter(Workspace.is_deleted==True)\
220
-            .filter(Workspace.workspace_id==workspace_id).one()
221
+    def undelete(self, workspace: Workspace, flush=True):
221
         workspace.is_deleted = False
222
         workspace.is_deleted = False
222
 
223
 
223
         if flush:
224
         if flush:

+ 34 - 0
backend/tracim_backend/lib/utils/authorization.py View File

90
     return decorator
90
     return decorator
91
 
91
 
92
 
92
 
93
+def require_profile_or_other_profile_with_workspace_role(
94
+        allow_all_group: int,
95
+        allow_if_role_group: int,
96
+        minimal_required_role: int,
97
+) -> typing.Callable:
98
+    """
99
+    Allow access for allow_all_group profile
100
+    or allow access for allow_if_role_group
101
+    profile if mininal_required_role is correct.
102
+    :param allow_all_group: value from Group Object
103
+    like Group.TIM_USER or Group.TIM_MANAGER
104
+    :param allow_if_role_group: value from Group Object
105
+    like Group.TIM_USER or Group.TIM_MANAGER
106
+    :param minimal_required_role: value from UserInWorkspace Object like
107
+    UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
108
+    :return: decorator
109
+    """
110
+    def decorator(func: typing.Callable) -> typing.Callable:
111
+        @functools.wraps(func)
112
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
113
+            user = request.current_user
114
+            workspace = request.current_workspace
115
+            if user.profile.id >= allow_all_group:
116
+                return func(self, context, request)
117
+            elif user.profile.id >= allow_if_role_group:
118
+                if workspace.get_user_role(user) >= minimal_required_role:
119
+                    return func(self, context, request)
120
+                raise InsufficientUserRoleInWorkspace()
121
+            else:
122
+                raise InsufficientUserProfile()
123
+        return wrapper
124
+    return decorator
125
+
126
+
93
 def require_workspace_role(minimal_required_role: int) -> typing.Callable:
127
 def require_workspace_role(minimal_required_role: int) -> typing.Callable:
94
     """
128
     """
95
     Restricts access to endpoint to minimal role or raise an exception.
129
     Restricts access to endpoint to minimal role or raise an exception.

+ 5 - 3
backend/tracim_backend/lib/utils/request.py View File

293
         :return: user found from header/body
293
         :return: user found from header/body
294
         """
294
         """
295
         app_config = request.registry.settings['CFG']
295
         app_config = request.registry.settings['CFG']
296
-        uapi = UserApi(None, session=request.dbsession, config=app_config)
296
+        uapi = UserApi(None, show_deleted=True, session=request.dbsession, config=app_config)
297
         login = ''
297
         login = ''
298
         try:
298
         try:
299
             login = None
299
             login = None
355
             wapi = WorkspaceApi(
355
             wapi = WorkspaceApi(
356
                 current_user=user,
356
                 current_user=user,
357
                 session=request.dbsession,
357
                 session=request.dbsession,
358
-                config=request.registry.settings['CFG']
358
+                config=request.registry.settings['CFG'],
359
+                show_deleted=True,
359
             )
360
             )
360
             workspace = wapi.get_one(workspace_id)
361
             workspace = wapi.get_one(workspace_id)
361
         except NoResultFound as exc:
362
         except NoResultFound as exc:
390
             wapi = WorkspaceApi(
391
             wapi = WorkspaceApi(
391
                 current_user=user,
392
                 current_user=user,
392
                 session=request.dbsession,
393
                 session=request.dbsession,
393
-                config=request.registry.settings['CFG']
394
+                config=request.registry.settings['CFG'],
395
+                show_deleted=True,
394
             )
396
             )
395
             workspace = wapi.get_one(workspace_id)
397
             workspace = wapi.get_one(workspace_id)
396
         except JSONDecodeError as exc:
398
         except JSONDecodeError as exc:

+ 0 - 8
backend/tracim_backend/lib/webdav/resources.py View File

295
             parent=self.content
295
             parent=self.content
296
         )
296
         )
297
 
297
 
298
-        subcontent = dict(
299
-            folder=True,
300
-            thread=True,
301
-            file=True,
302
-            page=True
303
-        )
304
-
305
-        self.content_api.set_allowed_content(folder, subcontent)
306
         self.content_api.save(folder)
298
         self.content_api.save(folder)
307
 
299
 
308
         transaction.commit()
300
         transaction.commit()

+ 26 - 0
backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py View File

1
+"""add_is_deleted_to_user
2
+
3
+Revision ID: 78b52ca39419
4
+Revises: ad79f58ec2bf
5
+Create Date: 2018-08-09 15:50:49.656925
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '78b52ca39419'
11
+down_revision = 'ad79f58ec2bf'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    # ### commands auto generated by Alembic - please adjust! ###
19
+    op.add_column('users', sa.Column('is_deleted', sa.Boolean(), server_default=sa.sql.expression.literal(False), nullable=False))
20
+    # ### end Alembic commands ###
21
+
22
+
23
+def downgrade():
24
+    # ### commands auto generated by Alembic - please adjust! ###
25
+    op.drop_column('users', 'is_deleted')
26
+    # ### end Alembic commands ###

+ 2 - 0
backend/tracim_backend/models/auth.py View File

15
 from hashlib import sha256
15
 from hashlib import sha256
16
 from typing import TYPE_CHECKING
16
 from typing import TYPE_CHECKING
17
 
17
 
18
+import sqlalchemy
18
 from sqlalchemy import Column
19
 from sqlalchemy import Column
19
 from sqlalchemy import ForeignKey
20
 from sqlalchemy import ForeignKey
20
 from sqlalchemy import Sequence
21
 from sqlalchemy import Sequence
135
     _password = Column('password', Unicode(128))
136
     _password = Column('password', Unicode(128))
136
     created = Column(DateTime, default=datetime.utcnow)
137
     created = Column(DateTime, default=datetime.utcnow)
137
     is_active = Column(Boolean, default=True, nullable=False)
138
     is_active = Column(Boolean, default=True, nullable=False)
139
+    is_deleted = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.sql.expression.literal(False))
138
     imported_from = Column(Unicode(32), nullable=True)
140
     imported_from = Column(Unicode(32), nullable=True)
139
     timezone = Column(Unicode(255), nullable=False, server_default='')
141
     timezone = Column(Unicode(255), nullable=False, server_default='')
140
     # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed
142
     # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed

+ 22 - 0
backend/tracim_backend/models/contents.py View File

122
             creation_label: str,
122
             creation_label: str,
123
             available_statuses: typing.List[ContentStatus],
123
             available_statuses: typing.List[ContentStatus],
124
             slug_alias: typing.List[str] = None,
124
             slug_alias: typing.List[str] = None,
125
+            allow_sub_content: bool = False,
125
     ):
126
     ):
126
         self.slug = slug
127
         self.slug = slug
127
         self.fa_icon = fa_icon
128
         self.fa_icon = fa_icon
130
         self.creation_label = creation_label
131
         self.creation_label = creation_label
131
         self.available_statuses = available_statuses
132
         self.available_statuses = available_statuses
132
         self.slug_alias = slug_alias
133
         self.slug_alias = slug_alias
134
+        self.allow_sub_content = allow_sub_content
133
 
135
 
134
 
136
 
135
 thread_type = ContentType(
137
 thread_type = ContentType(
177
     label='Folder',
179
     label='Folder',
178
     creation_label='Create a folder',
180
     creation_label='Create a folder',
179
     available_statuses=CONTENT_STATUS.get_all(),
181
     available_statuses=CONTENT_STATUS.get_all(),
182
+    allow_sub_content=True,
180
 )
183
 )
181
 
184
 
182
 
185
 
226
         """
229
         """
227
         content_types = self._content_types.copy()
230
         content_types = self._content_types.copy()
228
         content_types.extend(self._special_contents_types)
231
         content_types.extend(self._special_contents_types)
232
+        content_types.append(self.Event)
229
         for item in content_types:
233
         for item in content_types:
230
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
234
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
231
                 return item
235
                 return item
241
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
245
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
242
         return allowed_type_slug
246
         return allowed_type_slug
243
 
247
 
248
+    def extended_endpoint_allowed_types_slug(self) -> typing.List[str]:
249
+        allowed_types_slug = self.endpoint_allowed_types_slug().copy()
250
+        for content_type in self._special_contents_types:
251
+            allowed_types_slug.append(content_type.slug)
252
+        return allowed_types_slug
253
+
244
     def query_allowed_types_slugs(self) -> typing.List[str]:
254
     def query_allowed_types_slugs(self) -> typing.List[str]:
245
         """
255
         """
246
         Return alls allowed types slug : content_type slug + all alias, any
256
         Return alls allowed types slug : content_type slug + all alias, any
257
         allowed_types_slug.extend(self._extra_slugs)
267
         allowed_types_slug.extend(self._extra_slugs)
258
         return allowed_types_slug
268
         return allowed_types_slug
259
 
269
 
270
+    def default_allowed_content_properties(self, slug) -> dict:
271
+        content_type = self.get_one_by_slug(slug)
272
+        if content_type.allow_sub_content:
273
+            sub_content_allowed = self.extended_endpoint_allowed_types_slug()
274
+        else:
275
+            sub_content_allowed = [self.Comment.slug]
276
+
277
+        properties_dict = {}
278
+        for elem in sub_content_allowed:
279
+            properties_dict[elem] = True
280
+        return properties_dict
281
+
260
 
282
 
261
 CONTENT_TYPES = ContentTypeList(
283
 CONTENT_TYPES = ContentTypeList(
262
     [
284
     [

+ 37 - 11
backend/tracim_backend/models/context_models.py View File

297
     Content creation model
297
     Content creation model
298
     """
298
     """
299
     def __init__(
299
     def __init__(
300
-            self,
301
-            label: str,
302
-            content_type: str,
303
-            parent_id: typing.Optional[int] = None,
300
+        self,
301
+        label: str,
302
+        content_type: str,
303
+        parent_id: typing.Optional[int] = None,
304
     ) -> None:
304
     ) -> None:
305
         self.label = label
305
         self.label = label
306
         self.content_type = content_type
306
         self.content_type = content_type
312
     Comment creation model
312
     Comment creation model
313
     """
313
     """
314
     def __init__(
314
     def __init__(
315
-            self,
316
-            raw_content: str,
315
+        self,
316
+        raw_content: str,
317
     ) -> None:
317
     ) -> None:
318
         self.raw_content = raw_content
318
         self.raw_content = raw_content
319
 
319
 
323
     Set content status
323
     Set content status
324
     """
324
     """
325
     def __init__(
325
     def __init__(
326
-            self,
327
-            status: str,
326
+        self,
327
+        status: str,
328
     ) -> None:
328
     ) -> None:
329
         self.status = status
329
         self.status = status
330
 
330
 
334
     TextBasedContent update model
334
     TextBasedContent update model
335
     """
335
     """
336
     def __init__(
336
     def __init__(
337
-            self,
338
-            label: str,
339
-            raw_content: str,
337
+        self,
338
+        label: str,
339
+        raw_content: str,
340
+    ) -> None:
341
+        self.label = label
342
+        self.raw_content = raw_content
343
+
344
+
345
+class FolderContentUpdate(object):
346
+    """
347
+    Folder Content update model
348
+    """
349
+    def __init__(
350
+        self,
351
+        label: str,
352
+        raw_content: str,
353
+        sub_content_types: typing.List[str],
340
     ) -> None:
354
     ) -> None:
341
         self.label = label
355
         self.label = label
342
         self.raw_content = raw_content
356
         self.raw_content = raw_content
357
+        self.sub_content_types = sub_content_types
343
 
358
 
344
 
359
 
345
 class TypeUser(Enum):
360
 class TypeUser(Enum):
393
     def profile(self) -> Profile:
408
     def profile(self) -> Profile:
394
         return self.user.profile.name
409
         return self.user.profile.name
395
 
410
 
411
+    @property
412
+    def is_deleted(self) -> bool:
413
+        return self.user.is_deleted
414
+
396
     # Context related
415
     # Context related
397
 
416
 
398
     @property
417
     @property
457
         return slugify(self.workspace.label)
476
         return slugify(self.workspace.label)
458
 
477
 
459
     @property
478
     @property
479
+    def is_deleted(self) -> bool:
480
+        """
481
+        Is the workspace deleted ?
482
+        """
483
+        return self.workspace.is_deleted
484
+
485
+    @property
460
     def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
486
     def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
461
         """
487
         """
462
         get sidebar entries, those depends on activated apps.
488
         get sidebar entries, those depends on activated apps.

+ 24 - 43
backend/tracim_backend/models/data.py View File

32
 from tracim_backend.models.auth import User
32
 from tracim_backend.models.auth import User
33
 from tracim_backend.models.roles import WorkspaceRoles
33
 from tracim_backend.models.roles import WorkspaceRoles
34
 
34
 
35
-DEFAULT_PROPERTIES = dict(
36
-    allowed_content=dict(
37
-        folder=True,
38
-        file=True,
39
-        page=True,
40
-        thread=True,
41
-    ),
42
-)
43
-
44
 
35
 
45
 class Workspace(DeclarativeBase):
36
 class Workspace(DeclarativeBase):
46
 
37
 
96
 
87
 
97
     def get_allowed_content_types(self):
88
     def get_allowed_content_types(self):
98
         # @see Content.get_allowed_content_types()
89
         # @see Content.get_allowed_content_types()
99
-        return CONTENT_TYPES.endpoint_allowed_types_slug()
90
+        return CONTENT_TYPES.extended_endpoint_allowed_types_slug()
100
 
91
 
101
     def get_valid_children(
92
     def get_valid_children(
102
             self,
93
             self,
545
 
536
 
546
     @classmethod
537
     @classmethod
547
     def check_properties(cls, item):
538
     def check_properties(cls, item):
548
-        if item.type == CONTENT_TYPES.Folder.slug:
549
-            properties = item.properties
550
-            if 'allowed_content' not in properties.keys():
551
-                return False
552
-            if 'folders' not in properties['allowed_content']:
553
-                return False
554
-            if 'files' not in properties['allowed_content']:
555
-                return False
556
-            if 'pages' not in properties['allowed_content']:
557
-                return False
558
-            if 'threads' not in properties['allowed_content']:
559
-                return False
560
-            return True
561
-
539
+        properties = item.properties
562
         if item.type == CONTENT_TYPES.Event.slug:
540
         if item.type == CONTENT_TYPES.Event.slug:
563
-            properties = item.properties
564
             if 'name' not in properties.keys():
541
             if 'name' not in properties.keys():
565
                 return False
542
                 return False
566
             if 'raw' not in properties.keys():
543
             if 'raw' not in properties.keys():
570
             if 'end' not in properties.keys():
547
             if 'end' not in properties.keys():
571
                 return False
548
                 return False
572
             return True
549
             return True
573
-
574
-        # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
575
-        # Only content who can be copied need this
576
-        if item.type == CONTENT_TYPES.Any_SLUG:
577
-            properties = item.properties
550
+        else:
551
+            if 'allowed_content' in properties.keys():
552
+                for content_slug, value in properties['allowed_content'].items():  # nopep8
553
+                    if not isinstance(value, bool):
554
+                        return False
555
+                    if not content_slug in CONTENT_TYPES.extended_endpoint_allowed_types_slug():  # nopep8
556
+                        return False
578
             if 'origin' in properties.keys():
557
             if 'origin' in properties.keys():
579
-                return True
580
-        raise NotImplementedError
581
-
582
-    @classmethod
583
-    def reset_properties(cls, item):
584
-        if item.type == CONTENT_TYPES.Folder.slug:
585
-            item.properties = DEFAULT_PROPERTIES
586
-            return
587
-
588
-        raise NotImplementedError
558
+                pass
559
+            return True
589
 
560
 
590
 
561
 
591
 class ContentRevisionRO(DeclarativeBase):
562
 class ContentRevisionRO(DeclarativeBase):
1204
     @hybrid_property
1175
     @hybrid_property
1205
     def properties(self) -> dict:
1176
     def properties(self) -> dict:
1206
         """ return a structure decoded from json content of _properties """
1177
         """ return a structure decoded from json content of _properties """
1178
+
1207
         if not self._properties:
1179
         if not self._properties:
1208
-            return DEFAULT_PROPERTIES
1209
-        return json.loads(self._properties)
1180
+            properties = {}
1181
+        else:
1182
+            properties = json.loads(self._properties)
1183
+        if CONTENT_TYPES.get_one_by_slug(self.type) != CONTENT_TYPES.Event:
1184
+            if not 'allowed_content' in properties:
1185
+                properties['allowed_content'] = CONTENT_TYPES.default_allowed_content_properties(self.type)  # nopep8
1186
+        return properties
1210
 
1187
 
1211
     @properties.setter
1188
     @properties.setter
1212
     def properties(self, properties_struct: dict) -> None:
1189
     def properties(self, properties_struct: dict) -> None:
1347
             allowed_types = self.properties['allowed_content']
1324
             allowed_types = self.properties['allowed_content']
1348
             for type_label, is_allowed in allowed_types.items():
1325
             for type_label, is_allowed in allowed_types.items():
1349
                 if is_allowed:
1326
                 if is_allowed:
1350
-                    types.append(CONTENT_TYPES.get_one_by_slug(type_label))
1327
+                   types.append(
1328
+                        CONTENT_TYPES.get_one_by_slug(type_label)
1329
+                   )
1330
+        # TODO BS 2018-08-13: This try/except is not correct: except exception
1331
+        # if we know what to except.
1351
         except Exception as e:
1332
         except Exception as e:
1352
             print(e.__str__())
1333
             print(e.__str__())
1353
             print('----- /*\ *****')
1334
             print('----- /*\ *****')

+ 761 - 0
backend/tracim_backend/tests/functional/test_contents.py View File

24
 from tracim_backend.fixtures.content import Content as ContentFixtures
24
 from tracim_backend.fixtures.content import Content as ContentFixtures
25
 from tracim_backend.fixtures.users_and_groups import Base as BaseFixture
25
 from tracim_backend.fixtures.users_and_groups import Base as BaseFixture
26
 
26
 
27
+class TestFolder(FunctionalTest):
28
+    """
29
+    Tests for /api/v2/workspaces/{workspace_id}/folders/{content_id}
30
+    endpoint
31
+    """
32
+
33
+    fixtures = [BaseFixture]
34
+
35
+    def test_api__get_folder__ok_200__nominal_case(self) -> None:
36
+        """
37
+        Get one folder content
38
+        """
39
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
40
+        admin = dbsession.query(models.User) \
41
+            .filter(models.User.email == 'admin@admin.admin') \
42
+            .one()
43
+        workspace_api = WorkspaceApi(
44
+            current_user=admin,
45
+            session=dbsession,
46
+            config=self.app_config
47
+        )
48
+        content_api = ContentApi(
49
+            current_user=admin,
50
+            session=dbsession,
51
+            config=self.app_config
52
+        )
53
+        test_workspace = workspace_api.create_workspace(
54
+            label='test',
55
+            save_now=True,
56
+        )
57
+        folder = content_api.create(
58
+            label='test-folder',
59
+            content_type_slug=CONTENT_TYPES.Folder.slug,
60
+            workspace=test_workspace,
61
+            do_save=True,
62
+            do_notify=False
63
+        )
64
+        transaction.commit()
65
+
66
+        self.testapp.authorization = (
67
+            'Basic',
68
+            (
69
+                'admin@admin.admin',
70
+                'admin@admin.admin'
71
+            )
72
+        )
73
+        res = self.testapp.get(
74
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
75
+                workspace_id=test_workspace.workspace_id,
76
+                content_id=folder.content_id,
77
+            ),
78
+            status=200
79
+        )
80
+        content = res.json_body
81
+        assert content['content_type'] == 'folder'
82
+        assert content['content_id'] == folder.content_id
83
+        assert content['is_archived'] is False
84
+        assert content['is_deleted'] is False
85
+        assert content['label'] == 'test-folder'
86
+        assert content['parent_id'] is None
87
+        assert content['show_in_ui'] is True
88
+        assert content['slug'] == 'test-folder'
89
+        assert content['status'] == 'open'
90
+        assert content['workspace_id'] == test_workspace.workspace_id
91
+        assert content['current_revision_id'] == folder.revision_id
92
+        # TODO - G.M - 2018-06-173 - check date format
93
+        assert content['created']
94
+        assert content['author']
95
+        assert content['author']['user_id'] == 1
96
+        assert content['author']['avatar_url'] is None
97
+        assert content['author']['public_name'] == 'Global manager'
98
+        # TODO - G.M - 2018-06-173 - check date format
99
+        assert content['modified']
100
+        assert content['last_modifier']['user_id'] == 1
101
+        assert content['last_modifier']['public_name'] == 'Global manager'
102
+        assert content['last_modifier']['avatar_url'] is None
103
+        assert content['raw_content'] == ''
104
+
105
+    def test_api__get_folder__err_400__wrong_content_type(self) -> None:
106
+        """
107
+        Get one folder of a content content 7 is not folder
108
+        """
109
+        self.testapp.authorization = (
110
+            'Basic',
111
+            (
112
+                'admin@admin.admin',
113
+                'admin@admin.admin'
114
+            )
115
+        )
116
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
117
+        admin = dbsession.query(models.User) \
118
+            .filter(models.User.email == 'admin@admin.admin') \
119
+            .one()
120
+        workspace_api = WorkspaceApi(
121
+            current_user=admin,
122
+            session=dbsession,
123
+            config=self.app_config
124
+        )
125
+        content_api = ContentApi(
126
+            current_user=admin,
127
+            session=dbsession,
128
+            config=self.app_config
129
+        )
130
+        test_workspace = workspace_api.create_workspace(
131
+            label='test',
132
+            save_now=True,
133
+        )
134
+        thread = content_api.create(
135
+            label='thread',
136
+            content_type_slug=CONTENT_TYPES.Thread.slug,
137
+            workspace=test_workspace,
138
+            do_save=True,
139
+            do_notify=False
140
+        )
141
+        transaction.commit()
142
+        self.testapp.get(
143
+            '/api/v2/workspaces/2/folders/7',
144
+            status=400
145
+        )
146
+
147
+    def test_api__get_folder__err_400__content_does_not_exist(self) -> None:  # nopep8
148
+        """
149
+        Get one folder content (content 170 does not exist in db)
150
+        """
151
+        self.testapp.authorization = (
152
+            'Basic',
153
+            (
154
+                'admin@admin.admin',
155
+                'admin@admin.admin'
156
+            )
157
+        )
158
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
159
+        admin = dbsession.query(models.User) \
160
+            .filter(models.User.email == 'admin@admin.admin') \
161
+            .one()
162
+        workspace_api = WorkspaceApi(
163
+            current_user=admin,
164
+            session=dbsession,
165
+            config=self.app_config
166
+        )
167
+        content_api = ContentApi(
168
+            current_user=admin,
169
+            session=dbsession,
170
+            config=self.app_config
171
+        )
172
+        test_workspace = workspace_api.create_workspace(
173
+            label='test',
174
+            save_now=True,
175
+        )
176
+        transaction.commit()
177
+        self.testapp.get(
178
+            '/api/v2/workspaces/{workspace_id}/folders/170'.format(workspace_id=test_workspace.workspace_id),  # nopep8
179
+            status=400
180
+        )
181
+
182
+    def test_api__get_folder__err_400__content_not_in_workspace(self) -> None:  # nopep8
183
+        """
184
+        Get one folders of a content (content is in another workspace)
185
+        """
186
+        self.testapp.authorization = (
187
+            'Basic',
188
+            (
189
+                'admin@admin.admin',
190
+                'admin@admin.admin'
191
+            )
192
+        )
193
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
194
+        admin = dbsession.query(models.User) \
195
+            .filter(models.User.email == 'admin@admin.admin') \
196
+            .one()
197
+        workspace_api = WorkspaceApi(
198
+            current_user=admin,
199
+            session=dbsession,
200
+            config=self.app_config
201
+        )
202
+        content_api = ContentApi(
203
+            current_user=admin,
204
+            session=dbsession,
205
+            config=self.app_config
206
+        )
207
+        test_workspace = workspace_api.create_workspace(
208
+            label='test',
209
+            save_now=True,
210
+        )
211
+        folder = content_api.create(
212
+            label='test_folder',
213
+            content_type_slug=CONTENT_TYPES.Folder.slug,
214
+            workspace=test_workspace,
215
+            do_save=True,
216
+            do_notify=False
217
+        )
218
+        test_workspace2 = workspace_api.create_workspace(
219
+            label='test2',
220
+            save_now=True,
221
+        )
222
+        transaction.commit()
223
+        self.testapp.authorization = (
224
+            'Basic',
225
+            (
226
+                'admin@admin.admin',
227
+                'admin@admin.admin'
228
+            )
229
+        )
230
+        self.testapp.get(
231
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
232
+                workspace_id=test_workspace2.workspace_id,
233
+                content_id=folder.content_id,
234
+            ),
235
+            status=400
236
+        )
237
+
238
+    def test_api__get_folder__err_400__workspace_does_not_exist(self) -> None:  # nopep8
239
+        """
240
+        Get one folder content (Workspace 40 does not exist)
241
+        """
242
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
243
+        admin = dbsession.query(models.User) \
244
+            .filter(models.User.email == 'admin@admin.admin') \
245
+            .one()
246
+        workspace_api = WorkspaceApi(
247
+            current_user=admin,
248
+            session=dbsession,
249
+            config=self.app_config
250
+        )
251
+        content_api = ContentApi(
252
+            current_user=admin,
253
+            session=dbsession,
254
+            config=self.app_config
255
+        )
256
+        test_workspace = workspace_api.create_workspace(
257
+            label='test',
258
+            save_now=True,
259
+        )
260
+        folder = content_api.create(
261
+            label='test_folder',
262
+            content_type_slug=CONTENT_TYPES.Folder.slug,
263
+            workspace=test_workspace,
264
+            do_save=True,
265
+            do_notify=False
266
+        )
267
+        transaction.commit()
268
+        self.testapp.authorization = (
269
+            'Basic',
270
+            (
271
+                'admin@admin.admin',
272
+                'admin@admin.admin'
273
+            )
274
+        )
275
+        self.testapp.get(
276
+            '/api/v2/workspaces/40/folders/{content_id}'.format(content_id=folder.content_id),  # nopep8
277
+            status=400
278
+        )
279
+
280
+    def test_api__get_folder__err_400__workspace_id_is_not_int(self) -> None:  # nopep8
281
+        """
282
+        Get one folder content, workspace id is not int
283
+        """
284
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
285
+        admin = dbsession.query(models.User) \
286
+            .filter(models.User.email == 'admin@admin.admin') \
287
+            .one()
288
+        workspace_api = WorkspaceApi(
289
+            current_user=admin,
290
+            session=dbsession,
291
+            config=self.app_config
292
+        )
293
+        content_api = ContentApi(
294
+            current_user=admin,
295
+            session=dbsession,
296
+            config=self.app_config
297
+        )
298
+        test_workspace = workspace_api.create_workspace(
299
+            label='test',
300
+            save_now=True,
301
+        )
302
+        folder = content_api.create(
303
+            label='test_folder',
304
+            content_type_slug=CONTENT_TYPES.Folder.slug,
305
+            workspace=test_workspace,
306
+            do_save=True,
307
+            do_notify=False
308
+        )
309
+        transaction.commit()
310
+        self.testapp.authorization = (
311
+            'Basic',
312
+            (
313
+                'admin@admin.admin',
314
+                'admin@admin.admin'
315
+            )
316
+        )
317
+        self.testapp.get(
318
+            '/api/v2/workspaces/coucou/folders/{content_id}'.format(content_id=folder.content_id),  # nopep8
319
+            status=400
320
+        )
321
+
322
+    def test_api__get_folder__err_400__content_id_is_not_int(self) -> None:  # nopep8
323
+        """
324
+        Get one folder content, content_id is not int
325
+        """
326
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
327
+        admin = dbsession.query(models.User) \
328
+            .filter(models.User.email == 'admin@admin.admin') \
329
+            .one()
330
+        workspace_api = WorkspaceApi(
331
+            current_user=admin,
332
+            session=dbsession,
333
+            config=self.app_config
334
+        )
335
+        content_api = ContentApi(
336
+            current_user=admin,
337
+            session=dbsession,
338
+            config=self.app_config
339
+        )
340
+        test_workspace = workspace_api.create_workspace(
341
+            label='test',
342
+            save_now=True,
343
+        )
344
+        folder = content_api.create(
345
+            label='test_folder',
346
+            content_type_slug=CONTENT_TYPES.Folder.slug,
347
+            workspace=test_workspace,
348
+            do_save=True,
349
+            do_notify=False
350
+        )
351
+        transaction.commit()
352
+
353
+        self.testapp.authorization = (
354
+            'Basic',
355
+            (
356
+                'admin@admin.admin',
357
+                'admin@admin.admin'
358
+            )
359
+        )
360
+        self.testapp.get(
361
+            '/api/v2/workspaces/{workspace_id}/folders/coucou'.format(workspace_id=test_workspace.workspace_id),  # nopep8
362
+            status=400
363
+        )
364
+
365
+    def test_api__update_folder__err_400__empty_label(self) -> None:  # nopep8
366
+        """
367
+        Update(put) one folder content
368
+        """
369
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
370
+        admin = dbsession.query(models.User) \
371
+            .filter(models.User.email == 'admin@admin.admin') \
372
+            .one()
373
+        workspace_api = WorkspaceApi(
374
+            current_user=admin,
375
+            session=dbsession,
376
+            config=self.app_config
377
+        )
378
+        content_api = ContentApi(
379
+            current_user=admin,
380
+            session=dbsession,
381
+            config=self.app_config
382
+        )
383
+        test_workspace = workspace_api.create_workspace(
384
+            label='test',
385
+            save_now=True,
386
+        )
387
+        folder = content_api.create(
388
+            label='test_folder',
389
+            content_type_slug=CONTENT_TYPES.Folder.slug,
390
+            workspace=test_workspace,
391
+            do_save=True,
392
+            do_notify=False
393
+        )
394
+        transaction.commit()
395
+        self.testapp.authorization = (
396
+            'Basic',
397
+            (
398
+                'admin@admin.admin',
399
+                'admin@admin.admin'
400
+            )
401
+        )
402
+        params = {
403
+            'label': '',
404
+            'raw_content': '<p> Le nouveau contenu </p>',
405
+            'sub_content_types': [CONTENT_TYPES.Folder.slug]
406
+        }
407
+        self.testapp.put_json(
408
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
409
+                workspace_id=test_workspace.workspace_id,
410
+                content_id=folder.content_id,
411
+            ),
412
+            params=params,
413
+            status=400
414
+        )
415
+
416
+    def test_api__update_folder__ok_200__nominal_case(self) -> None:
417
+        """
418
+        Update(put) one html document of a content
419
+        """
420
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
421
+        admin = dbsession.query(models.User) \
422
+            .filter(models.User.email == 'admin@admin.admin') \
423
+            .one()
424
+        workspace_api = WorkspaceApi(
425
+            current_user=admin,
426
+            session=dbsession,
427
+            config=self.app_config
428
+        )
429
+        content_api = ContentApi(
430
+            current_user=admin,
431
+            session=dbsession,
432
+            config=self.app_config
433
+        )
434
+        test_workspace = workspace_api.create_workspace(
435
+            label='test',
436
+            save_now=True,
437
+        )
438
+        folder = content_api.create(
439
+            label='test_folder',
440
+            content_type_slug=CONTENT_TYPES.Folder.slug,
441
+            workspace=test_workspace,
442
+            do_save=True,
443
+            do_notify=False
444
+        )
445
+        transaction.commit()
446
+        self.testapp.authorization = (
447
+            'Basic',
448
+            (
449
+                'admin@admin.admin',
450
+                'admin@admin.admin'
451
+            )
452
+        )
453
+        params = {
454
+            'label': 'My New label',
455
+            'raw_content': '<p> Le nouveau contenu </p>',
456
+            'sub_content_types': [CONTENT_TYPES.Folder.slug]
457
+        }
458
+        res = self.testapp.put_json(
459
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
460
+                workspace_id=test_workspace.workspace_id,
461
+                content_id=folder.content_id,
462
+            ),
463
+            params=params,
464
+            status=200
465
+        )
466
+        content = res.json_body
467
+        assert content['content_type'] == 'folder'
468
+        assert content['content_id'] == folder.content_id
469
+        assert content['is_archived'] is False
470
+        assert content['is_deleted'] is False
471
+        assert content['label'] == 'My New label'
472
+        assert content['parent_id'] is None
473
+        assert content['show_in_ui'] is True
474
+        assert content['slug'] == 'my-new-label'
475
+        assert content['status'] == 'open'
476
+        assert content['workspace_id'] == test_workspace.workspace_id
477
+        assert content['current_revision_id']
478
+        # TODO - G.M - 2018-06-173 - check date format
479
+        assert content['created']
480
+        assert content['author']
481
+        assert content['author']['user_id'] == 1
482
+        assert content['author']['avatar_url'] is None
483
+        assert content['author']['public_name'] == 'Global manager'
484
+        # TODO - G.M - 2018-06-173 - check date format
485
+        assert content['modified']
486
+        assert content['last_modifier'] == content['author']
487
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
488
+        assert content['sub_content_types'] == [CONTENT_TYPES.Folder.slug]
489
+
490
+    def test_api__get_folder_revisions__ok_200__nominal_case(
491
+            self
492
+    ) -> None:
493
+        """
494
+        Get one html document of a content
495
+        """
496
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
497
+        admin = dbsession.query(models.User) \
498
+            .filter(models.User.email == 'admin@admin.admin') \
499
+            .one()
500
+        workspace_api = WorkspaceApi(
501
+            current_user=admin,
502
+            session=dbsession,
503
+            config=self.app_config
504
+        )
505
+        content_api = ContentApi(
506
+            current_user=admin,
507
+            session=dbsession,
508
+            config=self.app_config
509
+        )
510
+        test_workspace = workspace_api.create_workspace(
511
+            label='test',
512
+            save_now=True,
513
+        )
514
+        folder = content_api.create(
515
+            label='test-folder',
516
+            content_type_slug=CONTENT_TYPES.Folder.slug,
517
+            workspace=test_workspace,
518
+            do_save=True,
519
+            do_notify=False
520
+        )
521
+        with new_revision(
522
+           session=dbsession,
523
+           tm=transaction.manager,
524
+           content=folder,
525
+        ):
526
+            content_api.update_content(
527
+                folder,
528
+                new_label='test-folder-updated',
529
+                new_content='Just a test'
530
+            )
531
+        content_api.save(folder)
532
+        with new_revision(
533
+           session=dbsession,
534
+           tm=transaction.manager,
535
+           content=folder,
536
+        ):
537
+            content_api.archive(
538
+                folder,
539
+            )
540
+        content_api.save(folder)
541
+        with new_revision(
542
+           session=dbsession,
543
+           tm=transaction.manager,
544
+           content=folder,
545
+        ):
546
+            content_api.unarchive(
547
+                folder,
548
+            )
549
+        content_api.save(folder)
550
+        transaction.commit()
551
+        self.testapp.authorization = (
552
+            'Basic',
553
+            (
554
+                'admin@admin.admin',
555
+                'admin@admin.admin'
556
+            )
557
+        )
558
+        res = self.testapp.get(
559
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}/revisions'.format(  # nopep8
560
+                workspace_id=test_workspace.workspace_id,
561
+                content_id=folder.content_id,
562
+            ),
563
+            status=200
564
+        )
565
+        revisions = res.json_body
566
+        assert len(revisions) == 4
567
+        revision = revisions[0]
568
+        assert revision['content_type'] == 'folder'
569
+        assert revision['content_id'] == folder.content_id
570
+        assert revision['is_archived'] is False
571
+        assert revision['is_deleted'] is False
572
+        assert revision['label'] == 'test-folder'
573
+        assert revision['parent_id'] is None
574
+        assert revision['show_in_ui'] is True
575
+        assert revision['slug'] == 'test-folder'
576
+        assert revision['status'] == 'open'
577
+        assert revision['workspace_id'] == test_workspace.workspace_id
578
+        assert revision['revision_id']
579
+        assert revision['revision_type'] == 'creation'
580
+        assert revision['sub_content_types']
581
+        # TODO - G.M - 2018-06-173 - Test with real comments
582
+        assert revision['comment_ids'] == []
583
+        # TODO - G.M - 2018-06-173 - check date format
584
+        assert revision['created']
585
+        assert revision['author']
586
+        assert revision['author']['user_id'] == 1
587
+        assert revision['author']['avatar_url'] is None
588
+        assert revision['author']['public_name'] == 'Global manager'
589
+
590
+        revision = revisions[1]
591
+        assert revision['content_type'] == 'folder'
592
+        assert revision['content_id'] == folder.content_id
593
+        assert revision['is_archived'] is False
594
+        assert revision['is_deleted'] is False
595
+        assert revision['label'] == 'test-folder-updated'
596
+        assert revision['parent_id'] is None
597
+        assert revision['show_in_ui'] is True
598
+        assert revision['slug'] == 'test-folder-updated'
599
+        assert revision['status'] == 'open'
600
+        assert revision['workspace_id'] == test_workspace.workspace_id
601
+        assert revision['revision_id']
602
+        assert revision['revision_type'] == 'edition'
603
+        assert revision['sub_content_types']
604
+        # TODO - G.M - 2018-06-173 - Test with real comments
605
+        assert revision['comment_ids'] == []
606
+        # TODO - G.M - 2018-06-173 - check date format
607
+        assert revision['created']
608
+        assert revision['author']
609
+        assert revision['author']['user_id'] == 1
610
+        assert revision['author']['avatar_url'] is None
611
+        assert revision['author']['public_name'] == 'Global manager'
612
+
613
+        revision = revisions[2]
614
+        assert revision['content_type'] == 'folder'
615
+        assert revision['content_id'] == folder.content_id
616
+        assert revision['is_archived'] is True
617
+        assert revision['is_deleted'] is False
618
+        assert revision['label'] != 'test-folder-updated'
619
+        assert revision['label'].startswith('test-folder-updated')
620
+        assert revision['parent_id'] is None
621
+        assert revision['show_in_ui'] is True
622
+        assert revision['slug'] != 'test-folder-updated'
623
+        assert revision['slug'].startswith('test-folder-updated')
624
+        assert revision['status'] == 'open'
625
+        assert revision['workspace_id'] == test_workspace.workspace_id
626
+        assert revision['revision_id']
627
+        assert revision['revision_type'] == 'archiving'
628
+        assert revision['sub_content_types']
629
+        # TODO - G.M - 2018-06-173 - Test with real comments
630
+        assert revision['comment_ids'] == []
631
+        # TODO - G.M - 2018-06-173 - check date format
632
+        assert revision['created']
633
+        assert revision['author']
634
+        assert revision['author']['user_id'] == 1
635
+        assert revision['author']['avatar_url'] is None
636
+        assert revision['author']['public_name'] == 'Global manager'
637
+
638
+        revision = revisions[3]
639
+        assert revision['content_type'] == 'folder'
640
+        assert revision['content_id'] == folder.content_id
641
+        assert revision['is_archived'] is False
642
+        assert revision['is_deleted'] is False
643
+        assert revision['label'].startswith('test-folder-updated')
644
+        assert revision['parent_id'] is None
645
+        assert revision['show_in_ui'] is True
646
+        assert revision['slug'].startswith('test-folder-updated')
647
+        assert revision['status'] == 'open'
648
+        assert revision['workspace_id'] == test_workspace.workspace_id
649
+        assert revision['revision_id']
650
+        assert revision['revision_type'] == 'unarchiving'
651
+        assert revision['sub_content_types']
652
+        # TODO - G.M - 2018-06-173 - Test with real comments
653
+        assert revision['comment_ids'] == []
654
+        # TODO - G.M - 2018-06-173 - check date format
655
+        assert revision['created']
656
+        assert revision['author']
657
+        assert revision['author']['user_id'] == 1
658
+        assert revision['author']['avatar_url'] is None
659
+        assert revision['author']['public_name'] == 'Global manager'
660
+
661
+    def test_api__set_folder_status__ok_200__nominal_case(self) -> None:
662
+        """
663
+        Get one folder content
664
+        """
665
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
666
+        admin = dbsession.query(models.User) \
667
+            .filter(models.User.email == 'admin@admin.admin') \
668
+            .one()
669
+        workspace_api = WorkspaceApi(
670
+            current_user=admin,
671
+            session=dbsession,
672
+            config=self.app_config
673
+        )
674
+        content_api = ContentApi(
675
+            current_user=admin,
676
+            session=dbsession,
677
+            config=self.app_config
678
+        )
679
+        test_workspace = workspace_api.create_workspace(
680
+            label='test',
681
+            save_now=True,
682
+        )
683
+        folder = content_api.create(
684
+            label='test_folder',
685
+            content_type_slug=CONTENT_TYPES.Folder.slug,
686
+            workspace=test_workspace,
687
+            do_save=True,
688
+            do_notify=False
689
+        )
690
+        transaction.commit()
691
+        self.testapp.authorization = (
692
+            'Basic',
693
+            (
694
+                'admin@admin.admin',
695
+                'admin@admin.admin'
696
+            )
697
+        )
698
+        params = {
699
+            'status': 'closed-deprecated',
700
+        }
701
+
702
+        # before
703
+        res = self.testapp.get(
704
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(  # nopep8
705
+                # nopep8
706
+                workspace_id=test_workspace.workspace_id,
707
+                content_id=folder.content_id,
708
+            ),
709
+            status=200
710
+        )
711
+        content = res.json_body
712
+        assert content['content_type'] == 'folder'
713
+        assert content['content_id'] == folder.content_id
714
+        assert content['status'] == 'open'
715
+
716
+        # set status
717
+        self.testapp.put_json(
718
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}/status'.format(  # nopep8
719
+                workspace_id=test_workspace.workspace_id,
720
+                content_id=folder.content_id,
721
+            ),
722
+            params=params,
723
+            status=204
724
+        )
725
+
726
+        # after
727
+        res = self.testapp.get(
728
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
729
+                workspace_id=test_workspace.workspace_id,
730
+                content_id=folder.content_id,
731
+            ),
732
+            status=200
733
+        )
734
+        content = res.json_body
735
+        assert content['content_type'] == 'folder'
736
+        assert content['content_id'] == folder.content_id
737
+        assert content['status'] == 'closed-deprecated'
738
+
739
+    def test_api__set_folder_status__err_400__wrong_status(self) -> None:
740
+        """
741
+        Get one folder content
742
+        """
743
+        self.testapp.authorization = (
744
+            'Basic',
745
+            (
746
+                'admin@admin.admin',
747
+                'admin@admin.admin'
748
+            )
749
+        )
750
+        params = {
751
+            'status': 'unexistant-status',
752
+        }
753
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
754
+        admin = dbsession.query(models.User) \
755
+            .filter(models.User.email == 'admin@admin.admin') \
756
+            .one()
757
+        workspace_api = WorkspaceApi(
758
+            current_user=admin,
759
+            session=dbsession,
760
+            config=self.app_config
761
+        )
762
+        content_api = ContentApi(
763
+            current_user=admin,
764
+            session=dbsession,
765
+            config=self.app_config
766
+        )
767
+        test_workspace = workspace_api.create_workspace(
768
+            label='test',
769
+            save_now=True,
770
+        )
771
+        folder = content_api.create(
772
+            label='test_folder',
773
+            content_type_slug=CONTENT_TYPES.Folder.slug,
774
+            workspace=test_workspace,
775
+            do_save=True,
776
+            do_notify=False
777
+        )
778
+        transaction.commit()
779
+        self.testapp.put_json(
780
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}/status'.format(  # nopep8
781
+                workspace_id=test_workspace.workspace_id,
782
+                content_id=folder.content_id,
783
+            ),
784
+            params=params,
785
+            status=400
786
+        )
787
+
27
 
788
 
28
 class TestHtmlDocuments(FunctionalTest):
789
 class TestHtmlDocuments(FunctionalTest):
29
     """
790
     """

+ 52 - 2
backend/tracim_backend/tests/functional/test_user.py View File

2383
         assert workspace['workspace_id'] == 1
2383
         assert workspace['workspace_id'] == 1
2384
         assert workspace['label'] == 'Business'
2384
         assert workspace['label'] == 'Business'
2385
         assert workspace['slug'] == 'business'
2385
         assert workspace['slug'] == 'business'
2386
+        assert workspace['is_deleted'] is False
2386
         assert len(workspace['sidebar_entries']) == 5
2387
         assert len(workspace['sidebar_entries']) == 5
2387
 
2388
 
2388
         # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
2389
         # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
2480
 
2481
 
2481
 
2482
 
2482
 class TestUserEndpoint(FunctionalTest):
2483
 class TestUserEndpoint(FunctionalTest):
2484
+    # -*- coding: utf-8 -*-
2483
     """
2485
     """
2484
-    Tests for GET/POST /api/v2/users/{user_id}
2486
+    Tests for GET /api/v2/users/{user_id}
2485
     """
2487
     """
2486
     fixtures = [BaseFixture]
2488
     fixtures = [BaseFixture]
2487
 
2489
 
2533
         assert res['email'] == 'test@test.test'
2535
         assert res['email'] == 'test@test.test'
2534
         assert res['public_name'] == 'bob'
2536
         assert res['public_name'] == 'bob'
2535
         assert res['timezone'] == 'Europe/Paris'
2537
         assert res['timezone'] == 'Europe/Paris'
2538
+        assert res['is_deleted'] is False
2536
 
2539
 
2537
     def test_api__get_user__ok_200__user_itself(self):
2540
     def test_api__get_user__ok_200__user_itself(self):
2538
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2541
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2582
         assert res['email'] == 'test@test.test'
2585
         assert res['email'] == 'test@test.test'
2583
         assert res['public_name'] == 'bob'
2586
         assert res['public_name'] == 'bob'
2584
         assert res['timezone'] == 'Europe/Paris'
2587
         assert res['timezone'] == 'Europe/Paris'
2588
+        assert res['is_deleted'] is False
2585
 
2589
 
2586
     def test_api__get_user__err_403__other_normal_user(self):
2590
     def test_api__get_user__err_403__other_normal_user(self):
2587
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2591
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2931
         # TODO - G.M - 2018-08-02 - Place cleanup outside of the test
2935
         # TODO - G.M - 2018-08-02 - Place cleanup outside of the test
2932
         requests.delete('http://127.0.0.1:8025/api/v1/messages')
2936
         requests.delete('http://127.0.0.1:8025/api/v1/messages')
2933
 
2937
 
2938
+    def test_api_delete_user__ok_200__admin(self):
2939
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2940
+        admin = dbsession.query(models.User) \
2941
+            .filter(models.User.email == 'admin@admin.admin') \
2942
+            .one()
2943
+        uapi = UserApi(
2944
+            current_user=admin,
2945
+            session=dbsession,
2946
+            config=self.app_config,
2947
+        )
2948
+        gapi = GroupApi(
2949
+            current_user=admin,
2950
+            session=dbsession,
2951
+            config=self.app_config,
2952
+        )
2953
+        groups = [gapi.get_one_with_name('users')]
2954
+        test_user = uapi.create_user(
2955
+            email='test@test.test',
2956
+            password='pass',
2957
+            name='bob',
2958
+            groups=groups,
2959
+            timezone='Europe/Paris',
2960
+            do_save=True,
2961
+            do_notify=False,
2962
+        )
2963
+        uapi.save(test_user)
2964
+        transaction.commit()
2965
+        user_id = int(test_user.user_id)
2966
+
2967
+        self.testapp.authorization = (
2968
+            'Basic',
2969
+            (
2970
+                'admin@admin.admin',
2971
+                'admin@admin.admin'
2972
+            )
2973
+        )
2974
+        self.testapp.put(
2975
+            '/api/v2/users/{}/delete'.format(user_id),
2976
+            status=204
2977
+        )
2978
+        res = self.testapp.get(
2979
+            '/api/v2/users/{}'.format(user_id),
2980
+            status=200
2981
+        ).json_body
2982
+        assert res['is_deleted'] is True
2983
+
2934
 
2984
 
2935
 class TestUsersEndpoint(FunctionalTest):
2985
 class TestUsersEndpoint(FunctionalTest):
2936
     # -*- coding: utf-8 -*-
2986
     # -*- coding: utf-8 -*-
2937
     """
2987
     """
2938
-    Tests for GET /api/v2/users
2988
+    Tests for GET /api/v2/users/{user_id}
2939
     """
2989
     """
2940
     fixtures = [BaseFixture]
2990
     fixtures = [BaseFixture]
2941
 
2991
 

File diff suppressed because it is too large
+ 816 - 22
backend/tracim_backend/tests/functional/test_workspaces.py


+ 401 - 0
backend/tracim_backend/tests/library/test_content_api.py View File

10
 from tracim_backend.lib.core.group import GroupApi
10
 from tracim_backend.lib.core.group import GroupApi
11
 from tracim_backend.lib.core.user import UserApi
11
 from tracim_backend.lib.core.user import UserApi
12
 from tracim_backend.exceptions import SameValueError
12
 from tracim_backend.exceptions import SameValueError
13
+from tracim_backend.exceptions import EmptyLabelNotAllowed
14
+from tracim_backend.exceptions import UnallowedSubContent
13
 # TODO - G.M - 28-03-2018 - [RoleApi] Re-enable RoleApi
15
 # TODO - G.M - 28-03-2018 - [RoleApi] Re-enable RoleApi
14
 from tracim_backend.lib.core.workspace import RoleApi
16
 from tracim_backend.lib.core.workspace import RoleApi
15
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
17
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
101
             'value is {} instead of {}'.format(sorteds[1].content_id,
103
             'value is {} instead of {}'.format(sorteds[1].content_id,
102
                                                c1.content_id))
104
                                                c1.content_id))
103
 
105
 
106
+    def test_unit__create_content__OK_nominal_case(self):
107
+        uapi = UserApi(
108
+            session=self.session,
109
+            config=self.app_config,
110
+            current_user=None,
111
+        )
112
+        group_api = GroupApi(
113
+            current_user=None,
114
+            session=self.session,
115
+            config=self.app_config,
116
+        )
117
+        groups = [group_api.get_one(Group.TIM_USER),
118
+                  group_api.get_one(Group.TIM_MANAGER),
119
+                  group_api.get_one(Group.TIM_ADMIN)]
120
+
121
+        user = uapi.create_minimal_user(email='this.is@user',
122
+                                        groups=groups, save_now=True)
123
+        workspace = WorkspaceApi(
124
+            current_user=user,
125
+            session=self.session,
126
+            config=self.app_config,
127
+        ).create_workspace('test workspace', save_now=True)
128
+        api = ContentApi(
129
+            current_user=user,
130
+            session=self.session,
131
+            config=self.app_config,
132
+        )
133
+        item = api.create(
134
+            content_type_slug=CONTENT_TYPES.Folder.slug,
135
+            workspace=workspace,
136
+            parent=None,
137
+            label='not_deleted',
138
+            do_save=True
139
+        )
140
+        assert isinstance(item, Content)
141
+
142
+    def test_unit__create_content__err_empty_label(self):
143
+        uapi = UserApi(
144
+            session=self.session,
145
+            config=self.app_config,
146
+            current_user=None,
147
+        )
148
+        group_api = GroupApi(
149
+            current_user=None,
150
+            session=self.session,
151
+            config=self.app_config,
152
+        )
153
+        groups = [group_api.get_one(Group.TIM_USER),
154
+                  group_api.get_one(Group.TIM_MANAGER),
155
+                  group_api.get_one(Group.TIM_ADMIN)]
156
+
157
+        user = uapi.create_minimal_user(email='this.is@user',
158
+                                        groups=groups, save_now=True)
159
+        workspace = WorkspaceApi(
160
+            current_user=user,
161
+            session=self.session,
162
+            config=self.app_config,
163
+        ).create_workspace('test workspace', save_now=True)
164
+        api = ContentApi(
165
+            current_user=user,
166
+            session=self.session,
167
+            config=self.app_config,
168
+        )
169
+        with pytest.raises(EmptyLabelNotAllowed):
170
+            api.create(
171
+                content_type_slug=CONTENT_TYPES.Thread.slug,
172
+                workspace=workspace,
173
+                parent=None,
174
+                label='',
175
+                do_save=True
176
+            )
177
+
178
+    def test_unit__create_content__err_content_type_not_allowed_in_this_folder(self):
179
+        uapi = UserApi(
180
+            session=self.session,
181
+            config=self.app_config,
182
+            current_user=None,
183
+        )
184
+        group_api = GroupApi(
185
+            current_user=None,
186
+            session=self.session,
187
+            config=self.app_config,
188
+        )
189
+        groups = [group_api.get_one(Group.TIM_USER),
190
+                  group_api.get_one(Group.TIM_MANAGER),
191
+                  group_api.get_one(Group.TIM_ADMIN)]
192
+
193
+        user = uapi.create_minimal_user(email='this.is@user',
194
+                                        groups=groups, save_now=True)
195
+        workspace = WorkspaceApi(
196
+            current_user=user,
197
+            session=self.session,
198
+            config=self.app_config,
199
+        ).create_workspace('test workspace', save_now=True)
200
+        api = ContentApi(
201
+            current_user=user,
202
+            session=self.session,
203
+            config=self.app_config,
204
+        )
205
+        folder = api.create(
206
+            content_type_slug=CONTENT_TYPES.Folder.slug,
207
+            workspace=workspace,
208
+            parent=None,
209
+            label='plop',
210
+            do_save=False
211
+        )
212
+        allowed_content_dict = {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: False} # nopep8
213
+        api._set_allowed_content(
214
+            folder,
215
+            allowed_content_dict=allowed_content_dict
216
+        )
217
+        api.save(content=folder)
218
+        # not in list -> do not allow
219
+        with pytest.raises(UnallowedSubContent):
220
+            api.create(
221
+                content_type_slug=CONTENT_TYPES.Event.slug,
222
+                workspace=workspace,
223
+                parent=folder,
224
+                label='lapin',
225
+                do_save=True
226
+            )
227
+        # in list but false -> do not allow
228
+        with pytest.raises(UnallowedSubContent):
229
+            api.create(
230
+                content_type_slug=CONTENT_TYPES.File.slug,
231
+                workspace=workspace,
232
+                parent=folder,
233
+                label='lapin',
234
+                do_save=True
235
+            )
236
+        # in list and true -> allow
237
+        api.create(
238
+            content_type_slug=CONTENT_TYPES.Folder.slug,
239
+            workspace=workspace,
240
+            parent=folder,
241
+            label='lapin',
242
+            do_save=True
243
+        )
244
+
245
+    def test_unit__create_content__err_content_type_not_allowed_in_this_workspace(self):
246
+        uapi = UserApi(
247
+            session=self.session,
248
+            config=self.app_config,
249
+            current_user=None,
250
+        )
251
+        group_api = GroupApi(
252
+            current_user=None,
253
+            session=self.session,
254
+            config=self.app_config,
255
+        )
256
+        groups = [group_api.get_one(Group.TIM_USER),
257
+                  group_api.get_one(Group.TIM_MANAGER),
258
+                  group_api.get_one(Group.TIM_ADMIN)]
259
+
260
+        user = uapi.create_minimal_user(email='this.is@user',
261
+                                        groups=groups, save_now=True)
262
+        workspace = WorkspaceApi(
263
+            current_user=user,
264
+            session=self.session,
265
+            config=self.app_config,
266
+        ).create_workspace('test workspace', save_now=True)
267
+        api = ContentApi(
268
+            current_user=user,
269
+            session=self.session,
270
+            config=self.app_config,
271
+        )
272
+        with pytest.raises(UnallowedSubContent):
273
+            api.create(
274
+                content_type_slug=CONTENT_TYPES.Event.slug,
275
+                workspace=workspace,
276
+                parent=None,
277
+                label='lapin',
278
+                do_save=True
279
+           )
280
+
281
+    def test_unit__set_allowed_content__ok__private_method(self):
282
+        uapi = UserApi(
283
+            session=self.session,
284
+            config=self.app_config,
285
+            current_user=None,
286
+        )
287
+        group_api = GroupApi(
288
+            current_user=None,
289
+            session=self.session,
290
+            config=self.app_config,
291
+        )
292
+        groups = [group_api.get_one(Group.TIM_USER),
293
+                  group_api.get_one(Group.TIM_MANAGER),
294
+                  group_api.get_one(Group.TIM_ADMIN)]
295
+
296
+        user = uapi.create_minimal_user(email='this.is@user',
297
+                                        groups=groups, save_now=True)
298
+        workspace = WorkspaceApi(
299
+            current_user=user,
300
+            session=self.session,
301
+            config=self.app_config,
302
+        ).create_workspace('test workspace', save_now=True)
303
+        api = ContentApi(
304
+            current_user=user,
305
+            session=self.session,
306
+            config=self.app_config,
307
+        )
308
+        folder = api.create(
309
+            content_type_slug=CONTENT_TYPES.Folder.slug,
310
+            workspace=workspace,
311
+            parent=None,
312
+            label='plop',
313
+            do_save=False
314
+        )
315
+        allowed_content_dict = {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: False}  # nopep8
316
+        api._set_allowed_content(
317
+            folder,
318
+            allowed_content_dict=allowed_content_dict
319
+        )
320
+        assert 'allowed_content' in folder.properties
321
+        assert folder.properties['allowed_content'] == {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: False}
322
+
323
+    def test_unit__set_allowed_content__ok__nominal_case(self):
324
+        uapi = UserApi(
325
+            session=self.session,
326
+            config=self.app_config,
327
+            current_user=None,
328
+        )
329
+        group_api = GroupApi(
330
+            current_user=None,
331
+            session=self.session,
332
+            config=self.app_config,
333
+        )
334
+        groups = [group_api.get_one(Group.TIM_USER),
335
+                  group_api.get_one(Group.TIM_MANAGER),
336
+                  group_api.get_one(Group.TIM_ADMIN)]
337
+
338
+        user = uapi.create_minimal_user(email='this.is@user',
339
+                                        groups=groups, save_now=True)
340
+        workspace = WorkspaceApi(
341
+            current_user=user,
342
+            session=self.session,
343
+            config=self.app_config,
344
+        ).create_workspace('test workspace', save_now=True)
345
+        api = ContentApi(
346
+            current_user=user,
347
+            session=self.session,
348
+            config=self.app_config,
349
+        )
350
+        folder = api.create(
351
+            content_type_slug=CONTENT_TYPES.Folder.slug,
352
+            workspace=workspace,
353
+            parent=None,
354
+            label='plop',
355
+            do_save=False
356
+        )
357
+        allowed_content_type_slug_list = [CONTENT_TYPES.Folder.slug, CONTENT_TYPES.File.slug]  # nopep8
358
+        api.set_allowed_content(
359
+            folder,
360
+            allowed_content_type_slug_list=allowed_content_type_slug_list
361
+        )
362
+        assert 'allowed_content' in folder.properties
363
+        assert folder.properties['allowed_content'] == {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: True}
364
+
365
+    def test_unit__restore_content_default_allowed_content__ok__nominal_case(self):
366
+        uapi = UserApi(
367
+            session=self.session,
368
+            config=self.app_config,
369
+            current_user=None,
370
+        )
371
+        group_api = GroupApi(
372
+            current_user=None,
373
+            session=self.session,
374
+            config=self.app_config,
375
+        )
376
+        groups = [group_api.get_one(Group.TIM_USER),
377
+                  group_api.get_one(Group.TIM_MANAGER),
378
+                  group_api.get_one(Group.TIM_ADMIN)]
379
+
380
+        user = uapi.create_minimal_user(email='this.is@user',
381
+                                        groups=groups, save_now=True)
382
+        workspace = WorkspaceApi(
383
+            current_user=user,
384
+            session=self.session,
385
+            config=self.app_config,
386
+        ).create_workspace('test workspace', save_now=True)
387
+        api = ContentApi(
388
+            current_user=user,
389
+            session=self.session,
390
+            config=self.app_config,
391
+        )
392
+        folder = api.create(
393
+            content_type_slug=CONTENT_TYPES.Folder.slug,
394
+            workspace=workspace,
395
+            parent=None,
396
+            label='plop',
397
+            do_save=False
398
+        )
399
+        allowed_content_type_slug_list = [CONTENT_TYPES.Folder.slug, CONTENT_TYPES.File.slug]  # nopep8
400
+        api.set_allowed_content(
401
+            folder,
402
+            allowed_content_type_slug_list=allowed_content_type_slug_list
403
+        )
404
+        assert 'allowed_content' in folder.properties
405
+        assert folder.properties['allowed_content'] == {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: True} # nopep8
406
+        api.restore_content_default_allowed_content(folder)
407
+        assert 'allowed_content' in folder.properties
408
+        assert folder.properties['allowed_content'] == CONTENT_TYPES.default_allowed_content_properties(folder.type)  # nopep8
409
+
104
     def test_delete(self):
410
     def test_delete(self):
105
         uapi = UserApi(
411
         uapi = UserApi(
106
             session=self.session,
412
             session=self.session,
2057
         # (workspace2)
2363
         # (workspace2)
2058
         assert last_actives[8] == main_folder_workspace2
2364
         assert last_actives[8] == main_folder_workspace2
2059
 
2365
 
2366
+    def test_unit__get_last_active__ok__do_no_show_deleted_archived(self):
2367
+        uapi = UserApi(
2368
+            session=self.session,
2369
+            config=self.app_config,
2370
+            current_user=None,
2371
+        )
2372
+        group_api = GroupApi(
2373
+            current_user=None,
2374
+            session=self.session,
2375
+            config=self.app_config,
2376
+        )
2377
+        groups = [group_api.get_one(Group.TIM_USER),
2378
+                  group_api.get_one(Group.TIM_MANAGER),
2379
+                  group_api.get_one(Group.TIM_ADMIN)]
2380
+
2381
+        user = uapi.create_minimal_user(email='this.is@user',
2382
+                                        groups=groups, save_now=True)
2383
+        workspace = WorkspaceApi(
2384
+            current_user=user,
2385
+            session=self.session,
2386
+            config=self.app_config,
2387
+        ).create_workspace(
2388
+            'test workspace',
2389
+            save_now=True
2390
+        )
2391
+        workspace2 = WorkspaceApi(
2392
+            current_user=user,
2393
+            session=self.session,
2394
+            config=self.app_config,
2395
+        ).create_workspace(
2396
+            'test workspace2',
2397
+            save_now=True
2398
+        )
2399
+
2400
+        api = ContentApi(
2401
+            current_user=user,
2402
+            session=self.session,
2403
+            config=self.app_config,
2404
+            show_deleted=False,
2405
+            show_archived=False,
2406
+        )
2407
+        main_folder = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'this is randomized folder', '', True)  # nopep8
2408
+        archived = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'archived', '', True)  # nopep8
2409
+        deleted = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'deleted', '', True)  # nopep8
2410
+        comment_archived = api.create_comment(workspace, parent=archived, content='just a comment', do_save=True)  # nopep8
2411
+        comment_deleted = api.create_comment(workspace, parent=deleted, content='just a comment', do_save=True)  # nopep8
2412
+        with new_revision(
2413
+            session=self.session,
2414
+            tm=transaction.manager,
2415
+            content=archived,
2416
+        ):
2417
+            api.archive(archived)
2418
+            api.save(archived)
2419
+
2420
+        with new_revision(
2421
+            session=self.session,
2422
+            tm=transaction.manager,
2423
+            content=deleted,
2424
+        ):
2425
+            api.delete(deleted)
2426
+            api.save(deleted)
2427
+        normal = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'normal', '', True)  # nopep8
2428
+        comment_normal = api.create_comment(workspace, parent=normal, content='just a comment', do_save=True)  # nopep8
2429
+
2430
+        last_actives = api.get_last_active()
2431
+        assert len(last_actives) == 2
2432
+        assert last_actives[0].content_id == normal.content_id
2433
+        assert last_actives[1].content_id == main_folder.content_id
2434
+
2435
+
2436
+        api._show_deleted = True
2437
+        api._show_archived = False
2438
+        last_actives = api.get_last_active()
2439
+        assert len(last_actives) == 3
2440
+        assert last_actives[0] == normal
2441
+        assert last_actives[1] == deleted
2442
+        assert last_actives[2] == main_folder
2443
+
2444
+        api._show_deleted = False
2445
+        api._show_archived = True
2446
+        last_actives = api.get_last_active()
2447
+        assert len(last_actives) == 3
2448
+        assert last_actives[0]== normal
2449
+        assert last_actives[1] == archived
2450
+        assert last_actives[2] == main_folder
2451
+
2452
+        api._show_deleted = True
2453
+        api._show_archived = True
2454
+        last_actives = api.get_last_active()
2455
+        assert len(last_actives) == 4
2456
+        assert last_actives[0] == normal
2457
+        assert last_actives[1] == deleted
2458
+        assert last_actives[2] == archived
2459
+        assert last_actives[3] == main_folder
2460
+
2060
     def test_unit__get_last_active__ok__workspace_filter_workspace_full(self):
2461
     def test_unit__get_last_active__ok__workspace_filter_workspace_full(self):
2061
         uapi = UserApi(
2462
         uapi = UserApi(
2062
             session=self.session,
2463
             session=self.session,

+ 198 - 0
backend/tracim_backend/views/contents_api/folder_controller.py View File

1
+# coding=utf-8
2
+import typing
3
+
4
+import transaction
5
+from pyramid.config import Configurator
6
+from tracim_backend.models.data import UserRoleInWorkspace
7
+
8
+try:  # Python 3.5+
9
+    from http import HTTPStatus
10
+except ImportError:
11
+    from http import client as HTTPStatus
12
+
13
+from tracim_backend import TracimRequest
14
+from tracim_backend.extensions import hapic
15
+from tracim_backend.lib.core.content import ContentApi
16
+from tracim_backend.views.controllers import Controller
17
+from tracim_backend.views.core_api.schemas import TextBasedContentSchema
18
+from tracim_backend.views.core_api.schemas import FolderContentModifySchema
19
+from tracim_backend.views.core_api.schemas import TextBasedRevisionSchema
20
+from tracim_backend.views.core_api.schemas import SetContentStatusSchema
21
+from tracim_backend.views.core_api.schemas import WorkspaceAndContentIdPathSchema  # nopep8
22
+from tracim_backend.views.core_api.schemas import NoContentSchema
23
+from tracim_backend.lib.utils.authorization import require_content_types
24
+from tracim_backend.lib.utils.authorization import require_workspace_role
25
+from tracim_backend.exceptions import EmptyLabelNotAllowed
26
+from tracim_backend.models.context_models import ContentInContext
27
+from tracim_backend.models.context_models import RevisionInContext
28
+from tracim_backend.models.contents import CONTENT_TYPES
29
+from tracim_backend.models.contents import folder_type
30
+from tracim_backend.models.revision_protection import new_revision
31
+
32
+SWAGGER_TAG__Folders_ENDPOINTS = 'Folders'
33
+
34
+
35
+class FolderController(Controller):
36
+
37
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
38
+    @require_workspace_role(UserRoleInWorkspace.READER)
39
+    @require_content_types([folder_type])
40
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
41
+    @hapic.output_body(TextBasedContentSchema())
42
+    def get_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
43
+        """
44
+        Get folder info
45
+        """
46
+        app_config = request.registry.settings['CFG']
47
+        api = ContentApi(
48
+            show_archived=True,
49
+            show_deleted=True,
50
+            current_user=request.current_user,
51
+            session=request.dbsession,
52
+            config=app_config,
53
+        )
54
+        content = api.get_one(
55
+            hapic_data.path.content_id,
56
+            content_type=CONTENT_TYPES.Any_SLUG
57
+        )
58
+        return api.get_content_in_context(content)
59
+
60
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
61
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
62
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
63
+    @require_content_types([folder_type])
64
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
65
+    @hapic.input_body(FolderContentModifySchema())
66
+    @hapic.output_body(TextBasedContentSchema())
67
+    def update_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
68
+        """
69
+        update folder
70
+        """
71
+        app_config = request.registry.settings['CFG']
72
+        api = ContentApi(
73
+            show_archived=True,
74
+            show_deleted=True,
75
+            current_user=request.current_user,
76
+            session=request.dbsession,
77
+            config=app_config,
78
+        )
79
+        content = api.get_one(
80
+            hapic_data.path.content_id,
81
+            content_type=CONTENT_TYPES.Any_SLUG
82
+        )
83
+        with new_revision(
84
+                session=request.dbsession,
85
+                tm=transaction.manager,
86
+                content=content
87
+        ):
88
+            api.update_content(
89
+                item=content,
90
+                new_label=hapic_data.body.label,
91
+                new_content=hapic_data.body.raw_content,
92
+
93
+            )
94
+            api.set_allowed_content(
95
+                content=content,
96
+                allowed_content_type_slug_list=hapic_data.body.sub_content_types  # nopep8
97
+            )
98
+            api.save(content)
99
+        return api.get_content_in_context(content)
100
+
101
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
102
+    @require_workspace_role(UserRoleInWorkspace.READER)
103
+    @require_content_types([folder_type])
104
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
105
+    @hapic.output_body(TextBasedRevisionSchema(many=True))
106
+    def get_folder_revisions(
107
+            self,
108
+            context,
109
+            request: TracimRequest,
110
+            hapic_data=None
111
+    ) -> typing.List[RevisionInContext]:
112
+        """
113
+        get folder revisions
114
+        """
115
+        app_config = request.registry.settings['CFG']
116
+        api = ContentApi(
117
+            show_archived=True,
118
+            show_deleted=True,
119
+            current_user=request.current_user,
120
+            session=request.dbsession,
121
+            config=app_config,
122
+        )
123
+        content = api.get_one(
124
+            hapic_data.path.content_id,
125
+            content_type=CONTENT_TYPES.Any_SLUG
126
+        )
127
+        revisions = content.revisions
128
+        return [
129
+            api.get_revision_in_context(revision)
130
+            for revision in revisions
131
+        ]
132
+
133
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
134
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
135
+    @require_content_types([folder_type])
136
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
137
+    @hapic.input_body(SetContentStatusSchema())
138
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
139
+    def set_folder_status(self, context, request: TracimRequest, hapic_data=None) -> None:  # nopep8
140
+        """
141
+        set folder status
142
+        """
143
+        app_config = request.registry.settings['CFG']
144
+        api = ContentApi(
145
+            show_archived=True,
146
+            show_deleted=True,
147
+            current_user=request.current_user,
148
+            session=request.dbsession,
149
+            config=app_config,
150
+        )
151
+        content = api.get_one(
152
+            hapic_data.path.content_id,
153
+            content_type=CONTENT_TYPES.Any_SLUG
154
+        )
155
+        with new_revision(
156
+                session=request.dbsession,
157
+                tm=transaction.manager,
158
+                content=content
159
+        ):
160
+            api.set_status(
161
+                content,
162
+                hapic_data.body.status,
163
+            )
164
+            api.save(content)
165
+        return
166
+
167
+    def bind(self, configurator: Configurator) -> None:
168
+        # Get folder
169
+        configurator.add_route(
170
+            'folder',
171
+            '/workspaces/{workspace_id}/folders/{content_id}',
172
+            request_method='GET'
173
+        )
174
+        configurator.add_view(self.get_folder, route_name='folder')  # nopep8
175
+
176
+        # update folder
177
+        configurator.add_route(
178
+            'update_folder',
179
+            '/workspaces/{workspace_id}/folders/{content_id}',
180
+            request_method='PUT'
181
+        )  # nopep8
182
+        configurator.add_view(self.update_folder, route_name='update_folder')  # nopep8
183
+
184
+        # get folder revisions
185
+        configurator.add_route(
186
+            'folder_revisions',
187
+            '/workspaces/{workspace_id}/folders/{content_id}/revisions',  # nopep8
188
+            request_method='GET'
189
+        )
190
+        configurator.add_view(self.get_folder_revisions, route_name='folder_revisions')  # nopep8
191
+
192
+        # get folder revisions
193
+        configurator.add_route(
194
+            'set_folder_status',
195
+            '/workspaces/{workspace_id}/folders/{content_id}/status',  # nopep8
196
+            request_method='PUT'
197
+        )
198
+        configurator.add_view(self.set_folder_status, route_name='set_folder_status')  # nopep8

+ 24 - 1
backend/tracim_backend/views/core_api/schemas.py View File

13
 from tracim_backend.models.contents import CONTENT_TYPES
13
 from tracim_backend.models.contents import CONTENT_TYPES
14
 from tracim_backend.models.contents import open_status
14
 from tracim_backend.models.contents import open_status
15
 from tracim_backend.models.context_models import ActiveContentFilter
15
 from tracim_backend.models.context_models import ActiveContentFilter
16
+from tracim_backend.models.context_models import FolderContentUpdate
16
 from tracim_backend.models.context_models import AutocompleteQuery
17
 from tracim_backend.models.context_models import AutocompleteQuery
17
 from tracim_backend.models.context_models import ContentIdsQuery
18
 from tracim_backend.models.context_models import ContentIdsQuery
18
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
19
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
75
         example=True,
76
         example=True,
76
         description='Is user account activated ?'
77
         description='Is user account activated ?'
77
     )
78
     )
79
+    is_deleted = marshmallow.fields.Bool(
80
+        example=False,
81
+        description='Is user account deleted ?'
82
+    )
78
     # TODO - G.M - 17-04-2018 - Restrict timezone values
83
     # TODO - G.M - 17-04-2018 - Restrict timezone values
79
     timezone = marshmallow.fields.String(
84
     timezone = marshmallow.fields.String(
80
         example="Europe/Paris",
85
         example="Europe/Paris",
326
         example='test',
331
         example='test',
327
         description='search text to query',
332
         description='search text to query',
328
         validate=Length(min=2),
333
         validate=Length(min=2),
334
+        required=True,
329
     )
335
     )
330
     @post_load
336
     @post_load
331
     def make_autocomplete(self, data):
337
     def make_autocomplete(self, data):
536
         WorkspaceMenuEntrySchema,
542
         WorkspaceMenuEntrySchema,
537
         many=True,
543
         many=True,
538
     )
544
     )
545
+    is_deleted = marshmallow.fields.Bool(example=False, default=False)
539
 
546
 
540
     class Meta:
547
     class Meta:
541
         description = 'Digest of workspace informations'
548
         description = 'Digest of workspace informations'
728
     sub_content_types = marshmallow.fields.List(
735
     sub_content_types = marshmallow.fields.List(
729
         marshmallow.fields.String(
736
         marshmallow.fields.String(
730
             example='html-content',
737
             example='html-content',
731
-            validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug())
738
+            validate=OneOf(CONTENT_TYPES.extended_endpoint_allowed_types_slug())
732
         ),
739
         ),
733
         description='list of content types allowed as sub contents. '
740
         description='list of content types allowed as sub contents. '
734
                     'This field is required for folder contents, '
741
                     'This field is required for folder contents, '
874
         return TextBasedContentUpdate(**data)
881
         return TextBasedContentUpdate(**data)
875
 
882
 
876
 
883
 
884
+class FolderContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbstractSchema):  # nopep
885
+    sub_content_types = marshmallow.fields.List(
886
+        marshmallow.fields.String(
887
+            example='html-document',
888
+            validate=OneOf(CONTENT_TYPES.extended_endpoint_allowed_types_slug())
889
+        ),
890
+        description='list of content types allowed as sub contents. '
891
+                    'This field is required for folder contents, '
892
+                    'set it to empty list in other cases'
893
+    )
894
+
895
+    @post_load
896
+    def folder_content_update(self, data):
897
+        return FolderContentUpdate(**data)
898
+
899
+
877
 class FileContentModifySchema(TextBasedContentModifySchema):
900
 class FileContentModifySchema(TextBasedContentModifySchema):
878
     pass
901
     pass
879
 
902
 

+ 43 - 0
backend/tracim_backend/views/core_api/user_controller.py View File

247
     @require_profile(Group.TIM_ADMIN)
247
     @require_profile(Group.TIM_ADMIN)
248
     @hapic.input_path(UserIdPathSchema())
248
     @hapic.input_path(UserIdPathSchema())
249
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
249
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
250
+    def delete_user(self, context, request: TracimRequest, hapic_data=None):
251
+        """
252
+        delete user
253
+        """
254
+        app_config = request.registry.settings['CFG']
255
+        uapi = UserApi(
256
+            current_user=request.current_user,  # User
257
+            session=request.dbsession,
258
+            config=app_config,
259
+        )
260
+        uapi.delete(user=request.candidate_user, do_save=True)
261
+        return
262
+
263
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
264
+    @require_profile(Group.TIM_ADMIN)
265
+    @hapic.input_path(UserIdPathSchema())
266
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
267
+    def undelete_user(self, context, request: TracimRequest, hapic_data=None):
268
+        """
269
+        undelete user
270
+        """
271
+        app_config = request.registry.settings['CFG']
272
+        uapi = UserApi(
273
+            current_user=request.current_user,  # User
274
+            session=request.dbsession,
275
+            config=app_config,
276
+            show_deleted=True,
277
+        )
278
+        uapi.undelete(user=request.candidate_user, do_save=True)
279
+        return
280
+
281
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
282
+    @require_profile(Group.TIM_ADMIN)
283
+    @hapic.input_path(UserIdPathSchema())
284
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
250
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
285
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
251
         """
286
         """
252
         disable user
287
         disable user
467
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
502
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
468
         configurator.add_view(self.disable_user, route_name='disable_user')
503
         configurator.add_view(self.disable_user, route_name='disable_user')
469
 
504
 
505
+        # delete user
506
+        configurator.add_route('delete_user', '/users/{user_id}/delete', request_method='PUT')  # nopep8
507
+        configurator.add_view(self.delete_user, route_name='delete_user')
508
+
509
+        # undelete user
510
+        configurator.add_route('undelete_user', '/users/{user_id}/undelete', request_method='PUT')  # nopep8
511
+        configurator.add_view(self.undelete_user, route_name='undelete_user')
512
+
470
         # set user profile
513
         # set user profile
471
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
514
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
472
         configurator.add_view(self.set_profile, route_name='set_user_profile')
515
         configurator.add_view(self.set_profile, route_name='set_user_profile')

+ 146 - 0
backend/tracim_backend/views/core_api/workspace_controller.py View File

1
 import typing
1
 import typing
2
 import transaction
2
 import transaction
3
 from pyramid.config import Configurator
3
 from pyramid.config import Configurator
4
+from pyramid.httpexceptions import HTTPFound
4
 
5
 
5
 from tracim_backend.lib.core.user import UserApi
6
 from tracim_backend.lib.core.user import UserApi
6
 from tracim_backend.models.roles import WorkspaceRoles
7
 from tracim_backend.models.roles import WorkspaceRoles
11
     from http import client as HTTPStatus
12
     from http import client as HTTPStatus
12
 
13
 
13
 from tracim_backend import hapic
14
 from tracim_backend import hapic
15
+from tracim_backend import BASE_API_V2
14
 from tracim_backend import TracimRequest
16
 from tracim_backend import TracimRequest
15
 from tracim_backend.lib.core.workspace import WorkspaceApi
17
 from tracim_backend.lib.core.workspace import WorkspaceApi
16
 from tracim_backend.lib.core.content import ContentApi
18
 from tracim_backend.lib.core.content import ContentApi
17
 from tracim_backend.lib.core.userworkspace import RoleApi
19
 from tracim_backend.lib.core.userworkspace import RoleApi
18
 from tracim_backend.lib.utils.authorization import require_workspace_role
20
 from tracim_backend.lib.utils.authorization import require_workspace_role
21
+from tracim_backend.lib.utils.authorization import require_profile_or_other_profile_with_workspace_role
19
 from tracim_backend.lib.utils.authorization import require_profile
22
 from tracim_backend.lib.utils.authorization import require_profile
20
 from tracim_backend.models import Group
23
 from tracim_backend.models import Group
21
 from tracim_backend.lib.utils.authorization import require_candidate_workspace_role
24
 from tracim_backend.lib.utils.authorization import require_candidate_workspace_role
24
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
27
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
25
 from tracim_backend.models.context_models import ContentInContext
28
 from tracim_backend.models.context_models import ContentInContext
26
 from tracim_backend.exceptions import EmptyLabelNotAllowed
29
 from tracim_backend.exceptions import EmptyLabelNotAllowed
30
+from tracim_backend.exceptions import UnallowedSubContent
27
 from tracim_backend.exceptions import EmailValidationFailed
31
 from tracim_backend.exceptions import EmailValidationFailed
28
 from tracim_backend.exceptions import UserCreationFailed
32
 from tracim_backend.exceptions import UserCreationFailed
29
 from tracim_backend.exceptions import UserDoesNotExist
33
 from tracim_backend.exceptions import UserDoesNotExist
33
 from tracim_backend.views.controllers import Controller
37
 from tracim_backend.views.controllers import Controller
34
 from tracim_backend.lib.utils.utils import password_generator
38
 from tracim_backend.lib.utils.utils import password_generator
35
 from tracim_backend.views.core_api.schemas import FilterContentQuerySchema
39
 from tracim_backend.views.core_api.schemas import FilterContentQuerySchema
40
+from tracim_backend.views.core_api.schemas import ContentIdPathSchema
36
 from tracim_backend.views.core_api.schemas import WorkspaceMemberCreationSchema
41
 from tracim_backend.views.core_api.schemas import WorkspaceMemberCreationSchema
37
 from tracim_backend.views.core_api.schemas import WorkspaceMemberInviteSchema
42
 from tracim_backend.views.core_api.schemas import WorkspaceMemberInviteSchema
38
 from tracim_backend.views.core_api.schemas import RoleUpdateSchema
43
 from tracim_backend.views.core_api.schemas import RoleUpdateSchema
118
         return wapi.get_workspace_with_context(workspace)
123
         return wapi.get_workspace_with_context(workspace)
119
 
124
 
120
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
125
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
126
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
127
+    @require_profile_or_other_profile_with_workspace_role(
128
+        Group.TIM_ADMIN,
129
+        Group.TIM_MANAGER,
130
+        UserRoleInWorkspace.WORKSPACE_MANAGER,
131
+    )
132
+    @hapic.input_path(WorkspaceIdPathSchema())
133
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
134
+    def delete_workspace(self, context, request: TracimRequest, hapic_data=None):  # nopep8
135
+        """
136
+        delete workspace
137
+        """
138
+        app_config = request.registry.settings['CFG']
139
+        wapi = WorkspaceApi(
140
+            current_user=request.current_user,  # User
141
+            session=request.dbsession,
142
+            config=app_config,
143
+        )
144
+        wapi.delete(request.current_workspace, flush=True)
145
+        return
146
+
147
+    @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
148
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
149
+    @require_profile_or_other_profile_with_workspace_role(
150
+        Group.TIM_ADMIN,
151
+        Group.TIM_MANAGER,
152
+        UserRoleInWorkspace.WORKSPACE_MANAGER,
153
+    )
154
+    @hapic.input_path(WorkspaceIdPathSchema())
155
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
156
+    def undelete_workspace(self, context, request: TracimRequest, hapic_data=None):  # nopep8
157
+        """
158
+        restore deleted workspace
159
+        """
160
+        app_config = request.registry.settings['CFG']
161
+        wapi = WorkspaceApi(
162
+            current_user=request.current_user,  # User
163
+            session=request.dbsession,
164
+            config=app_config,
165
+            show_deleted=True,
166
+        )
167
+        wapi.undelete(request.current_workspace, flush=True)
168
+        return
169
+
170
+    @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
121
     @require_workspace_role(UserRoleInWorkspace.READER)
171
     @require_workspace_role(UserRoleInWorkspace.READER)
122
     @hapic.input_path(WorkspaceIdPathSchema())
172
     @hapic.input_path(WorkspaceIdPathSchema())
123
     @hapic.output_body(WorkspaceMemberSchema(many=True))
173
     @hapic.output_body(WorkspaceMemberSchema(many=True))
176
         return rapi.get_user_role_workspace_with_context(role)
226
         return rapi.get_user_role_workspace_with_context(role)
177
 
227
 
178
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
228
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
229
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
230
+    @hapic.input_path(WorkspaceAndUserIdPathSchema())
231
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
232
+    def delete_workspaces_members_role(
233
+            self,
234
+            context,
235
+            request: TracimRequest,
236
+            hapic_data=None
237
+    ) -> None:
238
+        app_config = request.registry.settings['CFG']
239
+        rapi = RoleApi(
240
+            current_user=request.current_user,
241
+            session=request.dbsession,
242
+            config=app_config,
243
+        )
244
+        rapi.delete_one(
245
+            user_id=hapic_data.path.user_id,
246
+            workspace_id=hapic_data.path.workspace_id,
247
+        )
248
+        return
249
+
250
+    @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
179
     @hapic.handle_exception(UserCreationFailed, HTTPStatus.BAD_REQUEST)
251
     @hapic.handle_exception(UserCreationFailed, HTTPStatus.BAD_REQUEST)
180
     @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
252
     @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
181
     @hapic.input_path(WorkspaceIdPathSchema())
253
     @hapic.input_path(WorkspaceIdPathSchema())
276
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
348
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
277
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
349
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
278
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
350
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
351
+    @hapic.handle_exception(UnallowedSubContent, HTTPStatus.BAD_REQUEST)
279
     @hapic.input_path(WorkspaceIdPathSchema())
352
     @hapic.input_path(WorkspaceIdPathSchema())
280
     @hapic.input_body(ContentCreationSchema())
353
     @hapic.input_body(ContentCreationSchema())
281
     @hapic.output_body(ContentDigestSchema())
354
     @hapic.output_body(ContentDigestSchema())
314
         return content
387
         return content
315
 
388
 
316
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
389
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
390
+    @require_workspace_role(UserRoleInWorkspace.READER)
391
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
392
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.FOUND)  # nopep8
393
+    def get_content_from_workspace(
394
+            self,
395
+            context,
396
+            request: TracimRequest,
397
+            hapic_data=None,
398
+    ) -> None:
399
+        """
400
+        redirect to correct content file endpoint
401
+        """
402
+        app_config = request.registry.settings['CFG']
403
+        content = request.current_content
404
+        content_type = CONTENT_TYPES.get_one_by_slug(content.type).slug
405
+        # TODO - G.M - 2018-08-03 - Jsonify redirect response ?
406
+        raise HTTPFound(
407
+            "{base_url}workspaces/{workspace_id}/{content_type}s/{content_id}".format(
408
+                base_url=BASE_API_V2,
409
+                workspace_id=content.workspace_id,
410
+                content_type=content_type,
411
+                content_id=content.content_id,
412
+            )
413
+        )
414
+
415
+    @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
416
+    @hapic.input_path(ContentIdPathSchema())
417
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.FOUND)  # nopep8
418
+    def get_content(
419
+            self,
420
+            context,
421
+            request: TracimRequest,
422
+            hapic_data=None,
423
+    ) -> None:
424
+        """
425
+        redirect to correct content file endpoint
426
+        """
427
+        app_config = request.registry.settings['CFG']
428
+        api = ContentApi(
429
+            current_user=request.current_user,
430
+            session=request.dbsession,
431
+            config=app_config,
432
+        )
433
+        content = api.get_one(
434
+            content_id=hapic_data.path['content_id'],
435
+            content_type=CONTENT_TYPES.Any_SLUG
436
+        )
437
+        content_type = CONTENT_TYPES.get_one_by_slug(content.type).slug
438
+        # TODO - G.M - 2018-08-03 - Jsonify redirect response ?
439
+        raise HTTPFound(
440
+            "{base_url}workspaces/{workspace_id}/{content_type}s/{content_id}".format(
441
+                base_url=BASE_API_V2,
442
+                workspace_id=content.workspace_id,
443
+                content_type=content_type,
444
+                content_id=content.content_id,
445
+            )
446
+        )
447
+
448
+    @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
317
     @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
449
     @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
318
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
450
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
319
     @require_candidate_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
451
     @require_candidate_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
512
         # Create workspace
644
         # Create workspace
513
         configurator.add_route('create_workspace', '/workspaces', request_method='POST')  # nopep8
645
         configurator.add_route('create_workspace', '/workspaces', request_method='POST')  # nopep8
514
         configurator.add_view(self.create_workspace, route_name='create_workspace')  # nopep8
646
         configurator.add_view(self.create_workspace, route_name='create_workspace')  # nopep8
647
+        # Delete/Undelete workpace
648
+        configurator.add_route('delete_workspace', '/workspaces/{workspace_id}/delete', request_method='PUT')  # nopep8
649
+        configurator.add_view(self.delete_workspace, route_name='delete_workspace')  # nopep8
650
+        configurator.add_route('undelete_workspace', '/workspaces/{workspace_id}/undelete', request_method='PUT')  # nopep8
651
+        configurator.add_view(self.undelete_workspace, route_name='undelete_workspace')  # nopep8
515
         # Update Workspace
652
         # Update Workspace
516
         configurator.add_route('update_workspace', '/workspaces/{workspace_id}', request_method='PUT')  # nopep8
653
         configurator.add_route('update_workspace', '/workspaces/{workspace_id}', request_method='PUT')  # nopep8
517
         configurator.add_view(self.update_workspace, route_name='update_workspace')  # nopep8
654
         configurator.add_view(self.update_workspace, route_name='update_workspace')  # nopep8
524
         # Create Workspace Members roles
661
         # Create Workspace Members roles
525
         configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
662
         configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
526
         configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member')  # nopep8
663
         configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member')  # nopep8
664
+        # Delete Workspace Members roles
665
+        configurator.add_route('delete_workspace_member', '/workspaces/{workspace_id}/members/{user_id}', request_method='DELETE')  # nopep8
666
+        configurator.add_view(self.delete_workspaces_members_role, route_name='delete_workspace_member')  # nopep8
527
         # Workspace Content
667
         # Workspace Content
528
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
668
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
529
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
669
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
530
         # Create Generic Content
670
         # Create Generic Content
531
         configurator.add_route('create_generic_content', '/workspaces/{workspace_id}/contents', request_method='POST')  # nopep8
671
         configurator.add_route('create_generic_content', '/workspaces/{workspace_id}/contents', request_method='POST')  # nopep8
532
         configurator.add_view(self.create_generic_empty_content, route_name='create_generic_content')  # nopep8
672
         configurator.add_view(self.create_generic_empty_content, route_name='create_generic_content')  # nopep8
673
+        # Get Content
674
+        configurator.add_route('get_content', '/contents/{content_id}', request_method='GET')  # nopep8
675
+        configurator.add_view(self.get_content, route_name='get_content')
676
+        # Get Content From workspace
677
+        configurator.add_route('get_content_from_workspace', '/workspaces/{workspace_id}/contents/{content_id}', request_method='GET')  # nopep8
678
+        configurator.add_view(self.get_content_from_workspace, route_name='get_content_from_workspace')  # nopep8
533
         # Move Content
679
         # Move Content
534
         configurator.add_route('move_content', '/workspaces/{workspace_id}/contents/{content_id}/move', request_method='PUT')  # nopep8
680
         configurator.add_route('move_content', '/workspaces/{workspace_id}/contents/{content_id}/move', request_method='PUT')  # nopep8
535
         configurator.add_view(self.move_content, route_name='move_content')  # nopep8
681
         configurator.add_view(self.move_content, route_name='move_content')  # nopep8

+ 52 - 0
backend_lib.sh View File

1
+. bash_library.sh # source bash_library.sh
2
+
3
+function install_backend_system_dep {
4
+    log "install base debian-packaged-dep for backend..."
5
+    sudo apt update
6
+    sudo apt install -y python3 python3-venv python3-dev python3-pip
7
+    sudo apt install -y redis-server
8
+
9
+    log "install deps for dealing with most preview..."
10
+    sudo apt install -y zlib1g-dev libjpeg-dev
11
+    sudo apt install -y imagemagick libmagickwand-dev ghostscript
12
+    sudo apt install -y libreoffice # most office documents file and text format
13
+    sudo apt install -y inkscape # for .svg files.
14
+}
15
+
16
+function setup_pyenv {
17
+   log "setup python3 env.."
18
+   python3 -m venv env
19
+   source env/bin/activate
20
+}
21
+
22
+function install_backend_python_packages {
23
+    pip install --upgrade pip setuptools
24
+
25
+    log "install tracim-backend (sqlite_backend)..."
26
+    pip install -e ".[testing]"
27
+}
28
+
29
+function setup_config_file {
30
+    log "configure tracim with default conf..."
31
+    if [ ! -f development.ini ]; then
32
+       log "generate missing development.ini ..."
33
+       cp development.ini.sample development.ini
34
+    fi
35
+
36
+    if [ ! -f wsgidav.conf ]; then
37
+       log "generate missing wsgidav.conf ..."
38
+       cp wsgidav.conf.sample wsgidav.conf
39
+    fi
40
+}
41
+
42
+function setup_db {
43
+    result=$(alembic -c development.ini current)
44
+    if [ $? -eq 0 ] && [ ! "$result" == '' ]; then
45
+       log "check database migration..."
46
+       alembic -c development.ini upgrade head
47
+    else
48
+       log "database seems missing, init it..."
49
+       tracimcli db init
50
+       alembic -c development.ini stamp head
51
+    fi
52
+}

+ 32 - 0
frontend/src/action-creator.async.js View File

17
   setFolderData,
17
   setFolderData,
18
   APP_LIST,
18
   APP_LIST,
19
   CONTENT_TYPE_LIST,
19
   CONTENT_TYPE_LIST,
20
+  WORKSPACE_CONTENT_ARCHIVED,
21
+  WORKSPACE_CONTENT_DELETED,
20
   WORKSPACE_RECENT_ACTIVITY,
22
   WORKSPACE_RECENT_ACTIVITY,
21
   WORKSPACE_READ_STATUS
23
   WORKSPACE_READ_STATUS
22
 } from './action-creator.sync.js'
24
 } from './action-creator.sync.js'
332
     dispatch
334
     dispatch
333
   })
335
   })
334
 }
336
 }
337
+
338
+export const putWorkspaceContentArchived = (user, idWorkspace, idContent) => dispatch => {
339
+  return fetchWrapper({
340
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/archive`,
341
+    param: {
342
+      headers: {
343
+        ...FETCH_CONFIG.headers,
344
+        'Authorization': 'Basic ' + user.auth
345
+      },
346
+      method: 'PUT'
347
+    },
348
+    actionName: WORKSPACE_CONTENT_ARCHIVED,
349
+    dispatch
350
+  })
351
+}
352
+
353
+export const putWorkspaceContentDeleted = (user, idWorkspace, idContent) => dispatch => {
354
+  return fetchWrapper({
355
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/delete`,
356
+    param: {
357
+      headers: {
358
+        ...FETCH_CONFIG.headers,
359
+        'Authorization': 'Basic ' + user.auth
360
+      },
361
+      method: 'PUT'
362
+    },
363
+    actionName: WORKSPACE_CONTENT_DELETED,
364
+    dispatch
365
+  })
366
+}

+ 5 - 0
frontend/src/action-creator.sync.js View File

38
 export const setWorkspaceContentList = workspaceContentList => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList })
38
 export const setWorkspaceContentList = workspaceContentList => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList })
39
 export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
39
 export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
40
 
40
 
41
+export const WORKSPACE_CONTENT_ARCHIVED = `${WORKSPACE_CONTENT}/Archived`
42
+export const WORKSPACE_CONTENT_DELETED = `${WORKSPACE_CONTENT}/Deleted`
43
+export const setWorkspaceContentArchived = (idWorkspace, idContent) => ({ type: `${SET}/${WORKSPACE_CONTENT_ARCHIVED}`, idWorkspace, idContent })
44
+export const setWorkspaceContentDeleted = (idWorkspace, idContent) => ({ type: `${SET}/${WORKSPACE_CONTENT_DELETED}`, idWorkspace, idContent })
45
+
41
 export const WORKSPACE_LIST = `${WORKSPACE}/List`
46
 export const WORKSPACE_LIST = `${WORKSPACE}/List`
42
 export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
47
 export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
43
 export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })
48
 export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })

+ 3 - 3
frontend/src/component/Account/Password.jsx View File

17
           {props.t('Password')}
17
           {props.t('Password')}
18
         </div>
18
         </div>
19
         <input
19
         <input
20
-          className='personaldata__form__txtinput form-control'
20
+          className='personaldata__form__txtinput primaryColorBorderLighten form-control'
21
           type='password'
21
           type='password'
22
           placeholder={props.t('Old password')}
22
           placeholder={props.t('Old password')}
23
         />
23
         />
24
         <input
24
         <input
25
-          className='personaldata__form__txtinput form-control mt-4'
25
+          className='personaldata__form__txtinput primaryColorBorderLighten form-control mt-4'
26
           type='password'
26
           type='password'
27
           placeholder={props.t('New password')}
27
           placeholder={props.t('New password')}
28
         />
28
         />
29
-        <button type='submit' className='personaldata__form__button btn btn-outline-primary mt-4'>
29
+        <button type='submit' className='personaldata__form__button primaryColorBorderLighten btn btn-outline-primary mt-4'>
30
           {props.t('Send')}
30
           {props.t('Send')}
31
         </button>
31
         </button>
32
       </form>
32
       </form>

+ 3 - 3
frontend/src/component/Account/PersonalData.jsx View File

19
         </div>
19
         </div>
20
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
20
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
21
           <input
21
           <input
22
-            className='personaldata__form__txtinput form-control mt-3 mt-sm-0'
22
+            className='personaldata__form__txtinput primaryColorBorderLighten form-control mt-3 mt-sm-0'
23
             type='text'
23
             type='text'
24
             placeholder={props.t('Change your name')}
24
             placeholder={props.t('Change your name')}
25
           />
25
           />
29
         </div>
29
         </div>
30
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
30
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
31
           <input
31
           <input
32
-            className='personaldata__form__txtinput form-control mt-3 mt-sm-0'
32
+            className='personaldata__form__txtinput primaryColorBorderLighten form-control mt-3 mt-sm-0'
33
             type='email'
33
             type='email'
34
             placeholder={props.t('Change your email')}
34
             placeholder={props.t('Change your email')}
35
           />
35
           />
36
         </div>
36
         </div>
37
-        <button type='submit' className='personaldata__form__button btn btn-outline-primary'>
37
+        <button type='submit' className='personaldata__form__button primaryColorBorderLighten btn btn-outline-primary'>
38
           {props.t('Send')}
38
           {props.t('Send')}
39
         </button>
39
         </button>
40
       </form>
40
       </form>

+ 2 - 2
frontend/src/component/Account/UserInfo.jsx View File

10
         <div className='account__userinformation__name mb-3'>
10
         <div className='account__userinformation__name mb-3'>
11
           {`${props.user.firstname} ${props.user.lastname}`}
11
           {`${props.user.firstname} ${props.user.lastname}`}
12
         </div>
12
         </div>
13
-        <a href={`mailto:${props.user.email}`} className='account__userinformation__email d-block mb-3'>
13
+        <a href={`mailto:${props.user.email}`} className='account__userinformation__email d-block primaryColorFontLighten mb-3'>
14
           {props.user.email}
14
           {props.user.email}
15
         </a>
15
         </a>
16
         <div className='account__userinformation__role mb-3'>
16
         <div className='account__userinformation__role mb-3'>
19
         { /* <div className='account__userinformation__job mb-3'>
19
         { /* <div className='account__userinformation__job mb-3'>
20
           {props.user.job}
20
           {props.user.job}
21
         </div>
21
         </div>
22
-        <a href='http://www.algoo.fr' className='account__userinformation__company'>
22
+        <a href='http://www.algoo.fr' className='account__userinformation__company primaryColorFontLighten'>
23
           {props.user.company}
23
           {props.user.company}
24
         </a> */ }
24
         </a> */ }
25
       </div>
25
       </div>

+ 1 - 5
frontend/src/component/Dashboard/ContentTypeBtn.styl View File

4
   display flex
4
   display flex
5
   flex-direction column
5
   flex-direction column
6
   justify-content center
6
   justify-content center
7
-  margin 15px
7
+  margin 15px 15px 0 0
8
   border-radius 10px
8
   border-radius 10px
9
   padding 15px
9
   padding 15px
10
   width 230px
10
   width 230px
12
   box-shadow shadow-all
12
   box-shadow shadow-all
13
   text-align center
13
   text-align center
14
   cursor pointer
14
   cursor pointer
15
-  &:nth-child(1)
16
-    margin-left 0
17
-  &:nth-last-child
18
-    margin-right 0

+ 3 - 2
frontend/src/component/Dashboard/MemberList.jsx View File

1
 import React from 'react'
1
 import React from 'react'
2
 import PropTypes from 'prop-types'
2
 import PropTypes from 'prop-types'
3
 // import { Checkbox } from 'tracim_frontend_lib'
3
 // import { Checkbox } from 'tracim_frontend_lib'
4
+import { generateAvatarFromPublicName } from 'tracim_frontend_lib'
4
 
5
 
5
 require('./MemberList.styl')
6
 require('./MemberList.styl')
6
 
7
 
110
                             onClick={() => props.onClickKnownMember(u)}
111
                             onClick={() => props.onClickKnownMember(u)}
111
                             key={u.user_id}
112
                             key={u.user_id}
112
                           >
113
                           >
113
-                            <div className='autocomplete__item__avatar primaryColorBorder'>
114
-                              <img src={u.avatar_url} />
114
+                            <div className='autocomplete__item__avatar'>
115
+                              <img src={u.avatar_url ? u.avatar_url : generateAvatarFromPublicName(u.public_name)} />
115
                             </div>
116
                             </div>
116
 
117
 
117
                             <div className='autocomplete__item__name'>
118
                             <div className='autocomplete__item__name'>

+ 18 - 4
frontend/src/component/Dashboard/MemberList.styl View File

97
               width 45px
97
               width 45px
98
               height 45px
98
               height 45px
99
               border-radius 50%
99
               border-radius 50%
100
-              border-width 1px
101
-              border-style solid
102
             &__name
100
             &__name
103
               margin-left 15px
101
               margin-left 15px
104
       .name__input
102
       .name__input
138
         padding 8px 30px
136
         padding 8px 30px
139
         cursor pointer
137
         cursor pointer
140
 
138
 
141
-@media (min-width min-sm) and (max-width: max-lg)
139
+/***** MEDIAQUERIES *****/
140
+
141
+/*** MEDIA 992px and 1199px ***/
142
+
143
+@media (min-width: min-lg) and (max-width: max-lg)
144
+
142
   .memberlist
145
   .memberlist
143
     width 50%
146
     width 50%
144
 
147
 
148
+/*** MEDIA 768px and 991px ***/
149
+
150
+@media (min-width: min-md) and (max-width: max-md)
151
+
152
+  .memberlist
153
+    width 50%
154
+
155
+/*** MEDIA 576px and 767px ***/
156
+
145
 @media (min-width: min-sm) and (max-width: max-sm)
157
 @media (min-width: min-sm) and (max-width: max-sm)
146
   .memberlist
158
   .memberlist
147
     margin 50px 0
159
     margin 50px 0
148
-    width 90%
160
+    width 100%
161
+
162
+/*** MEDIA 575px ***/
149
 
163
 
150
 @media (max-width: max-xs)
164
 @media (max-width: max-xs)
151
   .memberlist
165
   .memberlist

+ 1 - 1
frontend/src/component/Dashboard/MoreInfo.jsx View File

6
   <div className='moreinfo'>
6
   <div className='moreinfo'>
7
     <div className='moreinfo__webdav genericBtnInfoDashboard'>
7
     <div className='moreinfo__webdav genericBtnInfoDashboard'>
8
       <div
8
       <div
9
-        className='moreinfo__webdav__btn genericBtnInfoDashboard__btn'
9
+        className='moreinfo__webdav__btn genericBtnInfoDashboard__btn primaryColorBorderLighten primaryColorFontLighten'
10
         onClick={props.onClickToggleWebdav}
10
         onClick={props.onClickToggleWebdav}
11
       >
11
       >
12
         <div className='moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
12
         <div className='moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>

+ 19 - 3
frontend/src/component/Dashboard/RecentActivity.styl View File

43
       padding 10px 25px
43
       padding 10px 25px
44
       cursor pointer
44
       cursor pointer
45
 
45
 
46
-@media (min-width min-sm) and (max-width: max-lg)
46
+
47
+/**** MEDIAQUERIES ****/
48
+
49
+/**** MEDIA 992px and 1199px ****/
50
+
51
+@media (min-width min-lg) and (max-width: max-lg)
47
   .activity
52
   .activity
53
+    margin-right 0
48
     width 100%
54
     width 100%
49
 
55
 
56
+/**** MEDIA 768px and 991px ****/
57
+
50
 @media (min-width: min-md) and (max-width: max-md)
58
 @media (min-width: min-md) and (max-width: max-md)
51
   .activity
59
   .activity
52
-    margin 25px 15px 25px 0
60
+    margin 25px 0 25px 0
61
+    width 100%
62
+
63
+/**** MEDIA 576px and 767px ****/
53
 
64
 
54
 @media (min-width: min-sm) and (max-width: max-sm)
65
 @media (min-width: min-sm) and (max-width: max-sm)
55
   .activity
66
   .activity
56
-    margin 25px 15px 25px 0
67
+    margin 25px 0 25px 0
68
+    width 100%
69
+
70
+/**** MEDIA 575px ****/
57
 
71
 
58
 @media (max-width: max-xs)
72
 @media (max-width: max-xs)
59
   .activity
73
   .activity
60
     margin 25px 0
74
     margin 25px 0
61
     width 100%
75
     width 100%
76
+    &__wrapper
77
+      margin-top 45px
62
     &__header
78
     &__header
63
       display block
79
       display block
64
       height auto
80
       height auto

+ 2 - 2
frontend/src/component/Dashboard/UserStatus.jsx View File

40
         ? (
40
         ? (
41
           <div className='userstatus__notification__subscribe dropdown'>
41
           <div className='userstatus__notification__subscribe dropdown'>
42
             <button
42
             <button
43
-              className='userstatus__notification__subscribe__btn btn btn-outline-primary dropdown-toggle'
43
+              className='userstatus__notification__subscribe__btn btn btn-outline-primary dropdown-toggle primaryColorBorderLighten'
44
               type='button'
44
               type='button'
45
               id='dropdownMenuButton'
45
               id='dropdownMenuButton'
46
               data-toggle='dropdown'
46
               data-toggle='dropdown'
62
         )
62
         )
63
         : (
63
         : (
64
           <div
64
           <div
65
-            className='userstatus__notification__btn btn btn-outline-primary'
65
+            className='userstatus__notification__btn btn btn-outline-primary primaryColorBorderLighten'
66
             onClick={props.onClickToggleNotifBtn}
66
             onClick={props.onClickToggleNotifBtn}
67
           >
67
           >
68
             {props.t('Change your status')}
68
             {props.t('Change your status')}

+ 41 - 2
frontend/src/component/Dashboard/UserStatus.styl View File

16
     font-size 18px
16
     font-size 18px
17
     &__btn
17
     &__btn
18
       margin 20px 0
18
       margin 20px 0
19
-      border 1px solid thirdColor
19
+      border-width 1px
20
+      border-style solid
20
       padding 10px 15px
21
       padding 10px 15px
21
       cursor pointer
22
       cursor pointer
22
     &__subscribe
23
     &__subscribe
23
       &__btn
24
       &__btn
24
         margin 20px 0
25
         margin 20px 0
25
-        border 1px solid thirdColor
26
+        border-width 1px
27
+        border-style solid
26
         padding 10px 15px
28
         padding 10px 15px
27
       &__submenu
29
       &__submenu
28
         padding 0
30
         padding 0
29
         &__item
31
         &__item
30
           padding 10px
32
           padding 10px
33
+
34
+/**** MEDIAQUERIES ****/
35
+
36
+/**** MEDIA 992px & 1199px ****/
37
+
38
+@media (min-width: min-lg) and (max-width: max-lg)
39
+
40
+  .userstatus
41
+    width 100%
42
+    &__role
43
+      display flex
44
+
45
+/**** MEDIA 768px & 991px ****/
46
+
47
+@media (min-width: min-md) and (max-width: max-md)
48
+
49
+  .userstatus
50
+    width 100%
51
+    &__role
52
+      display flex
53
+
54
+/**** MEDIA 576px & 767px ****/
55
+
56
+@media (min-width: min-sm) and (max-width: max-sm)
57
+
58
+  .userstatus
59
+    width 100%
60
+    &__role
61
+      display flex
62
+
63
+/**** MEDIA 575px ****/
64
+
65
+@media (max-width: max-xs)
66
+
67
+  .userstatus
68
+    width 100%
69
+

+ 29 - 0
frontend/src/component/Header/MenuActionListItem/AdminLink.jsx View File

1
+import React from 'react'
2
+import { Link } from 'react-router-dom'
3
+import { PAGE } from '../../../helper.js'
4
+
5
+const AdminLink = props => {
6
+  return (
7
+    <li className='header__menu__rightside__adminlink'>
8
+      <div className='adminlink dropdown'>
9
+        <button className='adminlink__btn btn dropdown-toggle' type='button' data-toggle='dropdown'>
10
+          Administration
11
+        </button>
12
+
13
+        <div className='adminlink__setting dropdown-menu' aria-labelledby='dropdownMenuButton'>
14
+          <Link className='setting__link dropdown-item' to={PAGE.ADMIN.WORKSPACE}>
15
+            <i className='fa fa-fw fa-space-shuttle mr-2' />
16
+            {props.t('Admin workspace')}
17
+          </Link>
18
+
19
+          <Link className='setting__link dropdown-item' to={PAGE.ADMIN.USER}>
20
+            <i className='fa fa-fw fa-users mr-2' />
21
+            {props.t('Admin user')}
22
+          </Link>
23
+        </div>
24
+      </div>
25
+    </li>
26
+  )
27
+}
28
+
29
+export default AdminLink

+ 26 - 23
frontend/src/component/Header/MenuActionListItem/MenuProfil.jsx View File

5
 import { translate } from 'react-i18next'
5
 import { translate } from 'react-i18next'
6
 
6
 
7
 const MenuProfil = props => {
7
 const MenuProfil = props => {
8
-  return props.user.logged
9
-    ? (
10
-      <li className='header__menu__rightside__itemprofil'>
11
-        <div className='profilgroup dropdown'>
12
-          <button className='profilgroup__name btn btn-outline-primary dropdown-toggle' type='button' id='dropdownMenuButton' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
13
-            <img className='profilgroup__name__imgprofil' src={props.user.avatar_url} />
14
-            <div className='profilgroup__name__text'>
15
-              {props.user.name}
16
-            </div>
17
-          </button>
18
-          <div className='profilgroup__setting dropdown-menu' aria-labelledby='dropdownMenuButton'>
19
-            <Link className='setting__link dropdown-item' to={PAGE.ACCOUNT}>
20
-              <i className='fa fa-fw fa-user-o mr-2' />
21
-              {props.t('My Account')}
22
-            </Link>
23
-            {/* <div className='setting__link dropdown-item'>Mot de passe</div> */}
24
-            <div className='setting__link dropdown-item' onClick={props.onClickLogout}>
25
-              <i className='fa fa-fw fa-sign-out mr-2' />
26
-              {props.t('Logout')}
27
-            </div>
8
+  if (!props.user.logged) return null
9
+
10
+  return (
11
+    <li className='header__menu__rightside__itemprofil'>
12
+      <div className='profilgroup dropdown'>
13
+        <button className='profilgroup__name btn btn-outline-primary dropdown-toggle' type='button' id='dropdownMenuButton' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
14
+          <img className='profilgroup__name__imgprofil' src={props.user.avatar_url} />
15
+
16
+          <div className='profilgroup__name__text'>
17
+            {props.user.public_name}
18
+          </div>
19
+        </button>
20
+
21
+        <div className='profilgroup__setting dropdown-menu' aria-labelledby='dropdownMenuButton'>
22
+          <Link className='setting__link dropdown-item' to={PAGE.ACCOUNT}>
23
+            <i className='fa fa-fw fa-user-o mr-2' />
24
+            {props.t('My Account')}
25
+          </Link>
26
+
27
+          {/* <div className='setting__link dropdown-item'>Mot de passe</div> */}
28
+          <div className='setting__link dropdown-item' onClick={props.onClickLogout}>
29
+            <i className='fa fa-fw fa-sign-out mr-2' />
30
+            {props.t('Logout')}
28
           </div>
31
           </div>
29
         </div>
32
         </div>
30
-      </li>
31
-    )
32
-    : ''
33
+      </div>
34
+    </li>
35
+  )
33
 }
36
 }
34
 export default translate()(MenuProfil)
37
 export default translate()(MenuProfil)
35
 
38
 

+ 1 - 1
frontend/src/component/Header/MenuActionListItem/Search.jsx View File

14
           onChange={props.onChangeInput}
14
           onChange={props.onChangeInput}
15
         />
15
         />
16
         <button
16
         <button
17
-          className='search__addonsearch input-group-addon'
17
+          className='search__addonsearch input-group-addon primaryColorBgLightenHover primaryColorFontHover'
18
           id='headerInputSearch'
18
           id='headerInputSearch'
19
           onClick={props.onClickSubmit}
19
           onClick={props.onClickSubmit}
20
         >
20
         >

+ 1 - 1
frontend/src/container/Account.jsx View File

101
     })()
101
     })()
102
 
102
 
103
     return (
103
     return (
104
-      <div className='Account'>
104
+      <div className='account'>
105
         <PageWrapper customClass='account'>
105
         <PageWrapper customClass='account'>
106
           <PageTitle
106
           <PageTitle
107
             parentClass={'account'}
107
             parentClass={'account'}

+ 158 - 163
frontend/src/container/AdminWorkspacePage.jsx View File

1
 import React from 'react'
1
 import React from 'react'
2
-import Sidebar from './Sidebar.jsx'
3
 import {
2
 import {
4
   Delimiter,
3
   Delimiter,
5
   PageWrapper,
4
   PageWrapper,
11
 class AdminWorkspacePage extends React.Component {
10
 class AdminWorkspacePage extends React.Component {
12
   render () {
11
   render () {
13
     return (
12
     return (
14
-      <div className='sidebarpagecontainer'>
15
-        <Sidebar />
13
+      <PageWrapper customClass='adminWorkspacePage'>
14
+        <PageTitle
15
+          parentClass={'adminWorkspacePage'}
16
+          title={'Workspace management'}
17
+        />
16
 
18
 
17
-        <PageWrapper customClass='adminWorkspacePage'>
18
-          <PageTitle
19
-            parentClass={'adminWorkspacePage'}
20
-            title={'Workspace management'}
21
-          />
19
+        <PageContent parentClass='adminWorkspacePage'>
22
 
20
 
23
-          <PageContent parentClass='adminWorkspacePage'>
21
+          <div className='adminWorkspacePage__description'>
22
+            This page informs all workspaces of the instances
23
+          </div>
24
 
24
 
25
-            <div className='adminWorkspacePage__description'>
26
-              This page informs all workspaces of the instances
27
-            </div>
28
-
29
-            { /*
30
-              Alexi Cauvin 08/08/2018 => desactivate create workspace button due to redundancy
25
+          { /*
26
+            Alexi Cauvin 08/08/2018 => desactivate create workspace button due to redundancy
31
 
27
 
32
-              <div className='adminWorkspacePage__createworkspace'>
33
-                <button className='adminWorkspacePage__createworkspace__btncreate btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover'>
34
-                  {this.props.t('Create a workspace')}
35
-                </button>
36
-              </div>
37
-            */ }
28
+            <div className='adminWorkspacePage__createworkspace'>
29
+              <button className='adminWorkspacePage__createworkspace__btncreate btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover'>
30
+                {this.props.t('Create a workspace')}
31
+              </button>
32
+            </div>
33
+          */ }
38
 
34
 
39
-            <Delimiter customClass={'adminWorkspacePage__delimiter'} />
35
+          <Delimiter customClass={'adminWorkspacePage__delimiter'} />
40
 
36
 
41
-            <div className='adminWorkspacePage__workspaceTable'>
37
+          <div className='adminWorkspacePage__workspaceTable'>
42
 
38
 
43
-              <table className='table'>
44
-                <thead>
45
-                  <tr>
46
-                    <th scope='col'>ID</th>
47
-                    <th scope='col'>Workspace</th>
48
-                    <th scope='col'>Description</th>
49
-                    <th scope='col'>Member's number</th>
50
-                    { /*
51
-                      <th scope='col'>Calendar</th>
52
-                    */ }
53
-                    <th scope='col'>Delete workspace</th>
54
-                  </tr>
55
-                </thead>
56
-                <tbody>
57
-                  <tr>
58
-                    <th>1</th>
59
-                    <td>Design v_2</td>
60
-                    <td>Workspace about tracim v2 design</td>
61
-                    { /*
62
-                      <td className='d-flex align-items-center flex-wrap'>
63
-                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
64
-                          <i className='fa fa-fw fa-check-square-o' />
65
-                        </div>
66
-                        Enable
67
-                      </td>
68
-                    */ }
69
-                    <td>8</td>
70
-                    <td>
71
-                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
72
-                        <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
73
-                          <i className='fa fa-fw fa-trash-o' />
74
-                        </div>
75
-                        Delete
39
+            <table className='table'>
40
+              <thead>
41
+                <tr>
42
+                  <th scope='col'>ID</th>
43
+                  <th scope='col'>Workspace</th>
44
+                  <th scope='col'>Description</th>
45
+                  <th scope='col'>Member's number</th>
46
+                  { /*
47
+                    <th scope='col'>Calendar</th>
48
+                  */ }
49
+                  <th scope='col'>Delete workspace</th>
50
+                </tr>
51
+              </thead>
52
+              <tbody>
53
+                <tr>
54
+                  <th>1</th>
55
+                  <td>Design v_2</td>
56
+                  <td>Workspace about tracim v2 design</td>
57
+                  { /*
58
+                    <td className='d-flex align-items-center flex-wrap'>
59
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
60
+                        <i className='fa fa-fw fa-check-square-o' />
76
                       </div>
61
                       </div>
62
+                      Enable
77
                     </td>
63
                     </td>
78
-                  </tr>
79
-                  <tr>
80
-                    <th>2</th>
81
-                    <td>New features</td>
82
-                    <td>Add a new features : Annotated files</td>
83
-                    { /*
84
-                      <td className='d-flex align-items-center flex-wrap'>
85
-                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
86
-                          <i className='fa fa-fw fa-square-o' />
87
-                        </div>
88
-                        Disable
89
-                      </td>
90
-                    */ }
91
-                    <td>5</td>
92
-                    <td>
93
-                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
94
-                        <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
95
-                          <i className='fa fa-fw fa-trash-o' />
96
-                        </div>
97
-                        Delete
64
+                  */ }
65
+                  <td>8</td>
66
+                  <td>
67
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
68
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
69
+                        <i className='fa fa-fw fa-trash-o' />
98
                       </div>
70
                       </div>
71
+                      Delete
72
+                    </div>
73
+                  </td>
74
+                </tr>
75
+                <tr>
76
+                  <th>2</th>
77
+                  <td>New features</td>
78
+                  <td>Add a new features : Annotated files</td>
79
+                  { /*
80
+                    <td className='d-flex align-items-center flex-wrap'>
81
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
82
+                        <i className='fa fa-fw fa-square-o' />
83
+                      </div>
84
+                      Disable
99
                     </td>
85
                     </td>
100
-                  </tr>
101
-                  <tr>
102
-                    <th>3</th>
103
-                    <td>Fix Backend</td>
104
-                    <td>workspace referring to multiple issues on the backend </td>
105
-                    { /*
106
-                      <td className='d-flex align-items-center flex-wrap'>
107
-                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
108
-                          <i className='fa fa-fw fa-check-square-o' />
109
-                        </div>
110
-                        Enable
111
-                      </td>
112
-                    */ }
113
-                    <td>3</td>
114
-                    <td>
115
-                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
116
-                        <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
117
-                          <i className='fa fa-fw fa-trash-o' />
118
-                        </div>
119
-                        Delete
86
+                  */ }
87
+                  <td>5</td>
88
+                  <td>
89
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
90
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
91
+                        <i className='fa fa-fw fa-trash-o' />
92
+                      </div>
93
+                      Delete
94
+                    </div>
95
+                  </td>
96
+                </tr>
97
+                <tr>
98
+                  <th>3</th>
99
+                  <td>Fix Backend</td>
100
+                  <td>workspace referring to multiple issues on the backend </td>
101
+                  { /*
102
+                    <td className='d-flex align-items-center flex-wrap'>
103
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
104
+                        <i className='fa fa-fw fa-check-square-o' />
120
                       </div>
105
                       </div>
106
+                      Enable
121
                     </td>
107
                     </td>
122
-                  </tr>
123
-                  <tr>
124
-                    <th>4</th>
125
-                    <td>Design v_2</td>
126
-                    <td>Workspace about tracim v2 design</td>
127
-                    { /*
128
-                      <td className='d-flex align-items-center flex-wrap'>
129
-                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
130
-                          <i className='fa fa-fw fa-square-o' />
131
-                        </div>
132
-                        Disable
133
-                      </td>
134
-                    */ }
135
-                    <td>8</td>
136
-                    <td>
137
-                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
138
-                        <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
139
-                          <i className='fa fa-fw fa-trash-o' />
140
-                        </div>
141
-                        Delete
108
+                  */ }
109
+                  <td>3</td>
110
+                  <td>
111
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
112
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
113
+                        <i className='fa fa-fw fa-trash-o' />
142
                       </div>
114
                       </div>
115
+                      Delete
116
+                    </div>
117
+                  </td>
118
+                </tr>
119
+                <tr>
120
+                  <th>4</th>
121
+                  <td>Design v_2</td>
122
+                  <td>Workspace about tracim v2 design</td>
123
+                  { /*
124
+                    <td className='d-flex align-items-center flex-wrap'>
125
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
126
+                        <i className='fa fa-fw fa-square-o' />
127
+                      </div>
128
+                      Disable
143
                     </td>
129
                     </td>
144
-                  </tr>
145
-                  <tr>
146
-                    <th>5</th>
147
-                    <td>New features</td>
148
-                    <td>Add a new features : Annotated files</td>
149
-                    { /*
150
-                      <td className='d-flex align-items-center flex-wrap'>
151
-                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
152
-                          <i className='fa fa-fw fa-square-o' />
153
-                        </div>
154
-                        Disable
155
-                      </td>
156
-                    */ }
157
-                    <td>5</td>
158
-                    <td>
159
-                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
160
-                        <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
161
-                          <i className='fa fa-fw fa-trash-o' />
162
-                        </div>
163
-                        Delete
130
+                  */ }
131
+                  <td>8</td>
132
+                  <td>
133
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
134
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
135
+                        <i className='fa fa-fw fa-trash-o' />
136
+                      </div>
137
+                      Delete
138
+                    </div>
139
+                  </td>
140
+                </tr>
141
+                <tr>
142
+                  <th>5</th>
143
+                  <td>New features</td>
144
+                  <td>Add a new features : Annotated files</td>
145
+                  { /*
146
+                    <td className='d-flex align-items-center flex-wrap'>
147
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
148
+                        <i className='fa fa-fw fa-square-o' />
164
                       </div>
149
                       </div>
150
+                      Disable
165
                     </td>
151
                     </td>
166
-                  </tr>
167
-                  <tr>
168
-                    <th>6</th>
169
-                    <td>Fix Backend</td>
170
-                    <td>workspace referring to multiple issues on the backend </td>
171
-                    { /*
172
-                      <td className='d-flex align-items-center flex-wrap'>
173
-                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
174
-                          <i className='fa fa-fw fa-check-square-o' />
175
-                        </div>
176
-                        Enable
177
-                      </td>
178
-                    */ }
179
-                    <td>3</td>
180
-                    <td>
181
-                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
182
-                        <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
183
-                          <i className='fa fa-fw fa-trash-o' />
184
-                        </div>
185
-                        Delete
152
+                  */ }
153
+                  <td>5</td>
154
+                  <td>
155
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
156
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
157
+                        <i className='fa fa-fw fa-trash-o' />
158
+                      </div>
159
+                      Delete
160
+                    </div>
161
+                  </td>
162
+                </tr>
163
+                <tr>
164
+                  <th>6</th>
165
+                  <td>Fix Backend</td>
166
+                  <td>workspace referring to multiple issues on the backend </td>
167
+                  { /*
168
+                    <td className='d-flex align-items-center flex-wrap'>
169
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
170
+                        <i className='fa fa-fw fa-check-square-o' />
186
                       </div>
171
                       </div>
172
+                      Enable
187
                     </td>
173
                     </td>
188
-                  </tr>
189
-                </tbody>
190
-              </table>
191
-            </div>
174
+                  */ }
175
+                  <td>3</td>
176
+                  <td>
177
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
178
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
179
+                        <i className='fa fa-fw fa-trash-o' />
180
+                      </div>
181
+                      Delete
182
+                    </div>
183
+                  </td>
184
+                </tr>
185
+              </tbody>
186
+            </table>
187
+          </div>
192
 
188
 
193
-          </PageContent>
194
-        </PageWrapper>
195
-      </div>
189
+        </PageContent>
190
+      </PageWrapper>
196
     )
191
     )
197
   }
192
   }
198
 }
193
 }

frontend/src/container/AppFullscreenManager.jsx → frontend/src/container/AppFullscreenRouter.jsx View File

1
 import React from 'react'
1
 import React from 'react'
2
 import { connect } from 'react-redux'
2
 import { connect } from 'react-redux'
3
 import { withRouter } from 'react-router'
3
 import { withRouter } from 'react-router'
4
-import { Route } from 'react-router-dom'
5
-import { PAGE } from '../helper.js'
4
+import { Route, Redirect } from 'react-router-dom'
5
+import { PAGE, PROFILE } from '../helper.js'
6
 import appFactory from '../appFactory.js'
6
 import appFactory from '../appFactory.js'
7
 
7
 
8
-class AppFullscreenManager extends React.Component {
8
+class AppFullscreenRouter extends React.Component {
9
   constructor (props) {
9
   constructor (props) {
10
     super(props)
10
     super(props)
11
     this.state = {
11
     this.state = {
23
         <div id='appFullscreenContainer' />
23
         <div id='appFullscreenContainer' />
24
 
24
 
25
         {this.state.isMounted && (// we must wait for the component to be fully mounted to be sure the div#appFullscreenContainer exists in DOM
25
         {this.state.isMounted && (// we must wait for the component to be fully mounted to be sure the div#appFullscreenContainer exists in DOM
26
-          <div className='emptyDiForRoute'>
26
+          <div className='emptyDivForRoute'>
27
             <Route path={PAGE.ADMIN.WORKSPACE} render={() => {
27
             <Route path={PAGE.ADMIN.WORKSPACE} render={() => {
28
+              if (props.user.profile !== PROFILE.ADMINISTRATOR) return <Redirect to={{pathname: '/'}} />
29
+
28
               props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'workspace'}, props.user, {})
30
               props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'workspace'}, props.user, {})
29
               return null
31
               return null
30
             }} />
32
             }} />
31
 
33
 
32
             <Route path={PAGE.ADMIN.USER} render={() => {
34
             <Route path={PAGE.ADMIN.USER} render={() => {
35
+              if (props.user.profile !== PROFILE.ADMINISTRATOR) return <Redirect to={{pathname: '/'}} />
36
+
33
               props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'user'}, props.user, {})
37
               props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'user'}, props.user, {})
34
               return null
38
               return null
35
             }} />
39
             }} />
41
 }
45
 }
42
 
46
 
43
 const mapStateToProps = ({ user }) => ({ user })
47
 const mapStateToProps = ({ user }) => ({ user })
44
-export default connect(mapStateToProps)(withRouter(appFactory(AppFullscreenManager)))
48
+export default withRouter(connect(mapStateToProps)(appFactory(AppFullscreenRouter)))

+ 2 - 2
frontend/src/container/Dashboard.jsx View File

201
     const { props, state } = this
201
     const { props, state } = this
202
 
202
 
203
     return (
203
     return (
204
-      <div className='Dashboard' style={{width: '100%'}}>
204
+      <div className='dashboard'>
205
         <PageWrapper customeClass='dashboard'>
205
         <PageWrapper customeClass='dashboard'>
206
           <PageTitle
206
           <PageTitle
207
             parentClass='dashboard__header'
207
             parentClass='dashboard__header'
218
           <PageContent>
218
           <PageContent>
219
             <div className='dashboard__workspace-wrapper'>
219
             <div className='dashboard__workspace-wrapper'>
220
               <div className='dashboard__workspace'>
220
               <div className='dashboard__workspace'>
221
-                <div className='dashboard__workspace__title'>
221
+                <div className='dashboard__workspace__title primaryColorFont'>
222
                   {props.curWs.label}
222
                   {props.curWs.label}
223
                 </div>
223
                 </div>
224
 
224
 

+ 6 - 1
frontend/src/container/Header.jsx View File

13
 import MenuActionListItemHelp from '../component/Header/MenuActionListItem/Help.jsx'
13
 import MenuActionListItemHelp from '../component/Header/MenuActionListItem/Help.jsx'
14
 import MenuActionListItemMenuProfil from '../component/Header/MenuActionListItem/MenuProfil.jsx'
14
 import MenuActionListItemMenuProfil from '../component/Header/MenuActionListItem/MenuProfil.jsx'
15
 import MenuActionListItemNotification from '../component/Header/MenuActionListItem/Notification.jsx'
15
 import MenuActionListItemNotification from '../component/Header/MenuActionListItem/Notification.jsx'
16
+import MenuActionListAdminLink from '../component/Header/MenuActionListItem/AdminLink.jsx'
16
 import logoHeader from '../img/logo-tracim.png'
17
 import logoHeader from '../img/logo-tracim.png'
17
 import {
18
 import {
18
   newFlashMessage,
19
   newFlashMessage,
22
 import {
23
 import {
23
   postUserLogout
24
   postUserLogout
24
 } from '../action-creator.async.js'
25
 } from '../action-creator.async.js'
25
-import { COOKIE, PAGE } from '../helper.js'
26
+import { COOKIE, PAGE, PROFILE } from '../helper.js'
26
 
27
 
27
 class Header extends React.Component {
28
 class Header extends React.Component {
28
   handleClickLogo = () => {}
29
   handleClickLogo = () => {}
86
                 onClickSubmit={this.handleClickSubmit}
87
                 onClickSubmit={this.handleClickSubmit}
87
               />
88
               />
88
 
89
 
90
+              {user.profile === PROFILE.ADMINISTRATOR &&
91
+                <MenuActionListAdminLink t={this.props.t} />
92
+              }
93
+
89
               <MenuActionListItemDropdownLang
94
               <MenuActionListItemDropdownLang
90
                 langList={lang}
95
                 langList={lang}
91
                 idLangActive={user.lang}
96
                 idLangActive={user.lang}

+ 29 - 21
frontend/src/container/Login.jsx View File

17
   setUserConnected
17
   setUserConnected
18
 } from '../action-creator.sync.js'
18
 } from '../action-creator.sync.js'
19
 import { COOKIE, PAGE } from '../helper.js'
19
 import { COOKIE, PAGE } from '../helper.js'
20
+import { Checkbox } from 'tracim_frontend_lib'
20
 
21
 
21
 class Login extends React.Component {
22
 class Login extends React.Component {
22
   constructor (props) {
23
   constructor (props) {
36
 
37
 
37
   handleChangeLogin = e => this.setState({inputLogin: {...this.state.inputLogin, value: e.target.value}})
38
   handleChangeLogin = e => this.setState({inputLogin: {...this.state.inputLogin, value: e.target.value}})
38
   handleChangePassword = e => this.setState({inputPassword: {...this.state.inputPassword, value: e.target.value}})
39
   handleChangePassword = e => this.setState({inputPassword: {...this.state.inputPassword, value: e.target.value}})
39
-  handleChangeRememberMe = () => this.setState(prev => ({inputRememberMe: !prev.inputRememberMe}))
40
+  handleChangeRememberMe = e => {
41
+    e.preventDefault()
42
+    e.stopPropagation()
43
+    this.setState(prev => ({inputRememberMe: !prev.inputRememberMe}))
44
+  }
40
 
45
 
41
   handleClickSubmit = async () => {
46
   handleClickSubmit = async () => {
42
     const { history, dispatch, t } = this.props
47
     const { history, dispatch, t } = this.props
52
         logged: true
57
         logged: true
53
       }))
58
       }))
54
 
59
 
55
-      Cookies.set(COOKIE.USER_LOGIN, inputLogin.value)
56
-      Cookies.set(COOKIE.USER_AUTH, userAuth)
60
+      if (inputRememberMe) {
61
+        Cookies.set(COOKIE.USER_LOGIN, inputLogin.value, {expires: 365})
62
+        Cookies.set(COOKIE.USER_AUTH, userAuth, {expires: 365})
63
+      } else {
64
+        Cookies.set(COOKIE.USER_LOGIN, inputLogin.value)
65
+        Cookies.set(COOKIE.USER_AUTH, userAuth)
66
+      }
57
 
67
 
58
       history.push(PAGE.WORKSPACE.ROOT)
68
       history.push(PAGE.WORKSPACE.ROOT)
59
     } else if (fetchPostUserLogin.status === 403) {
69
     } else if (fetchPostUserLogin.status === 403) {
74
               <div className='col-12 col-sm-11 col-md-8 col-lg-6 col-xl-4'>
84
               <div className='col-12 col-sm-11 col-md-8 col-lg-6 col-xl-4'>
75
 
85
 
76
                 <Card customClass='loginpage__connection'>
86
                 <Card customClass='loginpage__connection'>
77
-                  <CardHeader customClass='connection__header text-center'>{this.props.t('Connection')}</CardHeader>
87
+                  <CardHeader customClass='connection__header primaryColorBgLighten text-center'>{this.props.t('Connection')}</CardHeader>
78
 
88
 
79
                   <CardBody formClass='connection__form'>
89
                   <CardBody formClass='connection__form'>
80
                     <div>
90
                     <div>
103
                       />
113
                       />
104
 
114
 
105
                       <div className='row align-items-center mt-4 mb-4'>
115
                       <div className='row align-items-center mt-4 mb-4'>
106
-
107
-                        {/*
108
-                          <div className='col-12 col-sm-6 col-md-6 col-lg-6 col-xl-6'>
109
-                            <InputCheckbox
110
-                              parentClassName='connection__form__rememberme'
111
-                              customClass=''
112
-                              label='Se souvenir de moi'
116
+                        <div className='col-12 col-sm-6 col-md-6 col-lg-6 col-xl-6'>
117
+                          <div className='connection__form__rememberme' onClick={this.handleChangeRememberMe}>
118
+                            <Checkbox
119
+                              name='inputRememberMe'
113
                               checked={this.state.inputRememberMe}
120
                               checked={this.state.inputRememberMe}
114
-                              onChange={this.handleChangeRememberMe}
115
                             />
121
                             />
122
+                            Se souvenir de moi
116
                           </div>
123
                           </div>
117
-                        */}
118
 
124
 
119
-                        <div className='col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6'>
120
                           <LoginBtnForgotPw
125
                           <LoginBtnForgotPw
121
                             customClass='connection__form__pwforgot'
126
                             customClass='connection__form__pwforgot'
122
                             label={this.props.t('Forgotten password ?')}
127
                             label={this.props.t('Forgotten password ?')}
123
                           />
128
                           />
124
                         </div>
129
                         </div>
125
-                        <Button
126
-                          htmlType='button'
127
-                          bootstrapType='primary'
128
-                          customClass='connection__form__btnsubmit ml-auto'
129
-                          label={this.props.t('Connection')}
130
-                          onClick={this.handleClickSubmit}
131
-                        />
130
+
131
+                        <div className='col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6'>
132
+                          <Button
133
+                            htmlType='button'
134
+                            bootstrapType='primary'
135
+                            customClass='connection__form__btnsubmit ml-auto'
136
+                            label={this.props.t('Connection')}
137
+                            onClick={this.handleClickSubmit}
138
+                          />
139
+                        </div>
132
                       </div>
140
                       </div>
133
                     </div>
141
                     </div>
134
 
142
 

+ 1 - 1
frontend/src/container/ProgressBar.jsx View File

13
               <span className='progress-right'>
13
               <span className='progress-right'>
14
                 <span className='progress-bar' />
14
                 <span className='progress-bar' />
15
               </span>
15
               </span>
16
-              <div className='progress-value'>
16
+              <div className='progress-value primaryColorBg'>
17
                 90%
17
                 90%
18
               </div>
18
               </div>
19
             </div>
19
             </div>

+ 1 - 0
frontend/src/container/Sidebar.jsx View File

83
     this.props.history.push(PAGE.WORKSPACE.CONTENT_LIST(idWs))
83
     this.props.history.push(PAGE.WORKSPACE.CONTENT_LIST(idWs))
84
   }
84
   }
85
 
85
 
86
+  // @DEPRECATED
86
   // not used, right now, link on sidebar filters is a <Link>
87
   // not used, right now, link on sidebar filters is a <Link>
87
   handleClickContentFilter = (idWs, filter) => {
88
   handleClickContentFilter = (idWs, filter) => {
88
     const { workspace, history } = this.props
89
     const { workspace, history } = this.props

+ 4 - 5
frontend/src/container/Tracim.jsx View File

5
 import Header from './Header.jsx'
5
 import Header from './Header.jsx'
6
 import Login from './Login.jsx'
6
 import Login from './Login.jsx'
7
 import Account from './Account.jsx'
7
 import Account from './Account.jsx'
8
-import AdminWorkspacePage from './AdminWorkspacePage.jsx'
9
-import AppFullscreenManager from './AppFullscreenManager.jsx'
8
+import AppFullscreenRouter from './AppFullscreenRouter.jsx'
10
 import FlashMessage from '../component/FlashMessage.jsx'
9
 import FlashMessage from '../component/FlashMessage.jsx'
11
 import WorkspaceContent from './WorkspaceContent.jsx'
10
 import WorkspaceContent from './WorkspaceContent.jsx'
12
 import WIPcomponent from './WIPcomponent.jsx'
11
 import WIPcomponent from './WIPcomponent.jsx'
130
           <Route path={PAGE.ADMIN.ROOT} render={() =>
129
           <Route path={PAGE.ADMIN.ROOT} render={() =>
131
             <div className='sidebarpagecontainer'>
130
             <div className='sidebarpagecontainer'>
132
               <Sidebar />
131
               <Sidebar />
133
-              <AppFullscreenManager />
132
+
133
+              <AppFullscreenRouter />
134
             </div>
134
             </div>
135
           } />
135
           } />
136
 
136
 
137
-          <Route path='/admin_temp/workspace' component={AdminWorkspacePage} />
138
-
139
           <Route path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
137
           <Route path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
140
 
138
 
141
           <div id='appFeatureContainer' />
139
           <div id='appFeatureContainer' />
140
+          <div id='popupCreateContentContainer' />
142
         </div>
141
         </div>
143
 
142
 
144
       </div>
143
       </div>

+ 30 - 8
frontend/src/container/WorkspaceContent.jsx View File

16
 } from 'tracim_frontend_lib'
16
 } from 'tracim_frontend_lib'
17
 import {
17
 import {
18
   getWorkspaceContentList,
18
   getWorkspaceContentList,
19
-  getFolderContent
19
+  getFolderContent,
20
+  putWorkspaceContentArchived,
21
+  putWorkspaceContentDeleted
20
 } from '../action-creator.async.js'
22
 } from '../action-creator.async.js'
21
 import {
23
 import {
22
   newFlashMessage,
24
   newFlashMessage,
23
-  setWorkspaceContentList
25
+  setWorkspaceContentList,
26
+  setWorkspaceContentArchived,
27
+  setWorkspaceContentDeleted
24
 } from '../action-creator.sync.js'
28
 } from '../action-creator.sync.js'
25
 
29
 
26
 const qs = require('query-string')
30
 const qs = require('query-string')
125
     console.log('%c<WorkspaceContent> download nyi', 'color: #c17838', content)
129
     console.log('%c<WorkspaceContent> download nyi', 'color: #c17838', content)
126
   }
130
   }
127
 
131
 
128
-  handleClickArchiveContentItem = (e, content) => {
132
+  handleClickArchiveContentItem = async (e, content) => {
133
+    const { props, state } = this
134
+
129
     e.stopPropagation()
135
     e.stopPropagation()
130
-    console.log('%c<WorkspaceContent> archive nyi', 'color: #c17838', content)
136
+
137
+    const fetchPutContentArchived = await props.dispatch(putWorkspaceContentArchived(props.user, content.idWorkspace, content.id))
138
+    switch (fetchPutContentArchived.status) {
139
+      case 204:
140
+        props.dispatch(setWorkspaceContentArchived(content.idWorkspace, content.id))
141
+        this.loadContentList(state.workspaceIdInUrl)
142
+        break
143
+      default: props.dispatch(newFlashMessage(props.t('Error while archiving document')))
144
+    }
131
   }
145
   }
132
 
146
 
133
-  handleClickDeleteContentItem = (e, content) => {
147
+  handleClickDeleteContentItem = async (e, content) => {
148
+    const { props, state } = this
149
+
134
     e.stopPropagation()
150
     e.stopPropagation()
135
-    console.log('%c<WorkspaceContent> delete nyi', 'color: #c17838', content)
151
+
152
+    const fetchPutContentDeleted = await props.dispatch(putWorkspaceContentDeleted(props.user, content.idWorkspace, content.id))
153
+    switch (fetchPutContentDeleted.status) {
154
+      case 204:
155
+        props.dispatch(setWorkspaceContentDeleted(content.idWorkspace, content.id))
156
+        this.loadContentList(state.workspaceIdInUrl)
157
+        break
158
+      default: props.dispatch(newFlashMessage(props.t('Error while deleting document')))
159
+    }
136
   }
160
   }
137
 
161
 
138
   handleClickFolder = folderId => {
162
   handleClickFolder = folderId => {
197
           </PageTitle>
221
           </PageTitle>
198
 
222
 
199
           <PageContent parentClass='workspace__content'>
223
           <PageContent parentClass='workspace__content'>
200
-            <div id='popupCreateContentContainer' />
201
-
202
             <div className='workspace__content__fileandfolder folder__content active'>
224
             <div className='workspace__content__fileandfolder folder__content active'>
203
               <ContentItemHeader />
225
               <ContentItemHeader />
204
 
226
 

+ 6 - 5
frontend/src/css/AccountPage.styl View File

42
   width 30px
42
   width 30px
43
 
43
 
44
 .account
44
 .account
45
+  margin 45px 0 0 0
46
+  width 100%
45
   .btn-outline-primary
47
   .btn-outline-primary
46
     color thirdColor
48
     color thirdColor
47
     border-color thirdColor
49
     border-color thirdColor
67
         height 150px
69
         height 150px
68
     &__name
70
     &__name
69
       font-size 22px
71
       font-size 22px
70
-    &__email
71
-      color thirdColor
72
     &__company
72
     &__company
73
       font-size 20px
73
       font-size 20px
74
-      color thirdColor
75
   &__delimiter
74
   &__delimiter
76
     position relative
75
     position relative
77
     top 3px
76
     top 3px
131
           &__txtinput
130
           &__txtinput
132
             display block
131
             display block
133
             width auto
132
             width auto
134
-            border 1px solid thirdColor
133
+            border-width 1px
134
+            border-style solid
135
             border-radius 5px
135
             border-radius 5px
136
           &__button
136
           &__button
137
             vertical-align top
137
             vertical-align top
138
-            border 1px solid thirdColor
138
+            border-width 1px
139
+            border-style solid
139
             border-radius 5px
140
             border-radius 5px
140
             padding 8px 25px
141
             padding 8px 25px
141
             cursor pointer
142
             cursor pointer

+ 0 - 21
frontend/src/css/AdminWorkspacePage.styl View File

1
-.table th
2
-  vertical-align middle
3
-
4
-.adminWorkspacePage
5
-  &__createworkspace
6
-    &__btncreate
7
-      margin 25px 15px
8
-  &__description
9
-    margin 25px 0
10
-    font-size 20px
11
-  &__delimiter
12
-    margin 65px auto
13
-  &__workspaceTable
14
-    margin 25px 15px
15
-    &__deleteworkspace
16
-      display flex
17
-      align-items center
18
-      flex-wrap wrap
19
-      cursor pointer
20
-
21
-

+ 22 - 12
frontend/src/css/Dashboard.styl View File

60
     &__calltoaction
60
     &__calltoaction
61
       justify-content center
61
       justify-content center
62
 
62
 
63
+
64
+/**** MEDIAQUERIES ****/
65
+
63
 /**** MEDIA 992px & 1199px ****/
66
 /**** MEDIA 992px & 1199px ****/
64
 
67
 
65
 @media (min-width: min-lg) and (max-width: max-lg)
68
 @media (min-width: min-lg) and (max-width: max-lg)
66
 
69
 
67
   .dashboard
70
   .dashboard
68
-    margin-left 15px
71
+    &__workspace
72
+      width 100%
73
+
74
+/**** MEDIA 768px & 991px ****/
75
+
76
+@media (min-width: min-md) and (max-width: max-md)
77
+
78
+  .dashboard
79
+    &__workspace
80
+      width 100%
69
 
81
 
70
 /**** MEDIA 576px & 767px ****/
82
 /**** MEDIA 576px & 767px ****/
71
 
83
 
72
 @media (min-width: min-sm) and (max-width: max-sm)
84
 @media (min-width: min-sm) and (max-width: max-sm)
73
 
85
 
74
   .dashboard
86
   .dashboard
75
-    &__activity
76
-      margin 25px 15px 25px 0
87
+    &__workspace
88
+      width 100%
77
 
89
 
78
 /**** MEDIA 575px ****/
90
 /**** MEDIA 575px ****/
79
 
91
 
80
 @media (max-width: max-xs)
92
 @media (max-width: max-xs)
81
 
93
 
82
   .dashboard
94
   .dashboard
83
-    margin-left 0
84
-    &__title
85
-      margin-left 10px
95
+    &__header
96
+      &__advancedmode
97
+        margin 25px 15px
86
     &__workspace
98
     &__workspace
87
-      margin-left 10px
88
-      width auto
89
-    &__userstatut
90
-      margin-left 10px
91
-      width auto
99
+      width 100%
92
     &__calltoaction
100
     &__calltoaction
101
+      flex-direction column
102
+      align-items center
93
       justify-content center
103
       justify-content center
94
       &__button
104
       &__button
95
-        margin 10px
105
+        margin 20px 0

+ 2 - 2
frontend/src/css/Generic.styl View File

202
   &__btn
202
   &__btn
203
     display flex
203
     display flex
204
     margin-bottom 30px
204
     margin-bottom 30px
205
-    border 1px solid thirdColor
205
+    border-width 1px
206
+    border-style solid
206
     border-radius 10px
207
     border-radius 10px
207
     padding 15px 25px
208
     padding 15px 25px
208
-    color thirdColor
209
     cursor pointer
209
     cursor pointer
210
     &:hover, &:focus
210
     &:hover, &:focus
211
       background-color thirdColor
211
       background-color thirdColor

+ 14 - 3
frontend/src/css/Header.styl View File

24
       display flex
24
       display flex
25
       margin-top 15px
25
       margin-top 15px
26
       list-style none
26
       list-style none
27
+      &__adminlink
28
+        .adminlink
29
+          &__btn
30
+            margin-right 15px
31
+            border 1px solid grey
32
+            border-radius 5px
33
+            background-color transparent
34
+            &::after
35
+              margin-left 15px
36
+          &__setting
37
+            padding 0
38
+            .setting__link
39
+              padding 10px
27
       &__itemsearch
40
       &__itemsearch
28
         display none
41
         display none
29
         margin-right 8%
42
         margin-right 8%
33
           .search__addonsearch
46
           .search__addonsearch
34
             background-color transparent
47
             background-color transparent
35
             cursor pointer
48
             cursor pointer
36
-            &:hover
37
-              background-color thirdColor
38
-              color mainColor
39
       .btnnavbar
49
       .btnnavbar
40
         border 1px solid grey
50
         border 1px solid grey
41
         background-color transparent
51
         background-color transparent
99
             padding 0
109
             padding 0
100
             left inherit
110
             left inherit
101
             right 0
111
             right 0
112
+            width 100%
102
             cursor pointer
113
             cursor pointer
103
             .setting__link
114
             .setting__link
104
               padding 10px
115
               padding 10px

+ 8 - 12
frontend/src/css/Login.styl View File

15
     border none
15
     border none
16
     box-shadow shadow-right
16
     box-shadow shadow-right
17
     .connection__header
17
     .connection__header
18
-      background-color thirdColor
19
-      color #FFF
18
+      color off-white
20
       font-size 25px
19
       font-size 25px
21
   .connection__form
20
   .connection__form
22
     &__rememberme
21
     &__rememberme
23
-      &__label
24
-        font-size 13px
22
+      margin-bottom 10px
23
+      font-size 14px
24
+      line-height 23px
25
+      cursor pointer
26
+      label
27
+        margin-right 8px
28
+        top 4px
25
     &__groupemail
29
     &__groupemail
26
       position relative
30
       position relative
27
       &__icon
31
       &__icon
55
     &__pwforgot
59
     &__pwforgot
56
       cursor pointer
60
       cursor pointer
57
       font-size 13px
61
       font-size 13px
58
-      &:hover::after
59
-        position absolute
60
-        top 20px
61
-        left 15px
62
-        border-bottom 1px solid darkGrey
63
-        padding-bottom 2px
64
-        content ' '
65
-        width 130px
66
   &__footer
62
   &__footer
67
     position fixed
63
     position fixed
68
     bottom 2%
64
     bottom 2%

+ 0 - 1
frontend/src/css/ProgressBar.styl View File

64
     height 90%
64
     height 90%
65
     border 10px solid off-white
65
     border 10px solid off-white
66
     border-radius 50%
66
     border-radius 50%
67
-    background darkBlue
68
     font-size 24px
67
     font-size 24px
69
     color off-white
68
     color off-white
70
     line-height 135px
69
     line-height 135px

+ 0 - 2
frontend/src/css/index.styl View File

30
 @import 'HomepageCard'
30
 @import 'HomepageCard'
31
 
31
 
32
 @import 'ExtandedAction'
32
 @import 'ExtandedAction'
33
-
34
-@import 'AdminWorkspacePage'

+ 6 - 1
frontend/src/helper.js View File

37
   ADMIN: {
37
   ADMIN: {
38
     ROOT: '/admin',
38
     ROOT: '/admin',
39
     WORKSPACE: '/admin/workspace',
39
     WORKSPACE: '/admin/workspace',
40
-    USEr: '/admin/user'
40
+    USER: '/admin/user'
41
   }
41
   }
42
 }
42
 }
43
 
43
 
67
   label: 'Workspace manager'
67
   label: 'Workspace manager'
68
 }]
68
 }]
69
 
69
 
70
+export const PROFILE = {
71
+  ADMINISTRATOR: 'administrators',
72
+  USER: 'users'
73
+}
74
+
70
 export const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route
75
 export const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route

+ 30 - 14
frontend/src/reducer/workspaceContentList.js View File

3
   UPDATE,
3
   UPDATE,
4
   WORKSPACE,
4
   WORKSPACE,
5
   WORKSPACE_CONTENT,
5
   WORKSPACE_CONTENT,
6
-  FOLDER
6
+  FOLDER,
7
+  WORKSPACE_CONTENT_ARCHIVED,
8
+  WORKSPACE_CONTENT_DELETED
7
 } from '../action-creator.sync.js'
9
 } from '../action-creator.sync.js'
8
 
10
 
9
 export default function workspaceContentList (state = [], action) {
11
 export default function workspaceContentList (state = [], action) {
10
   switch (action.type) {
12
   switch (action.type) {
11
     case `${SET}/${WORKSPACE_CONTENT}`:
13
     case `${SET}/${WORKSPACE_CONTENT}`:
12
-      return action.workspaceContentList.map(wsc => ({
13
-        id: wsc.content_id,
14
-        label: wsc.label,
15
-        slug: wsc.slug,
16
-        type: wsc.content_type,
17
-        idWorkspace: wsc.workspace_id,
18
-        isArchived: wsc.is_archived,
19
-        idParent: wsc.parent_id,
20
-        isDeleted: wsc.is_deleted,
21
-        showInUi: wsc.show_in_ui,
22
-        statusSlug: wsc.status,
23
-        subContentTypeSlug: wsc.sub_content_type_slug
24
-      }))
14
+      return action.workspaceContentList
15
+        .sort((a, b) => a.slug < b.slug ? -1 : 1)
16
+        .map(wsc => ({
17
+          id: wsc.content_id,
18
+          label: wsc.label,
19
+          slug: wsc.slug,
20
+          type: wsc.content_type,
21
+          idWorkspace: wsc.workspace_id,
22
+          isArchived: wsc.is_archived,
23
+          idParent: wsc.parent_id,
24
+          isDeleted: wsc.is_deleted,
25
+          showInUi: wsc.show_in_ui,
26
+          statusSlug: wsc.status,
27
+          subContentTypeSlug: wsc.sub_content_type_slug
28
+        }))
25
 
29
 
26
     case `${UPDATE}/${WORKSPACE}/Filter`: // not used anymore ?
30
     case `${UPDATE}/${WORKSPACE}/Filter`: // not used anymore ?
27
       return {...state, filter: action.filterList}
31
       return {...state, filter: action.filterList}
40
         content: state.content.map(c => setFolderContent(c, action))
44
         content: state.content.map(c => setFolderContent(c, action))
41
       }
45
       }
42
 
46
 
47
+    case `${SET}/${WORKSPACE_CONTENT_ARCHIVED}`:
48
+      return state.map(wsc => wsc.idWorkspace === action.idWorkspace && wsc.id === action.idContent
49
+        ? {...wsc, isArchived: true}
50
+        : wsc
51
+      )
52
+
53
+    case `${SET}/${WORKSPACE_CONTENT_DELETED}`:
54
+      return state.map(wsc => wsc.idWorkspace === action.idWorkspace && wsc.id === action.idContent
55
+        ? {...wsc, isDeleted: true}
56
+        : wsc
57
+      )
58
+
43
     default:
59
     default:
44
       return state
60
       return state
45
   }
61
   }

+ 194 - 0
frontend_app_admin_workspace_user/src/component/AdminWorkspace.jsx View File

1
+import React from 'react'
2
+import {
3
+  Delimiter,
4
+  PageWrapper,
5
+  PageTitle,
6
+  PageContent
7
+} from 'tracim_frontend_lib'
8
+
9
+export class AdminWorkspace extends React.Component {
10
+  render () {
11
+    return (
12
+      <PageWrapper customClass='adminWorkspacePage'>
13
+        <PageTitle
14
+          parentClass={'adminWorkspacePage'}
15
+          title={'Workspace management'}
16
+        />
17
+
18
+        <PageContent parentClass='adminWorkspacePage'>
19
+
20
+          <div className='adminWorkspacePage__description'>
21
+            List of every workspaces
22
+          </div>
23
+
24
+          { /*
25
+            Alexi Cauvin 08/08/2018 => desactivate create workspace button due to redundancy
26
+
27
+            <div className='adminWorkspacePage__createworkspace'>
28
+              <button className='adminWorkspacePage__createworkspace__btncreate btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover'>
29
+                {this.props.t('Create a workspace')}
30
+              </button>
31
+            </div>
32
+          */ }
33
+
34
+          <Delimiter customClass={'adminWorkspacePage__delimiter'} />
35
+
36
+          <div className='adminWorkspacePage__workspaceTable'>
37
+
38
+            <table className='table'>
39
+              <thead>
40
+                <tr>
41
+                  <th scope='col'>ID</th>
42
+                  <th scope='col'>Workspace</th>
43
+                  <th scope='col'>Description</th>
44
+                  <th scope='col'>Member count</th>
45
+                  { /*
46
+                      <th scope='col'>Calendar</th>
47
+                    */ }
48
+                  <th scope='col'>Delete workspace</th>
49
+                </tr>
50
+              </thead>
51
+              <tbody>
52
+                <tr>
53
+                  <th>1</th>
54
+                  <td>Design v_2</td>
55
+                  <td>Workspace about tracim v2 design</td>
56
+                  { /*
57
+                      <td className='d-flex align-items-center flex-wrap'>
58
+                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
59
+                          <i className='fa fa-fw fa-check-square-o' />
60
+                        </div>
61
+                        Enable
62
+                      </td>
63
+                    */ }
64
+                  <td>8</td>
65
+                  <td>
66
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
67
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
68
+                        <i className='fa fa-fw fa-trash-o' />
69
+                      </div>
70
+                      Delete
71
+                    </div>
72
+                  </td>
73
+                </tr>
74
+                <tr>
75
+                  <th>2</th>
76
+                  <td>New features</td>
77
+                  <td>Add a new features : Annotated files</td>
78
+                  { /*
79
+                      <td className='d-flex align-items-center flex-wrap'>
80
+                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
81
+                          <i className='fa fa-fw fa-square-o' />
82
+                        </div>
83
+                        Disable
84
+                      </td>
85
+                    */ }
86
+                  <td>5</td>
87
+                  <td>
88
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
89
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
90
+                        <i className='fa fa-fw fa-trash-o' />
91
+                      </div>
92
+                      Delete
93
+                    </div>
94
+                  </td>
95
+                </tr>
96
+                <tr>
97
+                  <th>3</th>
98
+                  <td>Fix Backend</td>
99
+                  <td>workspace referring to multiple issues on the backend </td>
100
+                  { /*
101
+                      <td className='d-flex align-items-center flex-wrap'>
102
+                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
103
+                          <i className='fa fa-fw fa-check-square-o' />
104
+                        </div>
105
+                        Enable
106
+                      </td>
107
+                    */ }
108
+                  <td>3</td>
109
+                  <td>
110
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
111
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
112
+                        <i className='fa fa-fw fa-trash-o' />
113
+                      </div>
114
+                      Delete
115
+                    </div>
116
+                  </td>
117
+                </tr>
118
+                <tr>
119
+                  <th>4</th>
120
+                  <td>Design v_2</td>
121
+                  <td>Workspace about tracim v2 design</td>
122
+                  { /*
123
+                      <td className='d-flex align-items-center flex-wrap'>
124
+                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
125
+                          <i className='fa fa-fw fa-square-o' />
126
+                        </div>
127
+                        Disable
128
+                      </td>
129
+                    */ }
130
+                  <td>8</td>
131
+                  <td>
132
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
133
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
134
+                        <i className='fa fa-fw fa-trash-o' />
135
+                      </div>
136
+                      Delete
137
+                    </div>
138
+                  </td>
139
+                </tr>
140
+                <tr>
141
+                  <th>5</th>
142
+                  <td>New features</td>
143
+                  <td>Add a new features : Annotated files</td>
144
+                  { /*
145
+                      <td className='d-flex align-items-center flex-wrap'>
146
+                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
147
+                          <i className='fa fa-fw fa-square-o' />
148
+                        </div>
149
+                        Disable
150
+                      </td>
151
+                    */ }
152
+                  <td>5</td>
153
+                  <td>
154
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
155
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
156
+                        <i className='fa fa-fw fa-trash-o' />
157
+                      </div>
158
+                      Delete
159
+                    </div>
160
+                  </td>
161
+                </tr>
162
+                <tr>
163
+                  <th>6</th>
164
+                  <td>Fix Backend</td>
165
+                  <td>workspace referring to multiple issues on the backend </td>
166
+                  { /*
167
+                      <td className='d-flex align-items-center flex-wrap'>
168
+                        <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
169
+                          <i className='fa fa-fw fa-check-square-o' />
170
+                        </div>
171
+                        Enable
172
+                      </td>
173
+                    */ }
174
+                  <td>3</td>
175
+                  <td>
176
+                    <div className='adminWorkspacePage__workspaceTable__deleteworkspace primaryColorFont primaryColorFontDarkenHover'>
177
+                      <div className='adminWorkspacePage__workspaceTable__deleteworkspace__removalicon mr-3'>
178
+                        <i className='fa fa-fw fa-trash-o' />
179
+                      </div>
180
+                      Delete
181
+                    </div>
182
+                  </td>
183
+                </tr>
184
+              </tbody>
185
+            </table>
186
+          </div>
187
+
188
+        </PageContent>
189
+      </PageWrapper>
190
+    )
191
+  }
192
+}
193
+
194
+export default AdminWorkspace

+ 17 - 17
frontend_app_admin_workspace_user/src/container/AdminWorkspaceUser.jsx View File

2
 import { translate } from 'react-i18next'
2
 import { translate } from 'react-i18next'
3
 import i18n from '../i18n.js'
3
 import i18n from '../i18n.js'
4
 import {
4
 import {
5
-  addAllResourceI18n,
6
-  // handleFetchResult,
7
-  PageWrapper,
8
-  PageTitle,
9
-  PageContent
5
+  addAllResourceI18n
6
+  // handleFetchResult
10
 } from 'tracim_frontend_lib'
7
 } from 'tracim_frontend_lib'
11
 import { debug } from '../helper.js'
8
 import { debug } from '../helper.js'
12
 import {
9
 import {
13
 } from '../action.async.js'
10
 } from '../action.async.js'
11
+import AdminWorkspace from '../component/AdminWorkspace.jsx'
12
+
13
+require('../css/index.styl')
14
 
14
 
15
 class AdminWorkspaceUser extends React.Component {
15
 class AdminWorkspaceUser extends React.Component {
16
   constructor (props) {
16
   constructor (props) {
32
 
32
 
33
   customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
33
   customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
34
     switch (type) {
34
     switch (type) {
35
-      // console.log('%c<AdminWorkspaceUser> Custom event', 'color: #28a745', type, data)
35
+      case 'admin_workspace_user_showApp':
36
+        console.log('%c<AdminWorkspaceUser> Custom event', 'color: #28a745', type, data)
37
+        this.setState({config: data.config})
38
+        break
36
       default:
39
       default:
37
         break
40
         break
38
     }
41
     }
48
     const { state } = this
51
     const { state } = this
49
 
52
 
50
     console.log('%c<AdminWorkspaceUser> did update', `color: ${this.state.config.hexcolor}`, prevState, state)
53
     console.log('%c<AdminWorkspaceUser> did update', `color: ${this.state.config.hexcolor}`, prevState, state)
54
+    if (prevState.config.type !== state.config.type) this.loadContent()
51
   }
55
   }
52
 
56
 
53
   componentWillUnmount () {
57
   componentWillUnmount () {
67
 
71
 
68
     return (
72
     return (
69
       <div>
73
       <div>
70
-        <PageWrapper customeClass='admin'>
71
-          <PageTitle
72
-            parentClass='admin__header'
73
-            customClass='justify-content-between'
74
-            title={'Admin'}
75
-          />
76
-
77
-          <PageContent parentClass='workspace__content'>
78
-            woot { this.state.config.type }
79
-          </PageContent>
80
-        </PageWrapper>
74
+        {this.state.config.type === 'workspace' &&
75
+          <AdminWorkspace />
76
+        }
77
+
78
+        {this.state.config.type === 'user' &&
79
+          <div>not yet implemented</div>
80
+        }
81
       </div>
81
       </div>
82
     )
82
     )
83
   }
83
   }

+ 20 - 0
frontend_app_admin_workspace_user/src/css/index.styl View File

1
 @import "../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
1
 @import "../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+
3
+.table th
4
+  vertical-align middle
5
+
6
+.adminWorkspacePage
7
+  &__createworkspace
8
+    &__btncreate
9
+      margin 25px 15px
10
+  &__description
11
+    margin 25px 0
12
+    font-size 20px
13
+  &__delimiter
14
+    margin 65px auto
15
+  &__workspaceTable
16
+    margin 25px 15px
17
+    &__deleteworkspace
18
+      display flex
19
+      align-items center
20
+      flex-wrap wrap
21
+      cursor pointer

+ 10 - 0
frontend_app_html-document/src/action.async.js View File

117
     method: 'PUT'
117
     method: 'PUT'
118
   })
118
   })
119
 }
119
 }
120
+
121
+export const putHtmlDocRead = (user, apiUrl, idWorkspace, idContent) => {
122
+  return fetch(`${apiUrl}/users/${user.user_id}/workspaces/${idWorkspace}/contents/${idContent}/read`, {
123
+    headers: {
124
+      'Authorization': 'Basic ' + user.auth,
125
+      ...FETCH_CONFIG.headers
126
+    },
127
+    method: 'PUT'
128
+  })
129
+}

+ 5 - 1
frontend_app_html-document/src/container/HtmlDocument.jsx View File

26
   putHtmlDocIsArchived,
26
   putHtmlDocIsArchived,
27
   putHtmlDocIsDeleted,
27
   putHtmlDocIsDeleted,
28
   putHtmlDocRestoreArchived,
28
   putHtmlDocRestoreArchived,
29
-  putHtmlDocRestoreDeleted
29
+  putHtmlDocRestoreDeleted,
30
+  putHtmlDocRead
30
 } from '../action.async.js'
31
 } from '../action.async.js'
31
 
32
 
32
 class HtmlDocument extends React.Component {
33
 class HtmlDocument extends React.Component {
165
         console.log('Error loading Timeline.', e)
166
         console.log('Error loading Timeline.', e)
166
         this.setState({timeline: []})
167
         this.setState({timeline: []})
167
       })
168
       })
169
+
170
+    await Promise.all([fetchResultHtmlDocument, fetchResultComment, fetchResultRevision])
171
+    putHtmlDocRead(loggedUser, config.apiUrl, content.workspace_id, content.content_id) // mark as read after all requests are finished
168
   }
172
   }
169
 
173
 
170
   handleClickBtnCloseApp = () => {
174
   handleClickBtnCloseApp = () => {

+ 10 - 0
frontend_app_thread/src/action.async.js View File

108
     method: 'PUT'
108
     method: 'PUT'
109
   })
109
   })
110
 }
110
 }
111
+
112
+export const putThreadRead = (user, apiUrl, idWorkspace, idContent) => {
113
+  return fetch(`${apiUrl}/users/${user.user_id}/workspaces/${idWorkspace}/contents/${idContent}/read`, {
114
+    headers: {
115
+      'Authorization': 'Basic ' + user.auth,
116
+      ...FETCH_CONFIG.headers
117
+    },
118
+    method: 'PUT'
119
+  })
120
+}

+ 20 - 15
frontend_app_thread/src/container/Thread.jsx View File

22
   putThreadIsArchived,
22
   putThreadIsArchived,
23
   putThreadIsDeleted,
23
   putThreadIsDeleted,
24
   putThreadRestoreArchived,
24
   putThreadRestoreArchived,
25
-  putThreadRestoreDeleted
25
+  putThreadRestoreDeleted,
26
+  putThreadRead
26
 } from '../action.async.js'
27
 } from '../action.async.js'
27
 
28
 
28
 class Thread extends React.Component {
29
 class Thread extends React.Component {
108
       handleFetchResult(await fetchResultThread),
109
       handleFetchResult(await fetchResultThread),
109
       handleFetchResult(await fetchResultThreadComment)
110
       handleFetchResult(await fetchResultThreadComment)
110
     ])
111
     ])
111
-      .then(([resThread, resComment]) => this.setState({
112
-        content: resThread.body,
113
-        listMessage: resComment.body.map(c => ({
114
-          ...c,
115
-          timelineType: 'comment',
116
-          created: (new Date(c.created)).toLocaleString(),
117
-          author: {
118
-            ...c.author,
119
-            avatar_url: c.author.avatar_url
120
-              ? c.author.avatar_url
121
-              : generateAvatarFromPublicName(c.author.public_name)
122
-          }
123
-        }))
124
-      }))
112
+      .then(([resThread, resComment]) => {
113
+        this.setState({
114
+          content: resThread.body,
115
+          listMessage: resComment.body.map(c => ({
116
+            ...c,
117
+            timelineType: 'comment',
118
+            created: (new Date(c.created)).toLocaleString(),
119
+            author: {
120
+              ...c.author,
121
+              avatar_url: c.author.avatar_url
122
+                ? c.author.avatar_url
123
+                : generateAvatarFromPublicName(c.author.public_name)
124
+            }
125
+          }))
126
+        })
127
+
128
+        putThreadRead(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
129
+      })
125
       .catch(e => console.log('Error loading Thread data.', e))
130
       .catch(e => console.log('Error loading Thread data.', e))
126
   }
131
   }
127
 
132
 

+ 0 - 2
frontend_lib/src/component/Input/Checkbox.jsx View File

59
 }
59
 }
60
 
60
 
61
 Checkbox.defaultProps = {
61
 Checkbox.defaultProps = {
62
-  name: '',
63
-  onClickCheckbox: () => {},
64
   checked: false,
62
   checked: false,
65
   disabled: false
63
   disabled: false
66
 }
64
 }

+ 1 - 1
frontend_lib/src/component/Timeline/Timeline.jsx View File

189
   isArchived: PropTypes.bool,
189
   isArchived: PropTypes.bool,
190
   onClickRestoreArchived: PropTypes.func,
190
   onClickRestoreArchived: PropTypes.func,
191
   isDeleted: PropTypes.bool,
191
   isDeleted: PropTypes.bool,
192
-  onClickRestoreDeleted: PropTypes.func,
192
+  onClickRestoreDeleted: PropTypes.func
193
 }
193
 }
194
 
194
 
195
 Timeline.defaultProps = {
195
 Timeline.defaultProps = {

+ 17 - 0
setup_default_backend.sh View File

1
+#!/bin/bash
2
+. bash_library.sh # source bash_library.sh
3
+. backend_lib.sh # source backend_lib.sh
4
+
5
+install_backend_system_deb
6
+
7
+log "go to backend subdir.."
8
+cd backend || exit 1;
9
+
10
+
11
+install_backend_system_dep
12
+setup_pyenv
13
+install_backend_python_packages
14
+setup_config_file
15
+setup_db
16
+
17
+log "backend of tracim was correctly set-up."