Parcourir la source

merge with some fix for folder controller

Guénaël Muller il y a 5 ans
Parent
révision
3bcb0b91a8
100 fichiers modifiés avec 4678 ajouts et 674 suppressions
  1. 1 0
      .travis.yml
  2. 56 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/app_models/applications.py
  8. 31 7
      backend/tracim_backend/app_models/contents.py
  9. 5 4
      backend/tracim_backend/command/user.py
  10. 1 0
      backend/tracim_backend/config.py
  11. 8 0
      backend/tracim_backend/exceptions.py
  12. 78 15
      backend/tracim_backend/lib/core/content.py
  13. 46 9
      backend/tracim_backend/lib/core/user.py
  14. 13 12
      backend/tracim_backend/lib/core/workspace.py
  15. 34 0
      backend/tracim_backend/lib/utils/authorization.py
  16. 9 3
      backend/tracim_backend/lib/utils/request.py
  17. 0 8
      backend/tracim_backend/lib/webdav/resources.py
  18. 26 0
      backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py
  19. 2 0
      backend/tracim_backend/models/auth.py
  20. 50 20
      backend/tracim_backend/models/context_models.py
  21. 23 42
      backend/tracim_backend/models/data.py
  22. 809 0
      backend/tracim_backend/tests/functional/test_contents.py
  23. 410 1
      backend/tracim_backend/tests/functional/test_user.py
  24. 816 22
      backend/tracim_backend/tests/functional/test_workspaces.py
  25. 401 0
      backend/tracim_backend/tests/library/test_content_api.py
  26. 198 0
      backend/tracim_backend/views/contents_api/folder_controller.py
  27. 57 6
      backend/tracim_backend/views/core_api/schemas.py
  28. 47 1
      backend/tracim_backend/views/core_api/user_controller.py
  29. 147 1
      backend/tracim_backend/views/core_api/workspace_controller.py
  30. 52 0
      backend_lib.sh
  31. 34 2
      frontend/src/action-creator.async.js
  32. 8 3
      frontend/src/action-creator.sync.js
  33. 3 3
      frontend/src/component/Account/Password.jsx
  34. 3 3
      frontend/src/component/Account/PersonalData.jsx
  35. 2 2
      frontend/src/component/Account/UserInfo.jsx
  36. 3 1
      frontend/src/component/Dashboard/ContentTypeBtn.jsx
  37. 1 5
      frontend/src/component/Dashboard/ContentTypeBtn.styl
  38. 4 3
      frontend/src/component/Dashboard/MemberList.jsx
  39. 18 12
      frontend/src/component/Dashboard/MemberList.styl
  40. 2 2
      frontend/src/component/Dashboard/MoreInfo.jsx
  41. 1 0
      frontend/src/component/Dashboard/MoreInfo.styl
  42. 9 4
      frontend/src/component/Dashboard/RecentActivity.jsx
  43. 22 6
      frontend/src/component/Dashboard/RecentActivity.styl
  44. 2 2
      frontend/src/component/Dashboard/UserStatus.jsx
  45. 41 2
      frontend/src/component/Dashboard/UserStatus.styl
  46. 29 0
      frontend/src/component/Header/MenuActionListItem/AdminLink.jsx
  47. 26 23
      frontend/src/component/Header/MenuActionListItem/MenuProfil.jsx
  48. 1 1
      frontend/src/component/Header/MenuActionListItem/Search.jsx
  49. 8 8
      frontend/src/component/Sidebar/WorkspaceListItem.jsx
  50. 1 1
      frontend/src/component/common/Input/SubDropdownCreateButton.jsx
  51. 1 1
      frontend/src/container/Account.jsx
  52. 128 71
      frontend/src/container/AdminWorkspacePage.jsx
  53. 9 5
      frontend/src/container/AppFullscreenRouter.jsx
  54. 36 13
      frontend/src/container/Dashboard.jsx
  55. 11 4
      frontend/src/container/Header.jsx
  56. 33 24
      frontend/src/container/Login.jsx
  57. 1 1
      frontend/src/container/ProgressBar.jsx
  58. 38 39
      frontend/src/container/Sidebar.jsx
  59. 11 7
      frontend/src/container/Tracim.jsx
  60. 30 8
      frontend/src/container/WorkspaceContent.jsx
  61. 6 5
      frontend/src/css/AccountPage.styl
  62. 0 11
      frontend/src/css/AdminWorkspacePage.styl
  63. 22 12
      frontend/src/css/Dashboard.styl
  64. 2 2
      frontend/src/css/Generic.styl
  65. 15 3
      frontend/src/css/Header.styl
  66. 9 13
      frontend/src/css/Login.styl
  67. 0 1
      frontend/src/css/ProgressBar.styl
  68. 94 74
      frontend/src/css/Sidebar.styl
  69. 0 2
      frontend/src/css/index.styl
  70. 6 1
      frontend/src/helper.js
  71. 22 15
      frontend/src/reducer/currentWorkspace.js
  72. 4 1
      frontend/src/reducer/user.js
  73. 30 14
      frontend/src/reducer/workspaceContentList.js
  74. 0 1
      frontend_app_admin_workspace_user/dist/dev
  75. 0 1
      frontend_app_admin_workspace_user/dist/font
  76. 5 5
      frontend_app_admin_workspace_user/dist/index.html
  77. 194 0
      frontend_app_admin_workspace_user/src/component/AdminWorkspace.jsx
  78. 17 17
      frontend_app_admin_workspace_user/src/container/AdminWorkspaceUser.jsx
  79. 20 0
      frontend_app_admin_workspace_user/src/css/index.styl
  80. 0 1
      frontend_app_html-document/dist/dev
  81. 0 1
      frontend_app_html-document/dist/font
  82. 7 7
      frontend_app_html-document/dist/index.html
  83. 50 0
      frontend_app_html-document/src/action.async.js
  84. 28 0
      frontend_app_html-document/src/component/HtmlDocument.jsx
  85. 90 17
      frontend_app_html-document/src/container/HtmlDocument.jsx
  86. 14 3
      frontend_app_html-document/src/css/index.styl
  87. 0 1
      frontend_app_thread/dist/dev
  88. 0 1
      frontend_app_thread/dist/font
  89. 5 5
      frontend_app_thread/dist/index.html
  90. 50 0
      frontend_app_thread/src/action.async.js
  91. 2 2
      frontend_app_thread/src/container/PopupCreateThread.jsx
  92. 101 22
      frontend_app_thread/src/container/Thread.jsx
  93. 0 4
      frontend_app_thread/src/css/index.styl
  94. 0 1
      frontend_app_workspace/dist/dev
  95. 0 1
      frontend_app_workspace/dist/font
  96. 7 7
      frontend_app_workspace/dist/index.html
  97. 0 1
      frontend_lib/dist/dev
  98. 0 1
      frontend_lib/dist/font
  99. 5 5
      frontend_lib/dist/index.html
  100. 0 0
      frontend_lib/package.json

+ 1 - 0
.travis.yml Voir le fichier

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

+ 56 - 7
backend/README.md Voir le fichier

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

+ 23 - 0
backend/doc/known_issues.md Voir le fichier

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 Voir le fichier

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

+ 4 - 2
backend/doc/roles.md Voir le fichier

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

+ 4 - 0
backend/tracim_backend/__init__.py Voir le fichier

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

+ 2 - 0
backend/tracim_backend/app_models/applications.py Voir le fichier

51
             creation_label: str,
51
             creation_label: str,
52
             available_statuses: typing.List['ContentStatus'],
52
             available_statuses: typing.List['ContentStatus'],
53
             slug_alias: typing.List[str] = None,
53
             slug_alias: typing.List[str] = None,
54
+            allow_sub_content: bool = False,
54
     ):
55
     ):
55
         content_type = ContentType(
56
         content_type = ContentType(
56
             slug=slug,
57
             slug=slug,
60
             creation_label=creation_label,
61
             creation_label=creation_label,
61
             available_statuses=available_statuses,
62
             available_statuses=available_statuses,
62
             slug_alias=slug_alias,
63
             slug_alias=slug_alias,
64
+            allow_sub_content=allow_sub_content,
63
         )
65
         )
64
         self.content_types.append(content_type)
66
         self.content_types.append(content_type)
65
 
67
 

+ 31 - 7
backend/tracim_backend/app_models/contents.py Voir le fichier

118
             creation_label: str,
118
             creation_label: str,
119
             available_statuses: typing.List[ContentStatus],
119
             available_statuses: typing.List[ContentStatus],
120
             slug_alias: typing.List[str] = None,
120
             slug_alias: typing.List[str] = None,
121
+            allow_sub_content: bool = False,
121
     ):
122
     ):
122
         self.slug = slug
123
         self.slug = slug
123
         self.fa_icon = fa_icon
124
         self.fa_icon = fa_icon
126
         self.creation_label = creation_label
127
         self.creation_label = creation_label
127
         self.available_statuses = available_statuses
128
         self.available_statuses = available_statuses
128
         self.slug_alias = slug_alias
129
         self.slug_alias = slug_alias
130
+        self.allow_sub_content = allow_sub_content
129
 
131
 
130
 
132
 
131
 thread_type = 'thread'
133
 thread_type = 'thread'
188
     def _content_types(self):
190
     def _content_types(self):
189
         app_api = ApplicationApi(self.app_list)
191
         app_api = ApplicationApi(self.app_list)
190
         content_types = app_api.get_content_types()
192
         content_types = app_api.get_content_types()
191
-        content_types.extend(self._special_contents_types)
193
+        # content_types.extend(self._special_contents_types)
192
         return content_types
194
         return content_types
193
 
195
 
194
     def get_one_by_slug(self, slug: str) -> ContentType:
196
     def get_one_by_slug(self, slug: str) -> ContentType:
198
         """
200
         """
199
         content_types = self._content_types.copy()
201
         content_types = self._content_types.copy()
200
         content_types.extend(self._special_contents_types)
202
         content_types.extend(self._special_contents_types)
203
+        content_types.append(self.Event)
201
         for item in content_types:
204
         for item in content_types:
202
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
205
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
203
                 return item
206
                 return item
204
         raise ContentTypeNotExist()
207
         raise ContentTypeNotExist()
205
 
208
 
206
-    def endpoint_allowed_types_slug(self) -> typing.List[str]:
209
+    def restricted_allowed_types_slug(self) -> typing.List[str]:
207
         """
210
         """
208
-        Return restricted list of content_type:
209
-        dont return special content_type like  comment, don't return
211
+        Return restricted list of content_type: don't return
210
         "any" slug, dont return content type slug alias , don't return event.
212
         "any" slug, dont return content type slug alias , don't return event.
211
         Useful to restrict slug param in schema.
213
         Useful to restrict slug param in schema.
212
         """
214
         """
213
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
215
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
214
         return allowed_type_slug
216
         return allowed_type_slug
215
 
217
 
218
+    def endpoint_allowed_types_slug(self) -> typing.List[str]:
219
+        """
220
+        Same as restricted_allowed_types_slug but with special content_type
221
+        included like comments.
222
+        """
223
+        content_types = self._content_types
224
+        content_types.extend(self._special_contents_types)
225
+        allowed_type_slug = [contents_type.slug for contents_type in content_types]  # nopep8
226
+        return allowed_type_slug
227
+
216
     def query_allowed_types_slugs(self) -> typing.List[str]:
228
     def query_allowed_types_slugs(self) -> typing.List[str]:
217
         """
229
         """
218
         Return alls allowed types slug : content_type slug + all alias, any
230
         Return alls allowed types slug : content_type slug + all alias, any
220
         Usefull allowed value to perform query to database.
232
         Usefull allowed value to perform query to database.
221
         """
233
         """
222
         allowed_types_slug = []
234
         allowed_types_slug = []
223
-        for content_type in self._content_types:
235
+        content_types = self._content_types
236
+        content_types.extend(self._special_contents_types)
237
+        for content_type in content_types:
224
             allowed_types_slug.append(content_type.slug)
238
             allowed_types_slug.append(content_type.slug)
225
             if content_type.slug_alias:
239
             if content_type.slug_alias:
226
                 allowed_types_slug.extend(content_type.slug_alias)
240
                 allowed_types_slug.extend(content_type.slug_alias)
227
-        for content_type in self._special_contents_types:
228
-            allowed_types_slug.append(content_type.slug)
229
         allowed_types_slug.extend(self._extra_slugs)
241
         allowed_types_slug.extend(self._extra_slugs)
230
         return allowed_types_slug
242
         return allowed_types_slug
231
 
243
 
244
+    def default_allowed_content_properties(self, slug) -> dict:
245
+        content_type = self.get_one_by_slug(slug)
246
+        if content_type.allow_sub_content:
247
+            sub_content_allowed = self.endpoint_allowed_types_slug()
248
+        else:
249
+            sub_content_allowed = [self.Comment.slug]
250
+
251
+        properties_dict = {}
252
+        for elem in sub_content_allowed:
253
+            properties_dict[elem] = True
254
+        return properties_dict
255
+
232
 
256
 
233
 CONTENT_TYPES = ContentTypeList(APP_LIST)
257
 CONTENT_TYPES = ContentTypeList(APP_LIST)

+ 5 - 4
backend/tracim_backend/command/user.py Voir le fichier

127
                     "You must provide -p/--password parameter"
127
                     "You must provide -p/--password parameter"
128
                 )
128
                 )
129
             password = ''
129
             password = ''
130
-
130
+        if self._user_api.check_email_already_in_db(login):
131
+            raise UserAlreadyExistError()
131
         try:
132
         try:
132
             user = self._user_api.create_user(
133
             user = self._user_api.create_user(
133
                 email=login,
134
                 email=login,
140
             # daemons = DaemonsManager()
141
             # daemons = DaemonsManager()
141
             # daemons.run('radicale', RadicaleDaemon)
142
             # daemons.run('radicale', RadicaleDaemon)
142
             self._user_api.execute_created_user_actions(user)
143
             self._user_api.execute_created_user_actions(user)
143
-        except IntegrityError:
144
+        except IntegrityError as exception:
144
             self._session.rollback()
145
             self._session.rollback()
145
-            raise UserAlreadyExistError()
146
+            raise UserAlreadyExistError() from exception
146
         except NotificationNotSend as exception:
147
         except NotificationNotSend as exception:
147
             self._session.rollback()
148
             self._session.rollback()
148
-            raise exception
149
+            raise exception from exception
149
 
150
 
150
         return user
151
         return user
151
 
152
 

+ 1 - 0
backend/tracim_backend/config.py Voir le fichier

551
             label='Folder',
551
             label='Folder',
552
             creation_label='Create a folder',
552
             creation_label='Create a folder',
553
             available_statuses=CONTENT_STATUS.get_all(),
553
             available_statuses=CONTENT_STATUS.get_all(),
554
+            allow_sub_content=True,
554
         )
555
         )
555
 
556
 
556
         _file = Application(
557
         _file = Application(

+ 8 - 0
backend/tracim_backend/exceptions.py Voir le fichier

205
     pass
205
     pass
206
 
206
 
207
 
207
 
208
+class UnallowedSubContent(TracimException):
209
+    pass
210
+
211
+
208
 class TooShortAutocompleteString(TracimException):
212
 class TooShortAutocompleteString(TracimException):
209
     pass
213
     pass
210
 
214
 
215
 
219
 
216
 class AppDoesNotExist(TracimException):
220
 class AppDoesNotExist(TracimException):
217
     pass
221
     pass
222
+
223
+
224
+class EmailAlreadyExistInDb(TracimException):
225
+    pass

+ 78 - 15
backend/tracim_backend/lib/core/content.py Voir le fichier

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

+ 46 - 9
backend/tracim_backend/lib/core/user.py Voir le fichier

13
 from tracim_backend.models.auth import User
13
 from tracim_backend.models.auth import User
14
 from tracim_backend.models.auth import Group
14
 from tracim_backend.models.auth import Group
15
 from tracim_backend.exceptions import NoUserSetted
15
 from tracim_backend.exceptions import NoUserSetted
16
+from tracim_backend.exceptions import EmailAlreadyExistInDb
16
 from tracim_backend.exceptions import TooShortAutocompleteString
17
 from tracim_backend.exceptions import TooShortAutocompleteString
17
 from tracim_backend.exceptions import PasswordDoNotMatch
18
 from tracim_backend.exceptions import PasswordDoNotMatch
18
 from tracim_backend.exceptions import EmailValidationFailed
19
 from tracim_backend.exceptions import EmailValidationFailed
34
             current_user: typing.Optional[User],
35
             current_user: typing.Optional[User],
35
             session: Session,
36
             session: Session,
36
             config: CFG,
37
             config: CFG,
38
+            show_deleted: bool = False,
37
     ) -> None:
39
     ) -> None:
38
         self._session = session
40
         self._session = session
39
         self._user = current_user
41
         self._user = current_user
40
         self._config = config
42
         self._config = config
43
+        self._show_deleted = show_deleted
41
 
44
 
42
     def _base_query(self):
45
     def _base_query(self):
43
-        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
44
 
50
 
45
     def get_user_with_context(self, user: User) -> UserInContext:
51
     def get_user_with_context(self, user: User) -> UserInContext:
46
         """
52
         """
267
         return user
273
         return user
268
 
274
 
269
     def _check_email(self, email: str) -> bool:
275
     def _check_email(self, email: str) -> bool:
