소스 검색

fix import issue with merging

Guénaël Muller 6 년 전
부모
커밋
e0c2ef6edf
72개의 변경된 파일3654개의 추가작업 그리고 494개의 파일을 삭제
  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 파일 보기

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

+ 57 - 7
backend/README.md 파일 보기

@@ -77,25 +77,41 @@ Initialize the database using [tracimcli](doc/cli.md) tool
77 77
 
78 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 84
 create wsgidav configuration file for webdav:
81 85
 
82 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 95
     # install uwsgi with pip ( unneeded if you already have uwsgi with python3 plugin enabled)
91 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 108
     # set tracim_conf_file path
93 109
     export TRACIM_CONF_PATH="$(pwd)/development.ini"
94 110
     export TRACIM_WEBDAV_CONF_PATH="$(pwd)/wsgidav.conf"
95 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 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 116
 to stop them:
101 117
 
@@ -104,7 +120,37 @@ to stop them:
104 120
     # webdav wsgidav server
105 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 155
 run tracim_backend web api:
110 156
 
@@ -159,4 +205,8 @@ For example, with default config:
159 205
 
160 206
 In Tracim, only some user can access to some informations, this is also true in
161 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 파일 보기

@@ -0,0 +1,23 @@
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 파일 보기

@@ -28,6 +28,10 @@ and active the Tracim virtualenv:
28 28
 
29 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 35
 ### Creating new schema migration ###
32 36
 
33 37
 This creates a new auto-generated python migration file 

+ 4 - 2
backend/doc/roles.md 파일 보기

@@ -9,7 +9,7 @@ The other is workspace related and is called "workspace role".
9 9
 
10 10
 |                               | Normal User | Managers    | Admin          |
11 11
 |-------------------------------|-------------|-------------|----------------|
12
-| slug                            | users       | managers    | administrators |
12
+| slug                          | users       | managers    | administrators |
13 13
 |-------------------------------|-------------|-------------|---------|
14 14
 
15 15
 
@@ -20,9 +20,11 @@ The other is workspace related and is called "workspace role".
20 20
 |-------------------------------|-------------|-------------|---------|
21 21
 | create workspace              |  no         | yes         | yes     |
22 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 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 29
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
28 30
 

+ 4 - 0
backend/tracim_backend/__init__.py 파일 보기

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

+ 2 - 0
backend/tracim_backend/exceptions.py 파일 보기

@@ -204,6 +204,8 @@ class PageOfPreviewNotFound(NotFound):
204 204
 class PreviewDimNotAllowed(TracimException):
205 205
     pass
206 206
 
207
+class UnallowedSubContent(TracimException):
208
+    pass
207 209
 
208 210
 class TooShortAutocompleteString(TracimException):
209 211
     pass

+ 78 - 15
backend/tracim_backend/lib/core/content.py 파일 보기

@@ -26,6 +26,8 @@ from sqlalchemy.sql.elements import and_
26 26
 from tracim_backend.lib.utils.utils import cmp_to_key
27 27
 from tracim_backend.lib.core.notifications import NotifierFactory
28 28
 from tracim_backend.exceptions import SameValueError
29
+from tracim_backend.exceptions import UnallowedSubContent
30
+from tracim_backend.exceptions import ContentTypeNotExist
29 31
 from tracim_backend.exceptions import PageOfPreviewNotFound
30 32
 from tracim_backend.exceptions import PreviewDimNotAllowed
31 33
 from tracim_backend.exceptions import RevisionDoesNotMatchThisContent
@@ -408,13 +410,37 @@ class ContentApi(object):
408 410
 
409 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 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 413
         assert content_type_slug != CONTENT_TYPES.Any_SLUG
413 414
         assert not (label and filename)
414 415
 
415 416
         if content_type_slug == CONTENT_TYPES.Folder.slug and not label:
416 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 444
         content = Content()
419 445
 
420 446
         if filename:
@@ -433,8 +459,9 @@ class ContentApi(object):
433 459
 
434 460
         content.owner = self._user
435 461
         content.parent = parent
462
+
436 463
         content.workspace = workspace
437
-        content.type = content_type_slug
464
+        content.type = content_type.slug
438 465
         content.is_temporary = is_temporary
439 466
         content.revision_type = ActionDescription.CREATION
440 467
 
@@ -450,18 +477,20 @@ class ContentApi(object):
450 477
         return content
451 478
 
452 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 481
         assert parent and parent.type != CONTENT_TYPES.Folder.slug
454 482
         if not content:
455 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 493
         item.description = content
464
-        item.label = ''
465 494
         item.revision_type = ActionDescription.COMMENT
466 495
 
467 496
         if do_save:
@@ -1001,6 +1030,13 @@ class ContentApi(object):
1001 1030
             else:
1002 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 1040
             if related_active_content not in active_contents and related_active_content not in too_recent_content:  # nopep8
1005 1041
 
1006 1042
                 if not before_content or before_content_find:
@@ -1076,9 +1112,9 @@ class ContentApi(object):
1076 1112
     #
1077 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 1118
         :param allowed_content_dict: must be something like this:
1083 1119
             dict(
1084 1120
                 folder = True
@@ -1086,10 +1122,37 @@ class ContentApi(object):
1086 1122
                 file = False,
1087 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 1157
     def set_status(self, content: Content, new_status: str):
1095 1158
         if new_status in CONTENT_STATUS.get_all_slugs_values():

+ 16 - 1
backend/tracim_backend/lib/core/user.py 파일 보기

@@ -35,13 +35,18 @@ class UserApi(object):
35 35
             current_user: typing.Optional[User],
36 36
             session: Session,
37 37
             config: CFG,
38
+            show_deleted: bool = False,
38 39
     ) -> None:
39 40
         self._session = session
40 41
         self._user = current_user
41 42
         self._config = config
43
+        self._show_deleted = show_deleted
42 44
 
43 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 51
     def get_user_with_context(self, user: User) -> UserInContext:
47 52
         """
@@ -404,6 +409,16 @@ class UserApi(object):
404 409
         if do_save:
405 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 422
     def save(self, user: User):
408 423
         self._session.flush()
409 424
 

+ 13 - 12
backend/tracim_backend/lib/core/workspace.py 파일 보기

@@ -27,7 +27,8 @@ class WorkspaceApi(object):
27 27
             session: Session,
28 28
             current_user: User,
29 29
             config: CFG,
30
-            force_role: bool=False
30
+            force_role: bool=False,
31
+            show_deleted: bool=False,
31 32
     ):
32 33
         """
33 34
         :param current_user: Current user of context
@@ -37,18 +38,22 @@ class WorkspaceApi(object):
37 38
         self._user = current_user
38 39
         self._config = config
39 40
         self._force_role = force_role
41
+        self.show_deleted = show_deleted
40 42
 
41 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 49
     def _base_query(self):
45 50
         if not self._force_role and self._user.profile.id>=Group.TIM_ADMIN:
46 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 58
     def get_workspace_with_context(
54 59
             self,
@@ -207,17 +212,13 @@ class WorkspaceApi(object):
207 212
     def save(self, workspace: Workspace):
208 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 216
         workspace.is_deleted = True
213 217
 
214 218
         if flush:
215 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 222
         workspace.is_deleted = False
222 223
 
223 224
         if flush:

+ 34 - 0
backend/tracim_backend/lib/utils/authorization.py 파일 보기

@@ -90,6 +90,40 @@ def require_profile(group: int) -> typing.Callable:
90 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 127
 def require_workspace_role(minimal_required_role: int) -> typing.Callable:
94 128
     """
95 129
     Restricts access to endpoint to minimal role or raise an exception.

+ 5 - 3
backend/tracim_backend/lib/utils/request.py 파일 보기

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

+ 0 - 8
backend/tracim_backend/lib/webdav/resources.py 파일 보기

@@ -295,14 +295,6 @@ class WorkspaceResource(DAVCollection):
295 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 298
         self.content_api.save(folder)
307 299
 
308 300
         transaction.commit()

+ 26 - 0
backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py 파일 보기

@@ -0,0 +1,26 @@
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 파일 보기

@@ -15,6 +15,7 @@ from datetime import datetime
15 15
 from hashlib import sha256
16 16
 from typing import TYPE_CHECKING
17 17
 
18
+import sqlalchemy
18 19
 from sqlalchemy import Column
19 20
 from sqlalchemy import ForeignKey
20 21
 from sqlalchemy import Sequence
@@ -135,6 +136,7 @@ class User(DeclarativeBase):
135 136
     _password = Column('password', Unicode(128))
136 137
     created = Column(DateTime, default=datetime.utcnow)
137 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 140
     imported_from = Column(Unicode(32), nullable=True)
139 141
     timezone = Column(Unicode(255), nullable=False, server_default='')
140 142
     # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed

+ 22 - 0
backend/tracim_backend/models/contents.py 파일 보기

@@ -122,6 +122,7 @@ class ContentType(object):
122 122
             creation_label: str,
123 123
             available_statuses: typing.List[ContentStatus],
124 124
             slug_alias: typing.List[str] = None,
125
+            allow_sub_content: bool = False,
125 126
     ):
126 127
         self.slug = slug
127 128
         self.fa_icon = fa_icon
@@ -130,6 +131,7 @@ class ContentType(object):
130 131
         self.creation_label = creation_label
131 132
         self.available_statuses = available_statuses
132 133
         self.slug_alias = slug_alias
134
+        self.allow_sub_content = allow_sub_content
133 135
 
134 136
 
135 137
 thread_type = ContentType(
@@ -177,6 +179,7 @@ folder_type = ContentType(
177 179
     label='Folder',
178 180
     creation_label='Create a folder',
179 181
     available_statuses=CONTENT_STATUS.get_all(),
182
+    allow_sub_content=True,
180 183
 )
181 184
 
182 185
 
@@ -226,6 +229,7 @@ class ContentTypeList(object):
226 229
         """
227 230
         content_types = self._content_types.copy()
228 231
         content_types.extend(self._special_contents_types)
232
+        content_types.append(self.Event)
229 233
         for item in content_types:
230 234
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
231 235
                 return item
@@ -241,6 +245,12 @@ class ContentTypeList(object):
241 245
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
242 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 254
     def query_allowed_types_slugs(self) -> typing.List[str]:
245 255
         """
246 256
         Return alls allowed types slug : content_type slug + all alias, any
@@ -257,6 +267,18 @@ class ContentTypeList(object):
257 267
         allowed_types_slug.extend(self._extra_slugs)
258 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 283
 CONTENT_TYPES = ContentTypeList(
262 284
     [

+ 37 - 11
backend/tracim_backend/models/context_models.py 파일 보기

@@ -297,10 +297,10 @@ class ContentCreation(object):
297 297
     Content creation model
298 298
     """
299 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 304
     ) -> None:
305 305
         self.label = label
306 306
         self.content_type = content_type
@@ -312,8 +312,8 @@ class CommentCreation(object):
312 312
     Comment creation model
313 313
     """
314 314
     def __init__(
315
-            self,
316
-            raw_content: str,
315
+        self,
316
+        raw_content: str,
317 317
     ) -> None:
318 318
         self.raw_content = raw_content
319 319
 
@@ -323,8 +323,8 @@ class SetContentStatus(object):
323 323
     Set content status
324 324
     """