276
+        """
277
+        Check if email is completely ok to be used in user db table
278
+        """
279
+        is_email_correct = self._check_email_correctness(email)
280
+        if not is_email_correct:
281
+            raise EmailValidationFailed(
282
+                'Email given form {} is uncorrect'.format(email))  # nopep8
283
+        email_already_exist_in_db = self.check_email_already_in_db(email)
284
+        if email_already_exist_in_db:
285
+            raise EmailAlreadyExistInDb(
286
+                'Email given {} already exist, please choose something else'.format(email)  # nopep8
287
+            )
288
+        return True
289
+
290
+    def check_email_already_in_db(self, email: str) -> bool:
291
+        """
292
+        Verify if given email does not already exist in db
293
+        """
294
+        return self._session.query(User.email).filter(User.email==email).count() != 0  # nopep8
295
+
296
+    def _check_email_correctness(self, email: str) -> bool:
297
+        """
298
+           Verify if given email is correct:
299
+           - check format
300
+           - futur active check for email ? (dns based ?)
301
+           """
270
         # TODO - G.M - 2018-07-05 - find a better way to check email
302
         # TODO - G.M - 2018-07-05 - find a better way to check email
271
         if not email:
303
         if not email:
272
             return False
304
             return False
288
         if name is not None:
320
         if name is not None:
289
             user.display_name = name
321
             user.display_name = name
290
 
322
 
291
-        if email is not None:
292
-            email_exist = self._check_email(email)
293
-            if not email_exist:
294
-                raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
323
+        if email is not None and email != user.email:
324
+            self._check_email(email)
295
             user.email = email
325
             user.email = email
296
 
326
 
297
         if password is not None:
327
         if password is not None:
354
             save_now=False
384
             save_now=False
355
     ) -> User:
385
     ) -> User:
356
         """Previous create_user method"""
386
         """Previous create_user method"""
387
+        self._check_email(email)
357
         user = User()
388
         user = User()
358
-
359
-        email_exist = self._check_email(email)
360
-        if not email_exist:
361
-            raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
362
         user.email = email
389
         user.email = email
363
         user.display_name = email.split('@')[0]
390
         user.display_name = email.split('@')[0]
364
 
391
 
382
         if do_save:
409
         if do_save:
383
             self.save(user)
410
             self.save(user)
384
 
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
+
385
     def save(self, user: User):
422
     def save(self, user: User):
386
         self._session.flush()
423
         self._session.flush()
387
 
424
 

+ 13 - 12
backend/tracim_backend/lib/core/workspace.py Voir le fichier

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

+ 34 - 0
backend/tracim_backend/lib/utils/authorization.py Voir le fichier

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

+ 9 - 3
backend/tracim_backend/lib/utils/request.py Voir le fichier

229
             api = ContentApi(
229
             api = ContentApi(
230
                 current_user=user,
230
                 current_user=user,
231
                 session=request.dbsession,
231
                 session=request.dbsession,
232
+                show_deleted=True,
233
+                show_archived=True,
232
                 config=request.registry.settings['CFG']
234
                 config=request.registry.settings['CFG']
233
             )
235
             )
234
             comment = api.get_one(
236
             comment = api.get_one(
268
                 raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
270
                 raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
269
             api = ContentApi(
271
             api = ContentApi(
270
                 current_user=user,
272
                 current_user=user,
273
+                show_deleted=True,
274
+                show_archived=True,
271
                 session=request.dbsession,
275
                 session=request.dbsession,
272
                 config=request.registry.settings['CFG']
276
                 config=request.registry.settings['CFG']
273
             )
277
             )
289
         :return: user found from header/body
293
         :return: user found from header/body
290
         """
294
         """
291
         app_config = request.registry.settings['CFG']
295
         app_config = request.registry.settings['CFG']
292
-        uapi = UserApi(None, session=request.dbsession, config=app_config)
296
+        uapi = UserApi(None, show_deleted=True, session=request.dbsession, config=app_config)
293
         login = ''
297
         login = ''
294
         try:
298
         try:
295
             login = None
299
             login = None
351
             wapi = WorkspaceApi(
355
             wapi = WorkspaceApi(
352
                 current_user=user,
356
                 current_user=user,
353
                 session=request.dbsession,
357
                 session=request.dbsession,
354
-                config=request.registry.settings['CFG']
358
+                config=request.registry.settings['CFG'],
359
+                show_deleted=True,
355
             )
360
             )
356
             workspace = wapi.get_one(workspace_id)
361
             workspace = wapi.get_one(workspace_id)
357
         except NoResultFound as exc:
362
         except NoResultFound as exc:
386
             wapi = WorkspaceApi(
391
             wapi = WorkspaceApi(
387
                 current_user=user,
392
                 current_user=user,
388
                 session=request.dbsession,
393
                 session=request.dbsession,
389
-                config=request.registry.settings['CFG']
394
+                config=request.registry.settings['CFG'],
395
+                show_deleted=True,
390
             )
396
             )
391
             workspace = wapi.get_one(workspace_id)
397
             workspace = wapi.get_one(workspace_id)
392
         except JSONDecodeError as exc:
398
         except JSONDecodeError as exc:

+ 0 - 8
backend/tracim_backend/lib/webdav/resources.py Voir le fichier

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

+ 26 - 0
backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py Voir le fichier

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 Voir le fichier

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

+ 50 - 20
backend/tracim_backend/models/context_models.py Voir le fichier

10
 from tracim_backend.extensions import APP_LIST
10
 from tracim_backend.extensions import APP_LIST
11
 from tracim_backend.lib.core.application import ApplicationApi
11
 from tracim_backend.lib.core.application import ApplicationApi
12
 from tracim_backend.lib.utils.utils import get_root_frontend_url
12
 from tracim_backend.lib.utils.utils import get_root_frontend_url
13
+from tracim_backend.lib.utils.utils import password_generator
13
 from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
14
 from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
14
 from tracim_backend.lib.utils.utils import WORKSPACE_FRONTEND_URL_SCHEMA
15
 from tracim_backend.lib.utils.utils import WORKSPACE_FRONTEND_URL_SCHEMA
15
 from tracim_backend.models import User
16
 from tracim_backend.models import User
16
 from tracim_backend.models.auth import Profile
17
 from tracim_backend.models.auth import Profile
18
+from tracim_backend.models.auth import Group
17
 from tracim_backend.models.data import Content
19
 from tracim_backend.models.data import Content
18
 from tracim_backend.models.data import ContentRevisionRO
20
 from tracim_backend.models.data import ContentRevisionRO
19
 from tracim_backend.models.data import Workspace
21
 from tracim_backend.models.data import Workspace
100
     def __init__(
102
     def __init__(
101
             self,
103
             self,
102
             email: str,
104
             email: str,
103
-            password: str,
104
-            public_name: str,
105
-            timezone: str,
106
-            profile: str,
107
-            email_notification: str,
105
+            password: str = None,
106
+            public_name: str = None,
107
+            timezone: str = None,
108
+            profile: str = None,
109
+            email_notification: bool = True,
108
     ) -> None:
110
     ) -> None:
109
         self.email = email
111
         self.email = email
110
-        self.password = password
111
-        self.public_name = public_name
112
-        self.timezone = timezone
113
-        self.profile = profile
112
+        # INFO - G.M - 2018-08-16 - cleartext password, default value
113
+        # is auto-generated.
114
+        self.password = password or password_generator()
115
+        self.public_name = public_name or None
116
+        self.timezone = timezone or ''
117
+        self.profile = profile or Group.TIM_USER_GROUPNAME
114
         self.email_notification = email_notification
118
         self.email_notification = email_notification
115
 
119
 
116
 
120
 
296
     Content creation model
300
     Content creation model
297
     """
301
     """
298
     def __init__(
302
     def __init__(
299
-            self,
300
-            label: str,
301
-            content_type: str,
302
-            parent_id: typing.Optional[int] = None,
303
+        self,
304
+        label: str,
305
+        content_type: str,
306
+        parent_id: typing.Optional[int] = None,
303
     ) -> None:
307
     ) -> None:
304
         self.label = label
308
         self.label = label
305
         self.content_type = content_type
309
         self.content_type = content_type
311
     Comment creation model
315
     Comment creation model
312
     """
316
     """
313
     def __init__(
317
     def __init__(
314
-            self,
315
-            raw_content: str,
318
+        self,
319
+        raw_content: str,
316
     ) -> None:
320
     ) -> None:
317
         self.raw_content = raw_content
321
         self.raw_content = raw_content
318
 
322
 
322
     Set content status
326
     Set content status
323
     """
327
     """
324
     def __init__(
328
     def __init__(
325
-            self,
326
-            status: str,
329
+        self,
330
+        status: str,
327
     ) -> None:
331
     ) -> None:
328
         self.status = status
332
         self.status = status
329
 
333
 
333
     TextBasedContent update model
337
     TextBasedContent update model
334
     """
338
     """
335
     def __init__(
339
     def __init__(
336
-            self,
337
-            label: str,
338
-            raw_content: str,
340
+        self,
341
+        label: str,
342
+        raw_content: str,
343
+    ) -> None:
344
+        self.label = label
345
+        self.raw_content = raw_content
346
+
347
+
348
+class FolderContentUpdate(object):
349
+    """
350
+    Folder Content update model
351
+    """
352
+    def __init__(
353
+        self,
354
+        label: str,
355
+        raw_content: str,
356
+        sub_content_types: typing.List[str],
339
     ) -> None:
357
     ) -> None:
340
         self.label = label
358
         self.label = label
341
         self.raw_content = raw_content
359
         self.raw_content = raw_content
360
+        self.sub_content_types = sub_content_types
342
 
361
 
343
 
362
 
344
 class TypeUser(Enum):
363
 class TypeUser(Enum):
392
     def profile(self) -> Profile:
411
     def profile(self) -> Profile:
393
         return self.user.profile.name
412
         return self.user.profile.name
394
 
413
 
414
+    @property
415
+    def is_deleted(self) -> bool:
416
+        return self.user.is_deleted
417
+
395
     # Context related
418
     # Context related
396
 
419
 
397
     @property
420
     @property
456
         return slugify(self.workspace.label)
479
         return slugify(self.workspace.label)
457
 
480
 
458
     @property
481
     @property
482
+    def is_deleted(self) -> bool:
483
+        """
484
+        Is the workspace deleted ?
485
+        """
486
+        return self.workspace.is_deleted
487
+
488
+    @property
459
     def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
489
     def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
460
         """
490
         """
461
         get sidebar entries, those depends on activated apps.
491
         get sidebar entries, those depends on activated apps.

+ 23 - 42
backend/tracim_backend/models/data.py Voir le fichier

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

+ 809 - 0
backend/tracim_backend/tests/functional/test_contents.py Voir le fichier

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

+ 410 - 1
backend/tracim_backend/tests/functional/test_user.py Voir le fichier

4
 """
4
 """
5
 from time import sleep
5
 from time import sleep
6
 import pytest
6
 import pytest
7
+import requests
7
 import transaction
8
 import transaction
8
 
9
 
9
 from tracim_backend import models
10
 from tracim_backend import models
2400
         assert workspace['workspace_id'] == 1
2401
         assert workspace['workspace_id'] == 1
2401
         assert workspace['label'] == 'Business'
2402
         assert workspace['label'] == 'Business'
2402
         assert workspace['slug'] == 'business'
2403
         assert workspace['slug'] == 'business'
2404
+        assert workspace['is_deleted'] is False
2405
+
2403
         assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
2406
         assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
2404
         for counter, sidebar_entry in enumerate(default_sidebar_entry):
2407
         for counter, sidebar_entry in enumerate(default_sidebar_entry):
2405
             workspace['sidebar_entries'][counter]['slug'] = sidebar_entry.slug
2408
             workspace['sidebar_entries'][counter]['slug'] = sidebar_entry.slug
2408
             workspace['sidebar_entries'][counter]['hexcolor'] = sidebar_entry.hexcolor  # nopep8
2411
             workspace['sidebar_entries'][counter]['hexcolor'] = sidebar_entry.hexcolor  # nopep8
2409
             workspace['sidebar_entries'][counter]['fa_icon'] = sidebar_entry.fa_icon  # nopep8
2412
             workspace['sidebar_entries'][counter]['fa_icon'] = sidebar_entry.fa_icon  # nopep8
2410
 
2413
 
2411
-
2412
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
2414
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
2413
         """
2415
         """
2414
         Check obtain all workspaces reachables for one user
2416
         Check obtain all workspaces reachables for one user
2520
         assert res['email'] == 'test@test.test'
2522
         assert res['email'] == 'test@test.test'
2521
         assert res['public_name'] == 'bob'
2523
         assert res['public_name'] == 'bob'
2522
         assert res['timezone'] == 'Europe/Paris'
2524
         assert res['timezone'] == 'Europe/Paris'
2525
+        assert res['is_deleted'] is False
2523
 
2526
 
2524
     def test_api__get_user__ok_200__user_itself(self):
2527
     def test_api__get_user__ok_200__user_itself(self):
2525
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2528
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2569
         assert res['email'] == 'test@test.test'
2572
         assert res['email'] == 'test@test.test'
2570
         assert res['public_name'] == 'bob'
2573
         assert res['public_name'] == 'bob'
2571
         assert res['timezone'] == 'Europe/Paris'
2574
         assert res['timezone'] == 'Europe/Paris'
2575
+        assert res['is_deleted'] is False
2572
 
2576
 
2573
     def test_api__get_user__err_403__other_normal_user(self):
2577
     def test_api__get_user__err_403__other_normal_user(self):
2574
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2578
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2621
             status=403
2625
             status=403
2622
         )
2626
         )
2623
 
2627
 