325 325
     def __init__(
326
-            self,
327
-            status: str,
326
+        self,
327
+        status: str,
328 328
     ) -> None:
329 329
         self.status = status
330 330
 
@@ -334,12 +334,27 @@ class TextBasedContentUpdate(object):
334 334
     TextBasedContent update model
335 335
     """
336 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 354
     ) -> None:
341 355
         self.label = label
342 356
         self.raw_content = raw_content
357
+        self.sub_content_types = sub_content_types
343 358
 
344 359
 
345 360
 class TypeUser(Enum):
@@ -393,6 +408,10 @@ class UserInContext(object):
393 408
     def profile(self) -> Profile:
394 409
         return self.user.profile.name
395 410
 
411
+    @property
412
+    def is_deleted(self) -> bool:
413
+        return self.user.is_deleted
414
+
396 415
     # Context related
397 416
 
398 417
     @property
@@ -457,6 +476,13 @@ class WorkspaceInContext(object):
457 476
         return slugify(self.workspace.label)
458 477
 
459 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 486
     def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
461 487
         """
462 488
         get sidebar entries, those depends on activated apps.

+ 24 - 43
backend/tracim_backend/models/data.py 파일 보기

@@ -32,15 +32,6 @@ from tracim_backend.models.meta import DeclarativeBase
32 32
 from tracim_backend.models.auth import User
33 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 36
 class Workspace(DeclarativeBase):
46 37
 
@@ -96,7 +87,7 @@ class Workspace(DeclarativeBase):
96 87
 
97 88
     def get_allowed_content_types(self):