2628
+    def test_api__create_user__ok_200__full_admin(self):
2629
+        self.testapp.authorization = (
2630
+            'Basic',
2631
+            (
2632
+                'admin@admin.admin',
2633
+                'admin@admin.admin'
2634
+            )
2635
+        )
2636
+        params = {
2637
+            'email': 'test@test.test',
2638
+            'password': 'mysuperpassword',
2639
+            'profile': 'users',
2640
+            'timezone': 'Europe/Paris',
2641
+            'public_name': 'test user',
2642
+            'email_notification': False,
2643
+        }
2644
+        res = self.testapp.post_json(
2645
+            '/api/v2/users',
2646
+            status=200,
2647
+            params=params,
2648
+        )
2649
+        res = res.json_body
2650
+        assert res['user_id']
2651
+        user_id = res['user_id']
2652
+        assert res['created']
2653
+        assert res['is_active'] is True
2654
+        assert res['profile'] == 'users'
2655
+        assert res['email'] == 'test@test.test'
2656
+        assert res['public_name'] == 'test user'
2657
+        assert res['timezone'] == 'Europe/Paris'
2658
+
2659
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2660
+        admin = dbsession.query(models.User) \
2661
+            .filter(models.User.email == 'admin@admin.admin') \
2662
+            .one()
2663
+        uapi = UserApi(
2664
+            current_user=admin,
2665
+            session=dbsession,
2666
+            config=self.app_config,
2667
+        )
2668
+        user = uapi.get_one(user_id)
2669
+        assert user.email == 'test@test.test'
2670
+        assert user.validate_password('mysuperpassword')
2671
+
2672
+    def test_api__create_user__ok_200__limited_admin(self):
2673
+        self.testapp.authorization = (
2674
+            'Basic',
2675
+            (
2676
+                'admin@admin.admin',
2677
+                'admin@admin.admin'
2678
+            )
2679
+        )
2680
+        params = {
2681
+            'email': 'test@test.test',
2682
+            'email_notification': False,
2683
+        }
2684
+        res = self.testapp.post_json(
2685
+            '/api/v2/users',
2686
+            status=200,
2687
+            params=params,
2688
+        )
2689
+        res = res.json_body
2690
+        assert res['user_id']
2691
+        user_id = res['user_id']
2692
+        assert res['created']
2693
+        assert res['is_active'] is True
2694
+        assert res['profile'] == 'users'
2695
+        assert res['email'] == 'test@test.test'
2696
+        assert res['public_name'] == 'test'
2697
+        assert res['timezone'] == ''
2698
+
2699
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2700
+        admin = dbsession.query(models.User) \
2701
+            .filter(models.User.email == 'admin@admin.admin') \
2702
+            .one()
2703
+        uapi = UserApi(
2704
+            current_user=admin,
2705
+            session=dbsession,
2706
+            config=self.app_config,
2707
+        )
2708
+        user = uapi.get_one(user_id)
2709
+        assert user.email == 'test@test.test'
2710
+        assert user.password
2711
+
2712
+    def test_api__create_user__err_400__email_already_in_db(self):
2713
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2714
+        admin = dbsession.query(models.User) \
2715
+            .filter(models.User.email == 'admin@admin.admin') \
2716
+            .one()
2717
+        uapi = UserApi(
2718
+            current_user=admin,
2719
+            session=dbsession,
2720
+            config=self.app_config,
2721
+        )
2722
+        gapi = GroupApi(
2723
+            current_user=admin,
2724
+            session=dbsession,
2725
+            config=self.app_config,
2726
+        )
2727
+        groups = [gapi.get_one_with_name('users')]
2728
+        test_user = uapi.create_user(
2729
+            email='test@test.test',
2730
+            password='pass',
2731
+            name='bob',
2732
+            groups=groups,
2733
+            timezone='Europe/Paris',
2734
+            do_save=True,
2735
+            do_notify=False,
2736
+        )
2737
+        uapi.save(test_user)
2738
+        transaction.commit()
2739
+        self.testapp.authorization = (
2740
+            'Basic',
2741
+            (
2742
+                'admin@admin.admin',
2743
+                'admin@admin.admin'
2744
+            )
2745
+        )
2746
+        params = {
2747
+            'email': 'test@test.test',
2748
+            'password': 'mysuperpassword',
2749
+            'profile': 'users',
2750
+            'timezone': 'Europe/Paris',
2751
+            'public_name': 'test user',
2752
+            'email_notification': False,
2753
+        }
2754
+        res = self.testapp.post_json(
2755
+            '/api/v2/users',
2756
+            status=400,
2757
+            params=params,
2758
+        )
2759
+
2760
+    def test_api__create_user__err_403__other_user(self):
2761
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2762
+        admin = dbsession.query(models.User) \
2763
+            .filter(models.User.email == 'admin@admin.admin') \
2764
+            .one()
2765
+        uapi = UserApi(
2766
+            current_user=admin,
2767
+            session=dbsession,
2768
+            config=self.app_config,
2769
+        )
2770
+        gapi = GroupApi(
2771
+            current_user=admin,
2772
+            session=dbsession,
2773
+            config=self.app_config,
2774
+        )
2775
+        groups = [gapi.get_one_with_name('users')]
2776
+        test_user = uapi.create_user(
2777
+            email='test@test.test',
2778
+            password='pass',
2779
+            name='bob',
2780
+            groups=groups,
2781
+            timezone='Europe/Paris',
2782
+            do_save=True,
2783
+            do_notify=False,
2784
+        )
2785
+        uapi.save(test_user)
2786
+        transaction.commit()
2787
+        self.testapp.authorization = (
2788
+            'Basic',
2789
+            (
2790
+                'test@test.test',
2791
+                'pass',
2792
+            )
2793
+        )
2794
+        params = {
2795
+            'email': 'test2@test2.test2',
2796
+            'password': 'mysuperpassword',
2797
+            'profile': 'users',
2798
+            'timezone': 'Europe/Paris',
2799
+            'public_name': 'test user',
2800
+            'email_notification': False,
2801
+        }
2802
+        res = self.testapp.post_json(
2803
+            '/api/v2/users',
2804
+            status=403,
2805
+            params=params,
2806
+        )
2807
+
2808
+
2809
+class TestUserWithNotificationEndpoint(FunctionalTest):
2810
+    """
2811
+    Tests for POST /api/v2/users/{user_id}
2812
+    """
2813
+    config_section = 'functional_test_with_mail_test_sync'
2814
+
2815
+    def test_api__create_user__ok_200__full_admin_with_notif(self):
2816
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')
2817
+        self.testapp.authorization = (
2818
+            'Basic',
2819
+            (
2820
+                'admin@admin.admin',
2821
+                'admin@admin.admin'
2822
+            )
2823
+        )
2824
+        params = {
2825
+            'email': 'test@test.test',
2826
+            'password': 'mysuperpassword',
2827
+            'profile': 'users',
2828
+            'timezone': 'Europe/Paris',
2829
+            'public_name': 'test user',
2830
+            'email_notification': True,
2831
+        }
2832
+        res = self.testapp.post_json(
2833
+            '/api/v2/users',
2834
+            status=200,
2835
+            params=params,
2836
+        )
2837
+        res = res.json_body
2838
+        assert res['user_id']
2839
+        user_id = res['user_id']
2840
+        assert res['created']
2841
+        assert res['is_active'] is True
2842
+        assert res['profile'] == 'users'
2843
+        assert res['email'] == 'test@test.test'
2844
+        assert res['public_name'] == 'test user'
2845
+        assert res['timezone'] == 'Europe/Paris'
2846
+
2847
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2848
+        admin = dbsession.query(models.User) \
2849
+            .filter(models.User.email == 'admin@admin.admin') \
2850
+            .one()
2851
+        uapi = UserApi(
2852
+            current_user=admin,
2853
+            session=dbsession,
2854
+            config=self.app_config,
2855
+        )
2856
+        user = uapi.get_one(user_id)
2857
+        assert user.email == 'test@test.test'
2858
+        assert user.validate_password('mysuperpassword')
2859
+
2860
+        # check mail received
2861
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
2862
+        response = response.json()
2863
+        assert len(response) == 1
2864
+        headers = response[0]['Content']['Headers']
2865
+        assert headers['From'][0] == 'Tracim Notifications <test_user_from+0@localhost>'  # nopep8
2866
+        assert headers['To'][0] == 'test user <test@test.test>'
2867
+        assert headers['Subject'][0] == '[TRACIM] Created account'
2868
+
2869
+        # TODO - G.M - 2018-08-02 - Place cleanup outside of the test
2870
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')
2871
+
2872
+    def test_api__create_user__ok_200__limited_admin_with_notif(self):
2873
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')
2874
+        self.testapp.authorization = (
2875
+            'Basic',
2876
+            (
2877
+                'admin@admin.admin',
2878
+                'admin@admin.admin'
2879
+            )
2880
+        )
2881
+        params = {
2882
+            'email': 'test@test.test',
2883
+            'email_notification': True,
2884
+        }
2885
+        res = self.testapp.post_json(
2886
+            '/api/v2/users',
2887
+            status=200,
2888
+            params=params,
2889
+        )
2890
+        res = res.json_body
2891
+        assert res['user_id']
2892
+        user_id = res['user_id']
2893
+        assert res['created']
2894
+        assert res['is_active'] is True
2895
+        assert res['profile'] == 'users'
2896
+        assert res['email'] == 'test@test.test'
2897
+        assert res['public_name'] == 'test'
2898
+        assert res['timezone'] == ''
2899
+
2900
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2901
+        admin = dbsession.query(models.User) \
2902
+            .filter(models.User.email == 'admin@admin.admin') \
2903
+            .one()
2904
+        uapi = UserApi(
2905
+            current_user=admin,
2906
+            session=dbsession,
2907
+            config=self.app_config,
2908
+        )
2909
+        user = uapi.get_one(user_id)
2910
+        assert user.email == 'test@test.test'
2911
+        assert user.password
2912
+
2913
+        # check mail received
2914
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
2915
+        response = response.json()
2916
+        assert len(response) == 1
2917
+        headers = response[0]['Content']['Headers']
2918
+        assert headers['From'][0] == 'Tracim Notifications <test_user_from+0@localhost>'  # nopep8
2919
+        assert headers['To'][0] == 'test <test@test.test>'
2920
+        assert headers['Subject'][0] == '[TRACIM] Created account'
2921
+
2922
+        # TODO - G.M - 2018-08-02 - Place cleanup outside of the test
2923
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')
2924
+
2925
+    def test_api_delete_user__ok_200__admin(self):
2926
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2927
+        admin = dbsession.query(models.User) \
2928
+            .filter(models.User.email == 'admin@admin.admin') \
2929
+            .one()
2930
+        uapi = UserApi(
2931
+            current_user=admin,
2932
+            session=dbsession,
2933
+            config=self.app_config,
2934
+        )
2935
+        gapi = GroupApi(
2936
+            current_user=admin,
2937
+            session=dbsession,
2938
+            config=self.app_config,
2939
+        )
2940
+        groups = [gapi.get_one_with_name('users')]
2941
+        test_user = uapi.create_user(
2942
+            email='test@test.test',
2943
+            password='pass',
2944
+            name='bob',
2945
+            groups=groups,
2946
+            timezone='Europe/Paris',
2947
+            do_save=True,
2948
+            do_notify=False,
2949
+        )
2950
+        uapi.save(test_user)
2951
+        transaction.commit()
2952
+        user_id = int(test_user.user_id)
2953
+
2954
+        self.testapp.authorization = (
2955
+            'Basic',
2956
+            (
2957
+                'admin@admin.admin',
2958
+                'admin@admin.admin'
2959
+            )
2960
+        )
2961
+        self.testapp.put(
2962
+            '/api/v2/users/{}/delete'.format(user_id),
2963
+            status=204
2964
+        )
2965
+        res = self.testapp.get(
2966
+            '/api/v2/users/{}'.format(user_id),
2967
+            status=200
2968
+        ).json_body
2969
+        assert res['is_deleted'] is True
2970
+
2624
 
2971
 
2625
 class TestUsersEndpoint(FunctionalTest):
2972
 class TestUsersEndpoint(FunctionalTest):
2626
     # -*- coding: utf-8 -*-
2973
     # -*- coding: utf-8 -*-
3075
         res = res.json_body
3422
         res = res.json_body
3076
         assert res['email'] == 'mysuperemail@email.fr'
3423
         assert res['email'] == 'mysuperemail@email.fr'
3077
 
3424
 
3425
+    def test_api__set_user_email__err_400__admin_same_email(self):
3426
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
3427
+        admin = dbsession.query(models.User) \
3428
+            .filter(models.User.email == 'admin@admin.admin') \
3429
+            .one()
3430
+        uapi = UserApi(
3431
+            current_user=admin,
3432
+            session=dbsession,
3433
+            config=self.app_config,
3434
+        )
3435
+        gapi = GroupApi(
3436
+            current_user=admin,
3437
+            session=dbsession,
3438
+            config=self.app_config,
3439
+        )
3440
+        groups = [gapi.get_one_with_name('users')]
3441
+        test_user = uapi.create_user(
3442
+            email='test@test.test',
3443
+            password='pass',
3444
+            name='bob',
3445
+            groups=groups,
3446
+            timezone='Europe/Paris',
3447
+            do_save=True,
3448
+            do_notify=False,
3449
+        )
3450
+        uapi.save(test_user)
3451
+        transaction.commit()
3452
+        user_id = int(test_user.user_id)
3453
+
3454
+        self.testapp.authorization = (
3455
+            'Basic',
3456
+            (
3457
+                'admin@admin.admin',
3458
+                'admin@admin.admin'
3459
+            )
3460
+        )
3461
+        # check before
3462
+        res = self.testapp.get(
3463
+            '/api/v2/users/{}'.format(user_id),
3464
+            status=200
3465
+        )
3466
+        res = res.json_body
3467
+        assert res['email'] == 'test@test.test'
3468
+
3469
+        # Set password
3470
+        params = {
3471
+            'email': 'admin@admin.admin',
3472
+            'loggedin_user_password': 'admin@admin.admin',
3473
+        }
3474
+        self.testapp.put_json(
3475
+            '/api/v2/users/{}/email'.format(user_id),
3476
+            params=params,
3477
+            status=400,
3478
+        )
3479
+        # Check After
3480
+        res = self.testapp.get(
3481
+            '/api/v2/users/{}'.format(user_id),
3482
+            status=200
3483
+        )
3484
+        res = res.json_body
3485
+        assert res['email'] == 'test@test.test'
3486
+
3078
     def test_api__set_user_email__err_403__admin_wrong_password(self):
3487
     def test_api__set_user_email__err_403__admin_wrong_password(self):
3079
         dbsession = get_tm_session(self.session_factory, transaction.manager)
3488
         dbsession = get_tm_session(self.session_factory, transaction.manager)
3080
         admin = dbsession.query(models.User) \
3489
         admin = dbsession.query(models.User) \

Fichier diff supprimé car celui-ci est trop grand
+ 816 - 22
backend/tracim_backend/tests/functional/test_workspaces.py


+ 401 - 0
backend/tracim_backend/tests/library/test_content_api.py Voir le fichier

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

+ 198 - 0
backend/tracim_backend/views/contents_api/folder_controller.py Voir le fichier

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.app_models.contents import CONTENT_TYPES
29
+from tracim_backend.app_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

+ 57 - 6
backend/tracim_backend/views/core_api/schemas.py Voir le fichier

7
 
7
 
8
 from tracim_backend.lib.utils.utils import DATETIME_FORMAT
8
 from tracim_backend.lib.utils.utils import DATETIME_FORMAT
9
 from tracim_backend.models.auth import Profile
9
 from tracim_backend.models.auth import Profile
10
+
10
 from tracim_backend.app_models.contents import GlobalStatus
11
 from tracim_backend.app_models.contents import GlobalStatus
11
 from tracim_backend.app_models.contents import CONTENT_STATUS
12
 from tracim_backend.app_models.contents import CONTENT_STATUS
12
 from tracim_backend.app_models.contents import CONTENT_TYPES
13
 from tracim_backend.app_models.contents import CONTENT_TYPES
13
 from tracim_backend.app_models.contents import open_status
14
 from tracim_backend.app_models.contents import open_status
15
+from tracim_backend.models.auth import Group
14
 from tracim_backend.models.context_models import ActiveContentFilter
16
 from tracim_backend.models.context_models import ActiveContentFilter
17
+from tracim_backend.models.context_models import FolderContentUpdate
15
 from tracim_backend.models.context_models import AutocompleteQuery
18
 from tracim_backend.models.context_models import AutocompleteQuery
16
 from tracim_backend.models.context_models import ContentIdsQuery
19
 from tracim_backend.models.context_models import ContentIdsQuery
17
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
20
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
75
         example=True,
78
         example=True,
76
         description='Is user account activated ?'
79
         description='Is user account activated ?'
77
     )
80
     )
81
+    is_deleted = marshmallow.fields.Bool(
82
+        example=False,
83
+        description='Is user account deleted ?'
84
+    )
78
     # TODO - G.M - 17-04-2018 - Restrict timezone values
85
     # TODO - G.M - 17-04-2018 - Restrict timezone values
79
     timezone = marshmallow.fields.String(
86
     timezone = marshmallow.fields.String(
80
         example="Europe/Paris",
87
         example="Europe/Paris",
155
         return UserProfile(**data)
162
         return UserProfile(**data)
156
 
163
 
157
 
164
 
158
-class UserCreationSchema(
159
-    SetEmailSchema,
160
-    SetPasswordSchema,
161
-    UserInfosSchema,
162
-    UserProfileSchema
163
-):
165
+class UserCreationSchema(marshmallow.Schema):
166
+    email = marshmallow.fields.Email(
167
+        required=True,
168
+        example='suri.cate@algoo.fr'
169
+    )
170
+    password = marshmallow.fields.String(
171
+        example='8QLa$<w',
172
+        required=False,
173
+    )
174
+    profile = marshmallow.fields.String(
175
+        attribute='profile',
176
+        validate=OneOf(Profile._NAME),
177
+        example='managers',
178
+        required=False,
179
+        default=Group.TIM_USER_GROUPNAME
180
+    )
181
+    timezone = marshmallow.fields.String(
182
+        example="Europe/Paris",
183
+        required=False,
184
+        default=''
185
+    )
186
+    public_name = marshmallow.fields.String(
187
+        example='Suri Cate',
188
+        required=False,
189
+        default=None,
190
+    )
191
+    email_notification = marshmallow.fields.Bool(
192
+        example=True,
193
+        required=False,
194
+        default=True,
195
+    )
196
+
164
     @post_load
197
     @post_load
165
     def create_user(self, data):
198
     def create_user(self, data):
166
         return UserCreation(**data)
199
         return UserCreation(**data)
300
         example='test',
333
         example='test',
301
         description='search text to query',
334
         description='search text to query',
302
         validate=Length(min=2),
335
         validate=Length(min=2),
336
+        required=True,
303
     )
337
     )
304
     @post_load
338
     @post_load
305
     def make_autocomplete(self, data):
339
     def make_autocomplete(self, data):
510
         WorkspaceMenuEntrySchema,
544
         WorkspaceMenuEntrySchema,
511
         many=True,
545
         many=True,
512
     )
546
     )
547
+    is_deleted = marshmallow.fields.Bool(example=False, default=False)
513
 
548
 
514
     class Meta:
549
     class Meta:
515
         description = 'Digest of workspace informations'
550
         description = 'Digest of workspace informations'
848
         return TextBasedContentUpdate(**data)
883
         return TextBasedContentUpdate(**data)
849
 
884
 
850
 
885
 
886
+class FolderContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbstractSchema):  # nopep
887
+    sub_content_types = marshmallow.fields.List(
888
+        marshmallow.fields.String(
889
+            example='html-document',
890
+            validate=ALL_CONTENT_TYPES_VALIDATOR,
891
+        ),
892
+        description='list of content types allowed as sub contents. '
893
+                    'This field is required for folder contents, '
894
+                    'set it to empty list in other cases'
895
+    )
896
+
897
+    @post_load
898
+    def folder_content_update(self, data):
899
+        return FolderContentUpdate(**data)
900
+
901
+
851
 class FileContentModifySchema(TextBasedContentModifySchema):
902
 class FileContentModifySchema(TextBasedContentModifySchema):
852
     pass
903
     pass
853
 
904
 

+ 47 - 1
backend/tracim_backend/views/core_api/user_controller.py Voir le fichier

1
 from pyramid.config import Configurator
1
 from pyramid.config import Configurator
2
+from tracim_backend.lib.utils.utils import password_generator
2
 
3
 
3
 try:  # Python 3.5+
4
 try:  # Python 3.5+
4
     from http import HTTPStatus
5
     from http import HTTPStatus
16
 from tracim_backend.lib.utils.authorization import require_same_user_or_profile
17
 from tracim_backend.lib.utils.authorization import require_same_user_or_profile
17
 from tracim_backend.lib.utils.authorization import require_profile
18
 from tracim_backend.lib.utils.authorization import require_profile
18
 from tracim_backend.exceptions import WrongUserPassword
19
 from tracim_backend.exceptions import WrongUserPassword