98 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 92
     def get_valid_children(
102 93
             self,
@@ -545,22 +536,8 @@ class ContentChecker(object):
545 536
 
546 537
     @classmethod
547 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 540
         if item.type == CONTENT_TYPES.Event.slug:
563
-            properties = item.properties
564 541
             if 'name' not in properties.keys():
565 542
                 return False
566 543
             if 'raw' not in properties.keys():
@@ -570,22 +547,16 @@ class ContentChecker(object):
570 547
             if 'end' not in properties.keys():
571 548
                 return False
572 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 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 562
 class ContentRevisionRO(DeclarativeBase):
@@ -1204,9 +1175,15 @@ class Content(DeclarativeBase):
1204 1175
     @hybrid_property
1205 1176
     def properties(self) -> dict:
1206 1177
         """ return a structure decoded from json content of _properties """
1178
+
1207 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 1188
     @properties.setter
1212 1189
     def properties(self, properties_struct: dict) -> None:
@@ -1347,7 +1324,11 @@ class Content(DeclarativeBase):
1347 1324
             allowed_types = self.properties['allowed_content']
1348 1325
             for type_label, is_allowed in allowed_types.items():
1349 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 1332
         except Exception as e:
1352 1333
             print(e.__str__())
1353 1334
             print('----- /*\ *****')

+ 761 - 0
backend/tracim_backend/tests/functional/test_contents.py 파일 보기

@@ -24,6 +24,767 @@ from tracim_backend.tests import set_html_document_slug_to_legacy
24 24
 from tracim_backend.fixtures.content import Content as ContentFixtures
25 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 789
 class TestHtmlDocuments(FunctionalTest):
29 790
     """

+ 52 - 2
backend/tracim_backend/tests/functional/test_user.py 파일 보기

@@ -2383,6 +2383,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
2383 2383
         assert workspace['workspace_id'] == 1
2384 2384
         assert workspace['label'] == 'Business'
2385 2385
         assert workspace['slug'] == 'business'
2386
+        assert workspace['is_deleted'] is False
2386 2387
         assert len(workspace['sidebar_entries']) == 5
2387 2388
 
2388 2389
         # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
@@ -2480,8 +2481,9 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
2480 2481
 
2481 2482
 
2482 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 2488
     fixtures = [BaseFixture]
2487 2489
 
@@ -2533,6 +2535,7 @@ class TestUserEndpoint(FunctionalTest):
2533 2535
         assert res['email'] == 'test@test.test'
2534 2536
         assert res['public_name'] == 'bob'
2535 2537
         assert res['timezone'] == 'Europe/Paris'
2538
+        assert res['is_deleted'] is False
2536 2539
 
2537 2540
     def test_api__get_user__ok_200__user_itself(self):
2538 2541
         dbsession = get_tm_session(self.session_factory, transaction.manager)
@@ -2582,6 +2585,7 @@ class TestUserEndpoint(FunctionalTest):
2582 2585
         assert res['email'] == 'test@test.test'
2583 2586
         assert res['public_name'] == 'bob'
2584 2587
         assert res['timezone'] == 'Europe/Paris'
2588
+        assert res['is_deleted'] is False
2585 2589
 
2586 2590
     def test_api__get_user__err_403__other_normal_user(self):
2587 2591
         dbsession = get_tm_session(self.session_factory, transaction.manager)
@@ -2931,11 +2935,57 @@ class TestUserWithNotificationEndpoint(FunctionalTest):
2931 2935
         # TODO - G.M - 2018-08-02 - Place cleanup outside of the test
2932 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 2985
 class TestUsersEndpoint(FunctionalTest):
2936 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 2990
     fixtures = [BaseFixture]
2941 2991
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 816 - 22
backend/tracim_backend/tests/functional/test_workspaces.py


+ 401 - 0
backend/tracim_backend/tests/library/test_content_api.py 파일 보기

@@ -10,6 +10,8 @@ from tracim_backend.lib.core.content import ContentApi
10 10
 from tracim_backend.lib.core.group import GroupApi
11 11
 from tracim_backend.lib.core.user import UserApi
12 12
 from tracim_backend.exceptions import SameValueError
13
+from tracim_backend.exceptions import EmptyLabelNotAllowed
14
+from tracim_backend.exceptions import UnallowedSubContent
13 15
 # TODO - G.M - 28-03-2018 - [RoleApi] Re-enable RoleApi
14 16
 from tracim_backend.lib.core.workspace import RoleApi
15 17
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
@@ -101,6 +103,310 @@ class TestContentApi(DefaultTest):
101 103
             'value is {} instead of {}'.format(sorteds[1].content_id,
102 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 410
     def test_delete(self):
105 411
         uapi = UserApi(
106 412
             session=self.session,
@@ -2057,6 +2363,101 @@ class TestContentApi(DefaultTest):
2057 2363
         # (workspace2)
2058 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 2461
     def test_unit__get_last_active__ok__workspace_filter_workspace_full(self):
2061 2462
         uapi = UserApi(
2062 2463
             session=self.session,

+ 198 - 0
backend/tracim_backend/views/contents_api/folder_controller.py 파일 보기

@@ -0,0 +1,198 @@
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 파일 보기

@@ -13,6 +13,7 @@ from tracim_backend.models.contents import CONTENT_STATUS
13 13
 from tracim_backend.models.contents import CONTENT_TYPES
14 14
 from tracim_backend.models.contents import open_status
15 15
 from tracim_backend.models.context_models import ActiveContentFilter
16
+from tracim_backend.models.context_models import FolderContentUpdate
16 17
 from tracim_backend.models.context_models import AutocompleteQuery
17 18
 from tracim_backend.models.context_models import ContentIdsQuery
18 19
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
@@ -75,6 +76,10 @@ class UserSchema(UserDigestSchema):
75 76
         example=True,
76 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 83
     # TODO - G.M - 17-04-2018 - Restrict timezone values
79 84
     timezone = marshmallow.fields.String(
80 85
         example="Europe/Paris",
@@ -326,6 +331,7 @@ class AutocompleteQuerySchema(marshmallow.Schema):
326 331
         example='test',
327 332
         description='search text to query',
328 333
         validate=Length(min=2),
334
+        required=True,
329 335
     )
330 336
     @post_load
331 337
     def make_autocomplete(self, data):
@@ -536,6 +542,7 @@ class WorkspaceDigestSchema(marshmallow.Schema):
536 542
         WorkspaceMenuEntrySchema,
537 543
         many=True,
538 544
     )
545
+    is_deleted = marshmallow.fields.Bool(example=False, default=False)
539 546
 
540 547
     class Meta:
541 548
         description = 'Digest of workspace informations'
@@ -728,7 +735,7 @@ class ContentDigestSchema(marshmallow.Schema):
728 735
     sub_content_types = marshmallow.fields.List(
729 736
         marshmallow.fields.String(
730 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 740
         description='list of content types allowed as sub contents. '
734 741
                     'This field is required for folder contents, '
@@ -874,6 +881,22 @@ class TextBasedContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbs
874 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 900
 class FileContentModifySchema(TextBasedContentModifySchema):
878 901
     pass
879 902
 

+ 43 - 0
backend/tracim_backend/views/core_api/user_controller.py 파일 보기

@@ -247,6 +247,41 @@ class UserController(Controller):
247 247
     @require_profile(Group.TIM_ADMIN)
248 248
     @hapic.input_path(UserIdPathSchema())
249 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 285
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
251 286
         """
252 287
         disable user
@@ -467,6 +502,14 @@ class UserController(Controller):
467 502
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
468 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 513
         # set user profile
471 514
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
472 515
         configurator.add_view(self.set_profile, route_name='set_user_profile')

+ 146 - 0
backend/tracim_backend/views/core_api/workspace_controller.py 파일 보기

@@ -1,6 +1,7 @@
1 1
 import typing
2 2
 import transaction
3 3
 from pyramid.config import Configurator
4
+from pyramid.httpexceptions import HTTPFound
4 5
 
5 6
 from tracim_backend.lib.core.user import UserApi
6 7
 from tracim_backend.models.roles import WorkspaceRoles
@@ -11,11 +12,13 @@ except ImportError:
11 12
     from http import client as HTTPStatus
12 13
 
13 14
 from tracim_backend import hapic
15
+from tracim_backend import BASE_API_V2
14 16
 from tracim_backend import TracimRequest
15 17
 from tracim_backend.lib.core.workspace import WorkspaceApi
16 18
 from tracim_backend.lib.core.content import ContentApi
17 19
 from tracim_backend.lib.core.userworkspace import RoleApi
18 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 22
 from tracim_backend.lib.utils.authorization import require_profile
20 23
 from tracim_backend.models import Group
21 24
 from tracim_backend.lib.utils.authorization import require_candidate_workspace_role
@@ -24,6 +27,7 @@ from tracim_backend.models.data import ActionDescription
24 27
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
25 28
 from tracim_backend.models.context_models import ContentInContext
26 29
 from tracim_backend.exceptions import EmptyLabelNotAllowed
30
+from tracim_backend.exceptions import UnallowedSubContent
27 31
 from tracim_backend.exceptions import EmailValidationFailed
28 32
 from tracim_backend.exceptions import UserCreationFailed
29 33
 from tracim_backend.exceptions import UserDoesNotExist
@@ -33,6 +37,7 @@ from tracim_backend.exceptions import ParentNotFound
33 37
 from tracim_backend.views.controllers import Controller
34 38
 from tracim_backend.lib.utils.utils import password_generator
35 39
 from tracim_backend.views.core_api.schemas import FilterContentQuerySchema
40
+from tracim_backend.views.core_api.schemas import ContentIdPathSchema
36 41
 from tracim_backend.views.core_api.schemas import WorkspaceMemberCreationSchema
37 42
 from tracim_backend.views.core_api.schemas import WorkspaceMemberInviteSchema
38 43
 from tracim_backend.views.core_api.schemas import RoleUpdateSchema
@@ -118,6 +123,51 @@ class WorkspaceController(Controller):
118 123
         return wapi.get_workspace_with_context(workspace)
119 124
 
120 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 171
     @require_workspace_role(UserRoleInWorkspace.READER)
122 172
     @hapic.input_path(WorkspaceIdPathSchema())
123 173
     @hapic.output_body(WorkspaceMemberSchema(many=True))
@@ -176,6 +226,28 @@ class WorkspaceController(Controller):
176 226
         return rapi.get_user_role_workspace_with_context(role)
177 227
 
178 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 251
     @hapic.handle_exception(UserCreationFailed, HTTPStatus.BAD_REQUEST)
180 252
     @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
181 253
     @hapic.input_path(WorkspaceIdPathSchema())
@@ -276,6 +348,7 @@ class WorkspaceController(Controller):
276 348
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
277 349
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
278 350
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
351
+    @hapic.handle_exception(UnallowedSubContent, HTTPStatus.BAD_REQUEST)
279 352
     @hapic.input_path(WorkspaceIdPathSchema())
280 353
     @hapic.input_body(ContentCreationSchema())
281 354
     @hapic.output_body(ContentDigestSchema())
@@ -314,6 +387,65 @@ class WorkspaceController(Controller):
314 387
         return content
315 388
 
316 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 449
     @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
318 450
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
319 451
     @require_candidate_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
@@ -512,6 +644,11 @@ class WorkspaceController(Controller):
512 644
         # Create workspace
513 645
         configurator.add_route('create_workspace', '/workspaces', request_method='POST')  # nopep8
514 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 652
         # Update Workspace
516 653
         configurator.add_route('update_workspace', '/workspaces/{workspace_id}', request_method='PUT')  # nopep8
517 654
         configurator.add_view(self.update_workspace, route_name='update_workspace')  # nopep8
@@ -524,12 +661,21 @@ class WorkspaceController(Controller):
524 661
         # Create Workspace Members roles
525 662
         configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
526 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 667
         # Workspace Content
528 668
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
529 669
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
530 670
         # Create Generic Content
531 671
         configurator.add_route('create_generic_content', '/workspaces/{workspace_id}/contents', request_method='POST')  # nopep8
532 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 679
         # Move Content
534 680
         configurator.add_route('move_content', '/workspaces/{workspace_id}/contents/{content_id}/move', request_method='PUT')  # nopep8
535 681
         configurator.add_view(self.move_content, route_name='move_content')  # nopep8

+ 52 - 0
backend_lib.sh 파일 보기

@@ -0,0 +1,52 @@
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 파일 보기

@@ -17,6 +17,8 @@ import {
17 17
   setFolderData,
18 18
   APP_LIST,
19 19
   CONTENT_TYPE_LIST,
20
+  WORKSPACE_CONTENT_ARCHIVED,
21
+  WORKSPACE_CONTENT_DELETED,
20 22
   WORKSPACE_RECENT_ACTIVITY,
21 23
   WORKSPACE_READ_STATUS
22 24
 } from './action-creator.sync.js'
@@ -332,3 +334,33 @@ export const getContentTypeList = user => dispatch => {
332 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 파일 보기

@@ -38,6 +38,11 @@ export const WORKSPACE_CONTENT = `${WORKSPACE}/Content`
38 38
 export const setWorkspaceContentList = workspaceContentList => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList })
39 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 46
 export const WORKSPACE_LIST = `${WORKSPACE}/List`
42 47
 export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
43 48
 export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })

+ 3 - 3
frontend/src/component/Account/Password.jsx 파일 보기

@@ -17,16 +17,16 @@ export const Password = props => {
17 17
           {props.t('Password')}
18 18
         </div>
19 19
         <input
20
-          className='personaldata__form__txtinput form-control'
20
+          className='personaldata__form__txtinput primaryColorBorderLighten form-control'
21 21
           type='password'
22 22
           placeholder={props.t('Old password')}
23 23
         />
24 24
         <input
25
-          className='personaldata__form__txtinput form-control mt-4'
25
+          className='personaldata__form__txtinput primaryColorBorderLighten form-control mt-4'
26 26
           type='password'
27 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 30
           {props.t('Send')}
31 31
         </button>
32 32
       </form>

+ 3 - 3
frontend/src/component/Account/PersonalData.jsx 파일 보기

@@ -19,7 +19,7 @@ export const PersonalData = props => {
19 19
         </div>
20 20
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
21 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 23
             type='text'
24 24
             placeholder={props.t('Change your name')}
25 25
           />
@@ -29,12 +29,12 @@ export const PersonalData = props => {
29 29
         </div>
30 30
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
31 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 33
             type='email'
34 34
             placeholder={props.t('Change your email')}
35 35
           />
36 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 38
           {props.t('Send')}
39 39
         </button>
40 40
       </form>

+ 2 - 2
frontend/src/component/Account/UserInfo.jsx 파일 보기

@@ -10,7 +10,7 @@ export const UserInfo = props => {
10 10
         <div className='account__userinformation__name mb-3'>
11 11
           {`${props.user.firstname} ${props.user.lastname}`}
12 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 14
           {props.user.email}
15 15
         </a>
16 16
         <div className='account__userinformation__role mb-3'>
@@ -19,7 +19,7 @@ export const UserInfo = props => {
19 19
         { /* <div className='account__userinformation__job mb-3'>
20 20
           {props.user.job}
21 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 23
           {props.user.company}
24 24
         </a> */ }
25 25
       </div>

+ 1 - 5
frontend/src/component/Dashboard/ContentTypeBtn.styl 파일 보기

@@ -4,7 +4,7 @@
4 4
   display flex
5 5
   flex-direction column
6 6
   justify-content center
7
-  margin 15px
7
+  margin 15px 15px 0 0
8 8
   border-radius 10px
9 9
   padding 15px
10 10
   width 230px
@@ -12,7 +12,3 @@
12 12
   box-shadow shadow-all
13 13
   text-align center
14 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 파일 보기

@@ -1,6 +1,7 @@
1 1
 import React from 'react'
2 2
 import PropTypes from 'prop-types'
3 3
 // import { Checkbox } from 'tracim_frontend_lib'
4
+import { generateAvatarFromPublicName } from 'tracim_frontend_lib'
4 5
 
5 6
 require('./MemberList.styl')
6 7
 
@@ -110,8 +111,8 @@ export class MemberList extends React.Component {
110 111
                             onClick={() => props.onClickKnownMember(u)}
111 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 116
                             </div>
116 117
 
117 118
                             <div className='autocomplete__item__name'>

+ 18 - 4
frontend/src/component/Dashboard/MemberList.styl 파일 보기

@@ -97,8 +97,6 @@
97 97
               width 45px
98 98
               height 45px
99 99
               border-radius 50%
100
-              border-width 1px
101
-              border-style solid
102 100
             &__name
103 101
               margin-left 15px
104 102
       .name__input
@@ -138,14 +136,30 @@
138 136
         padding 8px 30px
139 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 145
   .memberlist
143 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 157
 @media (min-width: min-sm) and (max-width: max-sm)
146 158
   .memberlist
147 159
     margin 50px 0
148
-    width 90%
160
+    width 100%
161
+
162
+/*** MEDIA 575px ***/
149 163
 
150 164
 @media (max-width: max-xs)
151 165
   .memberlist

+ 1 - 1
frontend/src/component/Dashboard/MoreInfo.jsx 파일 보기

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

+ 19 - 3
frontend/src/component/Dashboard/RecentActivity.styl 파일 보기

@@ -43,22 +43,38 @@
43 43
       padding 10px 25px
44 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 52
   .activity
53
+    margin-right 0
48 54
     width 100%
49 55
 
56
+/**** MEDIA 768px and 991px ****/
57
+
50 58
 @media (min-width: min-md) and (max-width: max-md)
51 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 65
 @media (min-width: min-sm) and (max-width: max-sm)
55 66
   .activity
56
-    margin 25px 15px 25px 0
67
+    margin 25px 0 25px 0
68
+    width 100%
69
+
70
+/**** MEDIA 575px ****/
57 71
 
58 72
 @media (max-width: max-xs)
59 73
   .activity
60 74
     margin 25px 0
61 75
     width 100%
76
+    &__wrapper
77
+      margin-top 45px
62 78
     &__header
63 79
       display block
64 80
       height auto

+ 2 - 2
frontend/src/component/Dashboard/UserStatus.jsx 파일 보기

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

+ 41 - 2
frontend/src/component/Dashboard/UserStatus.styl 파일 보기

@@ -16,15 +16,54 @@
16 16
     font-size 18px
17 17
     &__btn
18 18
       margin 20px 0
19
-      border 1px solid thirdColor
19
+      border-width 1px
20
+      border-style solid
20 21
       padding 10px 15px
21 22
       cursor pointer
22 23
     &__subscribe
23 24
       &__btn
24 25
         margin 20px 0
25
-        border 1px solid thirdColor
26
+        border-width 1px
27
+        border-style solid
26 28
         padding 10px 15px
27 29
       &__submenu
28 30
         padding 0
29 31
         &__item
30 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 파일 보기

@@ -0,0 +1,29 @@
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 파일 보기

@@ -5,31 +5,34 @@ import { PAGE } from '../../../helper.js'
5 5
 import { translate } from 'react-i18next'
6 6
 
7 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 31
           </div>
29 32
         </div>
30
-      </li>
31
-    )
32
-    : ''
33
+      </div>
34
+    </li>
35
+  )
33 36
 }
34 37
 export default translate()(MenuProfil)
35 38
 

+ 1 - 1
frontend/src/component/Header/MenuActionListItem/Search.jsx 파일 보기

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

+ 1 - 1
frontend/src/container/Account.jsx 파일 보기

@@ -101,7 +101,7 @@ class Account extends React.Component {
101 101
     })()
102 102
 
103 103
     return (
104
-      <div className='Account'>
104
+      <div className='account'>
105 105
         <PageWrapper customClass='account'>
106 106
           <PageTitle
107 107
             parentClass={'account'}

+ 158 - 163
frontend/src/container/AdminWorkspacePage.jsx 파일 보기

@@ -1,5 +1,4 @@
1 1
 import React from 'react'
2
-import Sidebar from './Sidebar.jsx'
3 2
 import {
4 3
   Delimiter,
5 4
   PageWrapper,
@@ -11,188 +10,184 @@ import { translate } from 'react-i18next'
11 10
 class AdminWorkspacePage extends React.Component {
12 11
   render () {
13 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 61
                       </div>
62
+                      Enable
77 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 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 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 105
                       </div>
106
+                      Enable
121 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 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 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 149
                       </div>
150
+                      Disable
165 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 171
                       </div>
172
+                      Enable
187 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 파일 보기

@@ -1,11 +1,11 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3 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 6
 import appFactory from '../appFactory.js'
7 7
 
8
-class AppFullscreenManager extends React.Component {
8
+class AppFullscreenRouter extends React.Component {
9 9
   constructor (props) {
10 10
     super(props)
11 11
     this.state = {
@@ -23,13 +23,17 @@ class AppFullscreenManager extends React.Component {
23 23
         <div id='appFullscreenContainer' />
24 24
 
25 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 27
             <Route path={PAGE.ADMIN.WORKSPACE} render={() => {
28
+              if (props.user.profile !== PROFILE.ADMINISTRATOR) return <Redirect to={{pathname: '/'}} />
29
+
28 30
               props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'workspace'}, props.user, {})
29 31
               return null
30 32
             }} />
31 33
 
32 34
             <Route path={PAGE.ADMIN.USER} render={() => {
35
+              if (props.user.profile !== PROFILE.ADMINISTRATOR) return <Redirect to={{pathname: '/'}} />
36
+
33 37
               props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'user'}, props.user, {})
34 38
               return null
35 39
             }} />
@@ -41,4 +45,4 @@ class AppFullscreenManager extends React.Component {
41 45
 }
42 46
 
43 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 파일 보기

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

+ 6 - 1
frontend/src/container/Header.jsx 파일 보기

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

+ 29 - 21
frontend/src/container/Login.jsx 파일 보기

@@ -17,6 +17,7 @@ import {
17 17
   setUserConnected
18 18
 } from '../action-creator.sync.js'
19 19
 import { COOKIE, PAGE } from '../helper.js'
20
+import { Checkbox } from 'tracim_frontend_lib'
20 21
 
21 22
 class Login extends React.Component {
22 23
   constructor (props) {
@@ -36,7 +37,11 @@ class Login extends React.Component {
36 37
 
37 38
   handleChangeLogin = e => this.setState({inputLogin: {...this.state.inputLogin, value: e.target.value}})
38 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 46
   handleClickSubmit = async () => {
42 47
     const { history, dispatch, t } = this.props
@@ -52,8 +57,13 @@ class Login extends React.Component {
52 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 68
       history.push(PAGE.WORKSPACE.ROOT)
59 69
     } else if (fetchPostUserLogin.status === 403) {
@@ -74,7 +84,7 @@ class Login extends React.Component {
74 84
               <div className='col-12 col-sm-11 col-md-8 col-lg-6 col-xl-4'>
75 85
 
76 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 89
                   <CardBody formClass='connection__form'>
80 90
                     <div>
@@ -103,32 +113,30 @@ class Login extends React.Component {
103 113
                       />
104 114
 
105 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 120
                               checked={this.state.inputRememberMe}
114
-                              onChange={this.handleChangeRememberMe}
115 121
                             />
122
+                            Se souvenir de moi
116 123
                           </div>
117
-                        */}
118 124
 
119
-                        <div className='col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6'>
120 125
                           <LoginBtnForgotPw
121 126
                             customClass='connection__form__pwforgot'
122 127
                             label={this.props.t('Forgotten password ?')}
123 128
                           />
124 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 140
                       </div>
133 141
                     </div>
134 142
 

+ 1 - 1
frontend/src/container/ProgressBar.jsx 파일 보기

@@ -13,7 +13,7 @@ class ProgressBar extends Component {
13 13
               <span className='progress-right'>
14 14
                 <span className='progress-bar' />
15 15
               </span>
16
-              <div className='progress-value'>
16
+              <div className='progress-value primaryColorBg'>
17 17
                 90%
18 18
               </div>
19 19
             </div>

+ 1 - 0
frontend/src/container/Sidebar.jsx 파일 보기

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

+ 4 - 5
frontend/src/container/Tracim.jsx 파일 보기

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

+ 30 - 8
frontend/src/container/WorkspaceContent.jsx 파일 보기

@@ -16,11 +16,15 @@ import {
16 16
 } from 'tracim_frontend_lib'
17 17
 import {
18 18
   getWorkspaceContentList,
19
-  getFolderContent
19
+  getFolderContent,
20
+  putWorkspaceContentArchived,
21
+  putWorkspaceContentDeleted
20 22
 } from '../action-creator.async.js'
21 23
 import {
22 24
   newFlashMessage,
23
-  setWorkspaceContentList
25
+  setWorkspaceContentList,
26
+  setWorkspaceContentArchived,
27
+  setWorkspaceContentDeleted
24 28
 } from '../action-creator.sync.js'
25 29
 
26 30
 const qs = require('query-string')
@@ -125,14 +129,34 @@ class WorkspaceContent extends React.Component {
125 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 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 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 162
   handleClickFolder = folderId => {
@@ -197,8 +221,6 @@ class WorkspaceContent extends React.Component {
197 221
           </PageTitle>
198 222
 
199 223
           <PageContent parentClass='workspace__content'>
200
-            <div id='popupCreateContentContainer' />
201
-
202 224
             <div className='workspace__content__fileandfolder folder__content active'>
203 225
               <ContentItemHeader />
204 226
 

+ 6 - 5
frontend/src/css/AccountPage.styl 파일 보기

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

+ 0 - 21
frontend/src/css/AdminWorkspacePage.styl 파일 보기

@@ -1,21 +0,0 @@
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 파일 보기

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

+ 2 - 2
frontend/src/css/Generic.styl 파일 보기

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

+ 14 - 3
frontend/src/css/Header.styl 파일 보기

@@ -24,6 +24,19 @@
24 24
       display flex
25 25
       margin-top 15px
26 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 40
       &__itemsearch
28 41
         display none
29 42
         margin-right 8%
@@ -33,9 +46,6 @@
33 46
           .search__addonsearch
34 47
             background-color transparent
35 48
             cursor pointer
36
-            &:hover
37
-              background-color thirdColor
38
-              color mainColor
39 49
       .btnnavbar
40 50
         border 1px solid grey
41 51
         background-color transparent
@@ -99,6 +109,7 @@
99 109
             padding 0
100 110
             left inherit
101 111
             right 0
112
+            width 100%
102 113
             cursor pointer
103 114
             .setting__link
104 115
               padding 10px

+ 8 - 12
frontend/src/css/Login.styl 파일 보기

@@ -15,13 +15,17 @@
15 15
     border none
16 16
     box-shadow shadow-right
17 17
     .connection__header
18
-      background-color thirdColor
19
-      color #FFF
18
+      color off-white
20 19
       font-size 25px
21 20
   .connection__form
22 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 29
     &__groupemail
26 30
       position relative
27 31
       &__icon
@@ -55,14 +59,6 @@
55 59
     &__pwforgot
56 60
       cursor pointer
57 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 62
   &__footer
67 63
     position fixed
68 64
     bottom 2%

+ 0 - 1
frontend/src/css/ProgressBar.styl 파일 보기

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

+ 0 - 2
frontend/src/css/index.styl 파일 보기

@@ -30,5 +30,3 @@ html, body, #content
30 30
 @import 'HomepageCard'
31 31
 
32 32
 @import 'ExtandedAction'
33
-
34
-@import 'AdminWorkspacePage'

+ 6 - 1
frontend/src/helper.js 파일 보기

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

+ 30 - 14
frontend/src/reducer/workspaceContentList.js 파일 보기

@@ -3,25 +3,29 @@ import {
3 3
   UPDATE,
4 4
   WORKSPACE,
5 5
   WORKSPACE_CONTENT,
6
-  FOLDER
6
+  FOLDER,
7
+  WORKSPACE_CONTENT_ARCHIVED,
8
+  WORKSPACE_CONTENT_DELETED
7 9
 } from '../action-creator.sync.js'
8 10
 
9 11
 export default function workspaceContentList (state = [], action) {
10 12
   switch (action.type) {
11 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 30
     case `${UPDATE}/${WORKSPACE}/Filter`: // not used anymore ?
27 31
       return {...state, filter: action.filterList}
@@ -40,6 +44,18 @@ export default function workspaceContentList (state = [], action) {
40 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 59
     default:
44 60
       return state
45 61
   }

+ 194 - 0
frontend_app_admin_workspace_user/src/component/AdminWorkspace.jsx 파일 보기

@@ -0,0 +1,194 @@
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 파일 보기

@@ -2,15 +2,15 @@ import React from 'react'
2 2
 import { translate } from 'react-i18next'
3 3
 import i18n from '../i18n.js'
4 4
 import {
5
-  addAllResourceI18n,
6
-  // handleFetchResult,
7
-  PageWrapper,
8
-  PageTitle,
9
-  PageContent
5
+  addAllResourceI18n
6
+  // handleFetchResult
10 7
 } from 'tracim_frontend_lib'
11 8
 import { debug } from '../helper.js'
12 9
 import {
13 10
 } from '../action.async.js'
11
+import AdminWorkspace from '../component/AdminWorkspace.jsx'
12
+
13
+require('../css/index.styl')
14 14
 
15 15
 class AdminWorkspaceUser extends React.Component {
16 16
   constructor (props) {
@@ -32,7 +32,10 @@ class AdminWorkspaceUser extends React.Component {
32 32
 
33 33
   customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
34 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 39
       default:
37 40
         break
38 41
     }
@@ -48,6 +51,7 @@ class AdminWorkspaceUser extends React.Component {
48 51
     const { state } = this
49 52
 
50 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 57
   componentWillUnmount () {
@@ -67,17 +71,13 @@ class AdminWorkspaceUser extends React.Component {
67 71
 
68 72
     return (
69 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 81
       </div>
82 82
     )
83 83
   }

+ 20 - 0
frontend_app_admin_workspace_user/src/css/index.styl 파일 보기

@@ -1 +1,21 @@
1 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 파일 보기

@@ -117,3 +117,13 @@ export const putHtmlDocRestoreDeleted = (user, apiUrl, idWorkspace, idContent) =
117 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 파일 보기

@@ -26,7 +26,8 @@ import {
26 26
   putHtmlDocIsArchived,
27 27
   putHtmlDocIsDeleted,
28 28
   putHtmlDocRestoreArchived,
29
-  putHtmlDocRestoreDeleted
29
+  putHtmlDocRestoreDeleted,
30
+  putHtmlDocRead
30 31
 } from '../action.async.js'
31 32
 
32 33
 class HtmlDocument extends React.Component {
@@ -165,6 +166,9 @@ class HtmlDocument extends React.Component {
165 166
         console.log('Error loading Timeline.', e)
166 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 174
   handleClickBtnCloseApp = () => {

+ 10 - 0
frontend_app_thread/src/action.async.js 파일 보기

@@ -108,3 +108,13 @@ export const putThreadRestoreDeleted = (user, apiUrl, idWorkspace, idContent) =>
108 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 파일 보기

@@ -22,7 +22,8 @@ import {
22 22
   putThreadIsArchived,
23 23
   putThreadIsDeleted,
24 24
   putThreadRestoreArchived,
25
-  putThreadRestoreDeleted
25
+  putThreadRestoreDeleted,
26
+  putThreadRead
26 27
 } from '../action.async.js'
27 28
 
28 29
 class Thread extends React.Component {
@@ -108,20 +109,24 @@ class Thread extends React.Component {
108 109
       handleFetchResult(await fetchResultThread),
109 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 130
       .catch(e => console.log('Error loading Thread data.', e))
126 131
   }
127 132
 

+ 0 - 2
frontend_lib/src/component/Input/Checkbox.jsx 파일 보기

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

+ 1 - 1
frontend_lib/src/component/Timeline/Timeline.jsx 파일 보기

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

+ 17 - 0
setup_default_backend.sh 파일 보기

@@ -0,0 +1,17 @@
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."