20
+from tracim_backend.exceptions import EmailAlreadyExistInDb
19
 from tracim_backend.exceptions import PasswordDoNotMatch
21
 from tracim_backend.exceptions import PasswordDoNotMatch
20
 from tracim_backend.views.core_api.schemas import UserSchema
22
 from tracim_backend.views.core_api.schemas import UserSchema
21
 from tracim_backend.views.core_api.schemas import AutocompleteQuerySchema
23
 from tracim_backend.views.core_api.schemas import AutocompleteQuerySchema
120
 
122
 
121
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
123
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
122
     @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
124
     @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
125
+    @hapic.handle_exception(EmailAlreadyExistInDb, HTTPStatus.BAD_REQUEST)
123
     @require_same_user_or_profile(Group.TIM_ADMIN)
126
     @require_same_user_or_profile(Group.TIM_ADMIN)
124
     @hapic.input_body(SetEmailSchema())
127
     @hapic.input_body(SetEmailSchema())
125
     @hapic.input_path(UserIdPathSchema())
128
     @hapic.input_path(UserIdPathSchema())
192
         return uapi.get_user_with_context(user)
195
         return uapi.get_user_with_context(user)
193
 
196
 
194
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
197
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
198
+    @hapic.handle_exception(EmailAlreadyExistInDb, HTTPStatus.BAD_REQUEST)
195
     @require_profile(Group.TIM_ADMIN)
199
     @require_profile(Group.TIM_ADMIN)
196
-    @hapic.input_path(UserIdPathSchema())
197
     @hapic.input_body(UserCreationSchema())
200
     @hapic.input_body(UserCreationSchema())
198
     @hapic.output_body(UserSchema())
201
     @hapic.output_body(UserSchema())
199
     def create_user(self, context, request: TracimRequest, hapic_data=None):
202
     def create_user(self, context, request: TracimRequest, hapic_data=None):
244
     @require_profile(Group.TIM_ADMIN)
247
     @require_profile(Group.TIM_ADMIN)
245
     @hapic.input_path(UserIdPathSchema())
248
     @hapic.input_path(UserIdPathSchema())
246
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
249
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
250
+    def delete_user(self, context, request: TracimRequest, hapic_data=None):
251
+        """
252
+        delete user
253
+        """
254
+        app_config = request.registry.settings['CFG']
255
+        uapi = UserApi(
256
+            current_user=request.current_user,  # User
257
+            session=request.dbsession,
258
+            config=app_config,
259
+        )
260
+        uapi.delete(user=request.candidate_user, do_save=True)
261
+        return
262
+
263
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
264
+    @require_profile(Group.TIM_ADMIN)
265
+    @hapic.input_path(UserIdPathSchema())
266
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
267
+    def undelete_user(self, context, request: TracimRequest, hapic_data=None):
268
+        """
269
+        undelete user
270
+        """
271
+        app_config = request.registry.settings['CFG']
272
+        uapi = UserApi(
273
+            current_user=request.current_user,  # User
274
+            session=request.dbsession,
275
+            config=app_config,
276
+            show_deleted=True,
277
+        )
278
+        uapi.undelete(user=request.candidate_user, do_save=True)
279
+        return
280
+
281
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
282
+    @require_profile(Group.TIM_ADMIN)
283
+    @hapic.input_path(UserIdPathSchema())
284
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
247
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
285
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
248
         """
286
         """
249
         disable user
287
         disable user
464
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
502
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
465
         configurator.add_view(self.disable_user, route_name='disable_user')
503
         configurator.add_view(self.disable_user, route_name='disable_user')
466
 
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
+
467
         # set user profile
513
         # set user profile
468
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
514
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
469
         configurator.add_view(self.set_profile, route_name='set_user_profile')
515
         configurator.add_view(self.set_profile, route_name='set_user_profile')

+ 147 - 1
backend/tracim_backend/views/core_api/workspace_controller.py Voir le fichier

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

+ 52 - 0
backend_lib.sh Voir le fichier

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
+}

+ 34 - 2
frontend/src/action-creator.async.js Voir le fichier

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

+ 8 - 3
frontend/src/action-creator.sync.js Voir le fichier

2
 export const UPDATE = 'Update'
2
 export const UPDATE = 'Update'
3
 export const ADD = 'Add'
3
 export const ADD = 'Add'
4
 export const REMOVE = 'Remove'
4
 export const REMOVE = 'Remove'
5
+export const APPEND = 'Append'
5
 
6
 
6
 export const TIMEZONE = 'Timezone'
7
 export const TIMEZONE = 'Timezone'
7
 export const setTimezone = timezone => ({ type: `${SET}/${TIMEZONE}`, timezone })
8
 export const setTimezone = timezone => ({ type: `${SET}/${TIMEZONE}`, timezone })
11
   msgDelay !== 0 && window.setTimeout(() => dispatch(removeFlashMessage(msgText)), msgDelay)
12
   msgDelay !== 0 && window.setTimeout(() => dispatch(removeFlashMessage(msgText)), msgDelay)
12
   return dispatch(addFlashMessage({message: msgText, type: msgType}))
13
   return dispatch(addFlashMessage({message: msgText, type: msgType}))
13
 }
14
 }
14
-export const addFlashMessage = msg => ({ type: `${ADD}/${FLASH_MESSAGE}`, msg })
15
+const addFlashMessage = msg => ({ type: `${ADD}/${FLASH_MESSAGE}`, msg }) // only newFlashMsg should be used by component and app so dont export this
15
 export const removeFlashMessage = msg => ({ type: `${REMOVE}/${FLASH_MESSAGE}`, msg })
16
 export const removeFlashMessage = msg => ({ type: `${REMOVE}/${FLASH_MESSAGE}`, msg })
16
 
17
 
17
 export const USER = 'User'
18
 export const USER = 'User'
37
 export const setWorkspaceContentList = workspaceContentList => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList })
38
 export const setWorkspaceContentList = workspaceContentList => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList })
38
 export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
39
 export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
39
 
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
+
40
 export const WORKSPACE_LIST = `${WORKSPACE}/List`
46
 export const WORKSPACE_LIST = `${WORKSPACE}/List`
41
 export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
47
 export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
42
 export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })
48
 export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })
52
 export const WORKSPACE_RECENT_ACTIVITY = `${WORKSPACE}/RecentActivity/List`
58
 export const WORKSPACE_RECENT_ACTIVITY = `${WORKSPACE}/RecentActivity/List`
53
 export const WORKSPACE_RECENT_ACTIVITY_LIST = `${WORKSPACE_RECENT_ACTIVITY}/List`
59
 export const WORKSPACE_RECENT_ACTIVITY_LIST = `${WORKSPACE_RECENT_ACTIVITY}/List`
54
 export const setWorkspaceRecentActivityList = workspaceRecentActivityList => ({ type: `${SET}/${WORKSPACE_RECENT_ACTIVITY_LIST}`, workspaceRecentActivityList })
60
 export const setWorkspaceRecentActivityList = workspaceRecentActivityList => ({ type: `${SET}/${WORKSPACE_RECENT_ACTIVITY_LIST}`, workspaceRecentActivityList })
55
-export const WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST = `${WORKSPACE_RECENT_ACTIVITY}/ForUser/List`
56
-export const setWorkspaceRecentActivityForUserList = workspaceRecentActivityForUserList => ({ type: `${SET}/${WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST}`, workspaceRecentActivityForUserList })
61
+export const appendWorkspaceRecentActivityList = workspaceRecentActivityList => ({ type: `${APPEND}/${WORKSPACE_RECENT_ACTIVITY_LIST}`, workspaceRecentActivityList })
57
 
62
 
58
 export const WORKSPACE_READ_STATUS = `${WORKSPACE}/ReadStatus`
63
 export const WORKSPACE_READ_STATUS = `${WORKSPACE}/ReadStatus`
59
 export const WORKSPACE_READ_STATUS_LIST = `${WORKSPACE_READ_STATUS}/List`
64
 export const WORKSPACE_READ_STATUS_LIST = `${WORKSPACE_READ_STATUS}/List`

+ 3 - 3
frontend/src/component/Account/Password.jsx Voir le fichier

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

+ 3 - 3
frontend/src/component/Account/PersonalData.jsx Voir le fichier

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

+ 2 - 2
frontend/src/component/Account/UserInfo.jsx Voir le fichier

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

+ 3 - 1
frontend/src/component/Dashboard/ContentTypeBtn.jsx Voir le fichier

15
         backgroundColor: color(props.hexcolor).darken(0.15).hexString()
15
         backgroundColor: color(props.hexcolor).darken(0.15).hexString()
16
       }
16
       }
17
     }}
17
     }}
18
+    onClick={props.onClickBtn}
18
   >
19
   >
19
     <div className={classnames(`${props.customClass}__text`)}>
20
     <div className={classnames(`${props.customClass}__text`)}>
20
       <div className={classnames(`${props.customClass}__text__icon`)}>
21
       <div className={classnames(`${props.customClass}__text__icon`)}>
33
   label: PropTypes.string.isRequired,
34
   label: PropTypes.string.isRequired,
34
   faIcon: PropTypes.string.isRequired,
35
   faIcon: PropTypes.string.isRequired,
35
   creationLabel: PropTypes.string.isRequired,
36
   creationLabel: PropTypes.string.isRequired,
36
-  customClass: PropTypes.string
37
+  customClass: PropTypes.string,
38
+  onClickBtn: PropTypes.func
37
 }
39
 }
38
 
40
 
39
 ContentTypeBtn.defaultProps = {
41
 ContentTypeBtn.defaultProps = {

+ 1 - 5
frontend/src/component/Dashboard/ContentTypeBtn.styl Voir le fichier

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

+ 4 - 3
frontend/src/component/Dashboard/MemberList.jsx Voir le fichier

1
 import React from 'react'
1
 import React from 'react'
2
 import PropTypes from 'prop-types'
2
 import PropTypes from 'prop-types'
3
 // import { Checkbox } from 'tracim_frontend_lib'
3
 // import { Checkbox } from 'tracim_frontend_lib'
4
+import { generateAvatarFromPublicName } from 'tracim_frontend_lib'
4
 
5
 
5
 require('./MemberList.styl')
6
 require('./MemberList.styl')
6
 
7
 
45
                   {props.memberList.map(m =>
46
                   {props.memberList.map(m =>
46
                     <li className='memberlist__list__item' key={m.id}>
47
                     <li className='memberlist__list__item' key={m.id}>
47
                       <div className='memberlist__list__item__avatar'>
48
                       <div className='memberlist__list__item__avatar'>
48
-                        {m.avatarUrl ? <img src={m.avatarUrl} /> : <img src='NYI' />}
49
+                        <img src={m.avatarUrl} />
49
                       </div>
50
                       </div>
50
 
51
 
51
                       <div className='memberlist__list__item__info mr-auto'>
52
                       <div className='memberlist__list__item__info mr-auto'>
110
                             onClick={() => props.onClickKnownMember(u)}
111
                             onClick={() => props.onClickKnownMember(u)}
111
                             key={u.user_id}
112
                             key={u.user_id}
112
                           >
113
                           >
113
-                            <div className='autocomplete__item__avatar primaryColorBorder'>
114
-                              <img src={u.avatar_url} />
114
+                            <div className='autocomplete__item__avatar'>
115
+                              <img src={u.avatar_url ? u.avatar_url : generateAvatarFromPublicName(u.public_name)} />
115
                             </div>
116
                             </div>
116
 
117
 
117
                             <div className='autocomplete__item__name'>
118
                             <div className='autocomplete__item__name'>

+ 18 - 12
frontend/src/component/Dashboard/MemberList.styl Voir le fichier

21
       display flex
21
       display flex
22
       border-bottom 1px solid grey
22
       border-bottom 1px solid grey
23
       padding 10px 15px
23
       padding 10px 15px
24
-      &:hover
25
-        background-color fourthColor
26
-      &:nth-last-child(1)
27
-        border-bottom 0
28
-      &:nth-child(even)
29
-        background-color grey-hover
30
-        &:hover
31
-          background-color fourthColor
32
       &__avatar
24
       &__avatar
33
         margin-right 20px
25
         margin-right 20px
34
         & > img
26
         & > img
105
               width 45px
97
               width 45px
106
               height 45px
98
               height 45px
107
               border-radius 50%
99
               border-radius 50%
108
-              border-width 1px
109
-              border-style solid
110
             &__name
100
             &__name
111
               margin-left 15px
101
               margin-left 15px
112
       .name__input
102
       .name__input
146
         padding 8px 30px
136
         padding 8px 30px
147
         cursor pointer
137
         cursor pointer
148
 
138
 
149
-@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
+
150
   .memberlist
145
   .memberlist
151
     width 50%
146
     width 50%
152
 
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
+
153
 @media (min-width: min-sm) and (max-width: max-sm)
157
 @media (min-width: min-sm) and (max-width: max-sm)
154
   .memberlist
158
   .memberlist
155
     margin 50px 0
159
     margin 50px 0
156
-    width 90%
160
+    width 100%
161
+
162
+/*** MEDIA 575px ***/
157
 
163
 
158
 @media (max-width: max-xs)
164
 @media (max-width: max-xs)
159
   .memberlist
165
   .memberlist

+ 2 - 2
frontend/src/component/Dashboard/MoreInfo.jsx Voir le fichier

6
   <div className='moreinfo'>
6
   <div className='moreinfo'>
7
     <div className='moreinfo__webdav genericBtnInfoDashboard'>
7
     <div className='moreinfo__webdav genericBtnInfoDashboard'>
8
       <div
8
       <div
9
-        className='moreinfo__webdav__btn genericBtnInfoDashboard__btn'
9
+        className='moreinfo__webdav__btn genericBtnInfoDashboard__btn primaryColorBorderLighten primaryColorFontLighten'
10
         onClick={props.onClickToggleWebdav}
10
         onClick={props.onClickToggleWebdav}
11
       >
11
       >
12
         <div className='moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
12
         <div className='moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
41
             <i className='fa fa-calendar' />
41
             <i className='fa fa-calendar' />
42
           </div>
42
           </div>
43
 
43
 
44
-          <div className='moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text'>
44
+          <div className='moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text d-flex align-self-center'>
45
             {props.t('Workspace Calendar')}
45
             {props.t('Workspace Calendar')}
46
           </div>
46
           </div>
47
         </div>
47
         </div>

+ 1 - 0
frontend/src/component/Dashboard/MoreInfo.styl Voir le fichier

9
     &__information
9
     &__information
10
       width 300px
10
       width 300px
11
   &__calendar
11
   &__calendar
12
+    display none
12
     margin-bottom 100px
13
     margin-bottom 100px
13
     &__wrapperBtn
14
     &__wrapperBtn
14
       margin-right 290px
15
       margin-right 290px

+ 9 - 4
frontend/src/component/Dashboard/RecentActivity.jsx Voir le fichier

20
     </div>
20
     </div>
21
 
21
 
22
     <div className='activity__wrapper'>
22
     <div className='activity__wrapper'>
23
-      {props.recentActivityFilteredForUser.map(content => {
23
+      {props.recentActivityList.map(content => {
24
         const contentType = props.contentTypeList.find(ct => ct.slug === content.type)
24
         const contentType = props.contentTypeList.find(ct => ct.slug === content.type)
25
         return (
25
         return (
26
           <div
26
           <div
27
-            className='activity__workspace'
27
+            className={classnames('activity__workspace', {'read': props.readByUserList.includes(content.id)})}
28
             onClick={() => props.onClickRecentContent(content.id, content.type)}
28
             onClick={() => props.onClickRecentContent(content.id, content.type)}
29
             key={content.id}
29
             key={content.id}
30
           >
30
           >
53
 
53
 
54
 RecentActivity.propTypes = {
54
 RecentActivity.propTypes = {
55
   t: PropTypes.func.isRequired,
55
   t: PropTypes.func.isRequired,
56
-  recentActivityFilteredForUser: PropTypes.array.isRequired,
56
+  recentActivityList: PropTypes.array.isRequired,
57
   contentTypeList: PropTypes.array.isRequired,
57
   contentTypeList: PropTypes.array.isRequired,
58
-  onClickSeeMore: PropTypes.func.isRequired
58
+  onClickSeeMore: PropTypes.func.isRequired,
59
+  readByUserList: PropTypes.array
60
+}
61
+
62
+RecentActivity.defaultProps = {
63
+  readByUserList: []
59
 }
64
 }

+ 22 - 6
frontend/src/component/Dashboard/RecentActivity.styl Voir le fichier

23
     border-bottom 1px solid grey
23
     border-bottom 1px solid grey
24
     padding 15px
24
     padding 15px
25
     cursor pointer
25
     cursor pointer
26
+    font-weight bold
27
+    &.read
28
+      font-weight normal
26
     &:hover
29
     &:hover
27
       background-color fourthColor
30
       background-color fourthColor
28
     &:nth-child(even)
31
     &:nth-child(even)
34
       font-size 25px
37
       font-size 25px
35
     &__name
38
     &__name
36
       font-size 18px
39
       font-size 18px
37
-      font-weight 500
38
-      span
39
-        font-weight 400
40
   &__more
40
   &__more
41
     &__btn
41
     &__btn
42
       margin 15px
42
       margin 15px
43
       padding 10px 25px
43
       padding 10px 25px
44
       cursor pointer
44
       cursor pointer
45
 
45
 
46
-@media (min-width min-sm) and (max-width: max-lg)
46
+
47
+/**** MEDIAQUERIES ****/
48
+
49
+/**** MEDIA 992px and 1199px ****/
50
+
51
+@media (min-width min-lg) and (max-width: max-lg)
47
   .activity
52
   .activity
53
+    margin-right 0
48
     width 100%
54
     width 100%
49
 
55
 
56
+/**** MEDIA 768px and 991px ****/
57
+
50
 @media (min-width: min-md) and (max-width: max-md)
58
 @media (min-width: min-md) and (max-width: max-md)
51
   .activity
59
   .activity
52
-    margin 25px 15px 25px 0
60
+    margin 25px 0 25px 0
61
+    width 100%
62
+
63
+/**** MEDIA 576px and 767px ****/
53
 
64
 
54
 @media (min-width: min-sm) and (max-width: max-sm)
65
 @media (min-width: min-sm) and (max-width: max-sm)
55
   .activity
66
   .activity
56
-    margin 25px 15px 25px 0
67
+    margin 25px 0 25px 0
68
+    width 100%
69
+
70
+/**** MEDIA 575px ****/
57
 
71
 
58
 @media (max-width: max-xs)
72
 @media (max-width: max-xs)
59
   .activity
73
   .activity
60
     margin 25px 0
74
     margin 25px 0
61
     width 100%
75
     width 100%
76
+    &__wrapper
77
+      margin-top 45px
62
     &__header
78
     &__header
63
       display block
79
       display block
64
       height auto
80
       height auto

+ 2 - 2
frontend/src/component/Dashboard/UserStatus.jsx Voir le fichier

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

+ 41 - 2
frontend/src/component/Dashboard/UserStatus.styl Voir le fichier

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

+ 29 - 0
frontend/src/component/Header/MenuActionListItem/AdminLink.jsx Voir le fichier

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 Voir le fichier

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

+ 1 - 1
frontend/src/component/Header/MenuActionListItem/Search.jsx Voir le fichier

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

+ 8 - 8
frontend/src/component/Sidebar/WorkspaceListItem.jsx Voir le fichier

7
 
7
 
8
 const WorkspaceListItem = props => {
8
 const WorkspaceListItem = props => {
9
   return (
9
   return (
10
-    <li className='sidebar__navigation__workspace__item'>
10
+    <li className='sidebar__content__navigation__workspace__item'>
11
       <div
11
       <div
12
-        className='sidebar__navigation__workspace__item__wrapper primaryColorBg primaryColorBgDarkenHover primaryColorBorder'
12
+        className='sidebar__content__navigation__workspace__item__wrapper primaryColorBg primaryColorBgDarkenHover primaryColorBorder'
13
         onClick={props.onClickTitle}
13
         onClick={props.onClickTitle}
14
       >
14
       >
15
-        <div className='sidebar__navigation__workspace__item__number'>
15
+        <div className='sidebar__content__navigation__workspace__item__number'>
16
           {props.label.substring(0, 2).toUpperCase()}
16
           {props.label.substring(0, 2).toUpperCase()}
17
         </div>
17
         </div>
18
 
18
 
19
-        <div className='sidebar__navigation__workspace__item__name' title={props.label}>
19
+        <div className='sidebar__content__navigation__workspace__item__name' title={props.label}>
20
           {props.label}
20
           {props.label}
21
         </div>
21
         </div>
22
 
22
 
23
-        <div className='sidebar__navigation__workspace__item__icon'>
23
+        <div className='sidebar__content__navigation__workspace__item__icon'>
24
           <i className={classnames(props.isOpenInSidebar ? 'fa fa-chevron-up' : 'fa fa-chevron-down')} />
24
           <i className={classnames(props.isOpenInSidebar ? 'fa fa-chevron-up' : 'fa fa-chevron-down')} />
25
         </div>
25
         </div>
26
       </div>
26
       </div>
27
 
27
 
28
       <AnimateHeight duration={500} height={props.isOpenInSidebar ? 'auto' : 0}>
28
       <AnimateHeight duration={500} height={props.isOpenInSidebar ? 'auto' : 0}>
29
         <ul
29
         <ul
30
-          className='sidebar__navigation__workspace__item__submenu'
30
+          className='sidebar__content__navigation__workspace__item__submenu'
31
           id={`sidebarSubMenu_${props.number}`}
31
           id={`sidebarSubMenu_${props.number}`}
32
         >
32
         >
33
           { props.allowedApp.map(aa =>
33
           { props.allowedApp.map(aa =>
37
             >
37
             >
38
               <Link to={aa.route}>
38
               <Link to={aa.route}>
39
                 <div className={classnames(
39
                 <div className={classnames(
40
-                  'sidebar__navigation__workspace__item__submenu__dropdown primaryColorBgLighten primaryColorBgHover primaryColorBorderDarken',
40
+                  'sidebar__content__navigation__workspace__item__submenu__dropdown primaryColorBgLighten primaryColorBgHover primaryColorBorderDarken',
41
                   {'activeFilter': props.activeFilterList.includes(aa.slug)}
41
                   {'activeFilter': props.activeFilterList.includes(aa.slug)}
42
                 )}>
42
                 )}>
43
                   <div className='dropdown__icon'>
43
                   <div className='dropdown__icon'>
44
                     <i className={classnames(`fa fa-${aa.faIcon}`)} style={{backgroudColor: aa.hexcolor}} />
44
                     <i className={classnames(`fa fa-${aa.faIcon}`)} style={{backgroudColor: aa.hexcolor}} />
45
                   </div>
45
                   </div>
46
 
46
 
47
-                  <div className='sidebar__navigation__workspace__item__submenu__dropdown__showdropdown'>
47
+                  <div className='sidebar__content__navigation__workspace__item__submenu__dropdown__showdropdown'>
48
                     <div className='dropdown__title' id='navbarDropdown'>
48
                     <div className='dropdown__title' id='navbarDropdown'>
49
                       <div className='dropdown__title__text'>
49
                       <div className='dropdown__title__text'>
50
                         {aa.label/* [props.lang.id] */}
50
                         {aa.label/* [props.lang.id] */}

+ 1 - 1
frontend/src/component/common/Input/SubDropdownCreateButton.jsx Voir le fichier

15
                 style={{color: app.hexcolor}}
15
                 style={{color: app.hexcolor}}
16
               />
16
               />
17
             </div>
17
             </div>
18
-            <div className='subdropdown__link__folder__text'>
18
+            <div className={`subdropdown__link__${app.slug}__text`}>
19
               {app.creationLabel}
19
               {app.creationLabel}
20
             </div>
20
             </div>
21
           </div>
21
           </div>

+ 1 - 1
frontend/src/container/Account.jsx Voir le fichier

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

+ 128 - 71
frontend/src/container/AdminWorkspacePage.jsx Voir le fichier

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

frontend/src/container/AppFullscreenManager.jsx → frontend/src/container/AppFullscreenRouter.jsx Voir le fichier

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

+ 36 - 13
frontend/src/container/Dashboard.jsx Voir le fichier

20
   setWorkspaceDetail,
20
   setWorkspaceDetail,
21
   setWorkspaceMemberList,
21
   setWorkspaceMemberList,
22
   setWorkspaceRecentActivityList,
22
   setWorkspaceRecentActivityList,
23
-  setWorkspaceRecentActivityForUserList,
23
+  appendWorkspaceRecentActivityList,
24
   setWorkspaceReadStatusList
24
   setWorkspaceReadStatusList
25
 } from '../action-creator.sync.js'
25
 } from '../action-creator.sync.js'
26
 import { ROLE, PAGE } from '../helper.js'
26
 import { ROLE, PAGE } from '../helper.js'
34
   constructor (props) {
34
   constructor (props) {
35
     super(props)
35
     super(props)
36
     this.state = {
36
     this.state = {
37
-      workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null, // this is used to avoid handling the parseInt everytime
37
+      workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null, // this is used to avoid handling the parseInt every time
38
       newMember: {
38
       newMember: {
39
         id: '',
39
         id: '',
40
         avatarUrl: '',
40
         avatarUrl: '',
51
   }
51
   }
52
 
52
 
53
   async componentDidMount () {
53
   async componentDidMount () {
54
+    this.loadWorkspaceDetail()
55
+    this.loadMemberList()
56
+    this.loadRecentActivity()
57
+  }
58
+
59
+  componentDidUpdate (prevProps, prevState) {
60
+    const { props, state } = this
61
+
62
+    if (prevProps.match.params.idws !== props.match.params.idws) {
63
+      this.setState({workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null})
64
+    }
65
+
66
+    if (prevState.workspaceIdInUrl !== state.workspaceIdInUrl) {
67
+      this.loadWorkspaceDetail()
68
+      this.loadMemberList()
69
+      this.loadRecentActivity()
70
+    }
71
+  }
72
+
73
+  loadWorkspaceDetail = async () => {
54
     const { props, state } = this
74
     const { props, state } = this
55
 
75
 
56
     const fetchWorkspaceDetail = await props.dispatch(getWorkspaceDetail(props.user, state.workspaceIdInUrl))
76
     const fetchWorkspaceDetail = await props.dispatch(getWorkspaceDetail(props.user, state.workspaceIdInUrl))
58
       case 200: props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json)); break
78
       case 200: props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json)); break
59
       default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('workspace detail')}`, 'warning')); break
79
       default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('workspace detail')}`, 'warning')); break
60
     }
80
     }
61
-    this.loadMemberList()
62
-    this.loadRecentActivity()
63
   }
81
   }
64
 
82
 
65
   loadMemberList = async () => {
83
   loadMemberList = async () => {
87
       case 200: props.dispatch(setWorkspaceReadStatusList(fetchWorkspaceReadStatusList.json)); break
105
       case 200: props.dispatch(setWorkspaceReadStatusList(fetchWorkspaceReadStatusList.json)); break
88
       default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('read status list')}`, 'warning')); break
106
       default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('read status list')}`, 'warning')); break
89
     }
107
     }
90
-
91
-    const readStatusForUserList = fetchWorkspaceReadStatusList.json.filter(c => c.read_by_user).map(c => c.content_id)
92
-    const recentActivityForUserList = fetchWorkspaceRecentActivityList.json.filter(content => !readStatusForUserList.includes(content.content_id))
93
-
94
-    props.dispatch(setWorkspaceRecentActivityForUserList(recentActivityForUserList))
95
   }
108
   }
96
 
109
 
97
   handleToggleNewMemberDashboard = () => this.setState(prevState => ({displayNewMemberDashboard: !prevState.displayNewMemberDashboard}))
110
   handleToggleNewMemberDashboard = () => this.setState(prevState => ({displayNewMemberDashboard: !prevState.displayNewMemberDashboard}))
114
   }
127
   }
115
 
128
 
116
   handleClickSeeMore = async () => {
129
   handleClickSeeMore = async () => {
117
-    console.log('nyi')
130
+    const { props, state } = this
131
+
132
+    const idLastRecentActivity = props.curWs.recentActivityList[props.curWs.recentActivityList.length - 1].id
133
+
134
+    const fetchWorkspaceRecentActivityList = await props.dispatch(getWorkspaceRecentActivityList(props.user, state.workspaceIdInUrl, idLastRecentActivity))
135
+    switch (fetchWorkspaceRecentActivityList.status) {
136
+      case 200: props.dispatch(appendWorkspaceRecentActivityList(fetchWorkspaceRecentActivityList.json)); break
137
+      default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('recent activity list')}`, 'warning')); break
138
+    }
118
   }
139
   }
119
 
140
 
120
   handleSearchUser = async userNameToSearch => {
141
   handleSearchUser = async userNameToSearch => {
180
     const { props, state } = this
201
     const { props, state } = this
181
 
202
 
182
     return (
203
     return (
183
-      <div className='Dashboard' style={{width: '100%'}}>
204
+      <div className='dashboard'>
184
         <PageWrapper customeClass='dashboard'>
205
         <PageWrapper customeClass='dashboard'>
185
           <PageTitle
206
           <PageTitle
186
             parentClass='dashboard__header'
207
             parentClass='dashboard__header'
197
           <PageContent>
218
           <PageContent>
198
             <div className='dashboard__workspace-wrapper'>
219
             <div className='dashboard__workspace-wrapper'>
199
               <div className='dashboard__workspace'>
220
               <div className='dashboard__workspace'>
200
-                <div className='dashboard__workspace__title'>
221
+                <div className='dashboard__workspace__title primaryColorFont'>
201
                   {props.curWs.label}
222
                   {props.curWs.label}
202
                 </div>
223
                 </div>
203
 
224
 
223
                   label={ct.label}
244
                   label={ct.label}
224
                   faIcon={ct.faIcon}
245
                   faIcon={ct.faIcon}
225
                   creationLabel={ct.creationLabel}
246
                   creationLabel={ct.creationLabel}
247
+                  onClickBtn={() => props.history.push(PAGE.WORKSPACE.NEW(props.curWs.id, ct.slug))}
226
                   key={ct.label}
248
                   key={ct.label}
227
                 />
249
                 />
228
               )}
250
               )}
231
             <div className='dashboard__workspaceInfo'>
253
             <div className='dashboard__workspaceInfo'>
232
               <RecentActivity
254
               <RecentActivity
233
                 customClass='dashboard__activity'
255
                 customClass='dashboard__activity'
234
-                recentActivityFilteredForUser={props.curWs.recentActivityForUserList}
256
+                recentActivityList={props.curWs.recentActivityList}
257
+                readByUserList={props.curWs.contentReadStatusList}
235
                 contentTypeList={props.contentType}
258
                 contentTypeList={props.contentType}
236
                 onClickRecentContent={this.handleClickRecentContent}
259
                 onClickRecentContent={this.handleClickRecentContent}
237
                 onClickEverythingAsRead={this.handleClickMarkRecentActivityAsRead}
260
                 onClickEverythingAsRead={this.handleClickMarkRecentActivityAsRead}

+ 11 - 4
frontend/src/container/Header.jsx Voir le fichier

13
 import MenuActionListItemHelp from '../component/Header/MenuActionListItem/Help.jsx'
13
 import MenuActionListItemHelp from '../component/Header/MenuActionListItem/Help.jsx'
14
 import MenuActionListItemMenuProfil from '../component/Header/MenuActionListItem/MenuProfil.jsx'
14
 import MenuActionListItemMenuProfil from '../component/Header/MenuActionListItem/MenuProfil.jsx'
15
 import MenuActionListItemNotification from '../component/Header/MenuActionListItem/Notification.jsx'
15
 import MenuActionListItemNotification from '../component/Header/MenuActionListItem/Notification.jsx'
16
+import MenuActionListAdminLink from '../component/Header/MenuActionListItem/AdminLink.jsx'
16
 import logoHeader from '../img/logo-tracim.png'
17
 import logoHeader from '../img/logo-tracim.png'
17
 import {
18
 import {
18
   newFlashMessage,
19
   newFlashMessage,
22
 import {
23
 import {
23
   postUserLogout
24
   postUserLogout
24
 } from '../action-creator.async.js'
25
 } from '../action-creator.async.js'
25
-import { COOKIE, PAGE } from '../helper.js'
26
+import { COOKIE, PAGE, PROFILE } from '../helper.js'
26
 
27
 
27
 class Header extends React.Component {
28
 class Header extends React.Component {
28
   handleClickLogo = () => {}
29
   handleClickLogo = () => {}
65
         <nav className='navbar navbar-expand-md navbar-light bg-light'>
66
         <nav className='navbar navbar-expand-md navbar-light bg-light'>
66
           <Logo logoSrc={logoHeader} onClickImg={this.handleClickLogo} />
67
           <Logo logoSrc={logoHeader} onClickImg={this.handleClickLogo} />
67
 
68
 
68
-          <div className='header__breadcrumb d-none d-lg-block ml-4'>
69
-            Dev Tracim - liste des contenus
70
-          </div>
69
+          { /*
70
+            <div className='header__breadcrumb d-none d-lg-block ml-4'>
71
+              Dev Tracim - liste des contenus
72
+            </div>
73
+          */ }
71
 
74
 
72
           <NavbarToggler />
75
           <NavbarToggler />
73
 
76
 
84
                 onClickSubmit={this.handleClickSubmit}
87
                 onClickSubmit={this.handleClickSubmit}
85
               />
88
               />
86
 
89
 
90
+              {user.profile === PROFILE.ADMINISTRATOR &&
91
+                <MenuActionListAdminLink t={this.props.t} />
92
+              }
93
+
87
               <MenuActionListItemDropdownLang
94
               <MenuActionListItemDropdownLang
88
                 langList={lang}
95
                 langList={lang}
89
                 idLangActive={user.lang}
96
                 idLangActive={user.lang}

+ 33 - 24
frontend/src/container/Login.jsx Voir le fichier

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

+ 1 - 1
frontend/src/container/ProgressBar.jsx Voir le fichier

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

+ 38 - 39
frontend/src/container/Sidebar.jsx Voir le fichier

83
     this.props.history.push(PAGE.WORKSPACE.CONTENT_LIST(idWs))
83
     this.props.history.push(PAGE.WORKSPACE.CONTENT_LIST(idWs))
84
   }
84
   }
85
 
85
 
86
+  // @DEPRECATED
86
   // not used, right now, link on sidebar filters is a <Link>
87
   // not used, right now, link on sidebar filters is a <Link>
87
   handleClickContentFilter = (idWs, filter) => {
88
   handleClickContentFilter = (idWs, filter) => {
88
     const { workspace, history } = this.props
89
     const { workspace, history } = this.props
106
 
107
 
107
     return (
108
     return (
108
       <div className={classnames('sidebar primaryColorBgDarken', {'sidebarclose': sidebarClose})}>
109
       <div className={classnames('sidebar primaryColorBgDarken', {'sidebarclose': sidebarClose})}>
109
-        <div className='sidebarSticky'>
110
-          <div className='sidebar__expand primaryColorBg' onClick={this.handleClickToggleSidebar}>
111
-            <i className={classnames('fa fa-chevron-left', {'fa-chevron-right': sidebarClose, 'fa-chevron-left': !sidebarClose})} />
112
-          </div>
113
 
110
 
114
-          <div className='sidebar__wrapper'>
115
-
116
-            <nav className='sidebar__navigation'>
117
-              <ul className='sidebar__navigation__workspace'>
118
-                { workspaceList.map(ws =>
119
-                  <WorkspaceListItem
120
-                    idWs={ws.id}
121
-                    label={ws.label}
122
-                    allowedApp={ws.sidebarEntry}
123
-                    lang={activeLang}
124
-                    activeFilterList={ws.id === workspaceIdInUrl ? [qs.parse(this.props.location.search).type] : []}
125
-                    isOpenInSidebar={ws.isOpenInSidebar}
126
-                    onClickTitle={() => this.handleClickWorkspace(ws.id, !ws.isOpenInSidebar)}
127
-                    onClickAllContent={this.handleClickAllContent}
128
-                    // onClickContentFilter={this.handleClickContentFilter}
129
-                    key={ws.id}
130
-                  />
131
-                )}
132
-              </ul>
133
-            </nav>
134
-
135
-            <div className='sidebar__btnnewworkspace'>
136
-              <button
137
-                className='sidebar__btnnewworkspace__btn btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover mb-5'
138
-                onClick={this.handleClickNewWorkspace}
139
-              >
140
-                {t('Create a workspace')}
141
-              </button>
142
-            </div>
111
+        <div className='sidebar__expand primaryColorBg' onClick={this.handleClickToggleSidebar}>
112
+          <i className={classnames('fa fa-chevron-left', {'fa-chevron-right': sidebarClose, 'fa-chevron-left': !sidebarClose})} />
113
+        </div>
143
 
114
 
115
+        <div className='sidebar__content'>
116
+          <nav className={classnames('sidebar__content__navigation', {'sidebarclose': sidebarClose})}>
117
+            <ul className='sidebar__content__navigation__workspace'>
118
+              { workspaceList.map(ws =>
119
+                <WorkspaceListItem
120
+                  idWs={ws.id}
121
+                  label={ws.label}
122
+                  allowedApp={ws.sidebarEntry}
123
+                  lang={activeLang}
124
+                  activeFilterList={ws.id === workspaceIdInUrl ? [qs.parse(this.props.location.search).type] : []}
125
+                  isOpenInSidebar={ws.isOpenInSidebar}
126
+                  onClickTitle={() => this.handleClickWorkspace(ws.id, !ws.isOpenInSidebar)}
127
+                  onClickAllContent={this.handleClickAllContent}
128
+                  // onClickContentFilter={this.handleClickContentFilter}
129
+                  key={ws.id}
130
+                />
131
+              )}
132
+            </ul>
133
+          </nav>
134
+
135
+          <div className='sidebar__content__btnnewworkspace'>
136
+            <button
137
+              className='sidebar__content__btnnewworkspace__btn btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover mb-5'
138
+              onClick={this.handleClickNewWorkspace}
139
+            >
140
+              {t('Create a workspace')}
141
+            </button>
144
           </div>
142
           </div>
145
 
143
 
146
-          <div className='sidebar__footer mb-2'>
147
-            <div className='sidebar__footer__text whiteFontColor d-flex align-items-end justify-content-center'>
148
-              Copyright - 2013 - 2018
149
-              <div className='sidebar__footer__text__link'>
150
-                <a href='http://www.tracim.fr/' target='_blank' className='ml-3'>tracim.fr</a>
151
-              </div>
144
+        </div>
145
+
146
+        <div className='sidebar__footer mb-2'>
147
+          <div className='sidebar__footer__text whiteFontColor d-flex align-items-end justify-content-center'>
148
+            Copyright - 2013 - 2018
149
+            <div className='sidebar__footer__text__link'>
150
+              <a href='http://www.tracim.fr/' target='_blank' className='ml-3'>tracim.fr</a>
152
             </div>
151
             </div>
153
           </div>
152
           </div>
154
         </div>
153
         </div>

+ 11 - 7
frontend/src/container/Tracim.jsx Voir le fichier

5
 import Header from './Header.jsx'
5
 import Header from './Header.jsx'
6
 import Login from './Login.jsx'
6
 import Login from './Login.jsx'
7
 import Account from './Account.jsx'
7
 import Account from './Account.jsx'
8
-import AdminWorkspacePage from './AdminWorkspacePage.jsx'
9
-import AppFullscreenManager from './AppFullscreenManager.jsx'
8
+import AppFullscreenRouter from './AppFullscreenRouter.jsx'
10
 import FlashMessage from '../component/FlashMessage.jsx'
9
 import FlashMessage from '../component/FlashMessage.jsx'
11
 import WorkspaceContent from './WorkspaceContent.jsx'
10
 import WorkspaceContent from './WorkspaceContent.jsx'
12
 import WIPcomponent from './WIPcomponent.jsx'
11
 import WIPcomponent from './WIPcomponent.jsx'
18
   getUserIsConnected
17
   getUserIsConnected
19
 } from '../action-creator.async.js'
18
 } from '../action-creator.async.js'
20
 import {
19
 import {
20
+  newFlashMessage,
21
   removeFlashMessage,
21
   removeFlashMessage,
22
   setUserConnected
22
   setUserConnected
23
 } from '../action-creator.sync.js'
23
 } from '../action-creator.sync.js'
37
         console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
37
         console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
38
         this.props.history.push(data.url)
38
         this.props.history.push(data.url)
39
         break
39
         break
40
+      case 'addFlashMsg':
41
+        console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
42
+        this.props.dispatch(newFlashMessage(data.msg, data.type, data.delay))
43
+        break
40
     }
44
     }
41
   }
45
   }
42
 
46
 
101
 
105
 
102
               <Route exact path={PAGE.WORKSPACE.ROOT} render={() => props.workspaceList.length === 0 // handle '/' and redirect to first workspace
106
               <Route exact path={PAGE.WORKSPACE.ROOT} render={() => props.workspaceList.length === 0 // handle '/' and redirect to first workspace
103
                 ? null // @FIXME this needs to be handled in case of new user that has no workspace
107
                 ? null // @FIXME this needs to be handled in case of new user that has no workspace
104
-                : <Redirect to={{pathname: `/workspaces/${props.workspaceList[0].id}/contents`, state: {from: props.location}}} />
108
+                : <Redirect to={{pathname: PAGE.WORKSPACE.DASHBOARD(props.workspaceList[0].id), state: {from: props.location}}} />
105
               } />
109
               } />
106
 
110
 
107
               <Route exact path={`${PAGE.WORKSPACE.ROOT}/:idws`} render={props2 => // handle '/workspaces/:id' and add '/contents'
111
               <Route exact path={`${PAGE.WORKSPACE.ROOT}/:idws`} render={props2 => // handle '/workspaces/:id' and add '/contents'
108
-                <Redirect to={{pathname: `/workspaces/${props2.match.params.idws}/contents`, state: {from: props.location}}} />
112
+                <Redirect to={{pathname: PAGE.WORKSPACE.CONTENT_LIST(props2.match.params.idws), state: {from: props.location}}} />
109
               } />
113
               } />
110
 
114
 
111
               <Route path={PAGE.WORKSPACE.DASHBOARD(':idws')} component={Dashboard} />
115
               <Route path={PAGE.WORKSPACE.DASHBOARD(':idws')} component={Dashboard} />
125
           <Route path={PAGE.ADMIN.ROOT} render={() =>
129
           <Route path={PAGE.ADMIN.ROOT} render={() =>
126
             <div className='sidebarpagecontainer'>
130
             <div className='sidebarpagecontainer'>
127
               <Sidebar />
131
               <Sidebar />
128
-              <AppFullscreenManager />
132
+
133
+              <AppFullscreenRouter />
129
             </div>
134
             </div>
130
           } />
135
           } />
131
 
136
 
132
-          <Route path='/admin_temp/workspace' component={AdminWorkspacePage} />
133
-
134
           <Route path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
137
           <Route path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
135
 
138
 
136
           <div id='appFeatureContainer' />
139
           <div id='appFeatureContainer' />
140
+          <div id='popupCreateContentContainer' />
137
         </div>
141
         </div>
138
 
142
 
139
       </div>
143
       </div>

+ 30 - 8
frontend/src/container/WorkspaceContent.jsx Voir le fichier

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

+ 6 - 5
frontend/src/css/AccountPage.styl Voir le fichier

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

+ 0 - 11
frontend/src/css/AdminWorkspacePage.styl Voir le fichier

1
-.adminWorkspacePage
2
-  &__createworkspace
3
-    &__btncreate
4
-      margin 25px 15px
5
-  &__description
6
-    margin 25px 15px
7
-    font-size 20px
8
-  &__delimiter
9
-    margin 25px auto 65px auto
10
-  &__workspaceTable
11
-    margin 25px 15px

+ 22 - 12
frontend/src/css/Dashboard.styl Voir le fichier

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

+ 2 - 2
frontend/src/css/Generic.styl Voir le fichier

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

+ 15 - 3
frontend/src/css/Header.styl Voir le fichier

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

+ 9 - 13
frontend/src/css/Login.styl Voir le fichier

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

+ 0 - 1
frontend/src/css/ProgressBar.styl Voir le fichier

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

+ 94 - 74
frontend/src/css/Sidebar.styl Voir le fichier

1
-sidebar-width = 280px
1
+sidebar-width = 300px
2
 sidebar-animate-speed = 0.5s
2
 sidebar-animate-speed = 0.5s
3
 
3
 
4
 .sidebarSticky
4
 .sidebarSticky
15
   background-color rgba(253, 253, 253, 0.3)
15
   background-color rgba(253, 253, 253, 0.3)
16
 
16
 
17
 .sidebar
17
 .sidebar
18
+  display flex
19
+  flex-direction column
20
+  justify-content space-between
18
   position relative
21
   position relative
19
   transition all sidebar-animate-speed
22
   transition all sidebar-animate-speed
20
   width sidebar-width
23
   width sidebar-width
26
     width 0
29
     width 0
27
   &__expand
30
   &__expand
28
     position absolute
31
     position absolute
29
-    top 0
30
-    right -43px
32
+    right -42px
31
     display flex
33
     display flex
32
     justify-content center
34
     justify-content center
33
     align-items center
35
     align-items center
38
     cursor pointer
40
     cursor pointer
39
     color white
41
     color white
40
     transition all sidebar-animate-speed
42
     transition all sidebar-animate-speed
41
-  &__btnnewworkspace
42
-    margin 50px 0
43
-    overflow hidden
44
-    &__btn
45
-      display block
46
-      margin 0 auto
47
-      padding 15px 30px
48
-  &__navigation
49
-    padding 0
50
-    overflow hidden
51
-    &__workspace
52
-      padding-left 0
53
-      list-style none
54
-      &__item
55
-        &__wrapper
56
-          display flex
57
-          align-items center
58
-          border-bottom 1px solid
59
-          width 100%
60
-          height 100%
61
-          cursor pointer
62
-        &__number
63
-          display flex
64
-          leftside()
65
-          padding 12px
66
-          width 50px
67
-          letter-spacing 2px
68
-        &__name
69
-          padding 10px
70
-          font-size 20px
71
-          color off-white
72
-          white-space nowrap
73
-          overflow hidden
74
-          text-overflow ellipsis
75
-        &__icon
76
-          margin 0 10px 0 auto
77
-          color white
78
-        &__submenu
79
-          margin 0
80
-          padding 0
81
-          width 100%
82
-          overflow hidden
83
-          & > li
84
-            display block
85
-          &__dropdown
43
+  &__content
44
+    height 100%
45
+    &__btnnewworkspace
46
+      margin 50px 0
47
+      overflow hidden
48
+      &__btn
49
+        display block
50
+        margin 0 auto
51
+        padding 15px 30px
52
+    &__navigation
53
+      padding 0
54
+      width sidebar-width
55
+      transition all sidebar-animate-speed
56
+      overflow hidden
57
+      &.sidebarclose
58
+        width 0
59
+      &__workspace
60
+        padding-left 0
61
+        list-style none
62
+        &__item
63
+          &__wrapper
86
             display flex
64
             display flex
87
             align-items center
65
             align-items center
88
             border-bottom 1px solid
66
             border-bottom 1px solid
67
+            width 100%
68
+            height 100%
89
             cursor pointer
69
             cursor pointer
90
-            &__showdropdown
70
+          &__number
71
+            display flex
72
+            leftside()
73
+            padding 12px
74
+            width 50px
75
+            letter-spacing 2px
76
+          &__name
77
+            padding 10px
78
+            width 224px
79
+            font-size 20px
80
+            color off-white
81
+            white-space nowrap
82
+            overflow hidden
83
+            text-overflow ellipsis
84
+          &__icon
85
+            display flex
86
+            align-items center
87
+            width 26px
88
+            height 50px
89
+            color white
90
+          &__submenu
91
+            margin 0
92
+            padding 0
93
+            width 100%
94
+            overflow hidden
95
+            & > li
96
+              display block
97
+            &__dropdown
91
               display flex
98
               display flex
92
-              justify-content space-between
93
               align-items center
99
               align-items center
94
-              padding 0 10px
95
-              width 100%
96
-            .dropdown__icon
97
-              padding 10px 15px
98
-              min-width 50px
99
-              leftside()
100
-            .dropdown__title
101
-              white-space nowrap
102
-              overflow hidden
103
-              text-overflow ellipsis
104
-              // color off-white
105
-            &.activeFilter
100
+              border-bottom 1px solid
101
+              cursor pointer
102
+              &__showdropdown
103
+                display flex
104
+                justify-content space-between
105
+                align-items center
106
+                padding 0 10px
107
+                width 100%
106
               .dropdown__icon
108
               .dropdown__icon
107
-                background-color rgba(253, 253, 253, 0.8)
109
+                padding 10px 15px
110
+                min-width 50px
111
+                leftside()
112
+              .dropdown__title
113
+                white-space nowrap
114
+                overflow hidden
115
+                text-overflow ellipsis
116
+                // color off-white
117
+              &.activeFilter
118
+                .dropdown__icon
119
+                  background-color rgba(253, 253, 253, 0.8)
108
   &__footer
120
   &__footer
109
     &__text
121
     &__text
110
       color off-white
122
       color off-white
117
             text-decoration underline
129
             text-decoration underline
118
             color fourthColor
130
             color fourthColor
119
 
131
 
132
+
133
+/***** MEDIAQUERIES ******/
134
+
120
 /***** MEDIA 992px and 1199px ******/
135
 /***** MEDIA 992px and 1199px ******/
121
 
136
 
122
 @media (min-width: min-lg) and (max-width: max-lg)
137
 @media (min-width: min-lg) and (max-width: max-lg)
123
 
138
 
139
+  .sidebarpagecontainer
140
+    position relative
141
+
124
   .sidebar
142
   .sidebar
125
-    position fixed
143
+    position absolute
126
 
144
 
127
-/***** MEDIA 576px and 991px ******/
145
+/*** MEDIA 768px and 991px ****/
128
 
146
 
129
-@media (min-width: min-sm) and (max-width: max-md)
147
+@media (min-width: min-md) and (max-width: max-md)
130
 
148
 
131
-  .sidebarSticky
132
-    top 69px
149
+  .sidebarpagecontainer
150
+    position relative
133
 
151
 
134
   .sidebar
152
   .sidebar
135
-    position fixed
153
+    position absolute
136
 
154
 
137
 /***** MEDIA 576px and 767px *****/
155
 /***** MEDIA 576px and 767px *****/
138
 
156
 
139
 @media (min-width: min-sm) and (max-width: max-sm)
157
 @media (min-width: min-sm) and (max-width: max-sm)
140
 
158
 
141
   .sidebarpagecontainer
159
   .sidebarpagecontainer
160
+    position relative
142
     padding-top 69px
161
     padding-top 69px
143
 
162
 
144
-/***** MEDIA  *****/
163
+  .sidebar
164
+    position absolute
165
+
166
+/***** MEDIA 575px *****/
145
 
167
 
146
 @media (max-width: 575px)
168
 @media (max-width: 575px)
147
 
169
 
148
   .sidebarpagecontainer
170
   .sidebarpagecontainer
171
+    position relative
149
     padding-top 69px
172
     padding-top 69px
150
 
173
 
151
-  .sidebarSticky
152
-    top 69px
153
-
154
   .sidebar
174
   .sidebar
155
-    position fixed
175
+    position absolute

+ 0 - 2
frontend/src/css/index.styl Voir le fichier

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

+ 6 - 1
frontend/src/helper.js Voir le fichier

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

+ 22 - 15
frontend/src/reducer/currentWorkspace.js Voir le fichier

1
 import {
1
 import {
2
   SET,
2
   SET,
3
+  APPEND,
3
   WORKSPACE_DETAIL,
4
   WORKSPACE_DETAIL,
4
   WORKSPACE_MEMBER_LIST,
5
   WORKSPACE_MEMBER_LIST,
5
-  WORKSPACE_READ_STATUS_LIST, WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST,
6
+  WORKSPACE_READ_STATUS_LIST,
6
   WORKSPACE_RECENT_ACTIVITY_LIST
7
   WORKSPACE_RECENT_ACTIVITY_LIST
7
 } from '../action-creator.sync.js'
8
 } from '../action-creator.sync.js'
8
 import { handleRouteFromApi } from '../helper.js'
9
 import { handleRouteFromApi } from '../helper.js'
10
+import { generateAvatarFromPublicName } from 'tracim_frontend_lib'
9
 
11
 
10
 const defaultWorkspace = {
12
 const defaultWorkspace = {
11
   id: 0,
13
   id: 0,
43
         memberList: action.workspaceMemberList.map(m => ({
45
         memberList: action.workspaceMemberList.map(m => ({
44
           id: m.user_id,
46
           id: m.user_id,
45
           publicName: m.user.public_name,
47
           publicName: m.user.public_name,
46
-          avatarUrl: m.user.avatar_url,
48
+          avatarUrl: m.user.avatar_url
49
+            ? m.user.avatar_url
50
+            : m.user.public_name ? generateAvatarFromPublicName(m.user.public_name) : '',
47
           role: m.role,
51
           role: m.role,
48
           isActive: m.is_active
52
           isActive: m.is_active
49
         }))
53
         }))
66
         }))
70
         }))
67
       }
71
       }
68
 
72
 
69
-    case `${SET}/${WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST}`:
73
+    case `${APPEND}/${WORKSPACE_RECENT_ACTIVITY_LIST}`:
70
       return {
74
       return {
71
         ...state,
75
         ...state,
72
-        recentActivityForUserList: action.workspaceRecentActivityForUserList.map(ra => ({
73
-          id: ra.content_id,
74
-          slug: ra.slug,
75
-          label: ra.label,
76
-          type: ra.content_type,
77
-          idParent: ra.parent_id,
78
-          showInUi: ra.show_in_ui,
79
-          isArchived: ra.is_archived,
80
-          isDeleted: ra.is_deleted,
81
-          statusSlug: ra.status,
82
-          subContentTypeSlug: ra.sub_content_types
83
-        }))
76
+        recentActivityList: [
77
+          ...state.recentActivityList,
78
+          ...action.workspaceRecentActivityList.map(ra => ({
79
+            id: ra.content_id,
80
+            slug: ra.slug,
81
+            label: ra.label,
82
+            type: ra.content_type,
83
+            idParent: ra.parent_id,
84
+            showInUi: ra.show_in_ui,
85
+            isArchived: ra.is_archived,
86
+            isDeleted: ra.is_deleted,
87
+            statusSlug: ra.status,
88
+            subContentTypeSlug: ra.sub_content_types
89
+          }))
90
+        ]
84
       }
91
       }
85
 
92
 
86
     case `${SET}/${WORKSPACE_READ_STATUS_LIST}`:
93
     case `${SET}/${WORKSPACE_READ_STATUS_LIST}`:

+ 4 - 1
frontend/src/reducer/user.js Voir le fichier

6
   USER_DATA,
6
   USER_DATA,
7
   USER_LANG
7
   USER_LANG
8
 } from '../action-creator.sync.js'
8
 } from '../action-creator.sync.js'
9
+import { generateAvatarFromPublicName } from 'tracim_frontend_lib'
9
 
10
 
10
 const defaultUser = {
11
 const defaultUser = {
11
   user_id: -1,
12
   user_id: -1,
31
       return {
32
       return {
32
         ...state,
33
         ...state,
33
         ...action.user,
34
         ...action.user,
34
-        avatar_url: 'https://www.algoo.fr/static/images/people_images/PERSO_SEUL.png' // @FIXME use avatar from api when db handles it
35
+        avatar_url: action.user.avatar_url
36
+          ? action.user.avatar_url
37
+          : action.user.public_name ? generateAvatarFromPublicName(action.user.public_name) : ''
35
       }
38
       }
36
 
39
 
37
     case `${SET}/${USER_DISCONNECTED}`:
40
     case `${SET}/${USER_DISCONNECTED}`:

+ 30 - 14
frontend/src/reducer/workspaceContentList.js Voir le fichier

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

+ 0 - 1
frontend_app_admin_workspace_user/dist/dev Voir le fichier

1
-../../frontend/dist/dev

+ 0 - 1
frontend_app_admin_workspace_user/dist/font Voir le fichier

1
-../../frontend/dist/font

+ 5 - 5
frontend_app_admin_workspace_user/dist/index.html Voir le fichier

6
   <title>Html-document App Tracim</title>
6
   <title>Html-document App Tracim</title>
7
   <link rel='shortcut icon' href='favicon.ico'>
7
   <link rel='shortcut icon' href='favicon.ico'>
8
 
8
 
9
-  <link rel="stylesheet" type="text/css" href="./font/font-awesome-4.7.0/css/font-awesome.css">
9
+  <link rel="stylesheet" type="text/css" href="./asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-  <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
11
+  <link rel="stylesheet" type="text/css" href="./asset/bootstrap/bootstrap-4.0.0-beta.css">
12
 </head>
12
 </head>
13
 <body>
13
 <body>
14
-  <script src="./dev/jquery-3.2.1.js"></script>
15
-  <script src="./dev/popper-1.12.3.js"></script>
16
-  <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
14
+  <script src="./asset/bootstrap/jquery-3.2.1.js"></script>
15
+  <script src="./asset/bootstrap/popper-1.12.3.js"></script>
16
+  <script src="./asset/bootstrap/bootstrap-4.0.0-beta.2.js"></script>
17
 
17
 
18
   <div id='content'></div>
18
   <div id='content'></div>
19
 
19
 

+ 194 - 0
frontend_app_admin_workspace_user/src/component/AdminWorkspace.jsx Voir le fichier

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 Voir le fichier

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

+ 20 - 0
frontend_app_admin_workspace_user/src/css/index.styl Voir le fichier

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

+ 0 - 1
frontend_app_html-document/dist/dev Voir le fichier

1
-../../frontend/dist/dev/

+ 0 - 1
frontend_app_html-document/dist/font Voir le fichier

1
-../../frontend/dist/font/

+ 7 - 7
frontend_app_html-document/dist/index.html Voir le fichier

6
   <title>Html-document App Tracim</title>
6
   <title>Html-document App Tracim</title>
7
   <link rel='shortcut icon' href='favicon.ico'>
7
   <link rel='shortcut icon' href='favicon.ico'>
8
 
8
 
9
-  <link rel="stylesheet" type="text/css" href="./font/font-awesome-4.7.0/css/font-awesome.css">
9
+  <link rel="stylesheet" type="text/css" href="./asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-  <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
11
+  <link rel="stylesheet" type="text/css" href="./asset/bootstrap/bootstrap-4.0.0-beta.css">
12
 </head>
12
 </head>
13
 <body>
13
 <body>
14
-  <script src="./dev/jquery-3.2.1.js"></script>
15
-  <script src="./dev/popper-1.12.3.js"></script>
16
-  <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
14
+  <script src="./asset/bootstrap/jquery-3.2.1.js"></script>
15
+  <script src="./asset/bootstrap/popper-1.12.3.js"></script>
16
+  <script src="./asset/bootstrap/bootstrap-4.0.0-beta.2.js"></script>
17
 
17
 
18
-  <script type="text/javascript" src="/asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
19
-  <script type="text/javascript" src="/asset/tinymce/js/tinymce/tinymce.min.js"></script>
18
+  <script type="text/javascript" src="./asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
19
+  <script type="text/javascript" src="./asset/tinymce/js/tinymce/tinymce.min.js"></script>
20
 
20
 
21
   <div id='content'></div>
21
   <div id='content'></div>
22
 
22
 

+ 50 - 0
frontend_app_html-document/src/action.async.js Voir le fichier

77
       label: newContentName
77
       label: newContentName
78
     })
78
     })
79
   })
79
   })
80
+
81
+export const putHtmlDocIsArchived = (user, apiUrl, idWorkspace, idContent) => {
82
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/archive`, {
83
+    headers: {
84
+      'Authorization': 'Basic ' + user.auth,
85
+      ...FETCH_CONFIG.headers
86
+    },
87
+    method: 'PUT'
88
+  })
89
+}
90
+
91
+export const putHtmlDocIsDeleted = (user, apiUrl, idWorkspace, idContent) => {
92
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/delete`, {
93
+    headers: {
94
+      'Authorization': 'Basic ' + user.auth,
95
+      ...FETCH_CONFIG.headers
96
+    },
97
+    method: 'PUT'
98
+  })
99
+}
100
+
101
+export const putHtmlDocRestoreArchived = (user, apiUrl, idWorkspace, idContent) => {
102
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/unarchive`, {
103
+    headers: {
104
+      'Authorization': 'Basic ' + user.auth,
105
+      ...FETCH_CONFIG.headers
106
+    },
107
+    method: 'PUT'
108
+  })
109
+}
110
+
111
+export const putHtmlDocRestoreDeleted = (user, apiUrl, idWorkspace, idContent) => {
112
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/undelete`, {
113
+    headers: {
114
+      'Authorization': 'Basic ' + user.auth,
115
+      ...FETCH_CONFIG.headers
116
+    },
117
+    method: 'PUT'
118
+  })
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
+}

+ 28 - 0
frontend_app_html-document/src/component/HtmlDocument.jsx Voir le fichier

5
 const HtmlDocument = props => {
5
 const HtmlDocument = props => {
6
   return (
6
   return (
7
     <div className='wsContentHtmlDocument__contentpage__textnote html-document__contentpage__textnote'>
7
     <div className='wsContentHtmlDocument__contentpage__textnote html-document__contentpage__textnote'>
8
+      {props.isArchived &&
9
+        <div className='html-document__contentpage__textnote__state'>
10
+          <div className='html-document__contentpage__textnote__state__msg'>
11
+            <i className='fa fa-fw fa-archive' />
12
+            This content is archived.
13
+          </div>
14
+
15
+          <button className='html-document__contentpage__textnote__state__btnrestore btn' onClick={props.onClickRestoreArchived}>
16
+            <i className='fa fa-fw fa-archive' />
17
+            Restore
18
+          </button>
19
+        </div>
20
+      }
21
+
22
+      {props.isDeleted &&
23
+        <div className='html-document__contentpage__textnote__state'>
24
+          <div className='html-document__contentpage__textnote__state__msg'>
25
+            <i className='fa fa-fw fa-trash' />
26
+            Ce contenu est supprimé.
27
+          </div>
28
+
29
+          <button className='html-document__contentpage__textnote__state__btnrestore btn' onClick={props.onClickRestoreDeleted}>
30
+            <i className='fa fa-fw fa-trash' />
31
+            Restore
32
+          </button>
33
+        </div>
34
+      }
35
+
8
       {(props.mode === MODE.VIEW || props.mode === MODE.REVISION) &&
36
       {(props.mode === MODE.VIEW || props.mode === MODE.REVISION) &&
9
         <div>
37
         <div>
10
           <div className='html-document__contentpage__textnote__version'>
38
           <div className='html-document__contentpage__textnote__version'>

+ 90 - 17
frontend_app_html-document/src/container/HtmlDocument.jsx Voir le fichier

5
 import {
5
 import {
6
   addAllResourceI18n,
6
   addAllResourceI18n,
7
   handleFetchResult,
7
   handleFetchResult,
8
+  generateAvatarFromPublicName,
8
   PopinFixed,
9
   PopinFixed,
9
   PopinFixedHeader,
10
   PopinFixedHeader,
10
   PopinFixedOption,
11
   PopinFixedOption,
21
   getHtmlDocRevision,
22
   getHtmlDocRevision,
22
   postHtmlDocNewComment,
23
   postHtmlDocNewComment,
23
   putHtmlDocContent,
24
   putHtmlDocContent,
24
-  putHtmlDocStatus
25
+  putHtmlDocStatus,
26
+  putHtmlDocIsArchived,
27
+  putHtmlDocIsDeleted,
28
+  putHtmlDocRestoreArchived,
29
+  putHtmlDocRestoreDeleted,
30
+  putHtmlDocRead
25
 } from '../action.async.js'
31
 } from '../action.async.js'
26
 
32
 
27
 class HtmlDocument extends React.Component {
33
 class HtmlDocument extends React.Component {
119
       handleFetchResult(await fetchResultRevision)
125
       handleFetchResult(await fetchResultRevision)
120
     ])
126
     ])
121
       .then(([resComment, resRevision]) => {
127
       .then(([resComment, resRevision]) => {
122
-        const resCommentWithProperDate = resComment.body.map(c => ({...c, created: (new Date(c.created)).toLocaleString()}))
128
+        const resCommentWithProperDateAndAvatar = resComment.body.map(c => ({
129
+          ...c,
130
+          created: (new Date(c.created)).toLocaleString(),
131
+          author: {
132
+            ...c.author,
133
+            avatar_url: c.author.avatar_url
134
+              ? c.author.avatar_url
135
+              : generateAvatarFromPublicName(c.author.public_name)
136
+          }
137
+        }))
123
 
138
 
124
         const revisionWithComment = resRevision.body
139
         const revisionWithComment = resRevision.body
125
           .map((r, i) => ({
140
           .map((r, i) => ({
128
             timelineType: 'revision',
143
             timelineType: 'revision',
129
             commentList: r.comment_ids.map(ci => ({
144
             commentList: r.comment_ids.map(ci => ({
130
               timelineType: 'comment',
145
               timelineType: 'comment',
131
-              ...resCommentWithProperDate.find(c => c.content_id === ci)
146
+              ...resCommentWithProperDateAndAvatar.find(c => c.content_id === ci)
132
             })),
147
             })),
133
             number: i + 1
148
             number: i + 1
134
           }))
149
           }))
151
         console.log('Error loading Timeline.', e)
166
         console.log('Error loading Timeline.', e)
152
         this.setState({timeline: []})
167
         this.setState({timeline: []})
153
       })
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
154
   }
172
   }
155
 
173
 
156
   handleClickBtnCloseApp = () => {
174
   handleClickBtnCloseApp = () => {
251
   }
269
   }
252
 
270
 
253
   handleClickArchive = async () => {
271
   handleClickArchive = async () => {
254
-    console.log('archive')
255
-    // const { config, content } = this.state
256
-    //
257
-    // const fetchResultArchive = await fetch(`${config.apiUrl}/workspaces/${content.workspace_id}/contents/${content.content_id}/archive`, {
258
-    //   ...FETCH_CONFIG,
259
-    //   method: 'PUT'
260
-    // })
272
+    const { loggedUser, config, content } = this.state
273
+
274
+    const fetchResultArchive = await putHtmlDocIsArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
275
+    switch (fetchResultArchive.status) {
276
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: true}})); break
277
+      default: GLOBAL_dispatchEvent({
278
+        type: 'addFlashMsg',
279
+        data: {
280
+          msg: this.props.t('Error while archiving document'),
281
+          type: 'warning',
282
+          delay: undefined
283
+        }
284
+      })
285
+    }
261
   }
286
   }
262
 
287
 
263
   handleClickDelete = async () => {
288
   handleClickDelete = async () => {
264
-    console.log('delete')
265
-    // const { config, content } = this.state
266
-    // const fetchResultDelete = await fetch(`${config.apiUrl}/workspaces/${content.workspace_id}/contents/${content.content_id}/delete`, {
267
-    //   ...FETCH_CONFIG,
268
-    //   method: 'PUT'
269
-    // })
289
+    const { loggedUser, config, content } = this.state
290
+
291
+    const fetchResultArchive = await putHtmlDocIsDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
292
+    switch (fetchResultArchive.status) {
293
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: true}})); break
294
+      default: GLOBAL_dispatchEvent({
295
+        type: 'addFlashMsg',
296
+        data: {
297
+          msg: this.props.t('Error while deleting document'),
298
+          type: 'warning',
299
+          delay: undefined
300
+        }
301
+      })
302
+    }
303
+  }
304
+
305
+  handleClickRestoreArchived = async () => {
306
+    const { loggedUser, config, content } = this.state
307
+
308
+    const fetchResultRestore = await putHtmlDocRestoreArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
309
+    switch (fetchResultRestore.status) {
310
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: false}})); break
311
+      default: GLOBAL_dispatchEvent({
312
+        type: 'addFlashMsg',
313
+        data: {
314
+          msg: this.props.t('Error while restoring document'),
315
+          type: 'warning',
316
+          delay: undefined
317
+        }
318
+      })
319
+    }
320
+  }
321
+
322
+  handleClickRestoreDeleted = async () => {
323
+    const { loggedUser, config, content } = this.state
324
+
325
+    const fetchResultRestore = await putHtmlDocRestoreDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
326
+    switch (fetchResultRestore.status) {
327
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: false}})); break
328
+      default: GLOBAL_dispatchEvent({
329
+        type: 'addFlashMsg',
330
+        data: {
331
+          msg: this.props.t('Error while restoring document'),
332
+          type: 'warning',
333
+          delay: undefined
334
+        }
335
+      })
336
+    }
270
   }
337
   }
271
 
338
 
272
   handleClickShowRevision = revision => {
339
   handleClickShowRevision = revision => {
288
         label: revision.label,
355
         label: revision.label,
289
         raw_content: revision.raw_content,
356
         raw_content: revision.raw_content,
290
         number: revision.number,
357
         number: revision.number,
291
-        status: revision.status
358
+        status: revision.status,
359
+        is_archived: prev.is_archived, // archived and delete should always be taken from last version
360
+        is_deleted: prev.is_deleted
292
       },
361
       },
293
       mode: MODE.REVISION
362
       mode: MODE.REVISION
294
     }))
363
     }))
376
             lastVersion={timeline.filter(t => t.timelineType === 'revision').length}
445
             lastVersion={timeline.filter(t => t.timelineType === 'revision').length}
377
             text={content.raw_content}
446
             text={content.raw_content}
378
             onChangeText={this.handleChangeText}
447
             onChangeText={this.handleChangeText}
448
+            isArchived={content.is_archived}
449
+            isDeleted={content.is_deleted}
450
+            onClickRestoreArchived={this.handleClickRestoreArchived}
451
+            onClickRestoreDeleted={this.handleClickRestoreDeleted}
379
             key={'html-document'}
452
             key={'html-document'}
380
           />
453
           />
381
 
454
 

+ 14 - 3
frontend_app_html-document/src/css/index.styl Voir le fichier

21
       height 100%
21
       height 100%
22
       overflow-y auto
22
       overflow-y auto
23
       background-color off-white
23
       background-color off-white
24
+      &__state
25
+        display flex
26
+        align-items center
27
+        justify-content space-between
28
+        margin-bottom 15px
29
+        padding 5px 15px
30
+        background-color #fee498
31
+        border-radius 10px
32
+        &__msg > i
33
+          margin-right 8px
34
+        &__btnrestore
35
+          cursor pointer
36
+          & > i
37
+            margin-right 8px
24
       &__version
38
       &__version
25
         display flex
39
         display flex
26
         justify-content flex-end
40
         justify-content flex-end
37
     &__messagelist
51
     &__messagelist
38
       min-height 70px
52
       min-height 70px
39
       &__item
53
       &__item
40
-        &__avatar
41
-          border 1px solid darkHtmlColor
42
-          background-color off-white
43
         &__content
54
         &__content
44
           color darkGrey
55
           color darkGrey
45
       &__version
56
       &__version

+ 0 - 1
frontend_app_thread/dist/dev Voir le fichier

1
-../../frontend/dist/dev

+ 0 - 1
frontend_app_thread/dist/font Voir le fichier

1
-../../frontend/dist/font

+ 5 - 5
frontend_app_thread/dist/index.html Voir le fichier

6
   <title>Thread App Tracim</title>
6
   <title>Thread App Tracim</title>
7
   <link rel='shortcut icon' href='favicon.ico'>
7
   <link rel='shortcut icon' href='favicon.ico'>
8
 
8
 
9
-  <link rel="stylesheet" type="text/css" href="./font/font-awesome-4.7.0/css/font-awesome.css">
9
+  <link rel="stylesheet" type="text/css" href="./asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-  <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
11
+  <link rel="stylesheet" type="text/css" href="./asset/bootstrap/bootstrap-4.0.0-beta.css">
12
 </head>
12
 </head>
13
 <body>
13
 <body>
14
-<script src="./dev/jquery-3.2.1.js"></script>
15
-<script src="./dev/popper-1.12.3.js"></script>
16
-<script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
14
+<script src="./asset/bootstrap/jquery-3.2.1.js"></script>
15
+<script src="./asset/bootstrap/popper-1.12.3.js"></script>
16
+<script src="./asset/bootstrap/bootstrap-4.0.0-beta.2.js"></script>
17
 
17
 
18
 <script type="text/javascript" src="/asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
18
 <script type="text/javascript" src="/asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
19
 <script type="text/javascript" src="/asset/tinymce/js/tinymce/tinymce.min.js"></script>
19
 <script type="text/javascript" src="/asset/tinymce/js/tinymce/tinymce.min.js"></script>

+ 50 - 0
frontend_app_thread/src/action.async.js Voir le fichier

68
       raw_content: '' // threads have no content
68
       raw_content: '' // threads have no content
69
     })
69
     })
70
   })
70
   })
71
+
72
+export const putThreadIsArchived = (user, apiUrl, idWorkspace, idContent) => {
73
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/archive`, {
74
+    headers: {
75
+      'Authorization': 'Basic ' + user.auth,
76
+      ...FETCH_CONFIG.headers
77
+    },
78
+    method: 'PUT'
79
+  })
80
+}
81
+
82
+export const putThreadIsDeleted = (user, apiUrl, idWorkspace, idContent) => {
83
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/delete`, {
84
+    headers: {
85
+      'Authorization': 'Basic ' + user.auth,
86
+      ...FETCH_CONFIG.headers
87
+    },
88
+    method: 'PUT'
89
+  })
90
+}
91
+
92
+export const putThreadRestoreArchived = (user, apiUrl, idWorkspace, idContent) => {
93
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/unarchive`, {
94
+    headers: {
95
+      'Authorization': 'Basic ' + user.auth,
96
+      ...FETCH_CONFIG.headers
97
+    },
98
+    method: 'PUT'
99
+  })
100
+}
101
+
102
+export const putThreadRestoreDeleted = (user, apiUrl, idWorkspace, idContent) => {
103
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/undelete`, {
104
+    headers: {
105
+      'Authorization': 'Basic ' + user.auth,
106
+      ...FETCH_CONFIG.headers
107
+    },
108
+    method: 'PUT'
109
+  })
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
+}

+ 2 - 2
frontend_app_thread/src/container/PopupCreateThread.jsx Voir le fichier

88
   })
88
   })
89
 
89
 
90
   handleValidate = async () => {
90
   handleValidate = async () => {
91
-    const { config, appName, idWorkspace, idFolder, newContentName } = this.state
91
+    const { loggedUser, config, appName, idWorkspace, idFolder, newContentName } = this.state
92
 
92
 
93
-    const fetchSaveThreadDoc = postThreadContent(config.apiUrl, idWorkspace, idFolder, config.slug, newContentName)
93
+    const fetchSaveThreadDoc = postThreadContent(loggedUser, config.apiUrl, idWorkspace, idFolder, config.slug, newContentName)
94
 
94
 
95
     handleFetchResult(await fetchSaveThreadDoc)
95
     handleFetchResult(await fetchSaveThreadDoc)
96
       .then(resSave => {
96
       .then(resSave => {

+ 101 - 22
frontend_app_thread/src/container/Thread.jsx Voir le fichier

4
 import {
4
 import {
5
   addAllResourceI18n,
5
   addAllResourceI18n,
6
   handleFetchResult,
6
   handleFetchResult,
7
+  generateAvatarFromPublicName,
7
   PopinFixed,
8
   PopinFixed,
8
   PopinFixedHeader,
9
   PopinFixedHeader,
9
   PopinFixedOption,
10
   PopinFixedOption,
17
   getThreadComment,
18
   getThreadComment,
18
   postThreadNewComment,
19
   postThreadNewComment,
19
   putThreadStatus,
20
   putThreadStatus,
20
-  putThreadContent
21
+  putThreadContent,
22
+  putThreadIsArchived,
23
+  putThreadIsDeleted,
24
+  putThreadRestoreArchived,
25
+  putThreadRestoreDeleted,
26
+  putThreadRead
21
 } from '../action.async.js'
27
 } from '../action.async.js'
22
 
28
 
23
 class Thread extends React.Component {
29
 class Thread extends React.Component {
76
   componentDidUpdate (prevProps, prevState) {
82
   componentDidUpdate (prevProps, prevState) {
77
     const { state } = this
83
     const { state } = this
78
 
84
 
79
-    console.log('%c<Thread> did Mount', `color: ${this.state.config.hexcolor}`, prevState, state)
85
+    console.log('%c<Thread> did Update', `color: ${this.state.config.hexcolor}`, prevState, state)
80
 
86
 
81
     if (!prevState.content || !state.content) return
87
     if (!prevState.content || !state.content) return
82
 
88
 
103
       handleFetchResult(await fetchResultThread),
109
       handleFetchResult(await fetchResultThread),
104
       handleFetchResult(await fetchResultThreadComment)
110
       handleFetchResult(await fetchResultThreadComment)
105
     ])
111
     ])
106
-      .then(([resThread, resComment]) => this.setState({
107
-        content: resThread.body,
108
-        listMessage: resComment.body.map(c => ({
109
-          ...c,
110
-          timelineType: 'comment',
111
-          created: (new Date(c.created)).toLocaleString()
112
-        }))
113
-      }))
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
+      })
114
       .catch(e => console.log('Error loading Thread data.', e))
130
       .catch(e => console.log('Error loading Thread data.', e))
115
   }
131
   }
116
 
132
 
166
 
182
 
167
     handleFetchResult(await fetchResultSaveEditStatus)
183
     handleFetchResult(await fetchResultSaveEditStatus)
168
       .then(resSave => {
184
       .then(resSave => {
169
-        if (resSave.status !== 204) { // 204 no content so dont take status from resSave.apiResponse.status
170
-          console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
171
-        } else {
185
+        if (resSave.status === 204) { // 204 no content so dont take status from resSave.apiResponse.status
172
           this.loadContent()
186
           this.loadContent()
187
+        } else {
188
+          console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
189
+        }
190
+      })
191
+  }
192
+
193
+  handleClickArchive = async () => {
194
+    const { loggedUser, config, content } = this.state
195
+
196
+    const fetchResultArchive = await putThreadIsArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
197
+    switch (fetchResultArchive.status) {
198
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: true}})); break
199
+      default: GLOBAL_dispatchEvent({
200
+        type: 'addFlashMsg',
201
+        data: {
202
+          msg: this.props.t('Error while archiving thread'),
203
+          type: 'warning',
204
+          delay: undefined
205
+        }
206
+      })
207
+    }
208
+  }
209
+
210
+  handleClickDelete = async () => {
211
+    const { loggedUser, config, content } = this.state
212
+
213
+    const fetchResultArchive = await putThreadIsDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
214
+    switch (fetchResultArchive.status) {
215
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: true}})); break
216
+      default: GLOBAL_dispatchEvent({
217
+        type: 'addFlashMsg',
218
+        data: {
219
+          msg: this.props.t('Error while deleting thread'),
220
+          type: 'warning',
221
+          delay: undefined
173
         }
222
         }
174
       })
223
       })
224
+    }
175
   }
225
   }
176
 
226
 
177
-  handleClickArchive = () => console.log('archive nyi')
227
+  handleClickRestoreArchived = async () => {
228
+    const { loggedUser, config, content } = this.state
178
 
229
 
179
-  handleClickDelete = () => console.log('delete nyi')
230
+    const fetchResultRestore = await putThreadRestoreArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
231
+    switch (fetchResultRestore.status) {
232
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: false}})); break
233
+      default: GLOBAL_dispatchEvent({
234
+        type: 'addFlashMsg',
235
+        data: {
236
+          msg: this.props.t('Error while restoring thread'),
237
+          type: 'warning',
238
+          delay: undefined
239
+        }
240
+      })
241
+    }
242
+  }
243
+
244
+  handleClickRestoreDeleted = async () => {
245
+    const { loggedUser, config, content } = this.state
246
+
247
+    const fetchResultRestore = await putThreadRestoreDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
248
+    switch (fetchResultRestore.status) {
249
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: false}})); break
250
+      default: GLOBAL_dispatchEvent({
251
+        type: 'addFlashMsg',
252
+        data: {
253
+          msg: this.props.t('Error while restoring thread'),
254
+          type: 'warning',
255
+          delay: undefined
256
+        }
257
+      })
258
+    }
259
+  }
180
 
260
 
181
   render () {
261
   render () {
182
     const { config, isVisible, loggedUser, content, listMessage, newComment, timelineWysiwyg } = this.state
262
     const { config, isVisible, loggedUser, content, listMessage, newComment, timelineWysiwyg } = this.state
184
     if (!isVisible) return null
264
     if (!isVisible) return null
185
 
265
 
186
     return (
266
     return (
187
-      <PopinFixed
188
-        customClass={config.slug}
189
-        customColor={config.hexcolor}
190
-      >
267
+      <PopinFixed customClass={config.slug} customColor={config.hexcolor}>
191
         <PopinFixedHeader
268
         <PopinFixedHeader
192
           customClass={`${config.slug}__contentpage`}
269
           customClass={`${config.slug}__contentpage`}
193
           customColor={config.hexcolor}
270
           customColor={config.hexcolor}
219
           </div>
296
           </div>
220
         </PopinFixedOption>
297
         </PopinFixedOption>
221
 
298
 
222
-        <PopinFixedContent
223
-          customClass={`${config.slug}__contentpage`}
224
-        >
299
+        <PopinFixedContent customClass={`${config.slug}__contentpage`}>
225
           <Timeline
300
           <Timeline
226
             customClass={`${config.slug}__contentpage`}
301
             customClass={`${config.slug}__contentpage`}
227
             customColor={config.hexcolor}
302
             customColor={config.hexcolor}
236
             onClickRevisionBtn={() => {}}
311
             onClickRevisionBtn={() => {}}
237
             shouldScrollToBottom
312
             shouldScrollToBottom
238
             showHeader={false}
313
             showHeader={false}
314
+            isArchived={content.is_archived}
315
+            onClickRestoreArchived={this.handleClickRestoreArchived}
316
+            isDeleted={content.is_deleted}
317
+            onClickRestoreDeleted={this.handleClickRestoreDeleted}
239
           />
318
           />
240
         </PopinFixedContent>
319
         </PopinFixedContent>
241
       </PopinFixed>
320
       </PopinFixed>

+ 0 - 4
frontend_app_thread/src/css/index.styl Voir le fichier

16
     &__contentpage
16
     &__contentpage
17
       &__content
17
       &__content
18
         width 100%
18
         width 100%
19
-      &__messagelist
20
-        &__item__avatar
21
-          border 1px solid darkenThread
22
-          background-color off-white
23
       &__texteditor
19
       &__texteditor
24
         flex 0 0 auto
20
         flex 0 0 auto
25
 
21
 

+ 0 - 1
frontend_app_workspace/dist/dev Voir le fichier

1
-../../frontend/dist/dev/

+ 0 - 1
frontend_app_workspace/dist/font Voir le fichier

1
-../../frontend/dist/font/

+ 7 - 7
frontend_app_workspace/dist/index.html Voir le fichier

6
   <title>Workspace App Tracim</title>
6
   <title>Workspace App Tracim</title>
7
   <link rel='shortcut icon' href='favicon.ico'>
7
   <link rel='shortcut icon' href='favicon.ico'>
8
 
8
 
9
-  <link rel="stylesheet" type="text/css" href="./font/font-awesome-4.7.0/css/font-awesome.css">
9
+  <link rel="stylesheet" type="text/css" href="./asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-  <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
11
+  <link rel="stylesheet" type="text/css" href="./asset/bootstrap/bootstrap-4.0.0-beta.css">
12
 </head>
12
 </head>
13
 
13
 
14
 <body>
14
 <body>
15
-  <script src="./dev/jquery-3.2.1.js"></script>
16
-  <script src="./dev/popper-1.12.3.js"></script>
17
-  <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
15
+  <script src="./asset/bootstrap/jquery-3.2.1.js"></script>
16
+  <script src="./asset/bootstrap/popper-1.12.3.js"></script>
17
+  <script src="./asset/bootstrap/bootstrap-4.0.0-beta.2.js"></script>
18
 
18
 
19
-  <script type="text/javascript" src="/asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
20
-  <script type="text/javascript" src="/asset/tinymce/js/tinymce/tinymce.min.js"></script>
19
+  <script type="text/javascript" src="./asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
20
+  <script type="text/javascript" src="./asset/tinymce/js/tinymce/tinymce.min.js"></script>
21
 
21
 
22
   <div id='content'></div>
22
   <div id='content'></div>
23
 
23
 

+ 0 - 1
frontend_lib/dist/dev Voir le fichier

1
-../../frontend/dist/dev

+ 0 - 1
frontend_lib/dist/font Voir le fichier

1
-../../frontend/dist/font

+ 5 - 5
frontend_lib/dist/index.html Voir le fichier

6
   <title>Tracim Lib</title>
6
   <title>Tracim Lib</title>
7
   <link rel='shortcut icon' href='favicon.ico'>
7
   <link rel='shortcut icon' href='favicon.ico'>
8
 
8
 
9
-  <link rel="stylesheet" type="text/css" href="./font/font-awesome-4.7.0/css/font-awesome.css">
9
+  <link rel="stylesheet" type="text/css" href="./asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
10
   <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-  <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
11
+  <link rel="stylesheet" type="text/css" href="./asset/bootstrap/bootstrap-4.0.0-beta.css">
12
 </head>
12
 </head>
13
 <body>
13
 <body>
14
-  <script src="./dev/jquery-3.2.1.js"></script>
15
-  <script src="./dev/popper-1.12.3.js"></script>
16
-  <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
14
+  <script src="./asset/bootstrap/jquery-3.2.1.js"></script>
15
+  <script src="./asset/bootstrap/popper-1.12.3.js"></script>
16
+  <script src="./asset/bootstrap/bootstrap-4.0.0-beta.2.js"></script>
17
 
17
 
18
   <div id='content'></div>
18
   <div id='content'></div>
19
 
19
 

+ 0 - 0
frontend_lib/package.json Voir le fichier


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff