Browse Source

Merge branch 'develop' into feature/612_get_known_user

inkhey 6 years ago
parent
commit
db17eacbaa
No account linked to committer's email
100 changed files with 4256 additions and 1655 deletions
  1. 74 0
      README_traduction.md
  2. 12 0
      backend/doc/roles.md
  3. 7 6
      backend/tracim_backend/config.py
  4. 18 18
      backend/tracim_backend/fixtures/content.py
  5. 74 103
      backend/tracim_backend/lib/core/content.py
  6. 10 2
      backend/tracim_backend/lib/core/workspace.py
  7. 10 10
      backend/tracim_backend/lib/mail_notifier/notifier.py
  8. 6 6
      backend/tracim_backend/lib/utils/authorization.py
  9. 3 3
      backend/tracim_backend/lib/utils/request.py
  10. 7 5
      backend/tracim_backend/lib/webdav/dav_provider.py
  11. 3 2
      backend/tracim_backend/lib/webdav/design.py
  12. 37 34
      backend/tracim_backend/lib/webdav/resources.py
  13. 5 3
      backend/tracim_backend/lib/webdav/utils.py
  14. 4 4
      backend/tracim_backend/models/applications.py
  15. 109 144
      backend/tracim_backend/models/contents.py
  16. 6 10
      backend/tracim_backend/models/context_models.py
  17. 26 28
      backend/tracim_backend/models/data.py
  18. 4 4
      backend/tracim_backend/tests/__init__.py
  19. 46 47
      backend/tracim_backend/tests/functional/test_contents.py
  20. 5 5
      backend/tracim_backend/tests/functional/test_mail_notification.py
  21. 9 8
      backend/tracim_backend/tests/functional/test_system.py
  22. 1859 224
      backend/tracim_backend/tests/functional/test_user.py
  23. 125 60
      backend/tracim_backend/tests/functional/test_workspaces.py
  24. 119 119
      backend/tracim_backend/tests/library/test_content_api.py
  25. 6 6
      backend/tracim_backend/tests/models/test_content.py
  26. 4 3
      backend/tracim_backend/tests/models/test_content_revision.py
  27. 5 5
      backend/tracim_backend/views/contents_api/comment_controller.py
  28. 14 14
      backend/tracim_backend/views/contents_api/file_controller.py
  29. 5 5
      backend/tracim_backend/views/contents_api/html_document_controller.py
  30. 5 5
      backend/tracim_backend/views/contents_api/threads_controller.py
  31. 24 18
      backend/tracim_backend/views/core_api/schemas.py
  32. 3 3
      backend/tracim_backend/views/core_api/system_controller.py
  33. 11 2
      backend/tracim_backend/views/core_api/user_controller.py
  34. 11 11
      backend/tracim_backend/views/core_api/workspace_controller.py
  35. 59 0
      build_full_frontend.sh
  36. 1 1
      frontend/README.md
  37. 25 6
      frontend/dist/appInterface.js
  38. 2 0
      frontend/dist/index.html
  39. 16 0
      frontend/i18next.scanner.js
  40. 68 0
      frontend/i18next.scanner/en/translation.json
  41. 68 0
      frontend/i18next.scanner/fr/translation.json
  42. 6 6
      frontend/jsonserver/static_db.json
  43. 5 1
      frontend/package.json
  44. 40 8
      frontend/src/action-creator.async.js
  45. 45 28
      frontend/src/action-creator.sync.js
  46. 24 8
      frontend/src/appFactory.js
  47. 2 2
      frontend/src/component/Account/Calendar.jsx
  48. 4 4
      frontend/src/component/Account/Notification.jsx
  49. 15 6
      frontend/src/component/Account/Password.jsx
  50. 22 7
      frontend/src/component/Account/PersonalData.jsx
  51. 3 2
      frontend/src/component/Account/Timezone.jsx
  52. 1 1
      frontend/src/component/FlashMessage.jsx
  53. 1 1
      frontend/src/component/Footer.jsx
  54. 4 4
      frontend/src/component/Header/MenuActionListItem/DropdownLang.jsx
  55. 4 3
      frontend/src/component/Header/MenuActionListItem/MenuProfil.jsx
  56. 5 4
      frontend/src/component/Header/MenuActionListItem/Notification.jsx
  57. 1 1
      frontend/src/component/Header/MenuActionListItem/Search.jsx
  58. 7 6
      frontend/src/component/Workspace/BtnExtandedAction.jsx
  59. 2 0
      frontend/src/component/Workspace/ContentItem.jsx
  60. 3 3
      frontend/src/component/Workspace/ContentItemHeader.jsx
  61. 1 1
      frontend/src/component/Workspace/Folder.jsx
  62. 8 7
      frontend/src/component/Workspace/OpenContentApp.jsx
  63. 2 1
      frontend/src/component/Workspace/OpenCreateContentApp.jsx
  64. 4 1
      frontend/src/component/common/Input/SubDropdownCreateButton.jsx
  65. 14 9
      frontend/src/container/Account.jsx
  66. 47 0
      frontend/src/container/AppFullscreenManager.jsx
  67. 189 377
      frontend/src/container/Dashboard.jsx
  68. 571 0
      frontend/src/container/Dashboard_old.jsx
  69. 13 8
      frontend/src/container/Header.jsx
  70. 3 3
      frontend/src/container/Login.jsx
  71. 47 9
      frontend/src/container/Sidebar.jsx
  72. 38 5
      frontend/src/container/Tracim.jsx
  73. 24 58
      frontend/src/container/WorkspaceContent.jsx
  74. 8 22
      frontend/src/css/Dashboard.styl
  75. 6 2
      frontend/src/css/Generic.styl
  76. 0 1
      frontend/src/css/Header.styl
  77. 3 3
      frontend/src/css/Login.styl
  78. 1 1
      frontend/src/css/Workspace.styl
  79. 17 1
      frontend/src/helper.js
  80. 19 9
      frontend/src/i18n.js
  81. 0 0
      frontend/src/img/flag_en.png
  82. 0 0
      frontend/src/img/flag_fr.png
  83. 2 2
      frontend/src/reducer/appList.js
  84. 2 2
      frontend/src/reducer/contentType.js
  85. 46 0
      frontend/src/reducer/currentWorkspace.js
  86. 3 5
      frontend/src/reducer/flashMessage.js
  87. 16 5
      frontend/src/reducer/lang.js
  88. 4 3
      frontend/src/reducer/root.js
  89. 2 2
      frontend/src/reducer/timezone.js
  90. 14 5
      frontend/src/reducer/user.js
  91. 8 5
      frontend/src/reducer/workspaceContentList.js
  92. 8 7
      frontend/src/reducer/workspaceList.js
  93. 0 41
      frontend/src/translate/en.js
  94. 0 41
      frontend/src/translate/fr.js
  95. 13 0
      frontend_app_admin_workspace_user/.editorconfig
  96. 5 0
      frontend_app_admin_workspace_user/.gitignore
  97. 1 0
      frontend_app_admin_workspace_user/README.md
  98. 17 0
      frontend_app_admin_workspace_user/build_admin_workspace_user.sh
  99. 1 0
      frontend_app_admin_workspace_user/dist/asset
  100. 0 0
      frontend_app_admin_workspace_user/dist/dev

+ 74 - 0
README_traduction.md View File

@@ -0,0 +1,74 @@
1
+# Tracim internationalization
2
+
3
+## How to for Frontend part
4
+
5
+In each frontend repo (frontend, frontend_app_..., frontend_lib), there is a folder i18next.scanner that holds every translation files in JSON.
6
+
7
+___
8
+
9
+### I have found a translation error, how do I fix it ?
10
+
11
+**If the error is in any language other than english:**
12
+
13
+a) you can edit the values of the json files.
14
+
15
+b) Then commit/push your changes
16
+
17
+**If the error is in en.json:**
18
+
19
+1) You must find the key in the according .jsx file of that same repo.
20
+
21
+2) Fix the error
22
+ 
23
+3) Rebuild the translation files with:
24
+
25
+`npm run build-translation`
26
+
27
+This will add your new key in the translation files and remove the old one.
28
+
29
+4) Add translations for your new key in other .json files.
30
+
31
+5) commit/push your changes
32
+
33
+___
34
+
35
+### I have found an untranslated key in a language, how do I fix it ?
36
+
37
+It means you have found an english text even though you have selected another language. 
38
+
39
+Do task a) and b) in the according .json file, in folder i18next.scanner.
40
+
41
+___
42
+
43
+### I have found an untranslated key in a language but the key does not appear in the .json file.
44
+
45
+Do step 3).
46
+
47
+If the key still isn't in the .json file, it means the text in the .jsx file does not implement the translation process.
48
+
49
+So you must:
50
+
51
+I) Find the according .jsx file that have your untranslated key
52
+
53
+II) wrap your untranslated key in the translation function `t`:
54
+
55
+Exemple: `<div>My untranslated key</div>` will become `<div>{this.props.t('My untranslated key')}</div>`
56
+
57
+III) Check that `t` in available in your component, meanings your component must be wraped in the `translate()` higher order function
58
+
59
+``` javascript
60
+import React from 'react'
61
+import { translate } from 'react-i18next'
62
+
63
+class MyComponent extends React.Component {
64
+  render () {
65
+    return (<div>{this.props.t('My untranslated key')}</div>)
66
+  }
67
+}
68
+
69
+export default translate()(MyComponent)
70
+```
71
+
72
+IV) You can destruct `t` from `this.props` in the `render()` like it is done in most components.
73
+
74
+V) Do steps 3), 4), 5)

+ 12 - 0
backend/doc/roles.md View File

@@ -7,6 +7,11 @@ The other is workspace related and is called "workspace role".
7 7
 
8 8
 ## Global profile
9 9
 
10
+|                               | Normal User | Managers    | Admin          |
11
+|-------------------------------|-------------|-------------|----------------|
12
+| slug                            | users       | managers    | administrators |
13
+|-------------------------------|-------------|-------------|---------|
14
+
10 15
 
11 16
 |                               | Normal User | Managers    | Admin   |
12 17
 |-------------------------------|-------------|-------------|---------|
@@ -22,11 +27,18 @@ The other is workspace related and is called "workspace role".
22 27
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
23 28
 
24 29
 
30
+
31
+
25 32
 ## Workspace Roles
26 33
 
27 34
 
28 35
 |                              | Reader | Contributor | Content Manager | Workspace Manager |
29 36
 |------------------------------|--------|-------------|-----------------|-------------------|
37
+| slug                         | reader | contributor | content-manager |  workspace-manager|
38
+|------------------------------|--------|-------------|-----------------|-------------------|
39
+
40
+|                              | Reader | Contributor | Content Manager | Workspace Manager |
41
+|------------------------------|--------|-------------|-----------------|-------------------|
30 42
 | read content                 |  yes   | yes         | yes             | yes               |
31 43
 |------------------------------|--------|-------------|-----------------|-------------------|
32 44
 | create content               |  no    | yes         | yes             | yes               |

+ 7 - 6
backend/tracim_backend/config.py View File

@@ -3,8 +3,9 @@ from urllib.parse import urlparse
3 3
 from paste.deploy.converters import asbool
4 4
 from tracim_backend.lib.utils.logger import logger
5 5
 from depot.manager import DepotManager
6
+from tracim_backend.models.contents import CONTENT_TYPES
7
+from tracim_backend.models.data import ActionDescription
6 8
 
7
-from tracim_backend.models.data import ActionDescription, ContentType
8 9
 
9 10
 
10 11
 class CFG(object):
@@ -145,11 +146,11 @@ class CFG(object):
145 146
         ]
146 147
 
147 148
         self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [
148
-            ContentType.Page,
149
-            ContentType.Thread,
150
-            ContentType.File,
151
-            ContentType.Comment,
152
-            # ContentType.Folder -- Folder is skipped
149
+            CONTENT_TYPES.Page.slug,
150
+            CONTENT_TYPES.Thread.slug,
151
+            CONTENT_TYPES.File.slug,
152
+            CONTENT_TYPES.Comment.slug,
153
+            # CONTENT_TYPES.Folder.slug -- Folder is skipped
153 154
         ]
154 155
         if settings.get('email.notification.from'):
155 156
             raise Exception(

+ 18 - 18
backend/tracim_backend/fixtures/content.py View File

@@ -8,7 +8,7 @@ from tracim_backend.fixtures.users_and_groups import Test
8 8
 from tracim_backend.lib.core.content import ContentApi
9 9
 from tracim_backend.lib.core.userworkspace import RoleApi
10 10
 from tracim_backend.lib.core.workspace import WorkspaceApi
11
-from tracim_backend.models.data import ContentType
11
+from tracim_backend.models.contents import CONTENT_TYPES
12 12
 from tracim_backend.models.data import UserRoleInWorkspace
13 13
 from tracim_backend.models.revision_protection import new_revision
14 14
 
@@ -91,14 +91,14 @@ class Content(Fixture):
91 91
         # Folders
92 92
 
93 93
         tool_workspace = content_api.create(
94
-            content_type=ContentType.Folder,
94
+            content_type_slug=CONTENT_TYPES.Folder.slug,
95 95
             workspace=business_workspace,
96 96
             label='Tools',
97 97
             do_save=True,
98 98
             do_notify=False,
99 99
         )
100 100
         menu_workspace = content_api.create(
101
-            content_type=ContentType.Folder,
101
+            content_type_slug=CONTENT_TYPES.Folder.slug,
102 102
             workspace=business_workspace,
103 103
             label='Menus',
104 104
             do_save=True,
@@ -106,21 +106,21 @@ class Content(Fixture):
106 106
         )
107 107
 
108 108
         dessert_folder = content_api.create(
109
-            content_type=ContentType.Folder,
109
+            content_type_slug=CONTENT_TYPES.Folder.slug,
110 110
             workspace=recipe_workspace,
111 111
             label='Desserts',
112 112
             do_save=True,
113 113
             do_notify=False,
114 114
         )
115 115
         salads_folder = content_api.create(
116
-            content_type=ContentType.Folder,
116
+            content_type_slug=CONTENT_TYPES.Folder.slug,
117 117
             workspace=recipe_workspace,
118 118
             label='Salads',
119 119
             do_save=True,
120 120
             do_notify=False,
121 121
         )
122 122
         other_folder = content_api.create(
123
-            content_type=ContentType.Folder,
123
+            content_type_slug=CONTENT_TYPES.Folder.slug,
124 124
             workspace=other_workspace,
125 125
             label='Infos',
126 126
             do_save=True,
@@ -129,7 +129,7 @@ class Content(Fixture):
129 129
 
130 130
         # Pages, threads, ..
131 131
         tiramisu_page = content_api.create(
132
-            content_type=ContentType.Page,
132
+            content_type_slug=CONTENT_TYPES.Page.slug,
133 133
             workspace=recipe_workspace,
134 134
             parent=dessert_folder,
135 135
             label='Tiramisu Recipes!!!',
@@ -149,7 +149,7 @@ class Content(Fixture):
149 149
             content_api.save(tiramisu_page)
150 150
 
151 151
         best_cake_thread = content_api.create(
152
-            content_type=ContentType.Thread,
152
+            content_type_slug=CONTENT_TYPES.Thread.slug,
153 153
             workspace=recipe_workspace,
154 154
             parent=dessert_folder,
155 155
             label='Best Cake',
@@ -159,7 +159,7 @@ class Content(Fixture):
159 159
         best_cake_thread.description = 'Which is the best cake?'
160 160
         self._session.add(best_cake_thread)
161 161
         apple_pie_recipe = content_api.create(
162
-            content_type=ContentType.File,
162
+            content_type_slug=CONTENT_TYPES.File.slug,
163 163
             workspace=recipe_workspace,
164 164
             parent=dessert_folder,
165 165
             label='Apple_Pie',
@@ -174,7 +174,7 @@ class Content(Fixture):
174 174
         )
175 175
         self._session.add(apple_pie_recipe)
176 176
         Brownie_recipe = content_api.create(
177
-            content_type=ContentType.File,
177
+            content_type_slug=CONTENT_TYPES.File.slug,
178 178
             workspace=recipe_workspace,
179 179
             parent=dessert_folder,
180 180
             label='Brownie Recipe',
@@ -189,7 +189,7 @@ class Content(Fixture):
189 189
         )
190 190
         self._session.add(Brownie_recipe)
191 191
         fruits_desserts_folder = content_api.create(
192
-            content_type=ContentType.Folder,
192
+            content_type_slug=CONTENT_TYPES.Folder.slug,
193 193
             workspace=recipe_workspace,
194 194
             label='Fruits Desserts',
195 195
             parent=dessert_folder,
@@ -197,7 +197,7 @@ class Content(Fixture):
197 197
         )
198 198
 
199 199
         menu_page = content_api.create(
200
-            content_type=ContentType.Page,
200
+            content_type_slug=CONTENT_TYPES.Page.slug,
201 201
             workspace=business_workspace,
202 202
             parent=menu_workspace,
203 203
             label='Current Menu',
@@ -205,14 +205,14 @@ class Content(Fixture):
205 205
         )
206 206
 
207 207
         new_fruit_salad = content_api.create(
208
-            content_type=ContentType.Page,
208
+            content_type_slug=CONTENT_TYPES.Page.slug,
209 209
             workspace=recipe_workspace,
210 210
             parent=fruits_desserts_folder,
211 211
             label='New Fruit Salad',
212 212
             do_save=True,
213 213
         )
214 214
         old_fruit_salad = content_api.create(
215
-            content_type=ContentType.Page,
215
+            content_type_slug=CONTENT_TYPES.Page.slug,
216 216
             workspace=recipe_workspace,
217 217
             parent=fruits_desserts_folder,
218 218
             label='Fruit Salad',
@@ -228,7 +228,7 @@ class Content(Fixture):
228 228
         content_api.save(old_fruit_salad)
229 229
 
230 230
         bad_fruit_salad = content_api.create(
231
-            content_type=ContentType.Page,
231
+            content_type_slug=CONTENT_TYPES.Page.slug,
232 232
             workspace=recipe_workspace,
233 233
             parent=fruits_desserts_folder,
234 234
             label='Bad Fruit Salad',
@@ -245,13 +245,13 @@ class Content(Fixture):
245 245
 
246 246
         # File at the root for test
247 247
         new_fruit_salad = content_api.create(
248
-            content_type=ContentType.Page,
248
+            content_type_slug=CONTENT_TYPES.Page.slug,
249 249
             workspace=other_workspace,
250 250
             label='New Fruit Salad',
251 251
             do_save=True,
252 252
         )
253 253
         old_fruit_salad = content_api.create(
254
-            content_type=ContentType.Page,
254
+            content_type_slug=CONTENT_TYPES.Page.slug,
255 255
             workspace=other_workspace,
256 256
             label='Fruit Salad',
257 257
             do_save=True,
@@ -265,7 +265,7 @@ class Content(Fixture):
265 265
         content_api.save(old_fruit_salad)
266 266
 
267 267
         bad_fruit_salad = content_api.create(
268
-            content_type=ContentType.Page,
268
+            content_type_slug=CONTENT_TYPES.Page.slug,
269 269
             workspace=other_workspace,
270 270
             label='Bad Fruit Salad',
271 271
             do_save=True,

+ 74 - 103
backend/tracim_backend/lib/core/content.py View File

@@ -34,13 +34,15 @@ from tracim_backend.exceptions import EmptyLabelNotAllowed
34 34
 from tracim_backend.exceptions import ContentNotFound
35 35
 from tracim_backend.exceptions import WorkspacesDoNotMatch
36 36
 from tracim_backend.lib.utils.utils import current_date_for_filename
37
+from tracim_backend.models.contents import CONTENT_STATUS
38
+from tracim_backend.models.contents import ContentType
39
+from tracim_backend.models.contents import CONTENT_TYPES
37 40
 from tracim_backend.models.revision_protection import new_revision
38 41
 from tracim_backend.models.auth import User
39 42
 from tracim_backend.models.data import ActionDescription
40
-from tracim_backend.models.data import ContentStatus
41 43
 from tracim_backend.models.data import ContentRevisionRO
42 44
 from tracim_backend.models.data import Content
43
-from tracim_backend.models.data import ContentType
45
+
44 46
 from tracim_backend.models.data import NodeTreeItem
45 47
 from tracim_backend.models.data import RevisionReadStatus
46 48
 from tracim_backend.models.data import UserRoleInWorkspace
@@ -75,10 +77,10 @@ def compare_content_for_sorting_by_type_and_name(
75 77
     else:
76 78
         # TODO - D.A. - 2014-12-02 - Manage Content Types Dynamically
77 79
         content_type_order = [
78
-            ContentType.Folder,
79
-            ContentType.Page,
80
-            ContentType.Thread,
81
-            ContentType.File,
80
+            CONTENT_TYPES.Folder.slug,
81
+            CONTENT_TYPES.Page.slug,
82
+            CONTENT_TYPES.Thread.slug,
83
+            CONTENT_TYPES.File.slug,
82 84
         ]
83 85
 
84 86
         content_1_type_index = content_type_order.index(content1.type)
@@ -105,15 +107,15 @@ class ContentApi(object):
105 107
     SEARCH_SEPARATORS = ',| '
106 108
     SEARCH_DEFAULT_RESULT_NB = 50
107 109
 
108
-    DISPLAYABLE_CONTENTS = (
109
-        ContentType.Folder,
110
-        ContentType.File,
111
-        ContentType.Comment,
112
-        ContentType.Thread,
113
-        ContentType.Page,
114
-        ContentType.PageLegacy,
115
-        ContentType.MarkdownPage,
116
-    )
110
+    # DISPLAYABLE_CONTENTS = (
111
+    #     CONTENT_TYPES.Folder.slug,
112
+    #     CONTENT_TYPES.File.slug,
113
+    #     CONTENT_TYPES.Comment.slug,
114
+    #     CONTENT_TYPES.Thread.slug,
115
+    #     CONTENT_TYPES.Page.slug,
116
+    #     CONTENT_TYPES.Page.slugLegacy,
117
+    #     ContentType.MarkdownPage,
118
+    # )
117 119
 
118 120
     def __init__(
119 121
             self,
@@ -230,7 +232,7 @@ class ContentApi(object):
230 232
 
231 233
         # Exclude non displayable types
232 234
         if not self._force_show_all_types:
233
-            result = result.filter(Content.type.in_(self.DISPLAYABLE_CONTENTS))
235
+            result = result.filter(Content.type.in_(CONTENT_TYPES.query_allowed_types_slugs()))
234 236
 
235 237
         if workspace:
236 238
             result = result.filter(Content.workspace_id == workspace.workspace_id)
@@ -369,8 +371,8 @@ class ContentApi(object):
369 371
     #     removed_item_ids = removed_item_ids or []  # FDV
370 372
     # 
371 373
     #     if not allowed_node_types:
372
-    #         allowed_node_types = [ContentType.Folder]
373
-    #     elif allowed_node_types==ContentType.Any:
374
+    #         allowed_node_types = [CONTENT_TYPES.Folder.slug]
375
+    #     elif allowed_node_types==CONTENT_TYPES.Any_SLUG:
374 376
     #         allowed_node_types = ContentType.all()
375 377
     # 
376 378
     #     parent_id = parent.content_id if parent else None
@@ -395,7 +397,7 @@ class ContentApi(object):
395 397
     #     for folder in folders:
396 398
     #         for allowed_content_type in filter_by_allowed_content_types:
397 399
     # 
398
-    #             is_folder = folder.type == ContentType.Folder
400
+    #             is_folder = folder.type == CONTENT_TYPES.Folder.slug
399 401
     #             content_type__allowed = folder.properties['allowed_content'][allowed_content_type] == True
400 402
     # 
401 403
     #             if is_folder and content_type__allowed:
@@ -404,12 +406,13 @@ class ContentApi(object):
404 406
     # 
405 407
     #     return result
406 408
 
407
-    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
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:
408 410
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
409
-        assert content_type in ContentType.allowed_types()
411
+        assert content_type_slug in CONTENT_TYPES.query_allowed_types_slugs()
412
+        assert content_type_slug != CONTENT_TYPES.Any_SLUG
410 413
         assert not (label and filename)
411 414
 
412
-        if content_type == ContentType.Folder and not label:
415
+        if content_type_slug == CONTENT_TYPES.Folder.slug and not label:
413 416
             label = self.generate_folder_label(workspace, parent)
414 417
 
415 418
         content = Content()
@@ -421,7 +424,7 @@ class ContentApi(object):
421 424
         elif label:
422 425
             content.label = label
423 426
         else:
424
-            if content_type == ContentType.Comment:
427
+            if content_type_slug == CONTENT_TYPES.Comment.slug:
425 428
                 # INFO - G.M - 2018-07-16 - Default label for comments is
426 429
                 # empty string.
427 430
                 content.label = ''
@@ -431,13 +434,13 @@ class ContentApi(object):
431 434
         content.owner = self._user
432 435
         content.parent = parent
433 436
         content.workspace = workspace
434
-        content.type = content_type
437
+        content.type = content_type_slug
435 438
         content.is_temporary = is_temporary
436 439
         content.revision_type = ActionDescription.CREATION
437 440
 
438 441
         if content.type in (
439
-                ContentType.Page,
440
-                ContentType.Thread,
442
+                CONTENT_TYPES.Page.slug,
443
+                CONTENT_TYPES.Thread.slug,
441 444
         ):
442 445
             content.file_extension = '.html'
443 446
 
@@ -447,7 +450,7 @@ class ContentApi(object):
447 450
         return content
448 451
 
449 452
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
450
-        assert parent and parent.type != ContentType.Folder
453
+        assert parent and parent.type != CONTENT_TYPES.Folder.slug
451 454
         if not content:
452 455
             raise EmptyCommentContentNotAllowed()
453 456
         item = Content()
@@ -456,7 +459,7 @@ class ContentApi(object):
456 459
         if not workspace:
457 460
             workspace = item.parent.workspace
458 461
         item.workspace = workspace
459
-        item.type = ContentType.Comment
462
+        item.type = CONTENT_TYPES.Comment.slug
460 463
         item.description = content
461 464
         item.label = ''
462 465
         item.revision_type = ActionDescription.COMMENT
@@ -492,7 +495,7 @@ class ContentApi(object):
492 495
 
493 496
         base_request = self._base_query(workspace).filter(Content.content_id==content_id)
494 497
 
495
-        if content_type!=ContentType.Any:
498
+        if content_type!=CONTENT_TYPES.Any_SLUG:
496 499
             base_request = base_request.filter(Content.type==content_type)
497 500
 
498 501
         if parent:
@@ -577,20 +580,20 @@ class ContentApi(object):
577 580
         return query.filter(
578 581
             or_(
579 582
                 and_(
580
-                    Content.type == ContentType.File,
583
+                    Content.type == CONTENT_TYPES.File.slug,
581 584
                     Content.label == file_name,
582 585
                     Content.file_extension == file_extension,
583 586
                 ),
584 587
                 and_(
585
-                    Content.type == ContentType.Thread,
588
+                    Content.type == CONTENT_TYPES.Thread.slug,
586 589
                     Content.label == file_name,
587 590
                 ),
588 591
                 and_(
589
-                    Content.type == ContentType.Page,
592
+                    Content.type == CONTENT_TYPES.Page.slug,
590 593
                     Content.label == file_name,
591 594
                 ),
592 595
                 and_(
593
-                    Content.type == ContentType.Folder,
596
+                    Content.type == CONTENT_TYPES.Folder.slug,
594 597
                     Content.label == content_label,
595 598
                 ),
596 599
             )
@@ -671,7 +674,7 @@ class ContentApi(object):
671 674
             # Filter query on label
672 675
             folder_query = query \
673 676
                 .filter(
674
-                    Content.type == ContentType.Folder,
677
+                    Content.type == CONTENT_TYPES.Folder.slug,
675 678
                     Content.label == label,
676 679
                     Content.workspace_id == workspace.workspace_id,
677 680
                 )
@@ -723,22 +726,22 @@ class ContentApi(object):
723 726
 
724 727
         return query.filter(or_(
725 728
             and_(
726
-                Content.type == ContentType.File,
729
+                Content.type == CONTENT_TYPES.File.slug,
727 730
                 file_name_filter,
728 731
                 file_extension_filter,
729 732
             ),
730 733
             and_(
731
-                Content.type == ContentType.Thread,
734
+                Content.type == CONTENT_TYPES.Thread.slug,
732 735
                 file_name_filter,
733 736
                 file_extension_filter,
734 737
             ),
735 738
             and_(
736
-                Content.type == ContentType.Page,
739
+                Content.type == CONTENT_TYPES.Page.slug,
737 740
                 file_name_filter,
738 741
                 file_extension_filter,
739 742
             ),
740 743
             and_(
741
-                Content.type == ContentType.Folder,
744
+                Content.type == CONTENT_TYPES.Folder.slug,
742 745
                 label_filter,
743 746
             ),
744 747
         ))
@@ -844,25 +847,28 @@ class ContentApi(object):
844 847
     def _get_all_query(
845 848
         self,
846 849
         parent_id: int = None,
847
-        content_type: str = ContentType.Any,
850
+        content_type_slug: str = CONTENT_TYPES.Any_SLUG,
848 851
         workspace: Workspace = None
849 852
     ) -> Query:
850 853
         """
851 854
         Extended filter for better "get all data" query
852 855
         :param parent_id: filter by parent_id
853
-        :param content_type: filter by content_type slug
856
+        :param content_type_slug: filter by content_type slug
854 857
         :param workspace: filter by workspace
855 858
         :return:
856 859
         """
857 860
         assert parent_id is None or isinstance(parent_id, int)
858
-        assert content_type is not None
861
+        assert content_type_slug is not None
859 862
         resultset = self._base_query(workspace)
860 863
 
861
-        if content_type!=ContentType.Any:
864
+        if content_type_slug != CONTENT_TYPES.Any_SLUG:
862 865
             # INFO - G.M - 2018-07-05 - convert with
863 866
             #  content type object to support legacy slug
864
-            content_type_object = ContentType(content_type)
865
-            resultset = resultset.filter(Content.type.in_(content_type_object.get_slug_aliases()))
867
+            content_type_object = CONTENT_TYPES.get_one_by_slug(content_type_slug)
868
+            all_slug_alias = [content_type_object.slug]
869
+            if content_type_object.slug_alias:
870
+                all_slug_alias.extend(content_type_object.slug_alias)
871
+            resultset = resultset.filter(Content.type.in_(all_slug_alias))
866 872
 
867 873
         if parent_id:
868 874
             resultset = resultset.filter(Content.parent_id==parent_id)
@@ -871,7 +877,7 @@ class ContentApi(object):
871 877
 
872 878
         return resultset
873 879
 
874
-    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
880
+    def get_all(self, parent_id: int=None, content_type: str=CONTENT_TYPES.Any_SLUG, workspace: Workspace=None) -> typing.List[Content]:
875 881
         return self._get_all_query(parent_id, content_type, workspace).all()
876 882
 
877 883
     # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
@@ -895,14 +901,14 @@ class ContentApi(object):
895 901
 
896 902
     # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
897 903
     # TODO find an other name to filter on is_deleted / is_archived
898
-    def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
904
+    def get_all_with_filter(self, parent_id: int=None, content_type: str=CONTENT_TYPES.Any_SLUG, workspace: Workspace=None) -> typing.List[Content]:
899 905
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
900 906
         assert content_type is not None# DYN_REMOVE
901 907
         assert isinstance(content_type, str) # DYN_REMOVE
902 908
 
903 909
         resultset = self._base_query(workspace)
904 910
 
905
-        if content_type != ContentType.Any:
911
+        if content_type != CONTENT_TYPES.Any_SLUG:
906 912
             resultset = resultset.filter(Content.type==content_type)
907 913
 
908 914
         resultset = resultset.filter(Content.is_deleted == self._show_deleted)
@@ -919,46 +925,11 @@ class ContentApi(object):
919 925
 
920 926
         resultset = self._base_query(workspace)
921 927
 
922
-        if content_type != ContentType.Any:
928
+        if content_type != CONTENT_TYPES.Any_SLUG:
923 929
             resultset = resultset.filter(Content.type==content_type)
924 930
 
925 931
         return resultset.all()
926 932
 
927
-    def get_last_active(self, parent_id: int, content_type: str, workspace: Workspace=None, limit=10) -> typing.List[Content]:
928
-        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
929
-        assert content_type is not None# DYN_REMOVE
930
-        assert isinstance(content_type, str) # DYN_REMOVE
931
-
932
-        resultset = self._base_query(workspace) \
933
-            .filter(Content.workspace_id == Workspace.workspace_id) \
934
-            .filter(Workspace.is_deleted.is_(False)) \
935
-            .order_by(desc(Content.updated))
936
-
937
-        if content_type!=ContentType.Any:
938
-            resultset = resultset.filter(Content.type==content_type)
939
-
940
-        if parent_id:
941
-            resultset = resultset.filter(Content.parent_id==parent_id)
942
-
943
-        result = []
944
-        for item in resultset:
945
-            new_item = None
946
-            if ContentType.Comment == item.type:
947
-                new_item = item.parent
948
-            else:
949
-                new_item = item
950
-
951
-            # INFO - D.A. - 2015-05-20
952
-            # We do not want to show only one item if the last 10 items are
953
-            # comments about one thread for example
954
-            if new_item not in result:
955
-                result.append(new_item)
956
-
957
-            if len(result) >= limit:
958
-                break
959
-
960
-        return result
961
-
962 933
     def get_last_unread(self, parent_id: int, content_type: str,
963 934
                         workspace: Workspace=None, limit=10) -> typing.List[Content]:
964 935
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
@@ -980,7 +951,7 @@ class ContentApi(object):
980 951
     #
981 952
     #     resultset = self._base_query(workspace)
982 953
     #
983
-    #     if content_type != ContentType.Any:
954
+    #     if content_type != CONTENT_TYPES.Any_SLUG:
984 955
     #         resultset = resultset.filter(Content.type==content_type)
985 956
     #
986 957
     #     return resultset.all()
@@ -989,7 +960,7 @@ class ContentApi(object):
989 960
             self,
990 961
             workspace: Workspace=None,
991 962
             limit: typing.Optional[int]=None,
992
-            before_datetime: typing.Optional[datetime.datetime]= None,
963
+            before_content: typing.Optional[Content]= None,
993 964
             content_ids: typing.Optional[typing.List[int]] = None,
994 965
     ) -> typing.List[Content]:
995 966
         """
@@ -997,7 +968,8 @@ class ContentApi(object):
997 968
         (last modification of content itself or one of this comment)
998 969
         :param workspace: Workspace to check
999 970
         :param limit: maximum number of elements to return
1000
-        :param before_datetime: date from where we check older content.
971
+        :param before_content: last_active content are only those updated
972
+         before this content given.
1001 973
         :param content_ids: restrict selection to some content ids and
1002 974
         related Comments
1003 975
         :return: list of content
@@ -1012,7 +984,7 @@ class ContentApi(object):
1012 984
                     Content.content_id.in_(content_ids),
1013 985
                     and_(
1014 986
                         Content.parent_id.in_(content_ids),
1015
-                        Content.type == ContentType.Comment
987
+                        Content.type == CONTENT_TYPES.Comment.slug
1016 988
                     )
1017 989
                 )
1018 990
             )
@@ -1021,25 +993,24 @@ class ContentApi(object):
1021 993
 
1022 994
         active_contents = []
1023 995
         too_recent_content = []
996
+        before_content_find = False
1024 997
         for content in resultset:
1025 998
             related_active_content = None
1026
-            if ContentType.Comment == content.type:
999
+            if CONTENT_TYPES.Comment.slug == content.type:
1027 1000
                 related_active_content = content.parent
1028 1001
             else:
1029 1002
                 related_active_content = content
1030 1003
 
1031
-            if not before_datetime:
1032
-                before_datetime = datetime.datetime.now()
1033
-            # INFO - D.A. - 2015-05-20
1034
-            # We do not want to show only one item if the last 10 items are
1035
-            # comments about one thread for example
1036 1004
             if related_active_content not in active_contents and related_active_content not in too_recent_content:  # nopep8
1037
-                # we verify that content is old enough
1038
-                if content.updated < before_datetime:
1005
+
1006
+                if not before_content or before_content_find:
1039 1007
                     active_contents.append(related_active_content)
1040 1008
                 else:
1041 1009
                     too_recent_content.append(related_active_content)
1042 1010
 
1011
+                if before_content and related_active_content == before_content:
1012
+                    before_content_find = True
1013
+
1043 1014
             if limit and len(active_contents) >= limit:
1044 1015
                 break
1045 1016
 
@@ -1075,12 +1046,12 @@ class ContentApi(object):
1075 1046
     #         .filter(Content.content_id.in_(not_read_content_ids)) \
1076 1047
     #         .order_by(desc(Content.updated))
1077 1048
     #
1078
-    #     if content_type != ContentType.Any:
1049
+    #     if content_type != CONTENT_TYPES.Any_SLUG:
1079 1050
     #         not_read_contents = not_read_contents.filter(
1080 1051
     #             Content.type==content_type)
1081 1052
     #     else:
1082 1053
     #         not_read_contents = not_read_contents.filter(
1083
-    #             Content.type!=ContentType.Folder)
1054
+    #             Content.type!=CONTENT_TYPES.Folder.slug)
1084 1055
     #
1085 1056
     #     if parent_id:
1086 1057
     #         not_read_contents = not_read_contents.filter(
@@ -1089,7 +1060,7 @@ class ContentApi(object):
1089 1060
     #     result = []
1090 1061
     #     for item in not_read_contents:
1091 1062
     #         new_item = None
1092
-    #         if ContentType.Comment == item.type:
1063
+    #         if CONTENT_TYPES.Comment.slug == item.type:
1093 1064
     #             new_item = item.parent
1094 1065
     #         else:
1095 1066
     #             new_item = item
@@ -1121,7 +1092,7 @@ class ContentApi(object):
1121 1092
         folder.properties = properties
1122 1093
 
1123 1094
     def set_status(self, content: Content, new_status: str):
1124
-        if new_status in ContentStatus.allowed_values():
1095
+        if new_status in CONTENT_STATUS.get_all_slugs_values():
1125 1096
             content.status = new_status
1126 1097
             content.revision_type = ActionDescription.STATUS_UPDATE
1127 1098
         else:
@@ -1331,7 +1302,7 @@ class ContentApi(object):
1331 1302
                 self.mark_read(child, read_datetime=read_datetime,
1332 1303
                                do_flush=False)
1333 1304
 
1334
-            if ContentType.Comment == content.type:
1305
+            if CONTENT_TYPES.Comment.slug == content.type:
1335 1306
                 self.mark_read(content.parent, read_datetime=read_datetime,
1336 1307
                                do_flush=False, recursive=False)
1337 1308
                 for comment in content.parent.get_comments():
@@ -1451,12 +1422,12 @@ class ContentApi(object):
1451 1422
         return title_keyworded_items
1452 1423
 
1453 1424
     def get_all_types(self) -> typing.List[ContentType]:
1454
-        labels = ContentType.all()
1425
+        labels = CONTENT_TYPES.endpoint_allowed_types_slug()
1455 1426
         content_types = []
1456 1427
         for label in labels:
1457
-            content_types.append(ContentType(label))
1428
+            content_types.append(CONTENT_TYPES.get_one_by_slug(label))
1458 1429
 
1459
-        return ContentType.sorted(content_types)
1430
+        return content_types
1460 1431
 
1461 1432
     # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1462 1433
     def exclude_unavailable(

+ 10 - 2
backend/tracim_backend/lib/core/workspace.py View File

@@ -3,9 +3,11 @@ import typing
3 3
 
4 4
 from sqlalchemy.orm import Query
5 5
 from sqlalchemy.orm import Session
6
+from sqlalchemy.orm.exc import NoResultFound
6 7
 
7 8
 from tracim_backend import CFG
8 9
 from tracim_backend.exceptions import EmptyLabelNotAllowed
10
+from tracim_backend.exceptions import WorkspaceNotFound
9 11
 from tracim_backend.lib.utils.translation import fake_translator as _
10 12
 
11 13
 from tracim_backend.lib.core.userworkspace import RoleApi
@@ -132,10 +134,16 @@ class WorkspaceApi(object):
132 134
         return workspace
133 135
 
134 136
     def get_one(self, id):
135
-        return self._base_query().filter(Workspace.workspace_id == id).one()
137
+        try:
138
+            return self._base_query().filter(Workspace.workspace_id == id).one()
139
+        except NoResultFound as exc:
140
+            raise WorkspaceNotFound('workspace {} does not exist or not visible for user'.format(id)) from exc  # nopep8
136 141
 
137 142
     def get_one_by_label(self, label: str) -> Workspace:
138
-        return self._base_query().filter(Workspace.label == label).one()
143
+        try:
144
+            return self._base_query().filter(Workspace.label == label).one()
145
+        except NoResultFound as exc:
146
+            raise WorkspaceNotFound('workspace {} does not exist or not visible for user'.format(id)) from exc  # nopep8
139 147
 
140 148
     """
141 149
     def get_one_for_current_user(self, id):

+ 10 - 10
backend/tracim_backend/lib/mail_notifier/notifier.py View File

@@ -19,9 +19,9 @@ from tracim_backend.lib.core.workspace import WorkspaceApi
19 19
 from tracim_backend.lib.utils.logger import logger
20 20
 from tracim_backend.models import User
21 21
 from tracim_backend.models.auth import User
22
+from tracim_backend.models.contents import CONTENT_TYPES
22 23
 from tracim_backend.models.data import ActionDescription
23 24
 from tracim_backend.models.data import Content
24
-from tracim_backend.models.data import ContentType
25 25
 from tracim_backend.models.data import UserRoleInWorkspace
26 26
 from tracim_backend.lib.utils.translation import fake_translator as l_, \
27 27
     fake_translator as _
@@ -233,8 +233,8 @@ class EmailManager(object):
233 233
             config=self.config,
234 234
             show_archived=True,
235 235
             show_deleted=True,
236
-        ).get_one(event_content_id, ContentType.Any)
237
-        main_content = content.parent if content.type == ContentType.Comment else content
236
+        ).get_one(event_content_id, CONTENT_TYPES.Any_SLUG)
237
+        main_content = content.parent if content.type == CONTENT_TYPES.Comment.slug else content
238 238
         notifiable_roles = WorkspaceApi(
239 239
             current_user=user,
240 240
             session=self.session,
@@ -455,20 +455,20 @@ class EmailManager(object):
455 455
             content_text = content.description
456 456
             call_to_action_text = l_('View online')
457 457
 
458
-            if ContentType.Thread == content.type:
458
+            if CONTENT_TYPES.Thread.slug == content.type:
459 459
                 call_to_action_text = l_('Answer')
460 460
                 content_intro = l_('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name)
461 461
                 content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + \
462 462
                                content.get_last_comment_from(actor).description
463 463
 
464
-            elif ContentType.File == content.type:
464
+            elif CONTENT_TYPES.File.slug == content.type:
465 465
                 content_intro = l_('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
466 466
                 if content.description:
467 467
                     content_text = content.description
468 468
                 else:
469 469
                     content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
470 470
 
471
-            elif ContentType.Page == content.type:
471
+            elif CONTENT_TYPES.Page.slug == content.type:
472 472
                 content_intro = l_('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)
473 473
                 content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
474 474
 
@@ -476,11 +476,11 @@ class EmailManager(object):
476 476
             content_text = content.description
477 477
             call_to_action_text = l_('View online')
478 478
 
479
-            if ContentType.File == content.type:
479
+            if CONTENT_TYPES.File.slug == content.type:
480 480
                 content_intro = l_('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name)
481 481
                 content_text = ''
482 482
 
483
-            elif ContentType.Page == content.type:
483
+            elif CONTENT_TYPES.Page.slug == content.type:
484 484
                 content_intro = l_('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
485 485
                 previous_revision = content.get_previous_revision()
486 486
                 title_diff = ''
@@ -490,7 +490,7 @@ class EmailManager(object):
490 490
                     title_diff + \
491 491
                     htmldiff(previous_revision.description, content.description)
492 492
 
493
-            elif ContentType.Thread == content.type:
493
+            elif CONTENT_TYPES.Thread.slug == content.type:
494 494
                 content_intro = l_('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name)
495 495
                 previous_revision = content.get_previous_revision()
496 496
                 title_diff = ''
@@ -503,7 +503,7 @@ class EmailManager(object):
503 503
         elif ActionDescription.EDITION == action:
504 504
             call_to_action_text = l_('View online')
505 505
 
506
-            if ContentType.File == content.type:
506
+            if CONTENT_TYPES.File.slug == content.type:
507 507
                 content_intro = l_('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name)
508 508
                 content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \
509 509
                     content.description

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

@@ -5,15 +5,15 @@ import functools
5 5
 from pyramid.interfaces import IAuthorizationPolicy
6 6
 from zope.interface import implementer
7 7
 
8
-from tracim_backend.models.contents import NewContentType
9
-from tracim_backend.models.context_models import ContentInContext
8
+from tracim_backend.models.contents import ContentType
9
+from tracim_backend.models.contents import CONTENT_TYPES
10 10
 
11 11
 try:
12 12
     from json.decoder import JSONDecodeError
13 13
 except ImportError:  # python3.4
14 14
     JSONDecodeError = ValueError
15 15
 
16
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
16
+from tracim_backend.models.contents import ContentType
17 17
 from tracim_backend.exceptions import InsufficientUserRoleInWorkspace
18 18
 from tracim_backend.exceptions import ContentTypeNotAllowed
19 19
 from tracim_backend.exceptions import InsufficientUserProfile
@@ -133,18 +133,18 @@ def require_candidate_workspace_role(minimal_required_role: int) -> typing.Calla
133 133
     return decorator
134 134
 
135 135
 
136
-def require_content_types(content_types: typing.List['NewContentType']) -> typing.Callable:  # nopep8
136
+def require_content_types(content_types: typing.List['ContentType']) -> typing.Callable:  # nopep8
137 137
     """
138 138
     Restricts access to specific file type or raise an exception.
139 139
     Check role for candidate_workspace.
140
-    :param content_types: list of NewContentType object
140
+    :param content_types: list of ContentType object
141 141
     :return: decorator
142 142
     """
143 143
     def decorator(func: typing.Callable) -> typing.Callable:
144 144
         @functools.wraps(func)
145 145
         def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
146 146
             content = request.current_content
147
-            current_content_type_slug = ContentType(content.type).slug
147
+            current_content_type_slug = CONTENT_TYPES.get_one_by_slug(content.type).slug
148 148
             content_types_slug = [content_type.slug for content_type in content_types]  # nopep8
149 149
             if current_content_type_slug in content_types_slug:
150 150
                 return func(self, context, request)

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

@@ -15,7 +15,7 @@ from tracim_backend.exceptions import UserNotFoundInTracimRequest
15 15
 from tracim_backend.exceptions import UserDoesNotExist
16 16
 from tracim_backend.exceptions import WorkspaceNotFound
17 17
 from tracim_backend.exceptions import ImmutableAttribute
18
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
18
+from tracim_backend.models.contents import CONTENT_TYPES
19 19
 from tracim_backend.lib.core.content import ContentApi
20 20
 from tracim_backend.lib.core.user import UserApi
21 21
 from tracim_backend.lib.core.workspace import WorkspaceApi
@@ -233,7 +233,7 @@ class TracimRequest(Request):
233 233
             )
234 234
             comment = api.get_one(
235 235
                 comment_id,
236
-                content_type=ContentType.Comment,
236
+                content_type=CONTENT_TYPES.Comment.slug,
237 237
                 workspace=workspace,
238 238
                 parent=content,
239 239
             )
@@ -271,7 +271,7 @@ class TracimRequest(Request):
271 271
                 session=request.dbsession,
272 272
                 config=request.registry.settings['CFG']
273 273
             )
274
-            content = api.get_one(content_id=content_id, workspace=workspace, content_type=ContentType.Any)  # nopep8
274
+            content = api.get_one(content_id=content_id, workspace=workspace, content_type=CONTENT_TYPES.Any_SLUG)  # nopep8
275 275
         except NoResultFound as exc:
276 276
             raise ContentNotFound(
277 277
                 'Content {} does not exist '

+ 7 - 5
backend/tracim_backend/lib/webdav/dav_provider.py View File

@@ -8,6 +8,7 @@ from sqlalchemy.orm.exc import NoResultFound
8 8
 from tracim_backend import CFG
9 9
 from tracim_backend.lib.webdav.utils import transform_to_bdd, HistoryType, \
10 10
     SpecialFolderExtension
11
+from tracim_backend.models.contents import CONTENT_TYPES
11 12
 
12 13
 from wsgidav.dav_provider import DAVProvider
13 14
 from wsgidav.lock_manager import LockManager
@@ -19,7 +20,8 @@ from tracim_backend.lib.core.content import ContentRevisionRO
19 20
 from tracim_backend.lib.core.workspace import WorkspaceApi
20 21
 from tracim_backend.lib.webdav import resources
21 22
 from tracim_backend.lib.webdav.utils import normpath
22
-from tracim_backend.models.data import ContentType, Content, Workspace
23
+from tracim_backend.models.data import Content
24
+from tracim_backend.models.data import Workspace
23 25
 
24 26
 
25 27
 class Provider(DAVProvider):
@@ -174,7 +176,7 @@ class Provider(DAVProvider):
174 176
             content_revision = content_api.get_one_revision(revision_id)
175 177
             content = self.get_content_from_revision(content_revision, content_api)
176 178
 
177
-            if content.type == ContentType.File:
179
+            if content.type == CONTENT_TYPES.File.slug:
178 180
                 return resources.HistoryFileResource(
179 181
                     path=path,
180 182
                     environ=environ,
@@ -198,7 +200,7 @@ class Provider(DAVProvider):
198 200
 
199 201
         if content is None:
200 202
             return None
201
-        if content.type == ContentType.Folder:
203
+        if content.type == CONTENT_TYPES.Folder.slug:
202 204
             return resources.FolderResource(
203 205
                 path=path,
204 206
                 environ=environ,
@@ -207,7 +209,7 @@ class Provider(DAVProvider):
207 209
                 session=session,
208 210
                 user=user,
209 211
             )
210
-        elif content.type == ContentType.File:
212
+        elif content.type == CONTENT_TYPES.File.slug:
211 213
             return resources.FileResource(
212 214
                 path=path,
213 215
                 environ=environ,
@@ -356,7 +358,7 @@ class Provider(DAVProvider):
356 358
 
357 359
     def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
358 360
         try:
359
-            return api.get_one(revision.content_id, ContentType.Any)
361
+            return api.get_one(revision.content_id, CONTENT_TYPES.Any_SLUG)
360 362
         except NoResultFound:
361 363
             return None
362 364
 

+ 3 - 2
backend/tracim_backend/lib/webdav/design.py View File

@@ -1,8 +1,8 @@
1 1
 #coding: utf8
2 2
 from datetime import datetime
3 3
 
4
+from tracim_backend.models.contents import CONTENT_TYPES
4 5
 from tracim_backend.models.data import VirtualEvent
5
-from tracim_backend.models.data import ContentType
6 6
 from tracim_backend.models import data
7 7
 
8 8
 # FIXME: fix temporaire ...
@@ -237,6 +237,7 @@ def designPage(content: data.Content, content_revision: data.ContentRevisionRO)
237 237
 
238 238
     return page
239 239
 
240
+
240 241
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
241 242
         hist = content.get_history(drop_empty_revision=False)
242 243
 
@@ -248,7 +249,7 @@ def designThread(content: data.Content, content_revision: data.ContentRevisionRO
248 249
         disc = ''
249 250
         participants = {}
250 251
         for t in allT:
251
-            if t.type == ContentType.Comment:
252
+            if t.type == CONTENT_TYPES.Comment.slug:
252 253
                 disc += '''
253 254
                     <div class="row comment comment-row">
254 255
                         <i class="fa fa-comment-o comment-icon"></i>

+ 37 - 34
backend/tracim_backend/lib/webdav/resources.py View File

@@ -19,10 +19,11 @@ from tracim_backend.lib.webdav.utils import transform_to_display, HistoryType, \
19 19
     FakeFileStream
20 20
 from tracim_backend.lib.webdav.utils import transform_to_bdd
21 21
 from tracim_backend.lib.core.workspace import WorkspaceApi
22
+from tracim_backend.models.contents import CONTENT_TYPES
22 23
 from tracim_backend.models.data import User, ContentRevisionRO
23 24
 from tracim_backend.models.data import Workspace
24
-from tracim_backend.models.data import Content, ActionDescription
25
-from tracim_backend.models.data import ContentType
25
+from tracim_backend.models.data import Content
26
+from tracim_backend.models.data import ActionDescription
26 27
 from tracim_backend.lib.webdav.design import designThread, designPage
27 28
 
28 29
 from wsgidav import compat
@@ -236,7 +237,7 @@ class WorkspaceResource(DAVCollection):
236 237
 
237 238
         for content in children:
238 239
             # the purpose is to display .history only if there's at least one content's type that has a history
239
-            if content.type != ContentType.Folder:
240
+            if content.type != CONTENT_TYPES.Folder.slug:
240 241
                 self._file_count += 1
241 242
             retlist.append(content.get_label_as_file())
242 243
 
@@ -288,7 +289,7 @@ class WorkspaceResource(DAVCollection):
288 289
             raise DAVError(HTTP_FORBIDDEN)
289 290
 
290 291
         folder = self.content_api.create(
291
-            content_type=ContentType.Folder,
292
+            content_type_slug=CONTENT_TYPES.Folder.slug,
292 293
             workspace=self.workspace,
293 294
             label=label,
294 295
             parent=self.content
@@ -331,12 +332,12 @@ class WorkspaceResource(DAVCollection):
331 332
     def getMemberList(self) -> [_DAVResource]:
332 333
         members = []
333 334
 
334
-        children = self.content_api.get_all(False, ContentType.Any, self.workspace)
335
+        children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
335 336
 
336 337
         for content in children:
337 338
             content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
338 339
 
339
-            if content.type == ContentType.Folder:
340
+            if content.type == CONTENT_TYPES.Folder.slug:
340 341
                 members.append(
341 342
                     FolderResource(
342 343
                         path=content_path,
@@ -347,7 +348,7 @@ class WorkspaceResource(DAVCollection):
347 348
                         session=self.session,
348 349
                     )
349 350
                 )
350
-            elif content.type == ContentType.File:
351
+            elif content.type == CONTENT_TYPES.File.slug:
351 352
                 self._file_count += 1
352 353
                 members.append(
353 354
                     FileResource(
@@ -553,7 +554,7 @@ class FolderResource(WorkspaceResource):
553 554
         )
554 555
         visible_children = content_api.get_all(
555 556
             self.content.content_id,
556
-            ContentType.Any,
557
+            CONTENT_TYPES.Any_SLUG,
557 558
             self.workspace,
558 559
         )
559 560
 
@@ -561,7 +562,7 @@ class FolderResource(WorkspaceResource):
561 562
             content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
562 563
 
563 564
             try:
564
-                if content.type == ContentType.Folder:
565
+                if content.type == CONTENT_TYPES.Folder.slug:
565 566
                     members.append(
566 567
                         FolderResource(
567 568
                             path=content_path,
@@ -572,7 +573,7 @@ class FolderResource(WorkspaceResource):
572 573
                             session=self.session,
573 574
                         )
574 575
                     )
575
-                elif content.type == ContentType.File:
576
+                elif content.type == CONTENT_TYPES.File.slug:
576 577
                     self._file_count += 1
577 578
                     members.append(
578 579
                         FileResource(
@@ -592,13 +593,15 @@ class FolderResource(WorkspaceResource):
592 593
                             user=self.user,
593 594
                             session=self.session,
594 595
                         ))
595
-            except Exception as exc:
596
-                logger.exception(
597
-                    'Unable to construct member {}'.format(
598
-                        content_path,
599
-                    ),
600
-                    exc_info=True,
601
-                )
596
+            except NotImplementedError as exc:
597
+                pass
598
+            # except Exception as exc:
599
+            #     logger.exception(
600
+            #         'Unable to construct member {}'.format(
601
+            #             content_path,
602
+            #         ),
603
+            #         exc_info=True,
604
+            #     )
602 605
 
603 606
         if self._file_count > 0 and self.provider.show_history():
604 607
             members.append(
@@ -708,11 +711,11 @@ class HistoryFolderResource(FolderResource):
708 711
         ret = []
709 712
 
710 713
         content_id = None if self.content is None else self.content.id
711
-        for content in self.content_api.get_all(content_id, ContentType.Any, self.workspace):
714
+        for content in self.content_api.get_all(content_id, CONTENT_TYPES.Any_SLUG, self.workspace):
712 715
             if (self._is_archived and content.is_archived or
713 716
                 self._is_deleted and content.is_deleted or
714 717
                 not (content.is_archived or self._is_archived or content.is_deleted or self._is_deleted))\
715
-                    and content.type != ContentType.Folder:
718
+                    and content.type != CONTENT_TYPES.Folder.slug:
716 719
                 ret.append(content.get_label_as_file())
717 720
 
718 721
         return ret
@@ -741,7 +744,7 @@ class HistoryFolderResource(FolderResource):
741 744
         if self.content:
742 745
             children = self.content.children
743 746
         else:
744
-            children = self.content_api.get_all(False, ContentType.Any, self.workspace)
747
+            children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
745 748
         
746 749
         for content in children:
747 750
             if content.is_archived == self._is_archived and content.is_deleted == self._is_deleted:
@@ -812,13 +815,13 @@ class DeletedFolderResource(HistoryFolderResource):
812 815
         if self.content:
813 816
             children = self.content.children
814 817
         else:
815
-            children = self.content_api.get_all(False, ContentType.Any, self.workspace)
818
+            children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
816 819
 
817 820
         for content in children:
818 821
             if content.is_deleted:
819 822
                 retlist.append(content.get_label_as_file())
820 823
 
821
-                if content.type != ContentType.Folder:
824
+                if content.type != CONTENT_TYPES.Folder.slug:
822 825
                     self._file_count += 1
823 826
 
824 827
         return retlist
@@ -829,13 +832,13 @@ class DeletedFolderResource(HistoryFolderResource):
829 832
         if self.content:
830 833
             children = self.content.children
831 834
         else:
832
-            children = self.content_api.get_all(False, ContentType.Any, self.workspace)
835
+            children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
833 836
 
834 837
         for content in children:
835 838
             if content.is_deleted:
836 839
                 content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
837 840
 
838
-                if content.type == ContentType.Folder:
841
+                if content.type == CONTENT_TYPES.Folder.slug:
839 842
                     members.append(
840 843
                         FolderResource(
841 844
                             content_path,
@@ -845,7 +848,7 @@ class DeletedFolderResource(HistoryFolderResource):
845 848
                             user=self.user,
846 849
                             session=self.session,
847 850
                         ))
848
-                elif content.type == ContentType.File:
851
+                elif content.type == CONTENT_TYPES.File.slug:
849 852
                     self._file_count += 1
850 853
                     members.append(
851 854
                         FileResource(
@@ -936,10 +939,10 @@ class ArchivedFolderResource(HistoryFolderResource):
936 939
         retlist = []
937 940
 
938 941
         for content in self.content_api.get_all_with_filter(
939
-                self.content if self.content is None else self.content.id, ContentType.Any):
942
+                self.content if self.content is None else self.content.id, CONTENT_TYPES.Any_SLUG):
940 943
             retlist.append(content.get_label_as_file())
941 944
 
942
-            if content.type != ContentType.Folder:
945
+            if content.type != CONTENT_TYPES.Folder.slug:
943 946
                 self._file_count += 1
944 947
 
945 948
         return retlist
@@ -950,13 +953,13 @@ class ArchivedFolderResource(HistoryFolderResource):
950 953
         if self.content:
951 954
             children = self.content.children
952 955
         else:
953
-            children = self.content_api.get_all(False, ContentType.Any, self.workspace)
956
+            children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
954 957
 
955 958
         for content in children:
956 959
             if content.is_archived:
957 960
                 content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
958 961
 
959
-                if content.type == ContentType.Folder:
962
+                if content.type == CONTENT_TYPES.Folder.slug:
960 963
                     members.append(
961 964
                         FolderResource(
962 965
                             content_path,
@@ -966,7 +969,7 @@ class ArchivedFolderResource(HistoryFolderResource):
966 969
                             user=self.user,
967 970
                             session=self.session,
968 971
                         ))
969
-                elif content.type == ContentType.File:
972
+                elif content.type == CONTENT_TYPES.File.slug:
970 973
                     self._file_count += 1
971 974
                     members.append(
972 975
                         FileResource(
@@ -1053,7 +1056,7 @@ class HistoryFileFolderResource(HistoryFolderResource):
1053 1056
 
1054 1057
         left_side = '%s/(%d - %s) ' % (self.path, revision.revision_id, revision.revision_type)
1055 1058
 
1056
-        if self.content.type == ContentType.File:
1059
+        if self.content.type == CONTENT_TYPES.File.slug:
1057 1060
             return HistoryFileResource(
1058 1061
                 path='%s%s' % (left_side, transform_to_display(revision.file_name)),
1059 1062
                 environ=self.environ,
@@ -1079,7 +1082,7 @@ class HistoryFileFolderResource(HistoryFolderResource):
1079 1082
 
1080 1083
             left_side = '%s/(%d - %s) ' % (self.path, content.revision_id, content.revision_type)
1081 1084
 
1082
-            if self.content.type == ContentType.File:
1085
+            if self.content.type == CONTENT_TYPES.File.slug:
1083 1086
                 members.append(HistoryFileResource(
1084 1087
                     path='%s%s' % (left_side, transform_to_display(content.file_name)),
1085 1088
                     environ=self.environ,
@@ -1425,13 +1428,13 @@ class OtherFileResource(FileResource):
1425 1428
         return filestream
1426 1429
 
1427 1430
     def design(self):
1428
-        if self.content.type == ContentType.Page:
1431
+        if self.content.type == CONTENT_TYPES.Page.slug:
1429 1432
             return designPage(self.content, self.content_revision)
1430 1433
         else:
1431 1434
             return designThread(
1432 1435
                 self.content,
1433 1436
                 self.content_revision,
1434
-                self.content_api.get_all(self.content.content_id, ContentType.Comment)
1437
+                self.content_api.get_all(self.content.content_id, CONTENT_TYPES.Comment.slug)
1435 1438
             )
1436 1439
 
1437 1440
 

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

@@ -4,12 +4,14 @@ import transaction
4 4
 from os.path import normpath as base_normpath
5 5
 
6 6
 from sqlalchemy.orm import Session
7
+from tracim_backend.models.contents import CONTENT_TYPES
7 8
 from wsgidav import util
8 9
 from wsgidav import compat
9 10
 
10 11
 from tracim_backend.lib.core.content import ContentApi
11
-from tracim_backend.models.data import Workspace, Content, ContentType, \
12
-    ActionDescription
12
+from tracim_backend.models.data import Workspace
13
+from tracim_backend.models.data import Content
14
+from tracim_backend.models.data import ActionDescription
13 15
 from tracim_backend.models.revision_protection import new_revision
14 16
 
15 17
 
@@ -177,7 +179,7 @@ class FakeFileStream(object):
177 179
 
178 180
         file = self._api.create(
179 181
             filename=self._file_name,
180
-            content_type=ContentType.File,
182
+            content_type_slug=CONTENT_TYPES.File.slug,
181 183
             workspace=self._workspace,
182 184
             parent=self._parent,
183 185
             is_temporary=is_temporary

+ 4 - 4
backend/tracim_backend/models/applications.py View File

@@ -50,7 +50,7 @@ calendar = Application(
50 50
 
51 51
 thread = Application(
52 52
     label='Threads',
53
-    slug='contents/threads',
53
+    slug='contents/thread',
54 54
     fa_icon='comments-o',
55 55
     hexcolor='#ad4cf9',
56 56
     is_active=True,
@@ -61,7 +61,7 @@ thread = Application(
61 61
 
62 62
 _file = Application(
63 63
     label='Files',
64
-    slug='contents/files',
64
+    slug='contents/file',
65 65
     fa_icon='paperclip',
66 66
     hexcolor='#FF9900',
67 67
     is_active=True,
@@ -81,12 +81,12 @@ markdownpluspage = Application(
81 81
 
82 82
 html_documents = Application(
83 83
     label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
84
-    slug='contents/html-documents',
84
+    slug='contents/html-document',
85 85
     fa_icon='file-text-o',
86 86
     hexcolor='#3f52e3',
87 87
     is_active=True,
88 88
     config={},
89
-    main_route='/#/workspaces/{workspace_id}/contents?type=html-documents',
89
+    main_route='/#/workspaces/{workspace_id}/contents?type=html-document',
90 90
 )
91 91
 # TODO - G.M - 08-06-2018 - This is hardcoded lists of app, make this dynamic.
92 92
 # List of applications

+ 109 - 144
backend/tracim_backend/models/contents.py View File

@@ -19,9 +19,9 @@ class GlobalStatus(Enum):
19 19
     CLOSED = 'closed'
20 20
 
21 21
 
22
-class NewContentStatus(object):
22
+class ContentStatus(object):
23 23
     """
24
-    Future ContentStatus object class
24
+    ContentStatus object class
25 25
     """
26 26
     def __init__(
27 27
             self,
@@ -38,7 +38,7 @@ class NewContentStatus(object):
38 38
         self.hexcolor = hexcolor
39 39
 
40 40
 
41
-open_status = NewContentStatus(
41
+open_status = ContentStatus(
42 42
     slug='open',
43 43
     global_status=GlobalStatus.OPEN.value,
44 44
     label='Open',
@@ -46,7 +46,7 @@ open_status = NewContentStatus(
46 46
     hexcolor='#3f52e3',
47 47
 )
48 48
 
49
-closed_validated_status = NewContentStatus(
49
+closed_validated_status = ContentStatus(
50 50
     slug='closed-validated',
51 51
     global_status=GlobalStatus.CLOSED.value,
52 52
     label='Validated',
@@ -54,7 +54,7 @@ closed_validated_status = NewContentStatus(
54 54
     hexcolor='#008000',
55 55
 )
56 56
 
57
-closed_unvalidated_status = NewContentStatus(
57
+closed_unvalidated_status = ContentStatus(
58 58
     slug='closed-unvalidated',
59 59
     global_status=GlobalStatus.CLOSED.value,
60 60
     label='Cancelled',
@@ -62,7 +62,7 @@ closed_unvalidated_status = NewContentStatus(
62 62
     hexcolor='#f63434',
63 63
 )
64 64
 
65
-closed_deprecated_status = NewContentStatus(
65
+closed_deprecated_status = ContentStatus(
66 66
     slug='closed-deprecated',
67 67
     global_status=GlobalStatus.CLOSED.value,
68 68
     label='Deprecated',
@@ -71,50 +71,44 @@ closed_deprecated_status = NewContentStatus(
71 71
 )
72 72
 
73 73
 
74
-CONTENT_DEFAULT_STATUS = [
75
-    open_status,
76
-    closed_validated_status,
77
-    closed_unvalidated_status,
78
-    closed_deprecated_status,
79
-]
74
+class ContentStatusList(object):
80 75
 
76
+    OPEN = open_status
81 77
 
82
-class ContentStatusLegacy(NewContentStatus):
83
-    """
84
-    Temporary remplacement object for Legacy ContentStatus Object
85
-    """
86
-    OPEN = open_status.slug
87
-    CLOSED_VALIDATED = closed_validated_status.slug
88
-    CLOSED_UNVALIDATED = closed_unvalidated_status.slug
89
-    CLOSED_DEPRECATED = closed_deprecated_status.slug
90
-
91
-    def __init__(self, slug: str):
92
-        for status in CONTENT_DEFAULT_STATUS:
93
-            if slug == status.slug:
94
-                super(ContentStatusLegacy, self).__init__(
95
-                    slug=status.slug,
96
-                    global_status=status.global_status,
97
-                    label=status.label,
98
-                    fa_icon=status.fa_icon,
99
-                    hexcolor=status.hexcolor,
100
-                )
101
-                return
78
+    def __init__(self, extend_content_status: typing.List[ContentStatus]):
79
+        self._content_status = [self.OPEN]
80
+        self._content_status.extend(extend_content_status)
81
+
82
+    def get_one_by_slug(self, slug: str) -> ContentStatus:
83
+        for item in self._content_status:
84
+            if item.slug == slug:
85
+                return item
102 86
         raise ContentStatusNotExist()
103 87
 
104
-    @classmethod
105
-    def all(cls, type='') -> ['NewContentStatus']:
106
-        return CONTENT_DEFAULT_STATUS
88
+    def get_all_slugs_values(self) -> typing.List[str]:
89
+        """ Get alls slugs"""
90
+        return [item.slug for item in self._content_status]
91
+
92
+    def get_all(self) -> typing.List[ContentStatus]:
93
+        """ Get all status"""
94
+        return [item for item in self._content_status]
107 95
 
108
-    @classmethod
109
-    def allowed_values(cls):
110
-        return [status.slug for status in CONTENT_DEFAULT_STATUS]
96
+    def get_default_status(self) -> ContentStatus:
97
+        return self.OPEN
111 98
 
112 99
 
100
+CONTENT_STATUS = ContentStatusList(
101
+    [
102
+        closed_validated_status,
103
+        closed_unvalidated_status,
104
+        closed_deprecated_status,
105
+    ]
106
+)
113 107
 ####
114 108
 # ContentType
115 109
 
116 110
 
117
-class NewContentType(object):
111
+class ContentType(object):
118 112
     """
119 113
     Future ContentType object class
120 114
     """
@@ -125,8 +119,8 @@ class NewContentType(object):
125 119
             hexcolor: str,
126 120
             label: str,
127 121
             creation_label: str,
128
-            available_statuses: typing.List[NewContentStatus],
129
-
122
+            available_statuses: typing.List[ContentStatus],
123
+            slug_alias: typing.List[str] = None,
130 124
     ):
131 125
         self.slug = slug
132 126
         self.fa_icon = fa_icon
@@ -134,169 +128,140 @@ class NewContentType(object):
134 128
         self.label = label
135 129
         self.creation_label = creation_label
136 130
         self.available_statuses = available_statuses
131
+        self.slug_alias = slug_alias
137 132
 
138 133
 
139
-thread_type = NewContentType(
134
+thread_type = ContentType(
140 135
     slug='thread',
141 136
     fa_icon=thread.fa_icon,
142 137
     hexcolor=thread.hexcolor,
143 138
     label='Thread',
144 139
     creation_label='Discuss about a topic',
145
-    available_statuses=CONTENT_DEFAULT_STATUS,
140
+    available_statuses=CONTENT_STATUS.get_all(),
146 141
 )
147 142
 
148
-file_type = NewContentType(
143
+file_type = ContentType(
149 144
     slug='file',
150 145
     fa_icon=_file.fa_icon,
151 146
     hexcolor=_file.hexcolor,
152 147
     label='File',
153 148
     creation_label='Upload a file',
154
-    available_statuses=CONTENT_DEFAULT_STATUS,
149
+    available_statuses=CONTENT_STATUS.get_all(),
155 150
 )
156 151
 
157
-markdownpluspage_type = NewContentType(
152
+markdownpluspage_type = ContentType(
158 153
     slug='markdownpage',
159 154
     fa_icon=markdownpluspage.fa_icon,
160 155
     hexcolor=markdownpluspage.hexcolor,
161 156
     label='Rich Markdown File',
162 157
     creation_label='Create a Markdown document',
163
-    available_statuses=CONTENT_DEFAULT_STATUS,
158
+    available_statuses=CONTENT_STATUS.get_all(),
164 159
 )
165 160
 
166
-html_documents_type = NewContentType(
167
-    slug='html-documents',
161
+html_documents_type = ContentType(
162
+    slug='html-document',
168 163
     fa_icon=html_documents.fa_icon,
169 164
     hexcolor=html_documents.hexcolor,
170 165
     label='Text Document',
171 166
     creation_label='Write a document',
172
-    available_statuses=CONTENT_DEFAULT_STATUS,
167
+    available_statuses=CONTENT_STATUS.get_all(),
168
+    slug_alias=['page']
173 169
 )
174 170
 
175 171
 # TODO - G.M - 31-05-2018 - Set Better folder params
176
-folder_type = NewContentType(
172
+folder_type = ContentType(
177 173
     slug='folder',
178 174
     fa_icon=thread.fa_icon,
179 175
     hexcolor=thread.hexcolor,
180 176
     label='Folder',
181 177
     creation_label='Create collection of any documents',
182
-    available_statuses=CONTENT_DEFAULT_STATUS,
178
+    available_statuses=CONTENT_STATUS.get_all(),
183 179
 )
184 180
 
185
-CONTENT_DEFAULT_TYPE = [
186
-    thread_type,
187
-    file_type,
188
-    markdownpluspage_type,
189
-    html_documents_type,
190
-    folder_type,
191
-]
192 181
 
193 182
 # TODO - G.M - 31-05-2018 - Set Better Event params
194
-event_type = NewContentType(
183
+event_type = ContentType(
195 184
     slug='event',
196 185
     fa_icon=thread.fa_icon,
197 186
     hexcolor=thread.hexcolor,
198 187
     label='Event',
199 188
     creation_label='Event',
200
-    available_statuses=CONTENT_DEFAULT_STATUS,
189
+    available_statuses=CONTENT_STATUS.get_all(),
201 190
 )
202 191
 
203 192
 # TODO - G.M - 31-05-2018 - Set Better Event params
204
-comment_type = NewContentType(
193
+comment_type = ContentType(
205 194
     slug='comment',
206 195
     fa_icon=thread.fa_icon,
207 196
     hexcolor=thread.hexcolor,
208 197
     label='Comment',
209 198
     creation_label='Comment',
210
-    available_statuses=CONTENT_DEFAULT_STATUS,
199
+    available_statuses=CONTENT_STATUS.get_all(),
211 200
 )
212 201
 
213
-CONTENT_DEFAULT_TYPE_SPECIAL = [
214
-    event_type,
215
-    comment_type,
216
-]
217
-
218
-ALL_CONTENTS_DEFAULT_TYPES = CONTENT_DEFAULT_TYPE + CONTENT_DEFAULT_TYPE_SPECIAL
219 202
 
220
-
221
-class ContentTypeLegacy(NewContentType):
203
+class ContentTypeList(object):
222 204
     """
223
-    Temporary remplacement object for Legacy ContentType Object
205
+    ContentType List
224 206
     """
225
-
226
-    # special type
227
-    Any = 'any'
228
-    Folder = 'folder'
229
-    Event = 'event'
230
-    Comment = 'comment'
231
-
232
-    File = file_type.slug
233
-    Thread = thread_type.slug
234
-    Page = html_documents_type.slug
235
-    PageLegacy = 'page'
236
-    MarkdownPage = markdownpluspage_type.slug
237
-
238
-    def __init__(self, slug: str):
239
-        if slug == 'page':
240
-            slug = ContentTypeLegacy.Page
241
-        for content_type in ALL_CONTENTS_DEFAULT_TYPES:
242
-            if slug == content_type.slug:
243
-                super(ContentTypeLegacy, self).__init__(
244
-                    slug=content_type.slug,
245
-                    fa_icon=content_type.fa_icon,
246
-                    hexcolor=content_type.hexcolor,
247
-                    label=content_type.label,
248
-                    creation_label=content_type.creation_label,
249
-                    available_statuses=content_type.available_statuses
250
-                )
251
-                return
207
+    Any_SLUG = 'any'
208
+    Folder = folder_type
209
+    Comment = comment_type
210
+    Event = event_type
211
+    File = file_type
212
+    Page = html_documents_type
213
+    Thread = thread_type
214
+
215
+    def __init__(self, extend_content_status: typing.List[ContentType]):
216
+        self._content_types = [self.Folder]
217
+        self._content_types.extend(extend_content_status)
218
+        self._special_contents_types = [self.Comment]
219
+        self._extra_slugs = [self.Any_SLUG]
220
+
221
+    def get_one_by_slug(self, slug: str) -> ContentType:
222
+        """
223
+        Get ContentType object according to slug
224
+        match for both slug and slug_alias
225
+        """
226
+        content_types = self._content_types.copy()
227
+        content_types.extend(self._special_contents_types)
228
+        for item in content_types:
229
+            if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
230
+                return item
252 231
         raise ContentTypeNotExist()
253 232
 
254
-    def get_slug_aliases(self) -> typing.List[str]:
233
+    def endpoint_allowed_types_slug(self) -> typing.List[str]:
255 234
         """
256
-        Get all slug aliases of a content,
257
-        useful for legacy code convertion
235
+        Return restricted list of content_type:
236
+        dont return special content_type like  comment, don't return
237
+        "any" slug, dont return content type slug alias , don't return event.
238
+        Useful to restrict slug param in schema.
258 239
         """
259
-        # TODO - G.M - 2018-07-05 - Remove this legacy compat code
260
-        # when possible.
261
-        page_alias = [self.Page, self.PageLegacy]
262
-        if self.slug in page_alias:
263
-            return page_alias
264
-        else:
265
-            return [self.slug]
266
-
267
-    @classmethod
268
-    def all(cls) -> typing.List[str]:
269
-        return cls.allowed_types()
270
-
271
-    @classmethod
272
-    def allowed_types(cls) -> typing.List[str]:
273
-        contents_types = [status.slug for status in ALL_CONTENTS_DEFAULT_TYPES]
274
-        return contents_types
275
-
276
-    @classmethod
277
-    def allowed_type_values(cls) -> typing.List[str]:
240
+        allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
241
+        return allowed_type_slug
242
+
243
+    def query_allowed_types_slugs(self) -> typing.List[str]:
278 244
         """
279
-        All content type slug + special values like any
245
+        Return alls allowed types slug : content_type slug + all alias, any
246
+        and special content_type like comment. Do not return event.
247
+        Usefull allowed value to perform query to database.
280 248
         """
281
-        content_types = cls.allowed_types()
282
-        content_types.append(ContentTypeLegacy.Any)
283
-        return content_types
284
-
285
-    @classmethod
286
-    def allowed_types_for_folding(cls):
287
-        # This method is used for showing only "main"
288
-        # types in the left-side treeview
289
-        contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
290
-        return contents_types
291
-
292
-    # TODO - G.M - 30-05-2018 - This method don't do anything.
293
-    @classmethod
294
-    def sorted(cls, types: ['ContentType']) -> ['ContentType']:
295
-        return types
296
-
297
-    @property
298
-    def id(self):
299
-        return self.slug
300
-
301
-    def toDict(self):
302
-        raise NotImplementedError()
249
+        allowed_types_slug = []
250
+        for content_type in self._content_types:
251
+            allowed_types_slug.append(content_type.slug)
252
+            if content_type.slug_alias:
253
+                allowed_types_slug.extend(content_type.slug_alias)
254
+        for content_type in self._special_contents_types:
255
+            allowed_types_slug.append(content_type.slug)
256
+        allowed_types_slug.extend(self._extra_slugs)
257
+        return allowed_types_slug
258
+
259
+
260
+CONTENT_TYPES = ContentTypeList(
261
+    [
262
+        thread_type,
263
+        file_type,
264
+        markdownpluspage_type,
265
+        html_documents_type,
266
+    ]
267
+)

+ 6 - 10
backend/tracim_backend/models/context_models.py View File

@@ -16,7 +16,7 @@ from tracim_backend.models.data import UserRoleInWorkspace
16 16
 from tracim_backend.models.roles import WorkspaceRoles
17 17
 from tracim_backend.models.workspace_menu_entries import default_workspace_menu_entry
18 18
 from tracim_backend.models.workspace_menu_entries import WorkspaceMenuEntry
19
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
19
+from tracim_backend.models.contents import CONTENT_TYPES
20 20
 
21 21
 
22 22
 class PreviewAllowedDim(object):
@@ -234,10 +234,10 @@ class ActiveContentFilter(object):
234 234
     def __init__(
235 235
             self,
236 236
             limit: int = None,
237
-            before_datetime: datetime = None,
237
+            before_content_id: datetime = None,
238 238
     ):
239 239
         self.limit = limit
240
-        self.before_datetime = before_datetime
240
+        self.before_content_id = before_content_id
241 241
 
242 242
 
243 243
 class ContentIdsQuery(object):
@@ -299,7 +299,7 @@ class ContentCreation(object):
299 299
     ) -> None:
300 300
         self.label = label
301 301
         self.content_type = content_type
302
-        self.parent_id = parent_id
302
+        self.parent_id = parent_id or None
303 303
 
304 304
 
305 305
 class CommentCreation(object):
@@ -586,7 +586,7 @@ class ContentInContext(object):
586 586
 
587 587
     @property
588 588
     def content_type(self) -> str:
589
-        content_type = ContentType(self.content.type)
589
+        content_type = CONTENT_TYPES.get_one_by_slug(self.content.type)
590 590
         return content_type.slug
591 591
 
592 592
     @property
@@ -698,11 +698,7 @@ class RevisionInContext(object):
698 698
 
699 699
     @property
700 700
     def content_type(self) -> str:
701
-        content_type = ContentType(self.revision.type)
702
-        if content_type:
703
-            return content_type.slug
704
-        else:
705
-            return None
701
+        return CONTENT_TYPES.get_one_by_slug(self.revision.type).slug
706 702
 
707 703
     @property
708 704
     def sub_content_types(self) -> typing.List[str]:

+ 26 - 28
backend/tracim_backend/models/data.py View File

@@ -96,7 +96,7 @@ class Workspace(DeclarativeBase):
96 96
 
97 97
     def get_allowed_content_types(self):
98 98
         # @see Content.get_allowed_content_types()
99
-        return [ContentType('folder')]
99
+        return CONTENT_TYPES.endpoint_allowed_types_slug()
100 100
 
101 101
     def get_valid_children(
102 102
             self,
@@ -291,8 +291,9 @@ class ActionDescription(object):
291 291
                 ]
292 292
 
293 293
 
294
-from tracim_backend.models.contents import ContentStatusLegacy as ContentStatus
295
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
294
+from tracim_backend.models.contents import CONTENT_STATUS
295
+from tracim_backend.models.contents import ContentStatus
296
+from tracim_backend.models.contents import CONTENT_TYPES
296 297
 # TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
297 298
 # is lost .
298 299
 
@@ -351,9 +352,9 @@ from tracim_backend.models.contents import ContentTypeLegacy as ContentType
351 352
 #         # self.icon = ContentStatus._ICONS[id]
352 353
 #         # self.css = ContentStatus._CSS[id]
353 354
 #         #
354
-#         # if type==ContentType.Thread:
355
+#         # if type==CONTENT_TYPES.Thread.slug:
355 356
 #         #     self.label = ContentStatus._LABELS_THREAD[id]
356
-#         # elif type==ContentType.File:
357
+#         # elif type==CONTENT_TYPES.File.slug:
357 358
 #         #     self.label = ContentStatus._LABELS_FILE[id]
358 359
 #         # else:
359 360
 #         #     self.label = ContentStatus._LABELS[id]
@@ -499,13 +500,13 @@ from tracim_backend.models.contents import ContentTypeLegacy as ContentType
499 500
 #     #     # TODO - DYNDATATYPE - D.A. - 2014-12-02
500 501
 #     #     # Make this code dynamic loading data types
501 502
 #     #
502
-#     #     if content.type==ContentType.Folder:
503
+#     #     if content.type==CONTENT_TYPES.Folder.slug:
503 504
 #     #         return '/workspaces/{}/folders/{}'.format(content.workspace_id, content.content_id)
504
-#     #     elif content.type==ContentType.File:
505
+#     #     elif content.type==CONTENT_TYPES.File.slug:
505 506
 #     #         return '/workspaces/{}/folders/{}/files/{}'.format(content.workspace_id, content.parent_id, content.content_id)
506
-#     #     elif content.type==ContentType.Thread:
507
+#     #     elif content.type==CONTENT_TYPES.Thread.slug:
507 508
 #     #         return '/workspaces/{}/folders/{}/threads/{}'.format(content.workspace_id, content.parent_id, content.content_id)
508
-#     #     elif content.type==ContentType.Page:
509
+#     #     elif content.type==CONTENT_TYPES.Page.slug:
509 510
 #     #         return '/workspaces/{}/folders/{}/pages/{}'.format(content.workspace_id, content.parent_id, content.content_id)
510 511
 #     #
511 512
 #     # @classmethod
@@ -544,7 +545,7 @@ class ContentChecker(object):
544 545
 
545 546
     @classmethod
546 547
     def check_properties(cls, item):
547
-        if item.type == ContentType.Folder:
548
+        if item.type == CONTENT_TYPES.Folder.slug:
548 549
             properties = item.properties
549 550
             if 'allowed_content' not in properties.keys():
550 551
                 return False
@@ -558,7 +559,7 @@ class ContentChecker(object):
558 559
                 return False
559 560
             return True
560 561
 
561
-        if item.type == ContentType.Event:
562
+        if item.type == CONTENT_TYPES.Event.slug:
562 563
             properties = item.properties
563 564
             if 'name' not in properties.keys():
564 565
                 return False
@@ -572,7 +573,7 @@ class ContentChecker(object):
572 573
 
573 574
         # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
574 575
         # Only content who can be copied need this
575
-        if item.type == ContentType.Any:
576
+        if item.type == CONTENT_TYPES.Any_SLUG:
576 577
             properties = item.properties
577 578
             if 'origin' in properties.keys():
578 579
                 return True
@@ -580,7 +581,7 @@ class ContentChecker(object):
580 581
 
581 582
     @classmethod
582 583
     def reset_properties(cls, item):
583
-        if item.type == ContentType.Folder:
584
+        if item.type == CONTENT_TYPES.Folder.slug:
584 585
             item.properties = DEFAULT_PROPERTIES
585 586
             return
586 587
 
@@ -616,7 +617,7 @@ class ContentRevisionRO(DeclarativeBase):
616 617
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
617 618
 
618 619
     type = Column(Unicode(32), unique=False, nullable=False)
619
-    status = Column(Unicode(32), unique=False, nullable=False, default=ContentStatus.OPEN)
620
+    status = Column(Unicode(32), unique=False, nullable=False, default=str(CONTENT_STATUS.get_default_status().slug))
620 621
     created = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
621 622
     updated = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
622 623
     is_deleted = Column(Boolean, unique=False, nullable=False, default=False)
@@ -754,7 +755,7 @@ class ContentRevisionRO(DeclarativeBase):
754 755
         super().__setattr__(key, value)
755 756
 
756 757
     def get_status(self) -> ContentStatus:
757
-        return ContentStatus(self.status)
758
+        return CONTENT_STATUS.get_one_by_slug(self.status)
758 759
 
759 760
     def get_label(self) -> str:
760 761
         return self.label or self.file_name or ''
@@ -779,9 +780,9 @@ class ContentRevisionRO(DeclarativeBase):
779 780
     def get_label_as_file(self):
780 781
         file_extension = self.file_extension or ''
781 782
 
782
-        if self.type == ContentType.Thread:
783
+        if self.type == CONTENT_TYPES.Thread.slug:
783 784
             file_extension = '.html'
784
-        elif self.type == ContentType.Page:
785
+        elif self.type == CONTENT_TYPES.Page.slug:
785 786
             file_extension = '.html'
786 787
 
787 788
         return '{0}{1}'.format(
@@ -1227,10 +1228,10 @@ class Content(DeclarativeBase):
1227 1228
         return format_timedelta(delta_from_datetime - datetime_object,
1228 1229
                                 locale=get_locale())
1229 1230
 
1230
-    def get_child_nb(self, content_type: ContentType, content_status = ''):
1231
+    def get_child_nb(self, content_type: str, content_status = ''):
1231 1232
         child_nb = 0
1232 1233
         for child in self.get_valid_children():
1233
-            if child.type == content_type or content_type == ContentType.Any:
1234
+            if child.type == content_type or content_type.slug == CONTENT_TYPES.Any_SLUG:
1234 1235
                 if not content_status:
1235 1236
                     child_nb = child_nb+1
1236 1237
                 elif content_status==child.status:
@@ -1247,10 +1248,8 @@ class Content(DeclarativeBase):
1247 1248
         return self.revision.get_label_as_file()
1248 1249
 
1249 1250
     def get_status(self) -> ContentStatus:
1250
-        return ContentStatus(
1251
+        return CONTENT_STATUS.get_one_by_slug(
1251 1252
             self.status,
1252
-            # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
1253
-            # self.type.__str__()
1254 1253
         )
1255 1254
 
1256 1255
     def get_last_action(self) -> ActionDescription:
@@ -1307,7 +1306,7 @@ class Content(DeclarativeBase):
1307 1306
     def get_comments(self):
1308 1307
         children = []
1309 1308
         for child in self.children:
1310
-            if ContentType.Comment==child.type and not child.is_deleted and not child.is_archived:
1309
+            if CONTENT_TYPES.Comment.slug == child.type and not child.is_deleted and not child.is_archived:
1311 1310
                 children.append(child.node)
1312 1311
         return children
1313 1312
 
@@ -1348,13 +1347,13 @@ class Content(DeclarativeBase):
1348 1347
             allowed_types = self.properties['allowed_content']
1349 1348
             for type_label, is_allowed in allowed_types.items():
1350 1349
                 if is_allowed:
1351
-                    types.append(ContentType(type_label))
1350
+                    types.append(CONTENT_TYPES.get_one_by_slug(type_label))
1352 1351
         except Exception as e:
1353 1352
             print(e.__str__())
1354 1353
             print('----- /*\ *****')
1355 1354
             raise ValueError('Not allowed content property')
1356 1355
 
1357
-        return ContentType.sorted(types)
1356
+        return types
1358 1357
 
1359 1358
     def get_history(self, drop_empty_revision=False) -> '[VirtualEvent]':
1360 1359
         events = []
@@ -1447,10 +1446,9 @@ class VirtualEvent(object):
1447 1446
 
1448 1447
     @classmethod
1449 1448
     def create_from_content(cls, content: Content):
1450
-        content_type = ContentType(content.type)
1451 1449
 
1452 1450
         label = content.get_label()
1453
-        if content.type == ContentType.Comment:
1451
+        if content.type == CONTENT_TYPES.Comment.slug:
1454 1452
             # TODO - G.M  - 10-04-2018 - [Cleanup] Remove label param
1455 1453
             # from this object ?
1456 1454
             label = l_('<strong>{}</strong> wrote:').format(content.owner.get_display_name())
@@ -1458,7 +1456,7 @@ class VirtualEvent(object):
1458 1456
         return VirtualEvent(id=content.content_id,
1459 1457
                             created=content.created,
1460 1458
                             owner=content.owner,
1461
-                            type=content_type,
1459
+                            type=ActionDescription(content.revision_type),
1462 1460
                             label=label,
1463 1461
                             content=content.description,
1464 1462
                             ref_object=content)

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

@@ -14,8 +14,8 @@ from tracim_backend.models import get_engine
14 14
 from tracim_backend.models import DeclarativeBase
15 15
 from tracim_backend.models import get_session_factory
16 16
 from tracim_backend.models import get_tm_session
17
+from tracim_backend.models.contents import CONTENT_TYPES
17 18
 from tracim_backend.models.data import Workspace
18
-from tracim_backend.models.data import ContentType
19 19
 from tracim_backend.models.data import ContentRevisionRO
20 20
 from tracim_backend.models.data import Content
21 21
 from tracim_backend.lib.utils.logger import logger
@@ -50,7 +50,7 @@ def set_html_document_slug_to_legacy(session_factory) -> None:
50 50
     )
51 51
     content_query = dbsession.query(ContentRevisionRO).filter(ContentRevisionRO.type == 'page').filter(ContentRevisionRO.content_id == 6)  # nopep8
52 52
     assert content_query.count() == 0
53
-    html_documents_query = dbsession.query(ContentRevisionRO).filter(ContentRevisionRO.type == 'html-documents')  # nopep8
53
+    html_documents_query = dbsession.query(ContentRevisionRO).filter(ContentRevisionRO.type == 'html-document')  # nopep8
54 54
     html_documents_query.update({ContentRevisionRO.type: 'page'})
55 55
     transaction.commit()
56 56
     assert content_query.count() > 0
@@ -268,13 +268,13 @@ class DefaultTest(StandardTest):
268 268
         workspace = self._create_workspace_and_test(workspace_name, user)
269 269
         folder = self._create_content_and_test(
270 270
             folder_name, workspace,
271
-            type=ContentType.Folder,
271
+            type=CONTENT_TYPES.Folder.slug,
272 272
             owner=user
273 273
         )
274 274
         thread = self._create_content_and_test(
275 275
             thread_name,
276 276
             workspace,
277
-            type=ContentType.Thread,
277
+            type=CONTENT_TYPES.Thread.slug,
278 278
             parent=folder,
279 279
             owner=user
280 280
         )

+ 46 - 47
backend/tracim_backend/tests/functional/test_contents.py View File

@@ -5,7 +5,7 @@ from tracim_backend import models
5 5
 from tracim_backend.lib.core.content import ContentApi
6 6
 from tracim_backend.lib.core.workspace import WorkspaceApi
7 7
 from tracim_backend.models import get_tm_session
8
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
8
+from tracim_backend.models.contents import CONTENT_TYPES
9 9
 from tracim_backend.models.revision_protection import new_revision
10 10
 import io
11 11
 
@@ -16,7 +16,6 @@ from depot.io.utils import FileIntent
16 16
 from tracim_backend import models
17 17
 from tracim_backend.lib.core.content import ContentApi
18 18
 from tracim_backend.lib.core.workspace import WorkspaceApi
19
-from tracim_backend.models.data import ContentType
20 19
 from tracim_backend.models import get_tm_session
21 20
 from tracim_backend.models.revision_protection import new_revision
22 21
 from tracim_backend.tests import FunctionalTest
@@ -51,7 +50,7 @@ class TestHtmlDocuments(FunctionalTest):
51 50
             status=200
52 51
         )
53 52
         content = res.json_body
54
-        assert content['content_type'] == 'html-documents'
53
+        assert content['content_type'] == 'html-document'
55 54
         assert content['content_id'] == 6
56 55
         assert content['is_archived'] is False
57 56
         assert content['is_deleted'] is False
@@ -92,7 +91,7 @@ class TestHtmlDocuments(FunctionalTest):
92 91
             status=200
93 92
         )
94 93
         content = res.json_body
95
-        assert content['content_type'] == 'html-documents'
94
+        assert content['content_type'] == 'html-document'
96 95
         assert content['content_id'] == 6
97 96
         assert content['is_archived'] is False
98 97
         assert content['is_deleted'] is False
@@ -255,7 +254,7 @@ class TestHtmlDocuments(FunctionalTest):
255 254
             status=200
256 255
         )
257 256
         content = res.json_body
258
-        assert content['content_type'] == 'html-documents'
257
+        assert content['content_type'] == 'html-document'
259 258
         assert content['content_id'] == 6
260 259
         assert content['is_archived'] is False
261 260
         assert content['is_deleted'] is False
@@ -282,7 +281,7 @@ class TestHtmlDocuments(FunctionalTest):
282 281
             status=200
283 282
         )
284 283
         content = res.json_body
285
-        assert content['content_type'] == 'html-documents'
284
+        assert content['content_type'] == 'html-document'
286 285
         assert content['content_id'] == 6
287 286
         assert content['is_archived'] is False
288 287
         assert content['is_deleted'] is False
@@ -324,7 +323,7 @@ class TestHtmlDocuments(FunctionalTest):
324 323
         revisions = res.json_body
325 324
         assert len(revisions) == 3
326 325
         revision = revisions[0]
327
-        assert revision['content_type'] == 'html-documents'
326
+        assert revision['content_type'] == 'html-document'
328 327
         assert revision['content_id'] == 6
329 328
         assert revision['is_archived'] is False
330 329
         assert revision['is_deleted'] is False
@@ -346,7 +345,7 @@ class TestHtmlDocuments(FunctionalTest):
346 345
         assert revision['author']['avatar_url'] is None
347 346
         assert revision['author']['public_name'] == 'Global manager'
348 347
         revision = revisions[1]
349
-        assert revision['content_type'] == 'html-documents'
348
+        assert revision['content_type'] == 'html-document'
350 349
         assert revision['content_id'] == 6
351 350
         assert revision['is_archived'] is False
352 351
         assert revision['is_deleted'] is False
@@ -368,7 +367,7 @@ class TestHtmlDocuments(FunctionalTest):
368 367
         assert revision['author']['avatar_url'] is None
369 368
         assert revision['author']['public_name'] == 'Global manager'
370 369
         revision = revisions[2]
371
-        assert revision['content_type'] == 'html-documents'
370
+        assert revision['content_type'] == 'html-document'
372 371
         assert revision['content_id'] == 6
373 372
         assert revision['is_archived'] is False
374 373
         assert revision['is_deleted'] is False
@@ -411,7 +410,7 @@ class TestHtmlDocuments(FunctionalTest):
411 410
             status=200
412 411
         )
413 412
         content = res.json_body
414
-        assert content['content_type'] == 'html-documents'
413
+        assert content['content_type'] == 'html-document'
415 414
         assert content['content_id'] == 6
416 415
         assert content['status'] == 'open'
417 416
 
@@ -428,7 +427,7 @@ class TestHtmlDocuments(FunctionalTest):
428 427
             status=200
429 428
         )
430 429
         content = res.json_body
431
-        assert content['content_type'] == 'html-documents'
430
+        assert content['content_type'] == 'html-document'
432 431
         assert content['content_id'] == 6
433 432
         assert content['status'] == 'closed-deprecated'
434 433
 
@@ -480,9 +479,9 @@ class TestFiles(FunctionalTest):
480 479
             config=self.app_config
481 480
         )
482 481
         business_workspace = workspace_api.get_one(1)
483
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
482
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
484 483
         test_file = content_api.create(
485
-            content_type=ContentType.File,
484
+            content_type_slug=CONTENT_TYPES.File.slug,
486 485
             workspace=business_workspace,
487 486
             parent=tool_folder,
488 487
             label='Test file',
@@ -648,9 +647,9 @@ class TestFiles(FunctionalTest):
648 647
             config=self.app_config
649 648
         )
650 649
         business_workspace = workspace_api.get_one(1)
651
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
650
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
652 651
         test_file = content_api.create(
653
-            content_type=ContentType.File,
652
+            content_type_slug=CONTENT_TYPES.File.slug,
654 653
             workspace=business_workspace,
655 654
             parent=tool_folder,
656 655
             label='Test file',
@@ -703,9 +702,9 @@ class TestFiles(FunctionalTest):
703 702
             config=self.app_config
704 703
         )
705 704
         business_workspace = workspace_api.get_one(1)
706
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
705
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
707 706
         test_file = content_api.create(
708
-            content_type=ContentType.File,
707
+            content_type_slug=CONTENT_TYPES.File.slug,
709 708
             workspace=business_workspace,
710 709
             parent=tool_folder,
711 710
             label='Test file',
@@ -809,9 +808,9 @@ class TestFiles(FunctionalTest):
809 808
             config=self.app_config
810 809
         )
811 810
         business_workspace = workspace_api.get_one(1)
812
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
811
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
813 812
         test_file = content_api.create(
814
-            content_type=ContentType.File,
813
+            content_type_slug=CONTENT_TYPES.File.slug,
815 814
             workspace=business_workspace,
816 815
             parent=tool_folder,
817 816
             label='Test file',
@@ -882,9 +881,9 @@ class TestFiles(FunctionalTest):
882 881
             config=self.app_config
883 882
         )
884 883
         business_workspace = workspace_api.get_one(1)
885
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
884
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
886 885
         test_file = content_api.create(
887
-            content_type=ContentType.File,
886
+            content_type_slug=CONTENT_TYPES.File.slug,
888 887
             workspace=business_workspace,
889 888
             parent=tool_folder,
890 889
             label='Test file',
@@ -978,9 +977,9 @@ class TestFiles(FunctionalTest):
978 977
             config=self.app_config
979 978
         )
980 979
         business_workspace = workspace_api.get_one(1)
981
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
980
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
982 981
         test_file = content_api.create(
983
-            content_type=ContentType.File,
982
+            content_type_slug=CONTENT_TYPES.File.slug,
984 983
             workspace=business_workspace,
985 984
             parent=tool_folder,
986 985
             label='Test file',
@@ -1031,9 +1030,9 @@ class TestFiles(FunctionalTest):
1031 1030
             config=self.app_config
1032 1031
         )
1033 1032
         business_workspace = workspace_api.get_one(1)
1034
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1033
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1035 1034
         test_file = content_api.create(
1036
-            content_type=ContentType.File,
1035
+            content_type_slug=CONTENT_TYPES.File.slug,
1037 1036
             workspace=business_workspace,
1038 1037
             parent=tool_folder,
1039 1038
             label='Test file',
@@ -1082,9 +1081,9 @@ class TestFiles(FunctionalTest):
1082 1081
             config=self.app_config
1083 1082
         )
1084 1083
         business_workspace = workspace_api.get_one(1)
1085
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1084
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1086 1085
         test_file = content_api.create(
1087
-            content_type=ContentType.File,
1086
+            content_type_slug=CONTENT_TYPES.File.slug,
1088 1087
             workspace=business_workspace,
1089 1088
             parent=tool_folder,
1090 1089
             label='Test file',
@@ -1137,9 +1136,9 @@ class TestFiles(FunctionalTest):
1137 1136
             config=self.app_config
1138 1137
         )
1139 1138
         business_workspace = workspace_api.get_one(1)
1140
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1139
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1141 1140
         test_file = content_api.create(
1142
-            content_type=ContentType.File,
1141
+            content_type_slug=CONTENT_TYPES.File.slug,
1143 1142
             workspace=business_workspace,
1144 1143
             parent=tool_folder,
1145 1144
             label='Test file',
@@ -1196,9 +1195,9 @@ class TestFiles(FunctionalTest):
1196 1195
             config=self.app_config
1197 1196
         )
1198 1197
         business_workspace = workspace_api.get_one(1)
1199
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1198
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1200 1199
         test_file = content_api.create(
1201
-            content_type=ContentType.File,
1200
+            content_type_slug=CONTENT_TYPES.File.slug,
1202 1201
             workspace=business_workspace,
1203 1202
             parent=tool_folder,
1204 1203
             label='Test file',
@@ -1251,9 +1250,9 @@ class TestFiles(FunctionalTest):
1251 1250
             config=self.app_config
1252 1251
         )
1253 1252
         business_workspace = workspace_api.get_one(1)
1254
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1253
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1255 1254
         test_file = content_api.create(
1256
-            content_type=ContentType.File,
1255
+            content_type_slug=CONTENT_TYPES.File.slug,
1257 1256
             workspace=business_workspace,
1258 1257
             parent=tool_folder,
1259 1258
             label='Test file',
@@ -1302,9 +1301,9 @@ class TestFiles(FunctionalTest):
1302 1301
             config=self.app_config
1303 1302
         )
1304 1303
         business_workspace = workspace_api.get_one(1)
1305
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1304
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1306 1305
         test_file = content_api.create(
1307
-            content_type=ContentType.File,
1306
+            content_type_slug=CONTENT_TYPES.File.slug,
1308 1307
             workspace=business_workspace,
1309 1308
             parent=tool_folder,
1310 1309
             label='Test file',
@@ -1375,9 +1374,9 @@ class TestFiles(FunctionalTest):
1375 1374
             config=self.app_config
1376 1375
         )
1377 1376
         business_workspace = workspace_api.get_one(1)
1378
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1377
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1379 1378
         test_file = content_api.create(
1380
-            content_type=ContentType.File,
1379
+            content_type_slug=CONTENT_TYPES.File.slug,
1381 1380
             workspace=business_workspace,
1382 1381
             parent=tool_folder,
1383 1382
             label='Test file',
@@ -1438,9 +1437,9 @@ class TestFiles(FunctionalTest):
1438 1437
             config=self.app_config
1439 1438
         )
1440 1439
         business_workspace = workspace_api.get_one(1)
1441
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1440
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1442 1441
         test_file = content_api.create(
1443
-            content_type=ContentType.File,
1442
+            content_type_slug=CONTENT_TYPES.File.slug,
1444 1443
             workspace=business_workspace,
1445 1444
             parent=tool_folder,
1446 1445
             label='Test file',
@@ -1489,9 +1488,9 @@ class TestFiles(FunctionalTest):
1489 1488
             config=self.app_config
1490 1489
         )
1491 1490
         business_workspace = workspace_api.get_one(1)
1492
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1491
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1493 1492
         test_file = content_api.create(
1494
-            content_type=ContentType.File,
1493
+            content_type_slug=CONTENT_TYPES.File.slug,
1495 1494
             workspace=business_workspace,
1496 1495
             parent=tool_folder,
1497 1496
             label='Test file',
@@ -1554,9 +1553,9 @@ class TestFiles(FunctionalTest):
1554 1553
             config=self.app_config
1555 1554
         )
1556 1555
         business_workspace = workspace_api.get_one(1)
1557
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1556
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1558 1557
         test_file = content_api.create(
1559
-            content_type=ContentType.File,
1558
+            content_type_slug=CONTENT_TYPES.File.slug,
1560 1559
             workspace=business_workspace,
1561 1560
             parent=tool_folder,
1562 1561
             label='Test file',
@@ -1618,9 +1617,9 @@ class TestFiles(FunctionalTest):
1618 1617
             config=self.app_config
1619 1618
         )
1620 1619
         business_workspace = workspace_api.get_one(1)
1621
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1620
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1622 1621
         test_file = content_api.create(
1623
-            content_type=ContentType.File,
1622
+            content_type_slug=CONTENT_TYPES.File.slug,
1624 1623
             workspace=business_workspace,
1625 1624
             parent=tool_folder,
1626 1625
             label='Test file',
@@ -1989,9 +1988,9 @@ class TestThreads(FunctionalTest):
1989 1988
             session=dbsession,
1990 1989
             config=self.app_config
1991 1990
         )
1992
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1991
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1993 1992
         test_thread = content_api.create(
1994
-            content_type=ContentType.Thread,
1993
+            content_type_slug=CONTENT_TYPES.Thread.slug,
1995 1994
             workspace=business_workspace,
1996 1995
             parent=tool_folder,
1997 1996
             label='Test Thread',

+ 5 - 5
backend/tracim_backend/tests/functional/test_mail_notification.py View File

@@ -11,7 +11,7 @@ from tracim_backend.fixtures.users_and_groups import Base as BaseFixture
11 11
 from tracim_backend.fixtures.content import Content as ContentFixture
12 12
 from tracim_backend.lib.utils.utils import get_redis_connection
13 13
 from tracim_backend.lib.utils.utils import get_rq_queue
14
-from tracim_backend.models.data import ContentType
14
+from tracim_backend.models.contents import CONTENT_TYPES
15 15
 
16 16
 from tracim_backend.lib.core.content import ContentApi
17 17
 from tracim_backend.lib.core.user import UserApi
@@ -139,7 +139,7 @@ class TestNotificationsSync(MailHogTest):
139 139
             config=self.app_config,
140 140
         )
141 141
         item = api.create(
142
-            ContentType.Folder,
142
+            CONTENT_TYPES.Folder.slug,
143 143
             workspace,
144 144
             None,
145 145
             'parent',
@@ -147,7 +147,7 @@ class TestNotificationsSync(MailHogTest):
147 147
             do_notify=False,
148 148
         )
149 149
         item2 = api.create(
150
-            ContentType.File,
150
+            CONTENT_TYPES.File.slug,
151 151
             workspace,
152 152
             item,
153 153
             'file1',
@@ -231,7 +231,7 @@ class TestNotificationsAsync(MailHogTest):
231 231
             config=self.app_config,
232 232
         )
233 233
         item = api.create(
234
-            ContentType.Folder,
234
+            CONTENT_TYPES.Folder.slug,
235 235
             workspace,
236 236
             None,
237 237
             'parent',
@@ -239,7 +239,7 @@ class TestNotificationsAsync(MailHogTest):
239 239
             do_notify=False,
240 240
         )
241 241
         item2 = api.create(
242
-            ContentType.File,
242
+            CONTENT_TYPES.File.slug,
243 243
             workspace,
244 244
             item,
245 245
             'file1',

+ 9 - 8
backend/tracim_backend/tests/functional/test_system.py View File

@@ -5,6 +5,7 @@ from tracim_backend.tests import FunctionalTest
5 5
 Tests for /api/v2/system subpath endpoints.
6 6
 """
7 7
 
8
+
8 9
 class TestApplicationEndpoint(FunctionalTest):
9 10
     """
10 11
     Tests for /api/v2/system/applications
@@ -25,7 +26,7 @@ class TestApplicationEndpoint(FunctionalTest):
25 26
         res = res.json_body
26 27
         application = res[0]
27 28
         assert application['label'] == "Text Documents"
28
-        assert application['slug'] == 'contents/html-documents'
29
+        assert application['slug'] == 'contents/html-document'
29 30
         assert application['fa_icon'] == 'file-text-o'
30 31
         assert application['hexcolor'] == '#3f52e3'
31 32
         assert application['is_active'] is True
@@ -39,14 +40,14 @@ class TestApplicationEndpoint(FunctionalTest):
39 40
         assert 'config' in application
40 41
         application = res[2]
41 42
         assert application['label'] == "Files"
42
-        assert application['slug'] == 'contents/files'
43
+        assert application['slug'] == 'contents/file'
43 44
         assert application['fa_icon'] == 'paperclip'
44 45
         assert application['hexcolor'] == '#FF9900'
45 46
         assert application['is_active'] is True
46 47
         assert 'config' in application
47 48
         application = res[3]
48 49
         assert application['label'] == "Threads"
49
-        assert application['slug'] == 'contents/threads'
50
+        assert application['slug'] == 'contents/thread'
50 51
         assert application['fa_icon'] == 'comments-o'
51 52
         assert application['hexcolor'] == '#ad4cf9'
52 53
         assert application['is_active'] is True
@@ -96,7 +97,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
96 97
         res = self.testapp.get('/api/v2/system/content_types', status=200)
97 98
         res = res.json_body
98 99
 
99
-        content_type = res[0]
100
+        content_type = res[1]
100 101
         assert content_type['slug'] == 'thread'
101 102
         assert content_type['fa_icon'] == 'comments-o'
102 103
         assert content_type['hexcolor'] == '#ad4cf9'
@@ -105,7 +106,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
105 106
         assert 'available_statuses' in content_type
106 107
         assert len(content_type['available_statuses']) == 4
107 108
 
108
-        content_type = res[1]
109
+        content_type = res[2]
109 110
         assert content_type['slug'] == 'file'
110 111
         assert content_type['fa_icon'] == 'paperclip'
111 112
         assert content_type['hexcolor'] == '#FF9900'
@@ -114,7 +115,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
114 115
         assert 'available_statuses' in content_type
115 116
         assert len(content_type['available_statuses']) == 4
116 117
 
117
-        content_type = res[2]
118
+        content_type = res[3]
118 119
         assert content_type['slug'] == 'markdownpage'
119 120
         assert content_type['fa_icon'] == 'file-code-o'
120 121
         assert content_type['hexcolor'] == '#f12d2d'
@@ -123,8 +124,8 @@ class TestContentsTypesEndpoint(FunctionalTest):
123 124
         assert 'available_statuses' in content_type
124 125
         assert len(content_type['available_statuses']) == 4
125 126
 
126
-        content_type = res[3]
127
-        assert content_type['slug'] == 'html-documents'
127
+        content_type = res[4]
128
+        assert content_type['slug'] == 'html-document'
128 129
         assert content_type['fa_icon'] == 'file-text-o'
129 130
         assert content_type['hexcolor'] == '#3f52e3'
130 131
         assert content_type['label'] == 'Text Document'

File diff suppressed because it is too large
+ 1859 - 224
backend/tracim_backend/tests/functional/test_user.py


+ 125 - 60
backend/tracim_backend/tests/functional/test_workspaces.py View File

@@ -10,7 +10,7 @@ from tracim_backend import models
10 10
 from tracim_backend.lib.core.content import ContentApi
11 11
 from tracim_backend.lib.core.workspace import WorkspaceApi
12 12
 from tracim_backend.models import get_tm_session
13
-from tracim_backend.models.data import ContentType
13
+from tracim_backend.models.contents import CONTENT_TYPES
14 14
 from tracim_backend.tests import FunctionalTest
15 15
 from tracim_backend.tests import set_html_document_slug_to_legacy
16 16
 from tracim_backend.fixtures.content import Content as ContentFixtures
@@ -58,9 +58,9 @@ class TestWorkspaceEndpoint(FunctionalTest):
58 58
         assert sidebar_entry['fa_icon'] == "th"
59 59
 
60 60
         sidebar_entry = workspace['sidebar_entries'][2]
61
-        assert sidebar_entry['slug'] == 'contents/html-documents'
61
+        assert sidebar_entry['slug'] == 'contents/html-document'
62 62
         assert sidebar_entry['label'] == 'Text Documents'
63
-        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=html-documents'  # nopep8
63
+        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=html-document'  # nopep8
64 64
         assert sidebar_entry['hexcolor'] == "#3f52e3"
65 65
         assert sidebar_entry['fa_icon'] == "file-text-o"
66 66
 
@@ -72,14 +72,14 @@ class TestWorkspaceEndpoint(FunctionalTest):
72 72
         assert sidebar_entry['fa_icon'] == "file-code-o"
73 73
 
74 74
         sidebar_entry = workspace['sidebar_entries'][4]
75
-        assert sidebar_entry['slug'] == 'contents/files'
75
+        assert sidebar_entry['slug'] == 'contents/file'
76 76
         assert sidebar_entry['label'] == 'Files'
77 77
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
78 78
         assert sidebar_entry['hexcolor'] == "#FF9900"
79 79
         assert sidebar_entry['fa_icon'] == "paperclip"
80 80
 
81 81
         sidebar_entry = workspace['sidebar_entries'][5]
82
-        assert sidebar_entry['slug'] == 'contents/threads'
82
+        assert sidebar_entry['slug'] == 'contents/thread'
83 83
         assert sidebar_entry['label'] == 'Threads'
84 84
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
85 85
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
@@ -643,7 +643,7 @@ class TestWorkspaceContents(FunctionalTest):
643 643
         assert content['show_in_ui'] is True
644 644
         assert content['slug'] == 'tools'
645 645
         assert content['status'] == 'open'
646
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
646
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
647 647
         assert content['workspace_id'] == 1
648 648
         content = res[1]
649 649
         assert content['content_id'] == 2
@@ -655,11 +655,11 @@ class TestWorkspaceContents(FunctionalTest):
655 655
         assert content['show_in_ui'] is True
656 656
         assert content['slug'] == 'menus'
657 657
         assert content['status'] == 'open'
658
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
658
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
659 659
         assert content['workspace_id'] == 1
660 660
         content = res[2]
661 661
         assert content['content_id'] == 11
662
-        assert content['content_type'] == 'html-documents'
662
+        assert content['content_type'] == 'html-document'
663 663
         assert content['is_archived'] is False
664 664
         assert content['is_deleted'] is False
665 665
         assert content['label'] == 'Current Menu'
@@ -667,7 +667,7 @@ class TestWorkspaceContents(FunctionalTest):
667 667
         assert content['show_in_ui'] is True
668 668
         assert content['slug'] == 'current-menu'
669 669
         assert content['status'] == 'open'
670
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
670
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
671 671
         assert content['workspace_id'] == 1
672 672
 
673 673
     def test_api__get_workspace_content__ok_200__get_default_html_documents(self):
@@ -682,14 +682,14 @@ class TestWorkspaceContents(FunctionalTest):
682 682
             )
683 683
         )
684 684
         params = {
685
-            'content_type': 'html-documents',
685
+            'content_type': 'html-document',
686 686
         }
687 687
         res = self.testapp.get('/api/v2/workspaces/1/contents', status=200, params=params).json_body   # nopep8
688 688
         assert len(res) == 1
689 689
         content = res[0]
690 690
         assert content
691 691
         assert content['content_id'] == 11
692
-        assert content['content_type'] == 'html-documents'
692
+        assert content['content_type'] == 'html-document'
693 693
         assert content['is_archived'] is False
694 694
         assert content['is_deleted'] is False
695 695
         assert content['label'] == 'Current Menu'
@@ -697,7 +697,7 @@ class TestWorkspaceContents(FunctionalTest):
697 697
         assert content['show_in_ui'] is True
698 698
         assert content['slug'] == 'current-menu'
699 699
         assert content['status'] == 'open'
700
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
700
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
701 701
         assert content['workspace_id'] == 1
702 702
 
703 703
     # Root related
@@ -727,7 +727,7 @@ class TestWorkspaceContents(FunctionalTest):
727 727
         # TODO - G.M - 30-05-2018 - Check this test
728 728
         assert len(res) == 4
729 729
         content = res[1]
730
-        assert content['content_type'] == 'html-documents'
730
+        assert content['content_type'] == 'html-document'
731 731
         assert content['content_id'] == 15
732 732
         assert content['is_archived'] is False
733 733
         assert content['is_deleted'] is False
@@ -736,11 +736,11 @@ class TestWorkspaceContents(FunctionalTest):
736 736
         assert content['show_in_ui'] is True
737 737
         assert content['slug'] == 'new-fruit-salad'
738 738
         assert content['status'] == 'open'
739
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
739
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
740 740
         assert content['workspace_id'] == 3
741 741
 
742 742
         content = res[2]
743
-        assert content['content_type'] == 'html-documents'
743
+        assert content['content_type'] == 'html-document'
744 744
         assert content['content_id'] == 16
745 745
         assert content['is_archived'] is True
746 746
         assert content['is_deleted'] is False
@@ -749,11 +749,11 @@ class TestWorkspaceContents(FunctionalTest):
749 749
         assert content['show_in_ui'] is True
750 750
         assert content['slug'].startswith('fruit-salad')
751 751
         assert content['status'] == 'open'
752
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
752
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
753 753
         assert content['workspace_id'] == 3
754 754
 
755 755
         content = res[3]
756
-        assert content['content_type'] == 'html-documents'
756
+        assert content['content_type'] == 'html-document'
757 757
         assert content['content_id'] == 17
758 758
         assert content['is_archived'] is False
759 759
         assert content['is_deleted'] is True
@@ -762,7 +762,7 @@ class TestWorkspaceContents(FunctionalTest):
762 762
         assert content['show_in_ui'] is True
763 763
         assert content['slug'].startswith('bad-fruit-salad')
764 764
         assert content['status'] == 'open'
765
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
765
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
766 766
         assert content['workspace_id'] == 3
767 767
 
768 768
     def test_api__get_workspace_content__ok_200__get_all_root_content(self):
@@ -790,7 +790,7 @@ class TestWorkspaceContents(FunctionalTest):
790 790
         # TODO - G.M - 30-05-2018 - Check this test
791 791
         assert len(res) == 4
792 792
         content = res[1]
793
-        assert content['content_type'] == 'html-documents'
793
+        assert content['content_type'] == 'html-document'
794 794
         assert content['content_id'] == 15
795 795
         assert content['is_archived'] is False
796 796
         assert content['is_deleted'] is False
@@ -799,11 +799,11 @@ class TestWorkspaceContents(FunctionalTest):
799 799
         assert content['show_in_ui'] is True
800 800
         assert content['slug'] == 'new-fruit-salad'
801 801
         assert content['status'] == 'open'
802
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
802
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
803 803
         assert content['workspace_id'] == 3
804 804
 
805 805
         content = res[2]
806
-        assert content['content_type'] == 'html-documents'
806
+        assert content['content_type'] == 'html-document'
807 807
         assert content['content_id'] == 16
808 808
         assert content['is_archived'] is True
809 809
         assert content['is_deleted'] is False
@@ -812,11 +812,11 @@ class TestWorkspaceContents(FunctionalTest):
812 812
         assert content['show_in_ui'] is True
813 813
         assert content['slug'].startswith('fruit-salad')
814 814
         assert content['status'] == 'open'
815
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
815
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
816 816
         assert content['workspace_id'] == 3
817 817
 
818 818
         content = res[3]
819
-        assert content['content_type'] == 'html-documents'
819
+        assert content['content_type'] == 'html-document'
820 820
         assert content['content_id'] == 17
821 821
         assert content['is_archived'] is False
822 822
         assert content['is_deleted'] is True
@@ -825,7 +825,7 @@ class TestWorkspaceContents(FunctionalTest):
825 825
         assert content['show_in_ui'] is True
826 826
         assert content['slug'].startswith('bad-fruit-salad')
827 827
         assert content['status'] == 'open'
828
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
828
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
829 829
         assert content['workspace_id'] == 3
830 830
 
831 831
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
@@ -853,7 +853,7 @@ class TestWorkspaceContents(FunctionalTest):
853 853
         # TODO - G.M - 30-05-2018 - Check this test
854 854
         assert len(res) == 2
855 855
         content = res[1]
856
-        assert content['content_type'] == 'html-documents'
856
+        assert content['content_type'] == 'html-document'
857 857
         assert content['content_id'] == 15
858 858
         assert content['is_archived'] is False
859 859
         assert content['is_deleted'] is False
@@ -862,7 +862,7 @@ class TestWorkspaceContents(FunctionalTest):
862 862
         assert content['show_in_ui'] is True
863 863
         assert content['slug'] == 'new-fruit-salad'
864 864
         assert content['status'] == 'open'
865
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
865
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
866 866
         assert content['workspace_id'] == 3
867 867
 
868 868
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
@@ -889,7 +889,7 @@ class TestWorkspaceContents(FunctionalTest):
889 889
         ).json_body   # nopep8
890 890
         assert len(res) == 1
891 891
         content = res[0]
892
-        assert content['content_type'] == 'html-documents'
892
+        assert content['content_type'] == 'html-document'
893 893
         assert content['content_id'] == 16
894 894
         assert content['is_archived'] is True
895 895
         assert content['is_deleted'] is False
@@ -898,7 +898,7 @@ class TestWorkspaceContents(FunctionalTest):
898 898
         assert content['show_in_ui'] is True
899 899
         assert content['slug'].startswith('fruit-salad')
900 900
         assert content['status'] == 'open'
901
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
901
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
902 902
         assert content['workspace_id'] == 3
903 903
 
904 904
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
@@ -927,7 +927,7 @@ class TestWorkspaceContents(FunctionalTest):
927 927
 
928 928
         assert len(res) == 1
929 929
         content = res[0]
930
-        assert content['content_type'] == 'html-documents'
930
+        assert content['content_type'] == 'html-document'
931 931
         assert content['content_id'] == 17
932 932
         assert content['is_archived'] is False
933 933
         assert content['is_deleted'] is True
@@ -936,7 +936,7 @@ class TestWorkspaceContents(FunctionalTest):
936 936
         assert content['show_in_ui'] is True
937 937
         assert content['slug'].startswith('bad-fruit-salad')
938 938
         assert content['status'] == 'open'
939
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
939
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
940 940
         assert content['workspace_id'] == 3
941 941
 
942 942
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
@@ -983,9 +983,9 @@ class TestWorkspaceContents(FunctionalTest):
983 983
             session=dbsession,
984 984
             config=self.app_config
985 985
         )
986
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
986
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
987 987
         test_thread = content_api.create(
988
-            content_type=ContentType.Thread,
988
+            content_type_slug=CONTENT_TYPES.Thread.slug,
989 989
             workspace=business_workspace,
990 990
             parent=tool_folder,
991 991
             label='Test Thread',
@@ -995,7 +995,7 @@ class TestWorkspaceContents(FunctionalTest):
995 995
         test_thread.description = 'Thread description'
996 996
         dbsession.add(test_thread)
997 997
         test_file = content_api.create(
998
-            content_type=ContentType.File,
998
+            content_type_slug=CONTENT_TYPES.File.slug,
999 999
             workspace=business_workspace,
1000 1000
             parent=tool_folder,
1001 1001
             label='Test file',
@@ -1009,16 +1009,16 @@ class TestWorkspaceContents(FunctionalTest):
1009 1009
             'text/plain',
1010 1010
         )
1011 1011
         test_page_legacy = content_api.create(
1012
-            content_type=ContentType.Page,
1012
+            content_type_slug=CONTENT_TYPES.Page.slug,
1013 1013
             workspace=business_workspace,
1014 1014
             label='test_page',
1015 1015
             do_save=False,
1016 1016
             do_notify=False,
1017 1017
         )
1018
-        test_page_legacy.type = ContentType.PageLegacy
1018
+        test_page_legacy.type = 'page'
1019 1019
         content_api.update_content(test_page_legacy, 'test_page', '<p>PAGE</p>')
1020 1020
         test_html_document = content_api.create(
1021
-            content_type=ContentType.Page,
1021
+            content_type_slug=CONTENT_TYPES.Page.slug,
1022 1022
             workspace=business_workspace,
1023 1023
             label='test_html_page',
1024 1024
             do_save=False,
@@ -1058,7 +1058,7 @@ class TestWorkspaceContents(FunctionalTest):
1058 1058
         assert content['show_in_ui'] is True
1059 1059
         assert content['slug'] == 'test-thread'
1060 1060
         assert content['status'] == 'open'
1061
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1061
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1062 1062
         assert content['workspace_id'] == 1
1063 1063
 
1064 1064
     def test_api__get_workspace_content__ok_200__get_all_filter_content_html_and_legacy_page(self):  # nopep8
@@ -1078,9 +1078,9 @@ class TestWorkspaceContents(FunctionalTest):
1078 1078
             session=dbsession,
1079 1079
             config=self.app_config
1080 1080
         )
1081
-        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1081
+        tool_folder = content_api.get_one(1, content_type=CONTENT_TYPES.Any_SLUG)
1082 1082
         test_thread = content_api.create(
1083
-            content_type=ContentType.Thread,
1083
+            content_type_slug=CONTENT_TYPES.Thread.slug,
1084 1084
             workspace=business_workspace,
1085 1085
             parent=tool_folder,
1086 1086
             label='Test Thread',
@@ -1090,7 +1090,7 @@ class TestWorkspaceContents(FunctionalTest):
1090 1090
         test_thread.description = 'Thread description'
1091 1091
         dbsession.add(test_thread)
1092 1092
         test_file = content_api.create(
1093
-            content_type=ContentType.File,
1093
+            content_type_slug=CONTENT_TYPES.File.slug,
1094 1094
             workspace=business_workspace,
1095 1095
             parent=tool_folder,
1096 1096
             label='Test file',
@@ -1104,17 +1104,17 @@ class TestWorkspaceContents(FunctionalTest):
1104 1104
             'text/plain',
1105 1105
         )
1106 1106
         test_page_legacy = content_api.create(
1107
-            content_type=ContentType.Page,
1107
+            content_type_slug=CONTENT_TYPES.Page.slug,
1108 1108
             workspace=business_workspace,
1109 1109
             parent=tool_folder,
1110 1110
             label='test_page',
1111 1111
             do_save=False,
1112 1112
             do_notify=False,
1113 1113
         )
1114
-        test_page_legacy.type = ContentType.PageLegacy
1114
+        test_page_legacy.type = 'page'
1115 1115
         content_api.update_content(test_page_legacy, 'test_page', '<p>PAGE</p>')
1116 1116
         test_html_document = content_api.create(
1117
-            content_type=ContentType.Page,
1117
+            content_type_slug=CONTENT_TYPES.Page.slug,
1118 1118
             workspace=business_workspace,
1119 1119
             parent=tool_folder,
1120 1120
             label='test_html_page',
@@ -1130,7 +1130,7 @@ class TestWorkspaceContents(FunctionalTest):
1130 1130
             'show_archived': 1,
1131 1131
             'show_deleted': 1,
1132 1132
             'show_active': 1,
1133
-            'content_type': 'html-documents',
1133
+            'content_type': 'html-document',
1134 1134
         }
1135 1135
         self.testapp.authorization = (
1136 1136
             'Basic',
@@ -1146,7 +1146,7 @@ class TestWorkspaceContents(FunctionalTest):
1146 1146
         ).json_body
1147 1147
         assert len(res) == 2
1148 1148
         content = res[0]
1149
-        assert content['content_type'] == 'html-documents'
1149
+        assert content['content_type'] == 'html-document'
1150 1150
         assert content['content_id']
1151 1151
         assert content['is_archived'] is False
1152 1152
         assert content['is_deleted'] is False
@@ -1155,10 +1155,10 @@ class TestWorkspaceContents(FunctionalTest):
1155 1155
         assert content['show_in_ui'] is True
1156 1156
         assert content['slug'] == 'test-page'
1157 1157
         assert content['status'] == 'open'
1158
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1158
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1159 1159
         assert content['workspace_id'] == 1
1160 1160
         content = res[1]
1161
-        assert content['content_type'] == 'html-documents'
1161
+        assert content['content_type'] == 'html-document'
1162 1162
         assert content['content_id']
1163 1163
         assert content['is_archived'] is False
1164 1164
         assert content['is_deleted'] is False
@@ -1167,7 +1167,7 @@ class TestWorkspaceContents(FunctionalTest):
1167 1167
         assert content['show_in_ui'] is True
1168 1168
         assert content['slug'] == 'test-html-page'
1169 1169
         assert content['status'] == 'open'
1170
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1170
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1171 1171
         assert content['workspace_id'] == 1
1172 1172
         assert res[0]['content_id'] != res[1]['content_id']
1173 1173
 
@@ -1180,7 +1180,7 @@ class TestWorkspaceContents(FunctionalTest):
1180 1180
             'show_archived': 1,
1181 1181
             'show_deleted': 1,
1182 1182
             'show_active': 1,
1183
-            'content_type': 'any'
1183
+         #   'content_type': 'any'
1184 1184
         }
1185 1185
         self.testapp.authorization = (
1186 1186
             'Basic',
@@ -1196,7 +1196,7 @@ class TestWorkspaceContents(FunctionalTest):
1196 1196
         ).json_body   # nopep8
1197 1197
         assert len(res) == 3
1198 1198
         content = res[0]
1199
-        assert content['content_type'] == 'html-documents'
1199
+        assert content['content_type'] == 'html-document'
1200 1200
         assert content['content_id'] == 12
1201 1201
         assert content['is_archived'] is False
1202 1202
         assert content['is_deleted'] is False
@@ -1205,11 +1205,11 @@ class TestWorkspaceContents(FunctionalTest):
1205 1205
         assert content['show_in_ui'] is True
1206 1206
         assert content['slug'] == 'new-fruit-salad'
1207 1207
         assert content['status'] == 'open'
1208
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1208
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1209 1209
         assert content['workspace_id'] == 2
1210 1210
 
1211 1211
         content = res[1]
1212
-        assert content['content_type'] == 'html-documents'
1212
+        assert content['content_type'] == 'html-document'
1213 1213
         assert content['content_id'] == 13
1214 1214
         assert content['is_archived'] is True
1215 1215
         assert content['is_deleted'] is False
@@ -1218,11 +1218,11 @@ class TestWorkspaceContents(FunctionalTest):
1218 1218
         assert content['show_in_ui'] is True
1219 1219
         assert content['slug'].startswith('fruit-salad')
1220 1220
         assert content['status'] == 'open'
1221
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1221
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1222 1222
         assert content['workspace_id'] == 2
1223 1223
 
1224 1224
         content = res[2]
1225
-        assert content['content_type'] == 'html-documents'
1225
+        assert content['content_type'] == 'html-document'
1226 1226
         assert content['content_id'] == 14
1227 1227
         assert content['is_archived'] is False
1228 1228
         assert content['is_deleted'] is True
@@ -1231,7 +1231,7 @@ class TestWorkspaceContents(FunctionalTest):
1231 1231
         assert content['show_in_ui'] is True
1232 1232
         assert content['slug'].startswith('bad-fruit-salad')
1233 1233
         assert content['status'] == 'open'
1234
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1234
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1235 1235
         assert content['workspace_id'] == 2
1236 1236
 
1237 1237
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
@@ -1267,7 +1267,7 @@ class TestWorkspaceContents(FunctionalTest):
1267 1267
         assert content['show_in_ui'] is True
1268 1268
         assert content['slug'] == 'new-fruit-salad'
1269 1269
         assert content['status'] == 'open'
1270
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1270
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1271 1271
         assert content['workspace_id'] == 2
1272 1272
 
1273 1273
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
@@ -1294,7 +1294,7 @@ class TestWorkspaceContents(FunctionalTest):
1294 1294
         ).json_body   # nopep8
1295 1295
         assert len(res) == 1
1296 1296
         content = res[0]
1297
-        assert content['content_type'] == 'html-documents'
1297
+        assert content['content_type'] == 'html-document'
1298 1298
         assert content['content_id'] == 13
1299 1299
         assert content['is_archived'] is True
1300 1300
         assert content['is_deleted'] is False
@@ -1303,7 +1303,7 @@ class TestWorkspaceContents(FunctionalTest):
1303 1303
         assert content['show_in_ui'] is True
1304 1304
         assert content['slug'].startswith('fruit-salad')
1305 1305
         assert content['status'] == 'open'
1306
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1306
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1307 1307
         assert content['workspace_id'] == 2
1308 1308
 
1309 1309
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
@@ -1331,7 +1331,7 @@ class TestWorkspaceContents(FunctionalTest):
1331 1331
 
1332 1332
         assert len(res) == 1
1333 1333
         content = res[0]
1334
-        assert content['content_type'] == 'html-documents'
1334
+        assert content['content_type'] == 'html-document'
1335 1335
         assert content['content_id'] == 14
1336 1336
         assert content['is_archived'] is False
1337 1337
         assert content['is_deleted'] is True
@@ -1340,7 +1340,7 @@ class TestWorkspaceContents(FunctionalTest):
1340 1340
         assert content['show_in_ui'] is True
1341 1341
         assert content['slug'].startswith('bad-fruit-salad')
1342 1342
         assert content['status'] == 'open'
1343
-        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1343
+        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1344 1344
         assert content['workspace_id'] == 2
1345 1345
 
1346 1346
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
@@ -1436,6 +1436,7 @@ class TestWorkspaceContents(FunctionalTest):
1436 1436
             )
1437 1437
         )
1438 1438
         params = {
1439
+            'parent_id': None,
1439 1440
             'label': 'GenericCreatedContent',
1440 1441
             'content_type': 'markdownpage',
1441 1442
         }
@@ -1466,6 +1467,70 @@ class TestWorkspaceContents(FunctionalTest):
1466 1467
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
1467 1468
         assert res.json_body in active_contents
1468 1469
 
1470
+    def test_api__post_content_create_generic_content__ok_200__no_parent_id_param(self) -> None:  # nopep8
1471
+        """
1472
+        Create generic content
1473
+        """
1474
+        self.testapp.authorization = (
1475
+            'Basic',
1476
+            (
1477
+                'admin@admin.admin',
1478
+                'admin@admin.admin'
1479
+            )
1480
+        )
1481
+        params = {
1482
+            'label': 'GenericCreatedContent',
1483
+            'content_type': 'markdownpage',
1484
+        }
1485
+        res = self.testapp.post_json(
1486
+            '/api/v2/workspaces/1/contents',
1487
+            params=params,
1488
+            status=200
1489
+        )
1490
+        assert res
1491
+        assert res.json_body
1492
+        assert res.json_body['status'] == 'open'
1493
+        assert res.json_body['content_id']
1494
+        assert res.json_body['content_type'] == 'markdownpage'
1495
+        assert res.json_body['is_archived'] is False
1496
+        assert res.json_body['is_deleted'] is False
1497
+        assert res.json_body['workspace_id'] == 1
1498
+        assert res.json_body['slug'] == 'genericcreatedcontent'
1499
+        assert res.json_body['parent_id'] is None
1500
+        assert res.json_body['show_in_ui'] is True
1501
+        assert res.json_body['sub_content_types']
1502
+        params_active = {
1503
+            'parent_id': 0,
1504
+            'show_archived': 0,
1505
+            'show_deleted': 0,
1506
+            'show_active': 1,
1507
+        }
1508
+        # INFO - G.M - 2018-06-165 - Verify if new content is correctly created
1509
+        active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
1510
+        assert res.json_body in active_contents
1511
+
1512
+    def test_api__post_content_create_generic_content__err_400__parent_id_0(self) -> None:  # nopep8
1513
+        """
1514
+        Create generic content
1515
+        """
1516
+        self.testapp.authorization = (
1517
+            'Basic',
1518
+            (
1519
+                'admin@admin.admin',
1520
+                'admin@admin.admin'
1521
+            )
1522
+        )
1523
+        params = {
1524
+            'parent_id': 0,
1525
+            'label': 'GenericCreatedContent',
1526
+            'content_type': 'markdownpage',
1527
+        }
1528
+        res = self.testapp.post_json(
1529
+            '/api/v2/workspaces/1/contents',
1530
+            params=params,
1531
+            status=400
1532
+        )
1533
+
1469 1534
     def test_api__post_content_create_generic_content__ok_200__in_folder(self) -> None:  # nopep8
1470 1535
         """
1471 1536
         Create generic content in folder

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

@@ -14,6 +14,7 @@ from tracim_backend.exceptions import SameValueError
14 14
 from tracim_backend.lib.core.workspace import RoleApi
15 15
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
16 16
 from tracim_backend.lib.core.workspace import WorkspaceApi
17
+from tracim_backend.models.contents import CONTENT_TYPES
17 18
 from tracim_backend.models.revision_protection import new_revision
18 19
 from tracim_backend.models.auth import User
19 20
 from tracim_backend.models.auth import Group
@@ -22,7 +23,6 @@ from tracim_backend.models.data import ActionDescription
22 23
 from tracim_backend.models.data import ContentRevisionRO
23 24
 from tracim_backend.models.data import Workspace
24 25
 from tracim_backend.models.data import Content
25
-from tracim_backend.models.data import ContentType
26 26
 from tracim_backend.models.data import UserRoleInWorkspace
27 27
 from tracim_backend.fixtures.users_and_groups import Test as FixtureTest
28 28
 from tracim_backend.tests import DefaultTest
@@ -129,14 +129,14 @@ class TestContentApi(DefaultTest):
129 129
             config=self.app_config,
130 130
         )
131 131
         item = api.create(
132
-            content_type=ContentType.Folder,
132
+            content_type_slug=CONTENT_TYPES.Folder.slug,
133 133
             workspace=workspace,
134 134
             parent=None,
135 135
             label='not_deleted',
136 136
             do_save=True
137 137
         )
138 138
         item2 = api.create(
139
-            content_type=ContentType.Folder,
139
+            content_type_slug=CONTENT_TYPES.Folder.slug,
140 140
             workspace=workspace,
141 141
             parent=None,
142 142
             label='to_delete',
@@ -159,10 +159,10 @@ class TestContentApi(DefaultTest):
159 159
             session=self.session,
160 160
             config=self.app_config,
161 161
         )
162
-        items = api.get_all(None, ContentType.Any, workspace)
162
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
163 163
         eq_(2, len(items))
164 164
 
165
-        items = api.get_all(None, ContentType.Any, workspace)
165
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
166 166
         with new_revision(
167 167
                 session=self.session,
168 168
                 tm=transaction.manager,
@@ -184,7 +184,7 @@ class TestContentApi(DefaultTest):
184 184
             session=self.session,
185 185
             config=self.app_config,
186 186
         )
187
-        items = api.get_all(None, ContentType.Any, workspace)
187
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
188 188
         eq_(1, len(items))
189 189
         transaction.commit()
190 190
 
@@ -202,7 +202,7 @@ class TestContentApi(DefaultTest):
202 202
             config=self.app_config,
203 203
             show_deleted=True,
204 204
         )
205
-        items = api.get_all(None, ContentType.Any, workspace)
205
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
206 206
         eq_(2, len(items))
207 207
 
208 208
     def test_archive(self):
@@ -240,14 +240,14 @@ class TestContentApi(DefaultTest):
240 240
             config=self.app_config,
241 241
         )
242 242
         item = api.create(
243
-            content_type=ContentType.Folder,
243
+            content_type_slug=CONTENT_TYPES.Folder.slug,
244 244
             workspace=workspace,
245 245
             parent=None,
246 246
             label='not_archived',
247 247
             do_save=True
248 248
         )
249 249
         item2 = api.create(
250
-            content_type=ContentType.Folder,
250
+            content_type_slug=CONTENT_TYPES.Folder.slug,
251 251
             workspace=workspace,
252 252
             parent=None,
253 253
             label='to_archive',
@@ -269,10 +269,10 @@ class TestContentApi(DefaultTest):
269 269
             config=self.app_config,
270 270
         )
271 271
 
272
-        items = api.get_all(None, ContentType.Any, workspace)
272
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
273 273
         eq_(2, len(items))
274 274
 
275
-        items = api.get_all(None, ContentType.Any, workspace)
275
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
276 276
         with new_revision(
277 277
                 session=self.session,
278 278
                 tm=transaction.manager,
@@ -295,7 +295,7 @@ class TestContentApi(DefaultTest):
295 295
             config=self.app_config,
296 296
         )
297 297
 
298
-        items = api.get_all(None, ContentType.Any, workspace)
298
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
299 299
         eq_(1, len(items))
300 300
         transaction.commit()
301 301
 
@@ -320,7 +320,7 @@ class TestContentApi(DefaultTest):
320 320
             config=self.app_config,
321 321
             show_archived=True,
322 322
         )
323
-        items = api.get_all(None, ContentType.Any, workspace)
323
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
324 324
         eq_(2, len(items))
325 325
 
326 326
     def test_get_all_with_filter(self):
@@ -358,14 +358,14 @@ class TestContentApi(DefaultTest):
358 358
             config=self.app_config,
359 359
         )
360 360
         item = api.create(
361
-            content_type=ContentType.Folder,
361
+            content_type_slug=CONTENT_TYPES.Folder.slug,
362 362
             workspace=workspace,
363 363
             parent=None,
364 364
             label='thefolder',
365 365
             do_save=True
366 366
         )
367 367
         item2 = api.create(
368
-            content_type=ContentType.File,
368
+            content_type_slug=CONTENT_TYPES.File.slug,
369 369
             workspace=workspace,
370 370
             parent=None,
371 371
             label='thefile',
@@ -389,14 +389,14 @@ class TestContentApi(DefaultTest):
389 389
             config=self.app_config,
390 390
         )
391 391
 
392
-        items = api.get_all(None, ContentType.Any, workspace)
392
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
393 393
         eq_(2, len(items))
394 394
 
395
-        items2 = api.get_all(None, ContentType.File, workspace)
395
+        items2 = api.get_all(None, CONTENT_TYPES.File.slug, workspace)
396 396
         eq_(1, len(items2))
397 397
         eq_('thefile', items2[0].label)
398 398
 
399
-        items3 = api.get_all(None, ContentType.Folder, workspace)
399
+        items3 = api.get_all(None, CONTENT_TYPES.Folder.slug, workspace)
400 400
         eq_(1, len(items3))
401 401
         eq_('thefolder', items3[0].label)
402 402
 
@@ -428,21 +428,21 @@ class TestContentApi(DefaultTest):
428 428
             config=self.app_config,
429 429
         )
430 430
         item = api.create(
431
-            ContentType.Folder,
431
+            CONTENT_TYPES.Folder.slug,
432 432
             workspace,
433 433
             None,
434 434
             'parent',
435 435
             do_save=True,
436 436
         )
437 437
         item2 = api.create(
438
-            ContentType.File,
438
+            CONTENT_TYPES.File.slug,
439 439
             workspace,
440 440
             item,
441 441
             'file1',
442 442
             do_save=True,
443 443
         )
444 444
         item3 = api.create(
445
-            ContentType.File,
445
+            CONTENT_TYPES.File.slug,
446 446
             workspace,
447 447
             None,
448 448
             'file2',
@@ -468,10 +468,10 @@ class TestContentApi(DefaultTest):
468 468
             config=self.app_config,
469 469
         )
470 470
 
471
-        items = api.get_all(None, ContentType.Any, workspace)
471
+        items = api.get_all(None, CONTENT_TYPES.Any_SLUG, workspace)
472 472
         eq_(3, len(items))
473 473
 
474
-        items2 = api.get_all(parent_id, ContentType.File, workspace)
474
+        items2 = api.get_all(parent_id, CONTENT_TYPES.File.slug, workspace)
475 475
         eq_(1, len(items2))
476 476
         eq_(child_id, items2[0].content_id)
477 477
 
@@ -506,7 +506,7 @@ class TestContentApi(DefaultTest):
506 506
             session=self.session,
507 507
             config=self.app_config,
508 508
         )
509
-        c = api.create(ContentType.Folder, workspace, None, 'parent', '', True)
509
+        c = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'parent', '', True)
510 510
         with new_revision(
511 511
             session=self.session,
512 512
             tm=transaction.manager,
@@ -546,7 +546,7 @@ class TestContentApi(DefaultTest):
546 546
             session=self.session,
547 547
             config=self.app_config,
548 548
         )
549
-        c = api.create(ContentType.Folder, workspace, None, 'parent', '', True)
549
+        c = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'parent', '', True)
550 550
         with new_revision(
551 551
             session=self.session,
552 552
             tm=transaction.manager,
@@ -591,14 +591,14 @@ class TestContentApi(DefaultTest):
591 591
             session=self.session,
592 592
             config=self.app_config,
593 593
         )
594
-        p = api.create(ContentType.Page, workspace, None, 'this_is_a_page')
594
+        p = api.create(CONTENT_TYPES.Page.slug, workspace, None, 'this_is_a_page')
595 595
         c = api.create_comment(workspace, p, 'this is the comment', True)
596 596
 
597 597
         eq_(Content, c.__class__)
598 598
         eq_(p.content_id, c.parent_id)
599 599
         eq_(user, c.owner)
600 600
         eq_(workspace, c.workspace)
601
-        eq_(ContentType.Comment, c.type)
601
+        eq_(CONTENT_TYPES.Comment.slug, c.type)
602 602
         eq_('this is the comment', c.description)
603 603
         eq_('', c.label)
604 604
         eq_(ActionDescription.COMMENT, c.revision_type)
@@ -652,7 +652,7 @@ class TestContentApi(DefaultTest):
652 652
             config=self.app_config,
653 653
         )
654 654
         foldera = api.create(
655
-            ContentType.Folder,
655
+            CONTENT_TYPES.Folder.slug,
656 656
             workspace,
657 657
             None,
658 658
             'folder a',
@@ -661,7 +661,7 @@ class TestContentApi(DefaultTest):
661 661
         )
662 662
         with self.session.no_autoflush:
663 663
             text_file = api.create(
664
-                content_type=ContentType.File,
664
+                content_type_slug=CONTENT_TYPES.File.slug,
665 665
                 workspace=workspace,
666 666
                 parent=foldera,
667 667
                 label='test_file',
@@ -689,7 +689,7 @@ class TestContentApi(DefaultTest):
689 689
             save_now=True
690 690
         )
691 691
         folderb = api2.create(
692
-            ContentType.Folder,
692
+            CONTENT_TYPES.Folder.slug,
693 693
             workspace2,
694 694
             None,
695 695
             'folder b',
@@ -773,7 +773,7 @@ class TestContentApi(DefaultTest):
773 773
             config=self.app_config,
774 774
         )
775 775
         foldera = api.create(
776
-            ContentType.Folder,
776
+            CONTENT_TYPES.Folder.slug,
777 777
             workspace,
778 778
             None,
779 779
             'folder a',
@@ -782,7 +782,7 @@ class TestContentApi(DefaultTest):
782 782
         )
783 783
         with self.session.no_autoflush:
784 784
             text_file = api.create(
785
-                content_type=ContentType.File,
785
+                content_type_slug=CONTENT_TYPES.File.slug,
786 786
                 workspace=workspace,
787 787
                 parent=foldera,
788 788
                 label='test_file',
@@ -810,7 +810,7 @@ class TestContentApi(DefaultTest):
810 810
             save_now=True
811 811
         )
812 812
         folderb = api2.create(
813
-            ContentType.Folder,
813
+            CONTENT_TYPES.Folder.slug,
814 814
             workspace2,
815 815
             None,
816 816
             'folder b',
@@ -891,7 +891,7 @@ class TestContentApi(DefaultTest):
891 891
             config=self.app_config,
892 892
         )
893 893
         foldera = api.create(
894
-            ContentType.Folder,
894
+            CONTENT_TYPES.Folder.slug,
895 895
             workspace,
896 896
             None,
897 897
             'folder a',
@@ -900,7 +900,7 @@ class TestContentApi(DefaultTest):
900 900
         )
901 901
         with self.session.no_autoflush:
902 902
             text_file = api.create(
903
-                content_type=ContentType.File,
903
+                content_type_slug=CONTENT_TYPES.File.slug,
904 904
                 workspace=workspace,
905 905
                 parent=foldera,
906 906
                 label='test_file',
@@ -1014,13 +1014,13 @@ class TestContentApi(DefaultTest):
1014 1014
 
1015 1015
         # Creates page_1 & page_2 in workspace 1
1016 1016
         #     and page_3 & page_4 in workspace 2
1017
-        page_1 = cont_api_a.create(ContentType.Page, workspace1, None,
1017
+        page_1 = cont_api_a.create(CONTENT_TYPES.Page.slug, workspace1, None,
1018 1018
                                    'this is a page', do_save=True)
1019
-        page_2 = cont_api_a.create(ContentType.Page, workspace1, None,
1019
+        page_2 = cont_api_a.create(CONTENT_TYPES.Page.slug, workspace1, None,
1020 1020
                                    'this is page1', do_save=True)
1021
-        page_3 = cont_api_a.create(ContentType.Thread, workspace2, None,
1021
+        page_3 = cont_api_a.create(CONTENT_TYPES.Thread.slug, workspace2, None,
1022 1022
                                    'this is page2', do_save=True)
1023
-        page_4 = cont_api_a.create(ContentType.File, workspace2, None,
1023
+        page_4 = cont_api_a.create(CONTENT_TYPES.File.slug, workspace2, None,
1024 1024
                                    'this is page3', do_save=True)
1025 1025
 
1026 1026
         for rev in page_1.revisions:
@@ -1118,7 +1118,7 @@ class TestContentApi(DefaultTest):
1118 1118
             config=self.app_config,
1119 1119
         )
1120 1120
 
1121
-        page_1 = cont_api_a.create(ContentType.Page, workspace, None,
1121
+        page_1 = cont_api_a.create(CONTENT_TYPES.Page.slug, workspace, None,
1122 1122
                                    'this is a page', do_save=True)
1123 1123
 
1124 1124
         for rev in page_1.revisions:
@@ -1187,21 +1187,21 @@ class TestContentApi(DefaultTest):
1187 1187
         )
1188 1188
 
1189 1189
         page_2 = cont_api_a.create(
1190
-            ContentType.Page,
1190
+            CONTENT_TYPES.Page.slug,
1191 1191
             workspace,
1192 1192
             None,
1193 1193
             'this is page1',
1194 1194
             do_save=True
1195 1195
         )
1196 1196
         page_3 = cont_api_a.create(
1197
-            ContentType.Thread,
1197
+            CONTENT_TYPES.Thread.slug,
1198 1198
             workspace,
1199 1199
             None,
1200 1200
             'this is page2',
1201 1201
             do_save=True
1202 1202
         )
1203 1203
         page_4 = cont_api_a.create(
1204
-            ContentType.File,
1204
+            CONTENT_TYPES.File.slug,
1205 1205
             workspace,
1206 1206
             None,
1207 1207
             'this is page3',
@@ -1285,7 +1285,7 @@ class TestContentApi(DefaultTest):
1285 1285
         )
1286 1286
 
1287 1287
         p = api.create(
1288
-            content_type=ContentType.Page,
1288
+            content_type_slug=CONTENT_TYPES.Page.slug,
1289 1289
             workspace=workspace,
1290 1290
             parent=None,
1291 1291
             label='this_is_a_page',
@@ -1312,7 +1312,7 @@ class TestContentApi(DefaultTest):
1312 1312
             config=self.app_config,
1313 1313
         )
1314 1314
 
1315
-        content = api.get_one(pcid, ContentType.Any, workspace)
1315
+        content = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1316 1316
         eq_(u1id, content.owner_id)
1317 1317
         eq_(poid, content.owner_id)
1318 1318
 
@@ -1326,7 +1326,7 @@ class TestContentApi(DefaultTest):
1326 1326
             session=self.session,
1327 1327
             config=self.app_config,
1328 1328
         )
1329
-        content2 = api2.get_one(pcid, ContentType.Any, workspace)
1329
+        content2 = api2.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1330 1330
         with new_revision(
1331 1331
            session=self.session,
1332 1332
            tm=transaction.manager,
@@ -1353,7 +1353,7 @@ class TestContentApi(DefaultTest):
1353 1353
             config=self.app_config,
1354 1354
         )
1355 1355
 
1356
-        updated = api.get_one(pcid, ContentType.Any, workspace)
1356
+        updated = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1357 1357
         eq_(u2id, updated.owner_id,
1358 1358
             'the owner id should be {} (found {})'.format(u2id,
1359 1359
                                                           updated.owner_id))
@@ -1412,7 +1412,7 @@ class TestContentApi(DefaultTest):
1412 1412
         )
1413 1413
         with self.session.no_autoflush:
1414 1414
             page = api.create(
1415
-                content_type=ContentType.Page,
1415
+                content_type_slug=CONTENT_TYPES.Page.slug,
1416 1416
                 workspace=workspace,
1417 1417
                 label="same_content",
1418 1418
                 do_save=False
@@ -1426,7 +1426,7 @@ class TestContentApi(DefaultTest):
1426 1426
             session=self.session,
1427 1427
             config=self.app_config,
1428 1428
         )
1429
-        content2 = api2.get_one(page.content_id, ContentType.Any, workspace)
1429
+        content2 = api2.get_one(page.content_id, CONTENT_TYPES.Any_SLUG, workspace)
1430 1430
         with new_revision(
1431 1431
            session=self.session,
1432 1432
            tm=transaction.manager,
@@ -1495,7 +1495,7 @@ class TestContentApi(DefaultTest):
1495 1495
             config=self.app_config,
1496 1496
         )
1497 1497
         p = api.create(
1498
-            content_type=ContentType.File,
1498
+            content_type_slug=CONTENT_TYPES.File.slug,
1499 1499
             workspace=workspace,
1500 1500
             parent=None,
1501 1501
             label='this_is_a_page',
@@ -1524,7 +1524,7 @@ class TestContentApi(DefaultTest):
1524 1524
             config=self.app_config,
1525 1525
         )
1526 1526
 
1527
-        content = api.get_one(pcid, ContentType.Any, workspace)
1527
+        content = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1528 1528
         eq_(u1id, content.owner_id)
1529 1529
         eq_(poid, content.owner_id)
1530 1530
 
@@ -1538,7 +1538,7 @@ class TestContentApi(DefaultTest):
1538 1538
             session=self.session,
1539 1539
             config=self.app_config,
1540 1540
         )
1541
-        content2 = api2.get_one(pcid, ContentType.Any, workspace)
1541
+        content2 = api2.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1542 1542
         with new_revision(
1543 1543
             session=self.session,
1544 1544
             tm=transaction.manager,
@@ -1561,7 +1561,7 @@ class TestContentApi(DefaultTest):
1561 1561
             config=self.app_config,
1562 1562
         ).get_one(wid)
1563 1563
 
1564
-        updated = api.get_one(pcid, ContentType.Any, workspace)
1564
+        updated = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1565 1565
         eq_(u2id, updated.owner_id,
1566 1566
             'the owner id should be {} (found {})'.format(u2id,
1567 1567
                                                           updated.owner_id))
@@ -1622,7 +1622,7 @@ class TestContentApi(DefaultTest):
1622 1622
         )
1623 1623
         with self.session.no_autoflush:
1624 1624
             page = api.create(
1625
-                content_type=ContentType.Page,
1625
+                content_type_slug=CONTENT_TYPES.Page.slug,
1626 1626
                 workspace=workspace,
1627 1627
                 label="same_content",
1628 1628
                 do_save=False
@@ -1641,7 +1641,7 @@ class TestContentApi(DefaultTest):
1641 1641
             session=self.session,
1642 1642
             config=self.app_config,
1643 1643
         )
1644
-        content2 = api2.get_one(page.content_id, ContentType.Any, workspace)
1644
+        content2 = api2.get_one(page.content_id, CONTENT_TYPES.Any_SLUG, workspace)
1645 1645
         with new_revision(
1646 1646
             session=self.session,
1647 1647
             tm=transaction.manager,
@@ -1713,7 +1713,7 @@ class TestContentApi(DefaultTest):
1713 1713
             config=self.app_config,
1714 1714
         )
1715 1715
         p = api.create(
1716
-            content_type=ContentType.File,
1716
+            content_type_slug=CONTENT_TYPES.File.slug,
1717 1717
             workspace=workspace,
1718 1718
             parent=None,
1719 1719
             label='this_is_a_page',
@@ -1741,7 +1741,7 @@ class TestContentApi(DefaultTest):
1741 1741
             config=self.app_config,
1742 1742
         ).get_one(wid)
1743 1743
 
1744
-        content = api.get_one(pcid, ContentType.Any, workspace)
1744
+        content = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1745 1745
         eq_(u1id, content.owner_id)
1746 1746
         eq_(poid, content.owner_id)
1747 1747
 
@@ -1757,7 +1757,7 @@ class TestContentApi(DefaultTest):
1757 1757
             config=self.app_config,
1758 1758
             show_archived=True,
1759 1759
         )
1760
-        content2 = api2.get_one(pcid, ContentType.Any, workspace)
1760
+        content2 = api2.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1761 1761
         with new_revision(
1762 1762
                 session=self.session,
1763 1763
                 tm=transaction.manager,
@@ -1796,7 +1796,7 @@ class TestContentApi(DefaultTest):
1796 1796
             show_archived=True,
1797 1797
         )
1798 1798
 
1799
-        updated = api2.get_one(pcid, ContentType.Any, workspace)
1799
+        updated = api2.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1800 1800
         eq_(u2id, updated.owner_id,
1801 1801
             'the owner id should be {} (found {})'.format(u2id,
1802 1802
                                                           updated.owner_id))
@@ -1805,7 +1805,7 @@ class TestContentApi(DefaultTest):
1805 1805
 
1806 1806
         ####
1807 1807
 
1808
-        updated2 = api.get_one(pcid, ContentType.Any, workspace)
1808
+        updated2 = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1809 1809
         with new_revision(
1810 1810
             session=self.session,
1811 1811
             tm=transaction.manager,
@@ -1874,7 +1874,7 @@ class TestContentApi(DefaultTest):
1874 1874
             show_deleted=True,
1875 1875
         )
1876 1876
         p = api.create(
1877
-            content_type=ContentType.File,
1877
+            content_type_slug=CONTENT_TYPES.File.slug,
1878 1878
             workspace=workspace,
1879 1879
             parent=None,
1880 1880
             label='this_is_a_page',
@@ -1900,7 +1900,7 @@ class TestContentApi(DefaultTest):
1900 1900
             config=self.app_config,
1901 1901
         ).get_one(wid)
1902 1902
 
1903
-        content = api.get_one(pcid, ContentType.Any, workspace)
1903
+        content = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1904 1904
         eq_(u1id, content.owner_id)
1905 1905
         eq_(poid, content.owner_id)
1906 1906
 
@@ -1915,7 +1915,7 @@ class TestContentApi(DefaultTest):
1915 1915
             config=self.app_config,
1916 1916
             show_deleted=True,
1917 1917
         )
1918
-        content2 = api2.get_one(pcid, ContentType.Any, workspace)
1918
+        content2 = api2.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1919 1919
         with new_revision(
1920 1920
                 session=self.session,
1921 1921
                 tm=transaction.manager,
@@ -1956,7 +1956,7 @@ class TestContentApi(DefaultTest):
1956 1956
             show_deleted=True
1957 1957
         )
1958 1958
 
1959
-        updated = api2.get_one(pcid, ContentType.Any, workspace)
1959
+        updated = api2.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1960 1960
         eq_(u2id, updated.owner_id,
1961 1961
             'the owner id should be {} (found {})'.format(u2id,
1962 1962
                                                           updated.owner_id))
@@ -1965,7 +1965,7 @@ class TestContentApi(DefaultTest):
1965 1965
 
1966 1966
         ####
1967 1967
 
1968
-        updated2 = api.get_one(pcid, ContentType.Any, workspace)
1968
+        updated2 = api.get_one(pcid, CONTENT_TYPES.Any_SLUG, workspace)
1969 1969
         with new_revision(
1970 1970
             tm=transaction.manager,
1971 1971
             session=self.session,
@@ -2016,14 +2016,14 @@ class TestContentApi(DefaultTest):
2016 2016
             session=self.session,
2017 2017
             config=self.app_config,
2018 2018
         )
2019
-        main_folder_workspace2 = api.create(ContentType.Folder, workspace2, None, 'Hepla', '', True)  # nopep8
2020
-        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2019
+        main_folder_workspace2 = api.create(CONTENT_TYPES.Folder.slug, workspace2, None, 'Hepla', '', True)  # nopep8
2020
+        main_folder = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'this is randomized folder', '', True)  # nopep8
2021 2021
         # creation order test
2022
-        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2023
-        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2022
+        firstly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2023
+        secondly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2024 2024
         # update order test
2025
-        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2026
-        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2025
+        firstly_created_but_recently_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2026
+        secondly_created_but_not_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2027 2027
         with new_revision(
2028 2028
             session=self.session,
2029 2029
             tm=transaction.manager,
@@ -2032,11 +2032,11 @@ class TestContentApi(DefaultTest):
2032 2032
             firstly_created_but_recently_updated.description = 'Just an update'
2033 2033
         api.save(firstly_created_but_recently_updated)
2034 2034
         # comment change order
2035
-        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2036
-        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2035
+        firstly_created_but_recently_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2036
+        secondly_created_but_not_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2037 2037
         comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2038 2038
 
2039
-        content_workspace_2 = api.create(ContentType.Page, workspace2 ,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
2039
+        content_workspace_2 = api.create(CONTENT_TYPES.Page.slug, workspace2 ,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
2040 2040
         last_actives = api.get_last_active()
2041 2041
         assert len(last_actives) == 9
2042 2042
         # workspace_2 content
@@ -2088,13 +2088,13 @@ class TestContentApi(DefaultTest):
2088 2088
             session=self.session,
2089 2089
             config=self.app_config,
2090 2090
         )
2091
-        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2091
+        main_folder = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'this is randomized folder', '', True)  # nopep8
2092 2092
         # creation order test
2093
-        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2094
-        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2093
+        firstly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2094
+        secondly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2095 2095
         # update order test
2096
-        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2097
-        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2096
+        firstly_created_but_recently_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2097
+        secondly_created_but_not_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2098 2098
         with new_revision(
2099 2099
             session=self.session,
2100 2100
             tm=transaction.manager,
@@ -2103,8 +2103,8 @@ class TestContentApi(DefaultTest):
2103 2103
             firstly_created_but_recently_updated.description = 'Just an update'
2104 2104
         api.save(firstly_created_but_recently_updated)
2105 2105
         # comment change order
2106
-        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2107
-        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2106
+        firstly_created_but_recently_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2107
+        secondly_created_but_not_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2108 2108
         comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2109 2109
 
2110 2110
         last_actives = api.get_last_active(workspace=workspace)
@@ -2153,13 +2153,13 @@ class TestContentApi(DefaultTest):
2153 2153
             session=self.session,
2154 2154
             config=self.app_config,
2155 2155
         )
2156
-        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2156
+        main_folder = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'this is randomized folder', '', True)  # nopep8
2157 2157
         # creation order test
2158
-        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2159
-        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2158
+        firstly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2159
+        secondly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2160 2160
         # update order test
2161
-        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2162
-        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2161
+        firstly_created_but_recently_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2162
+        secondly_created_but_not_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2163 2163
         with new_revision(
2164 2164
             session=self.session,
2165 2165
             tm=transaction.manager,
@@ -2168,8 +2168,8 @@ class TestContentApi(DefaultTest):
2168 2168
             firstly_created_but_recently_updated.description = 'Just an update'
2169 2169
         api.save(firstly_created_but_recently_updated)
2170 2170
         # comment change order
2171
-        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2172
-        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2171
+        firstly_created_but_recently_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2172
+        secondly_created_but_not_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2173 2173
         comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2174 2174
 
2175 2175
         selected_contents = [
@@ -2228,13 +2228,13 @@ class TestContentApi(DefaultTest):
2228 2228
             session=self.session,
2229 2229
             config=self.app_config,
2230 2230
         )
2231
-        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2231
+        main_folder = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'this is randomized folder', '', True)  # nopep8
2232 2232
         # creation order test
2233
-        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2234
-        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2233
+        firstly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2234
+        secondly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2235 2235
         # update order test
2236
-        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2237
-        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2236
+        firstly_created_but_recently_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2237
+        secondly_created_but_not_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2238 2238
         with new_revision(
2239 2239
             session=self.session,
2240 2240
             tm=transaction.manager,
@@ -2243,30 +2243,30 @@ class TestContentApi(DefaultTest):
2243 2243
             firstly_created_but_recently_updated.description = 'Just an update'
2244 2244
         api.save(firstly_created_but_recently_updated)
2245 2245
         # comment change order
2246
-        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2247
-        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2246
+        firstly_created_but_recently_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2247
+        secondly_created_but_not_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2248 2248
         comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2249 2249
 
2250
-        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=datetime.datetime.now())  # nopep8
2250
+        last_actives = api.get_last_active(workspace=workspace, limit=2)  # nopep8
2251 2251
         assert len(last_actives) == 2
2252 2252
         # comment is newest than page2
2253 2253
         assert last_actives[0] == firstly_created_but_recently_commented
2254 2254
         assert last_actives[1] == secondly_created_but_not_commented
2255 2255
 
2256
-        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=last_actives[1].get_simple_last_activity_date())  # nopep8
2256
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_content=last_actives[1])  # nopep8
2257 2257
         assert len(last_actives) == 2
2258 2258
         # last updated content is newer than other one despite creation
2259 2259
         # of the other is more recent
2260 2260
         assert last_actives[0] == firstly_created_but_recently_updated
2261 2261
         assert last_actives[1] == secondly_created_but_not_updated
2262 2262
 
2263
-        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=last_actives[1].get_simple_last_activity_date())  # nopep8
2263
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_content=last_actives[1])  # nopep8
2264 2264
         assert len(last_actives) == 2
2265 2265
         # creation order is inverted here as last created is last active
2266 2266
         assert last_actives[0] == secondly_created
2267 2267
         assert last_actives[1] == firstly_created
2268 2268
 
2269
-        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=last_actives[1].get_simple_last_activity_date())  # nopep8
2269
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_content=last_actives[1])  # nopep8
2270 2270
         assert len(last_actives) == 1
2271 2271
         # folder subcontent modification does not change folder order
2272 2272
         assert last_actives[0] == main_folder
@@ -2309,13 +2309,13 @@ class TestContentApi(DefaultTest):
2309 2309
             session=self.session,
2310 2310
             config=self.app_config,
2311 2311
         )
2312
-        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2312
+        main_folder = api.create(CONTENT_TYPES.Folder.slug, workspace, None, 'this is randomized folder', '', True)  # nopep8
2313 2313
         # creation order test
2314
-        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2315
-        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2314
+        firstly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2315
+        secondly_created = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2316 2316
         # update order test
2317
-        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2318
-        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2317
+        firstly_created_but_recently_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2318
+        secondly_created_but_not_updated = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2319 2319
         with new_revision(
2320 2320
             session=self.session,
2321 2321
             tm=transaction.manager,
@@ -2324,8 +2324,8 @@ class TestContentApi(DefaultTest):
2324 2324
             firstly_created_but_recently_updated.description = 'Just an update'
2325 2325
         api.save(firstly_created_but_recently_updated)
2326 2326
         # comment change order
2327
-        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2328
-        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2327
+        firstly_created_but_recently_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2328
+        secondly_created_but_not_commented = api.create(CONTENT_TYPES.Page.slug, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2329 2329
         comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2330 2330
 
2331 2331
         last_actives = api.get_last_active(workspace=workspace2)
@@ -2366,9 +2366,9 @@ class TestContentApi(DefaultTest):
2366 2366
             session=self.session,
2367 2367
             config=self.app_config,
2368 2368
         )
2369
-        a = api.create(ContentType.Folder, workspace, None,
2369
+        a = api.create(CONTENT_TYPES.Folder.slug, workspace, None,
2370 2370
                        'this is randomized folder', '', True)
2371
-        p = api.create(ContentType.Page, workspace, a,
2371
+        p = api.create(CONTENT_TYPES.Page.slug, workspace, a,
2372 2372
                        'this is randomized label content', '', True)
2373 2373
 
2374 2374
         with new_revision(
@@ -2422,9 +2422,9 @@ class TestContentApi(DefaultTest):
2422 2422
             session=self.session,
2423 2423
             config=self.app_config,
2424 2424
         )
2425
-        a = api.create(ContentType.Folder, workspace, None,
2425
+        a = api.create(CONTENT_TYPES.Folder.slug, workspace, None,
2426 2426
                        'this is randomized folder', '', True)
2427
-        p = api.create(ContentType.Page, workspace, a,
2427
+        p = api.create(CONTENT_TYPES.Page.slug, workspace, a,
2428 2428
                        'this is dummy label content', '', True)
2429 2429
 
2430 2430
         with new_revision(
@@ -2476,21 +2476,21 @@ class TestContentApi(DefaultTest):
2476 2476
             config=self.app_config,
2477 2477
         )
2478 2478
         a = api.create(
2479
-            content_type=ContentType.Folder,
2479
+            content_type_slug=CONTENT_TYPES.Folder.slug,
2480 2480
             workspace=workspace,
2481 2481
             parent=None,
2482 2482
             label='this is randomized folder',
2483 2483
             do_save=True
2484 2484
         )
2485 2485
         p1 = api.create(
2486
-            content_type=ContentType.Page,
2486
+            content_type_slug=CONTENT_TYPES.Page.slug,
2487 2487
             workspace=workspace,
2488 2488
             parent=a,
2489 2489
             label='this is dummy label content',
2490 2490
             do_save=True
2491 2491
         )
2492 2492
         p2 = api.create(
2493
-            content_type=ContentType.Page,
2493
+            content_type_slug=CONTENT_TYPES.Page.slug,
2494 2494
             workspace=workspace,
2495 2495
             parent=a,
2496 2496
             label='Hey ! Jon !',
@@ -2540,22 +2540,22 @@ class TestContentApi(DefaultTest):
2540 2540
         folder_1 = self._create_content_and_test(
2541 2541
             'folder_1',
2542 2542
             workspace=workspace,
2543
-            type=ContentType.Folder
2543
+            type=CONTENT_TYPES.Folder.slug
2544 2544
         )
2545 2545
         folder_2 = self._create_content_and_test(
2546 2546
             'folder_2',
2547 2547
             workspace=workspace,
2548
-            type=ContentType.Folder
2548
+            type=CONTENT_TYPES.Folder.slug
2549 2549
         )
2550 2550
         page_1 = self._create_content_and_test(
2551 2551
             'foo', workspace=workspace,
2552
-            type=ContentType.Page,
2552
+            type=CONTENT_TYPES.Page.slug,
2553 2553
             parent=folder_1
2554 2554
         )
2555 2555
         page_2 = self._create_content_and_test(
2556 2556
             'bar',
2557 2557
             workspace=workspace,
2558
-            type=ContentType.Page,
2558
+            type=CONTENT_TYPES.Page.slug,
2559 2559
             parent=folder_2
2560 2560
         )
2561 2561
 
@@ -2636,7 +2636,7 @@ class TestContentApiSecurity(DefaultTest):
2636 2636
             session=self.session,
2637 2637
             config=self.app_config,
2638 2638
         ).create(
2639
-            content_type=ContentType.Page,
2639
+            content_type_slug=CONTENT_TYPES.Page.slug,
2640 2640
             workspace=bob_workspace,
2641 2641
             label='bob_page',
2642 2642
             do_save=True,
@@ -2647,7 +2647,7 @@ class TestContentApiSecurity(DefaultTest):
2647 2647
             session=self.session,
2648 2648
             config=self.app_config,
2649 2649
         ).create(
2650
-            content_type=ContentType.Page,
2650
+            content_type_slug=CONTENT_TYPES.Page.slug,
2651 2651
             workspace=admin_workspace,
2652 2652
             label='admin_page',
2653 2653
             do_save=True,

+ 6 - 6
backend/tracim_backend/tests/models/test_content.py View File

@@ -15,7 +15,7 @@ from tracim_backend.models.revision_protection import new_revision
15 15
 from tracim_backend.models import User
16 16
 from tracim_backend.models.data import ActionDescription
17 17
 from tracim_backend.models.data import ContentRevisionRO
18
-from tracim_backend.models.data import ContentType
18
+from tracim_backend.models.contents import CONTENT_TYPES
19 19
 from tracim_backend.models.data import Workspace
20 20
 from tracim_backend.tests import StandardTest
21 21
 
@@ -95,7 +95,7 @@ class TestContent(StandardTest):
95 95
 
96 96
         content1_from_api = api.get_one(
97 97
             content1.id,
98
-            ContentType.Page,
98
+            CONTENT_TYPES.Page.slug,
99 99
             workspace1
100 100
         )
101 101
 
@@ -196,7 +196,7 @@ class TestContent(StandardTest):
196 196
         first_content = self._create_content(
197 197
             owner=user_admin,
198 198
             workspace=workspace,
199
-            type=ContentType.Page,
199
+            type=CONTENT_TYPES.Page.slug,
200 200
             label='TEST_CONTENT_1',
201 201
             description='TEST_CONTENT_DESCRIPTION_1',
202 202
             revision_type=ActionDescription.CREATION,
@@ -221,7 +221,7 @@ class TestContent(StandardTest):
221 221
         second_content = self._create_content(
222 222
             owner=user_admin,
223 223
             workspace=workspace,
224
-            type=ContentType.Page,
224
+            type=CONTENT_TYPES.Page.slug,
225 225
             label='TEST_CONTENT_2',
226 226
             description='TEST_CONTENT_DESCRIPTION_2',
227 227
             revision_type=ActionDescription.CREATION
@@ -268,7 +268,7 @@ class TestContent(StandardTest):
268 268
         created_content = self._create_content(
269 269
             owner=user_admin,
270 270
             workspace=workspace,
271
-            type=ContentType.Page,
271
+            type=CONTENT_TYPES.Page.slug,
272 272
             label='TEST_CONTENT_%s' % key,
273 273
             description='TEST_CONTENT_DESCRIPTION_%s' % key,
274 274
             revision_type=ActionDescription.CREATION
@@ -308,7 +308,7 @@ class TestContent(StandardTest):
308 308
         content = self._create_content(
309 309
             owner=user_admin,
310 310
             workspace=workspace,
311
-            type=ContentType.File,
311
+            type=CONTENT_TYPES.File.slug,
312 312
             label='TEST_CONTENT_1',
313 313
             description='TEST_CONTENT_DESCRIPTION_1',
314 314
             revision_type=ActionDescription.CREATION,

+ 4 - 3
backend/tracim_backend/tests/models/test_content_revision.py View File

@@ -5,10 +5,11 @@ from sqlalchemy import inspect
5 5
 
6 6
 from tracim_backend.models import ContentRevisionRO
7 7
 from tracim_backend.models import User
8
-from tracim_backend.models.data import ContentType
8
+from tracim_backend.models.contents import CONTENT_TYPES
9 9
 from tracim_backend.tests import DefaultTest
10 10
 from tracim_backend.tests import eq_
11 11
 
12
+
12 13
 class TestContentRevision(DefaultTest):
13 14
 
14 15
     def _new_from(self, revision):
@@ -47,14 +48,14 @@ class TestContentRevision(DefaultTest):
47 48
         folder = self._create_content_and_test(
48 49
             name='folder_1',
49 50
             workspace=workspace,
50
-            type=ContentType.Folder
51
+            type=CONTENT_TYPES.Folder.slug
51 52
         )
52 53
         page = self._create_content_and_test(
53 54
             workspace=workspace,
54 55
             parent=folder,
55 56
             name='file_1',
56 57
             description='content of file_1',
57
-            type=ContentType.Page,
58
+            type=CONTENT_TYPES.Page.slug,
58 59
             owner=admin
59 60
         )
60 61
 

+ 5 - 5
backend/tracim_backend/views/contents_api/comment_controller.py View File

@@ -20,7 +20,7 @@ from tracim_backend.views.core_api.schemas import SetCommentSchema
20 20
 from tracim_backend.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21 21
 from tracim_backend.views.core_api.schemas import NoContentSchema
22 22
 from tracim_backend.exceptions import EmptyCommentContentNotAllowed
23
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
23
+from tracim_backend.models.contents import CONTENT_TYPES
24 24
 from tracim_backend.models.revision_protection import new_revision
25 25
 from tracim_backend.models.data import UserRoleInWorkspace
26 26
 
@@ -47,7 +47,7 @@ class CommentController(Controller):
47 47
         )
48 48
         content = api.get_one(
49 49
             hapic_data.path.content_id,
50
-            content_type=ContentType.Any
50
+            content_type=CONTENT_TYPES.Any_SLUG
51 51
         )
52 52
         comments = content.get_comments()
53 53
         comments.sort(key=lambda comment: comment.created)
@@ -74,7 +74,7 @@ class CommentController(Controller):
74 74
         )
75 75
         content = api.get_one(
76 76
             hapic_data.path.content_id,
77
-            content_type=ContentType.Any
77
+            content_type=CONTENT_TYPES.Any_SLUG
78 78
         )
79 79
         comment = api.create_comment(
80 80
             content.workspace,
@@ -109,12 +109,12 @@ class CommentController(Controller):
109 109
         workspace = wapi.get_one(hapic_data.path.workspace_id)
110 110
         parent = api.get_one(
111 111
             hapic_data.path.content_id,
112
-            content_type=ContentType.Any,
112
+            content_type=CONTENT_TYPES.Any_SLUG,
113 113
             workspace=workspace
114 114
         )
115 115
         comment = api.get_one(
116 116
             hapic_data.path.comment_id,
117
-            content_type=ContentType.Comment,
117
+            content_type=CONTENT_TYPES.Comment.slug,
118 118
             workspace=workspace,
119 119
             parent=parent,
120 120
         )

+ 14 - 14
backend/tracim_backend/views/contents_api/file_controller.py View File

@@ -32,7 +32,7 @@ from tracim_backend.lib.utils.authorization import require_workspace_role
32 32
 from tracim_backend.models.data import UserRoleInWorkspace
33 33
 from tracim_backend.models.context_models import ContentInContext
34 34
 from tracim_backend.models.context_models import RevisionInContext
35
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
35
+from tracim_backend.models.contents import CONTENT_TYPES
36 36
 from tracim_backend.models.contents import file_type
37 37
 from tracim_backend.models.revision_protection import new_revision
38 38
 from tracim_backend.exceptions import EmptyLabelNotAllowed
@@ -67,7 +67,7 @@ class FileController(Controller):
67 67
         )
68 68
         content = api.get_one(
69 69
             hapic_data.path.content_id,
70
-            content_type=ContentType.Any
70
+            content_type=CONTENT_TYPES.Any_SLUG
71 71
         )
72 72
         file = request.POST['files']
73 73
         with new_revision(
@@ -101,7 +101,7 @@ class FileController(Controller):
101 101
         )
102 102
         content = api.get_one(
103 103
             hapic_data.path.content_id,
104
-            content_type=ContentType.Any
104
+            content_type=CONTENT_TYPES.Any_SLUG
105 105
         )
106 106
         file = DepotManager.get().get(content.depot_file)
107 107
         response = request.response
@@ -126,7 +126,7 @@ class FileController(Controller):
126 126
         )
127 127
         content = api.get_one(
128 128
             hapic_data.path.content_id,
129
-            content_type=ContentType.Any
129
+            content_type=CONTENT_TYPES.Any_SLUG
130 130
         )
131 131
         revision = api.get_one_revision(
132 132
             revision_id=hapic_data.path.revision_id,
@@ -160,7 +160,7 @@ class FileController(Controller):
160 160
         )
161 161
         content = api.get_one(
162 162
             hapic_data.path.content_id,
163
-            content_type=ContentType.Any
163
+            content_type=CONTENT_TYPES.Any_SLUG
164 164
         )
165 165
         pdf_preview_path = api.get_pdf_preview_path(
166 166
             content.content_id,
@@ -187,7 +187,7 @@ class FileController(Controller):
187 187
         )
188 188
         content = api.get_one(
189 189
             hapic_data.path.content_id,
190
-            content_type=ContentType.Any
190
+            content_type=CONTENT_TYPES.Any_SLUG
191 191
         )
192 192
         pdf_preview_path = api.get_full_pdf_preview_path(content.revision_id)
193 193
         return FileResponse(pdf_preview_path)
@@ -211,7 +211,7 @@ class FileController(Controller):
211 211
         )
212 212
         content = api.get_one(
213 213
             hapic_data.path.content_id,
214
-            content_type=ContentType.Any
214
+            content_type=CONTENT_TYPES.Any_SLUG
215 215
         )
216 216
         revision = api.get_one_revision(
217 217
             revision_id=hapic_data.path.revision_id,
@@ -244,7 +244,7 @@ class FileController(Controller):
244 244
         )
245 245
         content = api.get_one(
246 246
             hapic_data.path.content_id,
247
-            content_type=ContentType.Any
247
+            content_type=CONTENT_TYPES.Any_SLUG
248 248
         )
249 249
         allowed_dim = api.get_jpg_preview_allowed_dim()
250 250
         jpg_preview_path = api.get_jpg_preview_path(
@@ -276,7 +276,7 @@ class FileController(Controller):
276 276
         )
277 277
         content = api.get_one(
278 278
             hapic_data.path.content_id,
279
-            content_type=ContentType.Any
279
+            content_type=CONTENT_TYPES.Any_SLUG
280 280
         )
281 281
         jpg_preview_path = api.get_jpg_preview_path(
282 282
             content_id=content.content_id,
@@ -307,7 +307,7 @@ class FileController(Controller):
307 307
         )
308 308
         content = api.get_one(
309 309
             hapic_data.path.content_id,
310
-            content_type=ContentType.Any
310
+            content_type=CONTENT_TYPES.Any_SLUG
311 311
         )
312 312
         revision = api.get_one_revision(
313 313
             revision_id=hapic_data.path.revision_id,
@@ -358,7 +358,7 @@ class FileController(Controller):
358 358
         )
359 359
         content = api.get_one(
360 360
             hapic_data.path.content_id,
361
-            content_type=ContentType.Any
361
+            content_type=CONTENT_TYPES.Any_SLUG
362 362
         )
363 363
         return api.get_content_in_context(content)
364 364
 
@@ -381,7 +381,7 @@ class FileController(Controller):
381 381
         )
382 382
         content = api.get_one(
383 383
             hapic_data.path.content_id,
384
-            content_type=ContentType.Any
384
+            content_type=CONTENT_TYPES.Any_SLUG
385 385
         )
386 386
         with new_revision(
387 387
                 session=request.dbsession,
@@ -419,7 +419,7 @@ class FileController(Controller):
419 419
         )
420 420
         content = api.get_one(
421 421
             hapic_data.path.content_id,
422
-            content_type=ContentType.Any
422
+            content_type=CONTENT_TYPES.Any_SLUG
423 423
         )
424 424
         revisions = content.revisions
425 425
         return [
@@ -446,7 +446,7 @@ class FileController(Controller):
446 446
         )
447 447
         content = api.get_one(
448 448
             hapic_data.path.content_id,
449
-            content_type=ContentType.Any
449
+            content_type=CONTENT_TYPES.Any_SLUG
450 450
         )
451 451
         with new_revision(
452 452
                 session=request.dbsession,

+ 5 - 5
backend/tracim_backend/views/contents_api/html_document_controller.py View File

@@ -26,7 +26,7 @@ from tracim_backend.lib.utils.authorization import require_workspace_role
26 26
 from tracim_backend.exceptions import EmptyLabelNotAllowed
27 27
 from tracim_backend.models.context_models import ContentInContext
28 28
 from tracim_backend.models.context_models import RevisionInContext
29
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
29
+from tracim_backend.models.contents import CONTENT_TYPES
30 30
 from tracim_backend.models.contents import html_documents_type
31 31
 from tracim_backend.models.revision_protection import new_revision
32 32
 
@@ -52,7 +52,7 @@ class HTMLDocumentController(Controller):
52 52
         )
53 53
         content = api.get_one(
54 54
             hapic_data.path.content_id,
55
-            content_type=ContentType.Any
55
+            content_type=CONTENT_TYPES.Any_SLUG
56 56
         )
57 57
         return api.get_content_in_context(content)
58 58
 
@@ -75,7 +75,7 @@ class HTMLDocumentController(Controller):
75 75
         )
76 76
         content = api.get_one(
77 77
             hapic_data.path.content_id,
78
-            content_type=ContentType.Any
78
+            content_type=CONTENT_TYPES.Any_SLUG
79 79
         )
80 80
         with new_revision(
81 81
                 session=request.dbsession,
@@ -113,7 +113,7 @@ class HTMLDocumentController(Controller):
113 113
         )
114 114
         content = api.get_one(
115 115
             hapic_data.path.content_id,
116
-            content_type=ContentType.Any
116
+            content_type=CONTENT_TYPES.Any_SLUG
117 117
         )
118 118
         revisions = content.revisions
119 119
         return [
@@ -144,7 +144,7 @@ class HTMLDocumentController(Controller):
144 144
         )
145 145
         content = api.get_one(
146 146
             hapic_data.path.content_id,
147
-            content_type=ContentType.Any
147
+            content_type=CONTENT_TYPES.Any_SLUG
148 148
         )
149 149
         with new_revision(
150 150
                 session=request.dbsession,

+ 5 - 5
backend/tracim_backend/views/contents_api/threads_controller.py View File

@@ -25,7 +25,7 @@ from tracim_backend.lib.utils.authorization import require_workspace_role
25 25
 from tracim_backend.exceptions import EmptyLabelNotAllowed
26 26
 from tracim_backend.models.context_models import ContentInContext
27 27
 from tracim_backend.models.context_models import RevisionInContext
28
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
28
+from tracim_backend.models.contents import CONTENT_TYPES
29 29
 from tracim_backend.models.contents import thread_type
30 30
 from tracim_backend.models.revision_protection import new_revision
31 31
 
@@ -51,7 +51,7 @@ class ThreadController(Controller):
51 51
         )
52 52
         content = api.get_one(
53 53
             hapic_data.path.content_id,
54
-            content_type=ContentType.Any
54
+            content_type=CONTENT_TYPES.Any_SLUG
55 55
         )
56 56
         return api.get_content_in_context(content)
57 57
 
@@ -74,7 +74,7 @@ class ThreadController(Controller):
74 74
         )
75 75
         content = api.get_one(
76 76
             hapic_data.path.content_id,
77
-            content_type=ContentType.Any
77
+            content_type=CONTENT_TYPES.Any_SLUG
78 78
         )
79 79
         with new_revision(
80 80
                 session=request.dbsession,
@@ -112,7 +112,7 @@ class ThreadController(Controller):
112 112
         )
113 113
         content = api.get_one(
114 114
             hapic_data.path.content_id,
115
-            content_type=ContentType.Any
115
+            content_type=CONTENT_TYPES.Any_SLUG
116 116
         )
117 117
         revisions = content.revisions
118 118
         return [
@@ -138,7 +138,7 @@ class ThreadController(Controller):
138 138
         )
139 139
         content = api.get_one(
140 140
             hapic_data.path.content_id,
141
-            content_type=ContentType.Any
141
+            content_type=CONTENT_TYPES.Any_SLUG
142 142
         )
143 143
         with new_revision(
144 144
                 session=request.dbsession,

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

@@ -8,9 +8,9 @@ from marshmallow.validate import Range
8 8
 from tracim_backend.lib.utils.utils import DATETIME_FORMAT
9 9
 from tracim_backend.models.auth import Profile
10 10
 from tracim_backend.models.contents import GlobalStatus
11
+from tracim_backend.models.contents import CONTENT_STATUS
12
+from tracim_backend.models.contents import CONTENT_TYPES
11 13
 from tracim_backend.models.contents import open_status
12
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
13
-from tracim_backend.models.contents import ContentStatusLegacy as ContentStatus
14 14
 from tracim_backend.models.context_models import ActiveContentFilter
15 15
 from tracim_backend.models.context_models import AutocompleteQuery
16 16
 from tracim_backend.models.context_models import ContentIdsQuery
@@ -355,9 +355,9 @@ class FilterContentQuerySchema(marshmallow.Schema):
355 355
         validate=Range(min=0, max=1, error="Value must be 0 or 1"),
356 356
     )
357 357
     content_type = marshmallow.fields.String(
358
-        example=ContentType.Any,
359
-        default=ContentType.Any,
360
-        validate=OneOf(ContentType.allowed_type_values())
358
+        example=CONTENT_TYPES.Any_SLUG,
359
+        default=CONTENT_TYPES.Any_SLUG,
360
+        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug())
361 361
     )
362 362
 
363 363
     @post_load
@@ -373,9 +373,11 @@ class ActiveContentFilterQuerySchema(marshmallow.Schema):
373 373
                     'the first limit elem (according to offset)',
374 374
         validate=Range(min=0, error="Value must be positive or 0"),
375 375
     )
376
-    before_datetime = marshmallow.fields.DateTime(
377
-        format=DATETIME_FORMAT,
378
-        description='return only content lastly updated before this date',
376
+    before_content_id = marshmallow.fields.Int(
377
+        example=41,
378
+        default=None,
379
+        allow_none=True,
380
+        description='return only content updated before this content',
379 381
     )
380 382
     @post_load
381 383
     def make_content_filter(self, data):
@@ -605,7 +607,7 @@ class StatusSchema(marshmallow.Schema):
605 607
 class ContentTypeSchema(marshmallow.Schema):
606 608
     slug = marshmallow.fields.String(
607 609
         example='pagehtml',
608
-        validate=OneOf(ContentType.allowed_types()),
610
+        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug()),
609 611
     )
610 612
     fa_icon = marshmallow.fields.String(
611 613
         example='fa-file-text-o',
@@ -658,17 +660,20 @@ class ContentCreationSchema(marshmallow.Schema):
658 660
         description='Title of the content to create'
659 661
     )
660 662
     content_type = marshmallow.fields.String(
661
-        example='html-documents',
662
-        validate=OneOf(ContentType.allowed_types_for_folding()),  # nopep8
663
+        example='html-document',
664
+        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug()),
663 665
     )
664 666
     parent_id = marshmallow.fields.Integer(
665 667
         example=35,
666
-        description='content_id of parent content, if content should be placed in a folder, this should be folder content_id.'
668
+        description='content_id of parent content, if content should be placed in a folder, this should be folder content_id.', # nopep8
669
+        allow_none=True,
670
+        default=None,
671
+        validate=Range(min=1, error="Value must be positive"),
667 672
     )
668 673
 
669 674
 
670 675
     @post_load
671
-    def make_content_filter(self, data):
676
+    def make_content_creation(self, data):
672 677
         return ContentCreation(**data)
673 678
 
674 679
 
@@ -690,13 +695,13 @@ class ContentDigestSchema(marshmallow.Schema):
690 695
     )
691 696
     label = marshmallow.fields.Str(example='Intervention Report 12')
692 697
     content_type = marshmallow.fields.Str(
693
-        example='html-documents',
694
-        validate=OneOf(ContentType.allowed_types()),
698
+        example='html-document',
699
+        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug()),
695 700
     )
696 701
     sub_content_types = marshmallow.fields.List(
697 702
         marshmallow.fields.String(
698 703
             example='html-content',
699
-            validate=OneOf(ContentType.allowed_types())
704
+            validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug())
700 705
         ),
701 706
         description='list of content types allowed as sub contents. '
702 707
                     'This field is required for folder contents, '
@@ -704,7 +709,7 @@ class ContentDigestSchema(marshmallow.Schema):
704 709
     )
705 710
     status = marshmallow.fields.Str(
706 711
         example='closed-deprecated',
707
-        validate=OneOf(ContentStatus.allowed_values()),
712
+        validate=OneOf(CONTENT_STATUS.get_all_slugs_values()),
708 713
         description='this slug is found in content_type available statuses',
709 714
         default=open_status
710 715
     )
@@ -729,6 +734,7 @@ class ReadStatusSchema(marshmallow.Schema):
729 734
 # Content
730 735
 #####
731 736
 
737
+
732 738
 class ContentSchema(ContentDigestSchema):
733 739
     current_revision_id = marshmallow.fields.Int(example=12)
734 740
     created = marshmallow.fields.DateTime(
@@ -848,7 +854,7 @@ class FileContentModifySchema(TextBasedContentModifySchema):
848 854
 class SetContentStatusSchema(marshmallow.Schema):
849 855
     status = marshmallow.fields.Str(
850 856
         example='closed-deprecated',
851
-        validate=OneOf(ContentStatus.allowed_values()),
857
+        validate=OneOf(CONTENT_STATUS.get_all_slugs_values()),
852 858
         description='this slug is found in content_type available statuses',
853 859
         default=open_status,
854 860
         required=True,

+ 3 - 3
backend/tracim_backend/views/core_api/system_controller.py View File

@@ -5,7 +5,7 @@ from tracim_backend.exceptions import InsufficientUserProfile
5 5
 from tracim_backend.lib.utils.authorization import require_profile
6 6
 from tracim_backend.models import Group
7 7
 from tracim_backend.models.applications import applications
8
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
8
+from tracim_backend.models.contents import CONTENT_TYPES
9 9
 
10 10
 try:  # Python 3.5+
11 11
     from http import HTTPStatus
@@ -39,8 +39,8 @@ class SystemController(Controller):
39 39
         """
40 40
         Get list of alls content types availables in this tracim instance.
41 41
         """
42
-        content_types_slugs = ContentType.allowed_types_for_folding()
43
-        content_types = [ContentType(slug) for slug in content_types_slugs]
42
+        content_types_slugs = CONTENT_TYPES.endpoint_allowed_types_slug()
43
+        content_types = [CONTENT_TYPES.get_one_by_slug(slug) for slug in content_types_slugs]
44 44
         return content_types
45 45
 
46 46
     def bind(self, configurator: Configurator) -> None:

+ 11 - 2
backend/tracim_backend/views/core_api/user_controller.py View File

@@ -1,4 +1,5 @@
1 1
 from pyramid.config import Configurator
2
+
2 3
 try:  # Python 3.5+
3 4
     from http import HTTPStatus
4 5
 except ImportError:
@@ -33,6 +34,7 @@ from tracim_backend.views.core_api.schemas import UserWorkspaceAndContentIdPathS
33 34
 from tracim_backend.views.core_api.schemas import ContentDigestSchema
34 35
 from tracim_backend.views.core_api.schemas import ActiveContentFilterQuerySchema
35 36
 from tracim_backend.views.core_api.schemas import WorkspaceDigestSchema
37
+from tracim_backend.models.contents import CONTENT_TYPES
36 38
 
37 39
 SWAGGER_TAG__USER_ENDPOINTS = 'Users'
38 40
 
@@ -308,10 +310,17 @@ class UserController(Controller):
308 310
         workspace = None
309 311
         if hapic_data.path.workspace_id:
310 312
             workspace = wapi.get_one(hapic_data.path.workspace_id)
313
+        before_content = None
314
+        if content_filter.before_content_id:
315
+            before_content = api.get_one(
316
+                content_id=content_filter.before_content_id,
317
+                workspace=workspace,
318
+                content_type=CONTENT_TYPES.Any_SLUG
319
+            )
311 320
         last_actives = api.get_last_active(
312 321
             workspace=workspace,
313 322
             limit=content_filter.limit or None,
314
-            before_datetime=content_filter.before_datetime or None,
323
+            before_content=before_content,
315 324
         )
316 325
         return [
317 326
             api.get_content_in_context(content)
@@ -345,7 +354,7 @@ class UserController(Controller):
345 354
         last_actives = api.get_last_active(
346 355
             workspace=workspace,
347 356
             limit=None,
348
-            before_datetime=None,
357
+            before_content=None,
349 358
             content_ids=hapic_data.query.contents_ids or None
350 359
         )
351 360
         return [

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

@@ -46,7 +46,7 @@ from tracim_backend.views.core_api.schemas import ContentDigestSchema
46 46
 from tracim_backend.views.core_api.schemas import WorkspaceSchema
47 47
 from tracim_backend.views.core_api.schemas import WorkspaceIdPathSchema
48 48
 from tracim_backend.views.core_api.schemas import WorkspaceMemberSchema
49
-from tracim_backend.models.contents import ContentTypeLegacy as ContentType
49
+from tracim_backend.models.contents import CONTENT_TYPES
50 50
 from tracim_backend.models.revision_protection import new_revision
51 51
 
52 52
 SWAGGER_TAG_WORKSPACE_ENDPOINTS = 'Workspaces'
@@ -259,7 +259,7 @@ class WorkspaceController(Controller):
259 259
         contents = api.get_all(
260 260
             parent_id=content_filter.parent_id,
261 261
             workspace=request.current_workspace,
262
-            content_type=content_filter.content_type or ContentType.Any,
262
+            content_type=content_filter.content_type or CONTENT_TYPES.Any_SLUG,
263 263
         )
264 264
         contents = [
265 265
             api.get_content_in_context(content) for content in contents
@@ -291,14 +291,14 @@ class WorkspaceController(Controller):
291 291
         parent = None
292 292
         if creation_data.parent_id:
293 293
             try:
294
-                parent = api.get_one(content_id=creation_data.parent_id, content_type=ContentType.Any)  # nopep8
294
+                parent = api.get_one(content_id=creation_data.parent_id, content_type=CONTENT_TYPES.Any_SLUG)  # nopep8
295 295
             except ContentNotFound as exc:
296 296
                 raise ParentNotFound(
297 297
                     'Parent with content_id {} not found'.format(creation_data.parent_id)
298 298
                 ) from exc
299 299
         content = api.create(
300 300
             label=creation_data.label,
301
-            content_type=creation_data.content_type,
301
+            content_type_slug=creation_data.content_type,
302 302
             workspace=request.current_workspace,
303 303
             parent=parent,
304 304
         )
@@ -333,10 +333,10 @@ class WorkspaceController(Controller):
333 333
         )
334 334
         content = api.get_one(
335 335
             path_data.content_id,
336
-            content_type=ContentType.Any
336
+            content_type=CONTENT_TYPES.Any_SLUG
337 337
         )
338 338
         new_parent = api.get_one(
339
-            move_data.new_parent_id, content_type=ContentType.Any
339
+            move_data.new_parent_id, content_type=CONTENT_TYPES.Any_SLUG
340 340
         )
341 341
 
342 342
         new_workspace = request.candidate_workspace
@@ -354,7 +354,7 @@ class WorkspaceController(Controller):
354 354
             )
355 355
         updated_content = api.get_one(
356 356
             path_data.content_id,
357
-            content_type=ContentType.Any
357
+            content_type=CONTENT_TYPES.Any_SLUG
358 358
         )
359 359
         return api.get_content_in_context(updated_content)
360 360
 
@@ -380,7 +380,7 @@ class WorkspaceController(Controller):
380 380
         )
381 381
         content = api.get_one(
382 382
             path_data.content_id,
383
-            content_type=ContentType.Any
383
+            content_type=CONTENT_TYPES.Any_SLUG
384 384
         )
385 385
         with new_revision(
386 386
                 session=request.dbsession,
@@ -413,7 +413,7 @@ class WorkspaceController(Controller):
413 413
         )
414 414
         content = api.get_one(
415 415
             path_data.content_id,
416
-            content_type=ContentType.Any
416
+            content_type=CONTENT_TYPES.Any_SLUG
417 417
         )
418 418
         with new_revision(
419 419
                 session=request.dbsession,
@@ -443,7 +443,7 @@ class WorkspaceController(Controller):
443 443
             session=request.dbsession,
444 444
             config=app_config,
445 445
         )
446
-        content = api.get_one(path_data.content_id, content_type=ContentType.Any)  # nopep8
446
+        content = api.get_one(path_data.content_id, content_type=CONTENT_TYPES.Any_SLUG)  # nopep8
447 447
         with new_revision(
448 448
                 session=request.dbsession,
449 449
                 tm=transaction.manager,
@@ -475,7 +475,7 @@ class WorkspaceController(Controller):
475 475
         )
476 476
         content = api.get_one(
477 477
             path_data.content_id,
478
-            content_type=ContentType.Any
478
+            content_type=CONTENT_TYPES.Any_SLUG
479 479
         )
480 480
         with new_revision(
481 481
                 session=request.dbsession,

+ 59 - 0
build_full_frontend.sh View File

@@ -9,23 +9,82 @@ fi
9 9
 
10 10
 echo -e "\n${BROWN}/!\ ${NC}this script does not run 'npm install'\n${BROWN}/!\ ${NC}it also assumes your webpack dev server of frontend is running"
11 11
 
12
+# Tracim Lib
13
+
12 14
 log "cd frontend_lib"
13 15
 cd frontend_lib
14 16
 log "npm run buildtracimlib$windoz"
15 17
 npm run buildtracimlib$windoz
16 18
 cd -
17 19
 
20
+# app Html Document
21
+
18 22
 log "cd frontend_app_html-document"
19 23
 cd frontend_app_html-document
24
+
20 25
 log "npm run build$windoz # for frontend_app_html-document"
21 26
 npm run build$windoz
27
+
22 28
 log "cp dist/html-document.app.js"
23 29
 cp dist/html-document.app.js ../frontend/dist/app
30
+
31
+log "cp i18next.scanner/en/translation.json ../frontend/dist/app/tml-document_en_translation.json"
32
+cp i18next.scanner/en/translation.json ../frontend/dist/app/html-document_en_translation.json
33
+
34
+log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/html-document_fr_translation.json"
35
+cp i18next.scanner/fr/translation.json ../frontend/dist/app/html-document_fr_translation.json
24 36
 cd -
25 37
 
38
+# app Thread
39
+
26 40
 log "cd frontend_app_thread"
27 41
 cd frontend_app_thread
42
+
28 43
 log "npm run build$windoz # for frontend_app_thread"
29 44
 npm run build$windoz
45
+
30 46
 log "cp dist/thread.app.js"
31 47
 cp dist/thread.app.js ../frontend/dist/app
48
+
49
+log "cp i18next.scanner/en/translation.json ../frontend/dist/app/thread_en_translation.json"
50
+cp i18next.scanner/en/translation.json ../frontend/dist/app/thread_en_translation.json
51
+
52
+log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/thread_fr_translation.json"
53
+cp i18next.scanner/fr/translation.json ../frontend/dist/app/thread_fr_translation.json
54
+cd -
55
+
56
+# app Workspace
57
+
58
+log "cd frontend_app_workspace"
59
+cd frontend_app_workspace
60
+
61
+log "npm run build$windoz # for frontend_app_workspace"
62
+npm run build$windoz
63
+
64
+log "cp dist/workspace.app.js"
65
+cp dist/workspace.app.js ../frontend/dist/app
66
+
67
+log "cp i18next.scanner/en/translation.json ../frontend/dist/app/workspace_en_translation.json"
68
+cp i18next.scanner/en/translation.json ../frontend/dist/app/workspace_en_translation.json
69
+
70
+log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/workspace_fr_translation.json"
71
+cp i18next.scanner/fr/translation.json ../frontend/dist/app/workspace_fr_translation.json
72
+cd -
73
+
74
+# app Admin Workspace User
75
+
76
+log "cd frontend_app_admin_workspace_user"
77
+cd frontend_app_admin_workspace_user
78
+
79
+log "npm run build$windoz # for frontend_app_thread"
80
+npm run build$windoz
81
+
82
+log "cp dist/admin_workspace_user.app.js"
83
+cp dist/admin_workspace_user.app.js ../frontend/dist/app
84
+
85
+log "cp i18next.scanner/en/translation.json ../frontend/dist/app/admin_workspace_user_en_translation.json"
86
+cp i18next.scanner/en/translation.json ../frontend/dist/app/admin_workspace_user_en_translation.json
87
+
88
+log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/admin_workspace_user_fr_translation.json"
89
+cp i18next.scanner/fr/translation.json ../frontend/dist/app/admin_workspace_user_fr_translation.json
90
+cd -

+ 1 - 1
frontend/README.md View File

@@ -58,7 +58,7 @@ To test them inside Tracim, you need to build them and copast them to tracim_fro
58 58
 You also need to make the mock api able to tell tracim_frontend that it handle you app :
59 59
 - add an entry for you App in tracim_frontend/jsonserver/static_db.json in the `app_config` property
60 60
 - reload your mock api server
61
-- add the source of your app in tracim_frontend/dist/index.html and an entry to the switch case of the function `GLOBAL_renderAppFull`. All of this will be handled by backend later on, this is all work in progress stuffs.
61
+- add the source of your app in tracim_frontend/dist/index.html and an entry to the switch case of the function `GLOBAL_renderAppFeature`. All of this will be handled by backend later on, this is all work in progress stuffs.
62 62
 
63 63
 
64 64
 #### Urls list

+ 25 - 6
frontend/dist/appInterface.js View File

@@ -3,12 +3,16 @@
3 3
 
4 4
   getSelectedApp = name => {
5 5
     switch (name) {
6
-      case 'html-documents':
6
+      case 'workspace':
7
+        return appWorkspace
8
+      case 'html-document':
7 9
         return appHtmlDocument
8 10
       case 'thread':
9 11
         return appThread
10 12
       case 'file':
11 13
         return appFile
14
+      case 'admin_workspace_user':
15
+        return appAdminWorkspaceUser
12 16
       default:
13 17
         return null
14 18
     }
@@ -19,15 +23,30 @@
19 23
   // use module.export and require
20 24
   // doesn't work, cant resolve a file outside of the build dir
21 25
 
22
-  GLOBAL_renderAppFull = app => {
23
-    console.log('%cGLOBAL_renderAppFull', 'color: #5cebeb', app)
26
+  GLOBAL_renderAppFeature = app => {
27
+    console.log('%cGLOBAL_renderAppFeature', 'color: #5cebeb', app)
24 28
 
25 29
     const selectedApp = getSelectedApp(app.config.slug)
26 30
 
27 31
     if (selectedApp.isRendered) {
28
-      GLOBAL_dispatchEvent({type: `${app.config.slug}_showApp`, data: app}) // handled by html-documents:src/container/HtmlDocument.jsx
32
+      GLOBAL_dispatchEvent({type: `${app.config.slug}_showApp`, data: app}) // handled by html-documents:src/container/AdminWorkspaceUser.jsx
29 33
     } else {
30
-      selectedApp.renderAppFull(app)
34
+      selectedApp.renderAppFeature(app)
35
+      selectedApp.isRendered = true
36
+      prevSelectedApp.isRendered = false
37
+      prevSelectedApp = selectedApp
38
+    }
39
+  }
40
+
41
+  GLOBAL_renderAppFullscreen = app => {
42
+    console.log('%cGLOBAL_renderAppFullscreen', 'color: #5cebeb', app)
43
+
44
+    const selectedApp = getSelectedApp(app.config.slug)
45
+
46
+    if (selectedApp.isRendered) {
47
+      GLOBAL_dispatchEvent({type: `${app.config.slug}_showApp`, data: app}) // handled by html-documents:src/container/AdminWorkspaceUser.jsx
48
+    } else {
49
+      selectedApp.renderAppFullscreen(app)
31 50
       selectedApp.isRendered = true
32 51
       prevSelectedApp.isRendered = false
33 52
       prevSelectedApp = selectedApp
@@ -64,7 +83,7 @@
64 83
         console.log('%cGLOBAL_eventReducer Custom Event', 'color: #28a745', type, data)
65 84
         if (prevSelectedApp.name === '') return
66 85
 
67
-        prevSelectedApp.unmountApp('appContainer')
86
+        prevSelectedApp.unmountApp('appFeatureContainer')
68 87
         prevSelectedApp.unmountApp('popupCreateContentContainer')
69 88
         prevSelectedApp.isRendered = false
70 89
         break

+ 2 - 0
frontend/dist/index.html View File

@@ -51,9 +51,11 @@
51 51
     <script src='/tracim.vendor.bundle.js'></script>
52 52
     <script src='/tracim.app.entry.js'></script>
53 53
 
54
+    <script src='/app/workspace.app.js'></script>
54 55
     <script src='/app/html-document.app.js'></script>
55 56
     <script src='/app/thread.app.js'></script>
56 57
     <!-- <script src='/app/file.app.js'></script> -->
58
+    <script src='/app/admin_workspace_user.app.js'></script>
57 59
 
58 60
     <script src="/dev/jquery-3.2.1.js"></script>
59 61
     <script src="/dev/popper-1.12.3.js"></script>

+ 16 - 0
frontend/i18next.scanner.js View File

@@ -0,0 +1,16 @@
1
+const scanner = require('i18next-scanner')
2
+const vfs = require('vinyl-fs')
3
+
4
+const option = require('../i18next.option.js')
5
+
6
+// this script is run by npm run build-translation and generate i18next.scanner/**/*.json
7
+
8
+// --------------------
9
+// 2018/07/27 - currently, last version is 2.6.5 but a bug is spaming log with errors. So I'm using 2.6.1
10
+// this issue seems related : https://github.com/i18next/i18next-scanner/issues/88
11
+// --------------------
12
+
13
+vfs.src(['./src/**/*.jsx'])
14
+  // .pipe(sort()) // Sort files in stream by path
15
+  .pipe(scanner(option))
16
+  .pipe(vfs.dest('./i18next.scanner'))

+ 68 - 0
frontend/i18next.scanner/en/translation.json View File

@@ -0,0 +1,68 @@
1
+{
2
+  "Create your own collaborative workspaces on trac.im": "Create your own collaborative workspaces on trac.im",
3
+  "Copyright 2013 - 2017": "Copyright 2013 - 2017",
4
+  "Disconnection error": "Disconnection error",
5
+  "Error": "Error",
6
+  "Email or password invalid": "Email or password invalid",
7
+  "Type": "Type",
8
+  "Status": "Status",
9
+  "Create in folder...": "Create in folder...",
10
+  "Search...": "Search...",
11
+  "Create a workspace": "Create a workspace",
12
+  "Title": "Title",
13
+  "Change": "Change",
14
+  "Move": "Move",
15
+  "Download": "Download",
16
+  "Archive": "Archive",
17
+  "Delete": "Delete",
18
+  "My Account": "My Account",
19
+  "Logout": "Logout",
20
+  "Notification": "Notification",
21
+  "Archive Topic": "Archive Topic",
22
+  "Deleted File": "Deleted File",
23
+  "Send": "Send",
24
+  "Workspace and notifications": "Workspace and notifications",
25
+  "Workspace": "Workspace",
26
+  "Role": "Role",
27
+  "Account information": "Account information",
28
+  "Change your password": "Change your password",
29
+  "Password": "Password",
30
+  "Change your name": "Change your name",
31
+  "Change your email": "Change your email",
32
+  "Change your Timezone": "Change your Timezone",
33
+  "Calendar": "Calendar",
34
+  "Access your personal calendar": "Access your personal calendar",
35
+  "Dashboard": "Dashboard",
36
+  "Active advanced Dashboard": "Active advanced Dashboard",
37
+  "Change your status": "Change your status",
38
+  "subscriber": "subscriber",
39
+  "unsubscribed": "unsubscribed",
40
+  "Start a new Thread": "Start a new Thread",
41
+  "Writing a document": "Writing a document",
42
+  "Upload a file": "Upload a file",
43
+  "Start a videoconference": "Start a videoconference",
44
+  "View the Calendar": "View the Calendar",
45
+  "Recent activity": "Recent activity",
46
+  "Mark everything as read": "Mark everything as read",
47
+  "See more": "See more",
48
+  "Member List": "Member List",
49
+  "Add a member": "Add a member",
50
+  "Enter the name or email of the member": "Enter the name or email of the member",
51
+  "Create an account": "Create an account",
52
+  "Choose the role of the member": "Choose the role of the member",
53
+  "Supervisor": "Supervisor",
54
+  "Content Manager": "Content Manager",
55
+  "Contributor": "Contributor",
56
+  "Reader": "Reader",
57
+  "Validate": "Validate",
58
+  "Implement Tracim in your explorer": "Implement Tracim in your explorer",
59
+  "Find all your documents deposited online directly on your computer via the workstation, without going through the software.": "Find all your documents deposited online directly on your computer via the workstation, without going through the software.",
60
+  "Workspace Calendar": "Workspace Calendar",
61
+  "Each workspace has its own calendar.": "Each workspace has its own calendar.",
62
+  "Email Adress": "Email Adress",
63
+  "Explore the workspace": "Explore the workspace",
64
+  "Old password": "Old password",
65
+  "New password": "New password",
66
+  "Name:": "Name:",
67
+  "Email Adress:": "Email Adress:"
68
+}

+ 68 - 0
frontend/i18next.scanner/fr/translation.json View File

@@ -0,0 +1,68 @@
1
+{
2
+  "Create your own collaborative workspaces on trac.im": "Créer votre propre espace de travail sur trac.im",
3
+  "Copyright 2013 - 2017": "Copyright 2013 - 2017",
4
+  "Disconnection error": "Erreur de déconnexion",
5
+  "Error": "Erreur",
6
+  "Email or password invalid": "Email ou mot de passe invalid",
7
+  "Type": "Type",
8
+  "Status": "Statut",
9
+  "Create in folder...": "Créer dans le dossier ...",
10
+  "Search...": "Rechercher ...",
11
+  "Create a workspace": "Créer un espace de travail",
12
+  "Title": "Titre",
13
+  "Change": "Modifier",
14
+  "Move": "Déplacer",
15
+  "Download": "Télécharger",
16
+  "Archive": "Archiver",
17
+  "Delete": "Supprimer",
18
+  "My Account": "Mon Compte",
19
+  "Logout": "Se Déconnecter",
20
+  "Notification": "Notification",
21
+  "Archive Topic": "Conversation archivée",
22
+  "Deleted File": "Fichier supprimé",
23
+  "Send": "Envoyer",
24
+  "Workspace and notifications": "Espace de travail & Notifications",
25
+  "Workspace": "Espace de travail",
26
+  "Role": "Rôle",
27
+  "Account information": "Information du compte",
28
+  "Change your password": "Changer votre mot de passe",
29
+  "Password": "Mot de passe",
30
+  "Change your name": "Modifier votre nom",
31
+  "Change your email": "Modifier votre email",
32
+  "Change your Timezone": "Modifier votre fuseau horaire",
33
+  "Calendar": "Calendrier",
34
+  "Access your personal calendar": "Accéder à votre calendrier personnel",
35
+  "Dashboard": "Tableau de bord",
36
+  "Active advanced Dashboard": "Activer le mode avancé ",
37
+  "Change your status": "Changer de status",
38
+  "subscriber": "Abonné(e)",
39
+  "unsubscribed": "Non Abonné(e)",
40
+  "Start a new Thread": "Débuter une nouvelle discussion",
41
+  "Writing a document": "Rédiger un document",
42
+  "Upload a file": "Importer un fichier",
43
+  "Start a videoconference": "Débuter une visioconférence",
44
+  "View the Calendar": "Voir le Calendrier",
45
+  "Explore the workspace": "Explorer l'espace de travail",
46
+  "Recent activity": "Activité récente",
47
+  "Mark everything as read": "Tout marquer comme lu",
48
+  "See more": "Voir plus",
49
+  "Member List": "Liste des membres",
50
+  "Add a member": "Ajouter un membre",
51
+  "Enter the name or email of the member": "Renseigner le nom ou l'email de l'utilisateur",
52
+  "Create an account": "Créer un compte",
53
+  "Supervisor": "Responsable",
54
+  "Content Manager": "Gestionnaire de contenu",
55
+  "Contributor": "Contributeur",
56
+  "Reader": "Lecteur",
57
+  "Validate": "Validé",
58
+  "Implement Tracim in your explorer": "Implémenter Tracim dans votre explorateur",
59
+  "Find all your documents deposited online directly on your computer via the workstation, without going through the software.": "Retrouvez tous vos documents déposés en ligne directement sur votre ordinateur via le poste de travail, sans passer par le logiciel.",
60
+  "Workspace Calendar": "Calendrier de l'espace de travail",
61
+  "Each workspace has its own calendar.": "Chaque espace de travail à son propre calendrier",
62
+  "Choose the role of the member": "Choisissez le rôle du membre",
63
+  "Email Adress": "Adresse email",
64
+  "Old password": "Ancien mot de passe",
65
+  "New password": "Nouveau mot de passe",
66
+  "Name:": "Nom :",
67
+  "Email Adress:": "Adresse mail :"
68
+}

+ 6 - 6
frontend/jsonserver/static_db.json View File

@@ -35,7 +35,7 @@
35 35
     "customClass": "wsContentPageHtml",
36 36
     "icon": "fa fa-fw fa-file-text-o",
37 37
     "color": "#3f52e3",
38
-    "domContainer": "appContainer"
38
+    "domContainer": "appFeatureContainer"
39 39
   }, {
40 40
     "name": "PageMarkdown",
41 41
     "label": {
@@ -47,7 +47,7 @@
47 47
     "customClass": "wsContentPageMarkdown",
48 48
     "icon": "fa fa-fw fa-file-code-o",
49 49
     "color": "#e0082b",
50
-    "domContainer": "appContainer"
50
+    "domContainer": "appFeatureContainer"
51 51
   }, {
52 52
     "name": "File",
53 53
     "label": {
@@ -59,7 +59,7 @@
59 59
     "customClass": "wsContentFile",
60 60
     "icon": "fa fa-fw fa-file-image-o",
61 61
     "color": "#263462",
62
-    "domContainer": "appContainer"
62
+    "domContainer": "appFeatureContainer"
63 63
   }, {
64 64
     "name": "Thread",
65 65
     "label": {
@@ -71,7 +71,7 @@
71 71
     "customClass": "wsContentThread",
72 72
     "icon": "fa fa-fw fa-comments-o",
73 73
     "color": "#2674d3",
74
-    "domContainer": "appContainer"
74
+    "domContainer": "appFeatureContainer"
75 75
   }, {
76 76
     "name": "Task",
77 77
     "label": {
@@ -83,7 +83,7 @@
83 83
     "customClass": "wsContentTask",
84 84
     "icon": "fa fa-fw fa-list-ul",
85 85
     "color": "#2d5a88",
86
-    "domContainer": "appContainer"
86
+    "domContainer": "appFeatureContainer"
87 87
   }, {
88 88
     "name": "Issue",
89 89
     "label": {
@@ -95,7 +95,7 @@
95 95
     "customClass": "wsContentIssue",
96 96
     "icon": "fa fa-fw fa-ticket",
97 97
     "color": "#a4835e",
98
-    "domContainer": "appContainer"
98
+    "domContainer": "appFeatureContainer"
99 99
   }],
100 100
   "workspace_detail": {
101 101
     "id": 1,

+ 5 - 1
frontend/package.json View File

@@ -10,6 +10,7 @@
10 10
     "servdev-dashboard": "NODE_ENV=development webpack-dashboard -m -- webpack-dev-server --watch --colors --inline --hot --progress",
11 11
     "buildwindoz": "set NODE_ENV=production&& webpack -p",
12 12
     "build": "NODE_ENV=production webpack -p",
13
+    "build-translation": "node i18next.scanner.js",
13 14
     "test": "echo \"Error: no test specified\" && exit 1"
14 15
   },
15 16
   "author": "",
@@ -31,6 +32,7 @@
31 32
     "js-cookie": "^2.2.0",
32 33
     "prop-types": "^15.6.0",
33 34
     "query-string": "^6.1.0",
35
+    "radium": "^0.24.1",
34 36
     "react": "^16.0.0",
35 37
     "react-animate-height": "^0.10.10",
36 38
     "react-dom": "^16.0.0",
@@ -52,6 +54,7 @@
52 54
     "whatwg-fetch": "^2.0.3"
53 55
   },
54 56
   "devDependencies": {
57
+    "i18next-scanner": "^2.6.1",
55 58
     "json-server": "^0.12.0",
56 59
     "webpack-dashboard": "^1.0.2",
57 60
     "webpack-dev-server": "^2.9.2"
@@ -61,7 +64,8 @@
61 64
       "fetch",
62 65
       "btoa",
63 66
       "history",
64
-      "GLOBAL_renderAppFull",
67
+      "GLOBAL_renderAppFeature",
68
+      "GLOBAL_renderAppFullscreen",
65 69
       "GLOBAL_renderAppPopupCreation",
66 70
       "GLOBAL_dispatchEvent",
67 71
       "GLOBAL_hideApp"

+ 40 - 8
frontend/src/action-creator.async.js View File

@@ -11,6 +11,8 @@ import {
11 11
   setUserRole,
12 12
   WORKSPACE,
13 13
   WORKSPACE_LIST,
14
+  WORKSPACE_DETAIL,
15
+  WORKSPACE_MEMBER_LIST,
14 16
   FOLDER,
15 17
   setFolderData,
16 18
   APP_LIST,
@@ -57,8 +59,20 @@ const fetchWrapper = async ({url, param, actionName, dispatch, debug = false}) =
57 59
   })()
58 60
   if (debug) console.log(`fetch ${param.method}/${actionName} result: `, fetchResult)
59 61
 
60
-  if ([200, 204, 304].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/SUCCESS`, data: fetchResult.json})
61
-  else if ([400, 404, 500].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/FAILED`, data: fetchResult.json})
62
+  // if ([200, 204, 304].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/SUCCESS`, data: fetchResult.json})
63
+  // else if ([400, 404, 500].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/FAILED`, data: fetchResult.json})
64
+  switch (fetchResult.status) {
65
+    case 200:
66
+    case 204:
67
+    case 304:
68
+      dispatch({type: `${param.method}/${actionName}/SUCCESS`, data: fetchResult.json})
69
+      break
70
+    case 400:
71
+    case 404:
72
+    case 500:
73
+      dispatch({type: `${param.method}/${actionName}/FAILED`, data: fetchResult.json})
74
+      break
75
+  }
62 76
 
63 77
   return fetchResult
64 78
 }
@@ -161,9 +175,9 @@ export const getWorkspaceList = user => dispatch => {
161 175
   })
162 176
 }
163 177
 
164
-export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch => {
178
+export const getWorkspaceDetail = (user, idWorkspace) => dispatch => {
165 179
   return fetchWrapper({
166
-    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents?parent_id=${idParent}`,
180
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}`,
167 181
     param: {
168 182
       headers: {
169 183
         ...FETCH_CONFIG.headers,
@@ -171,16 +185,34 @@ export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch
171 185
       },
172 186
       method: 'GET'
173 187
     },
174
-    actionName: WORKSPACE,
188
+    actionName: WORKSPACE_DETAIL,
175 189
     dispatch
176 190
   })
177 191
 }
178 192
 
179
-export const getWorkspaceContent = (idWorkspace, idContent) => dispatch => {
193
+export const getWorkspaceMemberList = (user, idWorkspace) => dispatch => {
180 194
   return fetchWrapper({
181
-    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents/${idContent}`,
195
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/members`,
182 196
     param: {
183
-      headers: {...FETCH_CONFIG.headers},
197
+      headers: {
198
+        ...FETCH_CONFIG.headers,
199
+        'Authorization': 'Basic ' + user.auth
200
+      },
201
+      method: 'GET'
202
+    },
203
+    actionName: WORKSPACE_MEMBER_LIST,
204
+    dispatch
205
+  })
206
+}
207
+
208
+export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch => {
209
+  return fetchWrapper({
210
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents?parent_id=${idParent}`,
211
+    param: {
212
+      headers: {
213
+        ...FETCH_CONFIG.headers,
214
+        'Authorization': 'Basic ' + user.auth
215
+      },
184 216
       method: 'GET'
185 217
     },
186 218
     actionName: WORKSPACE,

+ 45 - 28
frontend/src/action-creator.sync.js View File

@@ -1,44 +1,61 @@
1
+export const SET = 'Set'
2
+export const UPDATE = 'Update'
3
+export const ADD = 'Add'
4
+export const REMOVE = 'Remove'
5
+
1 6
 export const TIMEZONE = 'Timezone'
2
-export const setTimezone = timezone => ({ type: `Set/${TIMEZONE}`, timezone })
7
+export const setTimezone = timezone => ({ type: `${SET}/${TIMEZONE}`, timezone })
3 8
 
4 9
 export const FLASH_MESSAGE = 'FlashMessage'
5 10
 export const newFlashMessage = (msgText = '', msgType = 'info', msgDelay = 5000) => dispatch => {
6 11
   msgDelay !== 0 && window.setTimeout(() => dispatch(removeFlashMessage(msgText)), msgDelay)
7 12
   return dispatch(addFlashMessage({message: msgText, type: msgType}))
8 13
 }
9
-export const addFlashMessage = msg => ({ type: `Add/${FLASH_MESSAGE}`, msg })
10
-export const removeFlashMessage = msg => ({ type: `Remove/${FLASH_MESSAGE}`, msg })
11
-
12
-export const USER_LOGIN = 'User/Login'
13
-export const USER_LOGOUT = 'User/Logout'
14
-export const USER_DATA = 'User/Data'
15
-export const USER_ROLE = 'User/Role'
16
-export const USER_CONNECTED = 'User/Connected'
17
-export const USER_DISCONNECTED = 'User/Disconnected'
18
-export const setUserConnected = user => ({ type: `Set/${USER_CONNECTED}`, user })
19
-export const setUserDisconnected = () => ({ type: `Set/${USER_DISCONNECTED}` })
20
-export const updateUserData = userData => ({ type: `Update/${USER_DATA}`, data: userData })
21
-export const setUserRole = userRole => ({ type: `Set/${USER_ROLE}`, userRole }) // this actually update workspaceList state
14
+export const addFlashMessage = msg => ({ type: `${ADD}/${FLASH_MESSAGE}`, msg })
15
+export const removeFlashMessage = msg => ({ type: `${REMOVE}/${FLASH_MESSAGE}`, msg })
16
+
17
+export const USER = 'User'
18
+export const USER_LOGIN = `${USER}/Login`
19
+export const USER_LOGOUT = `${USER}/Logout`
20
+export const USER_DATA = `${USER}/Data`
21
+export const USER_ROLE = `${USER}/Role`
22
+export const USER_CONNECTED = `${USER}/Connected`
23
+export const USER_DISCONNECTED = `${USER}/Disconnected`
24
+export const USER_LANG = `${USER}/Lang`
25
+export const setUserConnected = user => ({ type: `${SET}/${USER}/Connected`, user })
26
+export const setUserDisconnected = () => ({ type: `${SET}/${USER}/Disconnected` })
27
+export const updateUserData = userData => ({ type: `${UPDATE}/${USER}/Data`, data: userData })
28
+export const setUserRole = userRole => ({ type: `${SET}/${USER}/Role`, userRole }) // this actually update workspaceList state
29
+export const setUserLang = lang => ({ type: `${SET}/${USER}/Lang`, lang })
22 30
 export const updateUserWorkspaceSubscriptionNotif = (workspaceId, subscriptionNotif) =>
23
-  ({ type: `Update/${USER_ROLE}/SubscriptionNotif`, workspaceId, subscriptionNotif })
31
+  ({ type: `${UPDATE}/${USER_ROLE}/SubscriptionNotif`, workspaceId, subscriptionNotif })
24 32
 
25 33
 export const WORKSPACE = 'Workspace'
26
-export const setWorkspaceContent = (workspaceContent, filterStr = '') => ({ type: `Set/${WORKSPACE}/Content`, workspaceContent, filterStr })
27
-export const updateWorkspaceFilter = filterList => ({ type: `Update/${WORKSPACE}/Filter`, filterList })
34
+export const WORKSPACE_CONTENT = `${WORKSPACE}/Content`
35
+export const setWorkspaceContentList = (workspaceContentList, filterStr = '') => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList, filterStr })
36
+export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
28 37
 
29
-export const FOLDER = 'Folder'
30
-export const setFolderData = (folderId, content) => ({ type: `Set/${WORKSPACE}/${FOLDER}/Content`, folderId, content })
38
+export const WORKSPACE_LIST = `${WORKSPACE}/List`
39
+export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
40
+export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })
41
+
42
+export const WORKSPACE_DETAIL = `${WORKSPACE}/Detail`
43
+export const setWorkspaceDetail = workspaceDetail => ({ type: `${SET}/${WORKSPACE_DETAIL}`, workspaceDetail })
31 44
 
32
-export const WORKSPACE_LIST = 'WorkspaceList'
33
-export const updateWorkspaceListData = workspaceList => ({ type: `Update/${WORKSPACE_LIST}`, workspaceList })
34
-export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `Set/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })
45
+export const WORKSPACE_MEMBER = `${WORKSPACE}/Member`
46
+export const WORKSPACE_MEMBER_LIST = `${WORKSPACE_MEMBER}/List`
47
+export const setWorkspaceMemberList = workspaceMemberList => ({ type: `${SET}/${WORKSPACE_MEMBER_LIST}`, workspaceMemberList })
48
+
49
+export const FOLDER = 'Folder'
50
+export const setFolderData = (folderId, content) => ({ type: `${SET}/${WORKSPACE}/${FOLDER}/Content`, folderId, content })
35 51
 
36
-export const APP_LIST = 'App/List'
37
-export const setAppList = appList => ({ type: `Set/${APP_LIST}`, appList })
52
+export const APP = 'App'
53
+export const APP_LIST = `${APP}/List`
54
+export const setAppList = appList => ({ type: `${SET}/${APP_LIST}`, appList })
38 55
 
39
-export const CONTENT_TYPE_LIST = 'ContentType/List'
40
-export const setContentTypeList = contentTypeList => ({ type: `Set/${CONTENT_TYPE_LIST}`, contentTypeList })
56
+export const CONTENT_TYPE = 'ContentType'
57
+export const CONTENT_TYPE_LIST = `${CONTENT_TYPE}/List`
58
+export const setContentTypeList = contentTypeList => ({ type: `${SET}/${CONTENT_TYPE_LIST}`, contentTypeList })
41 59
 
42 60
 export const LANG = 'Lang'
43
-export const updateLangList = langList => ({ type: `Update/${LANG}`, langList })
44
-export const setLangActive = langId => ({ type: `Set/${LANG}/Active`, langId })
61
+export const updateLangList = langList => ({ type: `${UPDATE}/${LANG}`, langList })

+ 24 - 8
frontend/src/appFactory.js View File

@@ -1,16 +1,30 @@
1 1
 import React from 'react'
2 2
 import { FETCH_CONFIG } from './helper.js'
3
+import i18n from './i18n.js'
3 4
 
4 5
 export function appFactory (WrappedComponent) {
5 6
   return class AppFactory extends React.Component {
6
-    renderAppFull = (appConfig, user, content) => GLOBAL_renderAppFull({
7
+    renderAppFeature = (appConfig, user, content) => GLOBAL_renderAppFeature({
7 8
       loggedUser: user.logged ? user : {},
8 9
       config: {
9 10
         ...appConfig,
10
-        domContainer: 'appContainer',
11
+        domContainer: 'appFeatureContainer',
11 12
         apiUrl: FETCH_CONFIG.apiUrl,
12
-        mockApiUrl: FETCH_CONFIG.mockApiUrl,
13
-        apiHeader: FETCH_CONFIG.headers
13
+        mockApiUrl: FETCH_CONFIG.mockApiUrl, // Côme - 2018/07/31 - this should not be used, I deprecate it
14
+        apiHeader: FETCH_CONFIG.headers,
15
+        translation: i18n.store.data
16
+      },
17
+      content
18
+    })
19
+
20
+    renderAppFullscreen = (appConfig, user, content) => GLOBAL_renderAppFullscreen({
21
+      loggedUser: user.logged ? user : {},
22
+      config: {
23
+        ...appConfig,
24
+        domContainer: 'appFullscreenContainer',
25
+        apiUrl: FETCH_CONFIG.apiUrl,
26
+        apiHeader: FETCH_CONFIG.headers,
27
+        translation: i18n.store.data
14 28
       },
15 29
       content
16 30
     })
@@ -22,21 +36,23 @@ export function appFactory (WrappedComponent) {
22 36
         domContainer: 'popupCreateContentContainer',
23 37
         apiUrl: FETCH_CONFIG.apiUrl,
24 38
         mockApiUrl: FETCH_CONFIG.mockApiUrl,
25
-        apiHeader: FETCH_CONFIG.headers // should this be used by app ? right now, apps have their own headers
39
+        apiHeader: FETCH_CONFIG.headers, // should this be used by app ? right now, apps have their own headers
40
+        translation: i18n.store.data
26 41
       },
27 42
       idWorkspace,
28 43
       idFolder: idFolder === 'null' ? null : idFolder
29 44
     })
30 45
 
31
-    emitEventApp = (type, data) => GLOBAL_dispatchEvent({ type, data })
46
+    dispatchCustomEvent = (type, data) => GLOBAL_dispatchEvent({ type, data })
32 47
 
33 48
     render () {
34 49
       return (
35 50
         <WrappedComponent
36 51
           {...this.props}
37
-          renderAppFull={this.renderAppFull}
52
+          renderAppFeature={this.renderAppFeature}
53
+          renderAppFullscreen={this.renderAppFullscreen}
38 54
           renderAppPopupCreation={this.renderAppPopupCreation}
39
-          emitEventApp={this.emitEventApp}
55
+          dispatchCustomEvent={this.dispatchCustomEvent}
40 56
           // hideApp={this.hideApp}
41 57
         />
42 58
       )

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

@@ -5,7 +5,7 @@ export const Calendar = props => {
5 5
     <div className='account__userpreference__setting__calendar'>
6 6
 
7 7
       <div className='calendar__title subTitle ml-2 ml-sm-0'>
8
-        Calendrier
8
+        {props.t('Calendar')}
9 9
       </div>
10 10
 
11 11
       <div className='calendar__text ml-2 ml-sm-0'>
@@ -13,7 +13,7 @@ export const Calendar = props => {
13 13
       </div>
14 14
 
15 15
       <div className='calendar__title ml-2 ml-sm-0'>
16
-        Accèder à votre Calendrier personnel
16
+        {props.t('Access your personal calendar')}
17 17
       </div>
18 18
       <div className='calendar__link ml-2 ml-sm-0'>
19 19
         {props.user.caldavUrl}

+ 4 - 4
frontend/src/component/Account/Notification.jsx View File

@@ -9,7 +9,7 @@ export const Notification = props => {
9 9
   return (
10 10
     <div className='account__userpreference__setting__notification'>
11 11
       <div className='notification__sectiontitle subTitle ml-2 ml-sm-0'>
12
-        Espace de Travail & Notification
12
+        {props.t('Workspace and notifications')}
13 13
       </div>
14 14
 
15 15
       <div className='notification__text ml-2 ml-sm-0'>
@@ -20,9 +20,9 @@ export const Notification = props => {
20 20
         <table className='table'>
21 21
           <thead>
22 22
             <tr>
23
-              <th>Espace de travail</th>
24
-              <th>Role</th>
25
-              <th>Notification</th>
23
+              <th>{props.t('Workspace')}</th>
24
+              <th>{props.t('Role')}</th>
25
+              <th>{props.t('Notification')}</th>
26 26
             </tr>
27 27
           </thead>
28 28
           <tbody>

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

@@ -1,10 +1,11 @@
1 1
 import React from 'react'
2
+import { translate } from 'react-i18next'
2 3
 
3 4
 export const Password = props => {
4 5
   return (
5 6
     <div className='account__userpreference__setting__personaldata'>
6 7
       <div className='personaldata__sectiontitle subTitle ml-2 ml-sm-0'>
7
-        Changer de mot de passe
8
+        {props.t('Change your password')}
8 9
       </div>
9 10
 
10 11
       <div className='personaldata__text ml-2 ml-sm-0'>
@@ -13,12 +14,20 @@ export const Password = props => {
13 14
 
14 15
       <form className='personaldata__form mr-5'>
15 16
         <div className='personaldata__form__title'>
16
-          Mot de passe :
17
+          {props.t('Password')}
17 18
         </div>
18
-        <input className='personaldata__form__txtinput form-control' type='password' placeholder='Ancien mot de passe' />
19
-        <input className='personaldata__form__txtinput form-control mt-4' type='password' placeholder='Nouveau mot de passe' />
19
+        <input
20
+          className='personaldata__form__txtinput form-control'
21
+          type='password'
22
+          placeholder={props.t('Old password')}
23
+        />
24
+        <input
25
+          className='personaldata__form__txtinput form-control mt-4'
26
+          type='password'
27
+          placeholder={props.t('New password')}
28
+        />
20 29
         <button type='submit' className='personaldata__form__button btn btn-outline-primary mt-4'>
21
-          Envoyer
30
+          {props.t('Send')}
22 31
         </button>
23 32
       </form>
24 33
 
@@ -26,4 +35,4 @@ export const Password = props => {
26 35
   )
27 36
 }
28 37
 
29
-export default Password
38
+export default translate()(Password)

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

@@ -1,10 +1,12 @@
1 1
 import React from 'react'
2
+import PropTypes from 'prop-types'
3
+import { translate } from 'react-i18next'
2 4
 
3 5
 export const PersonalData = props => {
4 6
   return (
5 7
     <div className='account__userpreference__setting__personaldata'>
6 8
       <div className='personaldata__sectiontitle subTitle ml-2 ml-sm-0'>
7
-        Information du compte
9
+        {props.t('Account information')}
8 10
       </div>
9 11
 
10 12
       <div className='personaldata__text ml-2 ml-sm-0'>
@@ -13,23 +15,36 @@ export const PersonalData = props => {
13 15
 
14 16
       <form className='personaldata__form'>
15 17
         <div className='personaldata__form__title'>
16
-          Nom :
18
+          {props.t('Name:')}
17 19
         </div>
18 20
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
19
-          <input className='personaldata__form__txtinput form-control mt-3 mt-sm-0' type='text' placeholder='Nom' />
21
+          <input
22
+            className='personaldata__form__txtinput form-control mt-3 mt-sm-0'
23
+            type='text'
24
+            placeholder={props.t('Change your name')}
25
+          />
20 26
         </div>
21 27
         <div className='personaldata__form__title'>
22
-          Adresse mail :
28
+          {props.t('Email Adress:')}
23 29
         </div>
24 30
         <div className='d-flex align-items-center justify-content-between flex-wrap mb-4'>
25
-          <input className='personaldata__form__txtinput form-control mt-3 mt-sm-0' type='email' placeholder='Nouvelle adresse mail' />
31
+          <input
32
+            className='personaldata__form__txtinput form-control mt-3 mt-sm-0'
33
+            type='email'
34
+            placeholder={props.t('Change your email')}
35
+          />
26 36
         </div>
27 37
         <button type='submit' className='personaldata__form__button btn btn-outline-primary'>
28
-          Envoyer
38
+          {props.t('Send')}
29 39
         </button>
30 40
       </form>
31 41
     </div>
32 42
   )
33 43
 }
34 44
 
35
-export default PersonalData
45
+PersonalData.propTypes = {
46
+  inputPlaceholderNameUser: PropTypes.string,
47
+  inputPlaceholderEmailUser: PropTypes.string
48
+}
49
+
50
+export default translate()(PersonalData)

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

@@ -1,6 +1,7 @@
1 1
 import React from 'react'
2 2
 import Select from 'react-select'
3 3
 import 'react-select/dist/react-select.css'
4
+import { translate } from 'react-i18next'
4 5
 
5 6
 export const Timezone = props => {
6 7
   const handleChangeTimezone = selectedTimezone => props.onChangeTimezone(props.timezone.find(t => t.place === selectedTimezone.place))
@@ -9,7 +10,7 @@ export const Timezone = props => {
9 10
     <div className='account__userpreference__setting__timezone'>
10 11
 
11 12
       <div className='timezone__title subTitle ml-2 ml-sm-0'>
12
-        Changer de Fuseau Horaire
13
+        {props.t('Change your Timezone')}
13 14
       </div>
14 15
 
15 16
       <div className='timezone__text ml-2 ml-sm-0'>
@@ -32,4 +33,4 @@ export const Timezone = props => {
32 33
   )
33 34
 }
34 35
 
35
-export default Timezone
36
+export default translate()(Timezone)

+ 1 - 1
frontend/src/component/FlashMessage.jsx View File

@@ -22,7 +22,7 @@ const FlashMessage = props => {
22 22
 
23 23
               <div className='flashmessage__container__content__text'>
24 24
                 <div className='flashmessage__container__content__text__title'>
25
-                  {props.t('FlashMessage.error')}
25
+                  {props.t('Error')}
26 26
                 </div>
27 27
                 <div className='flashmessage__container__content__text__paragraph'>
28 28
                   {props.flashMessage[0].message}

+ 1 - 1
frontend/src/component/Footer.jsx View File

@@ -7,7 +7,7 @@ const Footer = ({ t }) => {
7 7
   return (
8 8
     <footer className='footer text-right'>
9 9
       <div className='footer__text'>
10
-        {t('Footer.marketing_msg')} - {t('Footer.copyright')}
10
+        {t('Create your own collaborative workspaces on trac.im')} - {t('Copyright 2013 - 2017')}
11 11
       </div>
12 12
       <img className='footer__logo' src={logoFooter} />
13 13
     </footer>

+ 4 - 4
frontend/src/component/Header/MenuActionListItem/DropdownLang.jsx View File

@@ -2,7 +2,6 @@ import React from 'react'
2 2
 import PropTypes from 'prop-types'
3 3
 
4 4
 const DropdownLang = props => {
5
-  const activeLang = props.langList.find(l => l.active) || {id: 'fr', name: 'Français', src: '', active: true}
6 5
   return (
7 6
     <li className='header__menu__rightside__itemlanguage'>
8 7
       <div className='header__menu__rightside__itemlanguage__languagedropdown dropdown'>
@@ -14,12 +13,12 @@ const DropdownLang = props => {
14 13
           aria-haspopup='true'
15 14
           aria-expanded='false'
16 15
         >
17
-          <img className='languagedropdown__btnlanguage__imgselected' src={activeLang.src} />
16
+          <img className='languagedropdown__btnlanguage__imgselected' src={props.langList.find(l => l.id === props.idLangActive).icon} />
18 17
         </button>
19 18
         <div className='languagedropdown__subdropdown dropdown-menu' aria-labelledby='headerDropdownMenuButton'>
20
-          { props.langList.map((l, i) => l.active === false &&
19
+          { props.langList.filter(l => l.id !== props.idLangActive).map((l, i) =>
21 20
             <div className='subdropdown__link dropdown-item' onClick={() => props.onChangeLang(l.id)} key={i}>
22
-              <img className='subdropdown__flag' src={l.src} />
21
+              <img className='subdropdown__flag' src={l.icon} />
23 22
             </div>
24 23
           )}
25 24
         </div>
@@ -31,5 +30,6 @@ export default DropdownLang
31 30
 
32 31
 DropdownLang.propTypes = {
33 32
   langList: PropTypes.array.isRequired,
33
+  idLangActive: PropTypes.string.isRequired,
34 34
   onChangeLang: PropTypes.func.isRequired
35 35
 }

+ 4 - 3
frontend/src/component/Header/MenuActionListItem/MenuProfil.jsx View File

@@ -2,6 +2,7 @@ import React from 'react'
2 2
 import { Link } from 'react-router-dom'
3 3
 import PropTypes from 'prop-types'
4 4
 import { PAGE } from '../../../helper.js'
5
+import { translate } from 'react-i18next'
5 6
 
6 7
 const MenuProfil = props => {
7 8
   return props.user.logged
@@ -17,12 +18,12 @@ const MenuProfil = props => {
17 18
           <div className='profilgroup__setting dropdown-menu' aria-labelledby='dropdownMenuButton'>
18 19
             <Link className='setting__link dropdown-item' to={PAGE.ACCOUNT}>
19 20
               <i className='fa fa-fw fa-user-o mr-2' />
20
-              Mon compte
21
+              {props.t('My Account')}
21 22
             </Link>
22 23
             {/* <div className='setting__link dropdown-item'>Mot de passe</div> */}
23 24
             <div className='setting__link dropdown-item' onClick={props.onClickLogout}>
24 25
               <i className='fa fa-fw fa-sign-out mr-2' />
25
-              Se déconnecter
26
+              {props.t('Logout')}
26 27
             </div>
27 28
           </div>
28 29
         </div>
@@ -30,7 +31,7 @@ const MenuProfil = props => {
30 31
     )
31 32
     : ''
32 33
 }
33
-export default MenuProfil
34
+export default translate()(MenuProfil)
34 35
 
35 36
 MenuProfil.propTypes = {
36 37
   user: PropTypes.object.isRequired,

+ 5 - 4
frontend/src/component/Header/MenuActionListItem/Notification.jsx View File

@@ -1,4 +1,5 @@
1 1
 import React from 'react'
2
+import { translate } from 'react-i18next'
2 3
 // import PropTypes from 'prop-types'
3 4
 
4 5
 const Notification = props => {
@@ -13,18 +14,18 @@ const Notification = props => {
13 14
           aria-haspopup='true'
14 15
           aria-expanded='false'
15 16
         >
16
-          Notification
17
+          {props.t('Notification')}
17 18
         </button>
18 19
         <div className='timeline__subdropdown dropdown-menu' aria-labelledby='headerDropdownMenuButton'>
19 20
           <div className='timeline__subdropdown__text dropdown-item' >
20
-            Conversation archivé
21
+            {props.t('Archive Topic')}
21 22
           </div>
22 23
           <div className='timeline__subdropdown__text dropdown-item' >
23
-            Fichier supprimé
24
+            {props.t('Deleted File')}
24 25
           </div>
25 26
         </div>
26 27
       </div>
27 28
     </li>
28 29
   )
29 30
 }
30
-export default Notification
31
+export default translate()(Notification)

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

@@ -9,7 +9,7 @@ const Search = props => {
9 9
         <input
10 10
           type='text'
11 11
           className='search__input form-control'
12
-          placeholder={`${props.t('Header.Search')}...`}
12
+          placeholder={`${props.t('Search...')}`}
13 13
           aria-describedby='headerInputSearch'
14 14
           onChange={props.onChangeInput}
15 15
         />

+ 7 - 6
frontend/src/component/Workspace/BtnExtandedAction.jsx View File

@@ -1,5 +1,6 @@
1 1
 import React from 'react'
2 2
 import PropTypes from 'prop-types'
3
+import { translate } from 'react-i18next'
3 4
 
4 5
 const ExtandedAction = props => {
5 6
   return (
@@ -22,7 +23,7 @@ const ExtandedAction = props => {
22 23
             <i className='fa fa-fw fa-pencil' />
23 24
           </div>
24 25
           <div className='subdropdown__item__text'>
25
-            Modifier
26
+            {props.t('Change')}
26 27
           </div>
27 28
         </div>
28 29
 
@@ -31,7 +32,7 @@ const ExtandedAction = props => {
31 32
             <i className='fa fa-fw fa-arrows-alt' />
32 33
           </div>
33 34
           <div className='subdropdown__item__text'>
34
-            Déplacer
35
+            {props.t('Move')}
35 36
           </div>
36 37
         </div>
37 38
 
@@ -40,7 +41,7 @@ const ExtandedAction = props => {
40 41
             <i className='fa fa-fw fa-download' />
41 42
           </div>
42 43
           <div className='subdropdown__item__text'>
43
-            Télécharger
44
+            {props.t('Download')}
44 45
           </div>
45 46
         </div> */ }
46 47
 
@@ -49,7 +50,7 @@ const ExtandedAction = props => {
49 50
             <i className='fa fa-fw fa-archive' />
50 51
           </div>
51 52
           <div className='subdropdown__item__text'>
52
-            Archiver
53
+            {props.t('Archive')}
53 54
           </div>
54 55
         </div>
55 56
 
@@ -58,7 +59,7 @@ const ExtandedAction = props => {
58 59
             <i className='fa fa-fw fa-trash-o' />
59 60
           </div>
60 61
           <div className='subdropdown__item__text'>
61
-            Supprimer
62
+            {props.t('Delete')}
62 63
           </div>
63 64
         </div>
64 65
 
@@ -67,7 +68,7 @@ const ExtandedAction = props => {
67 68
   )
68 69
 }
69 70
 
70
-export default ExtandedAction
71
+export default translate()(ExtandedAction)
71 72
 
72 73
 ExtandedAction.propTypes = {
73 74
   onClickExtendedAction: PropTypes.object.isRequired

+ 2 - 0
frontend/src/component/Workspace/ContentItem.jsx View File

@@ -4,6 +4,8 @@ import classnames from 'classnames'
4 4
 import BtnExtandedAction from './BtnExtandedAction.jsx'
5 5
 
6 6
 const ContentItem = props => {
7
+  if (props.contentType === null) return null // this means the endpoint system/content_type hasn't responded yet
8
+
7 9
   const status = props.contentType.availableStatuses.find(s => s.slug === props.statusSlug)
8 10
   return (
9 11
     <div className={classnames('content', 'align-items-center', {'item-last': props.isLast}, props.customClass)} onClick={props.onClickItem}>

+ 3 - 3
frontend/src/component/Workspace/ContentItemHeader.jsx View File

@@ -5,13 +5,13 @@ const FileItemHeader = props => {
5 5
   return (
6 6
     <div className='content__header'>
7 7
       <div className='content__header__type'>
8
-        {props.t('FileItemHeader.type')}
8
+        {props.t('Type')}
9 9
       </div>
10 10
       <div className='content__header__name'>
11
-        {props.t('FileItemHeader.document_name')}
11
+        {props.t('Title')}
12 12
       </div>
13 13
       <div className='content__header__status'>
14
-        {props.t('FileItemHeader.status')}
14
+        {props.t('Status')}
15 15
       </div>
16 16
     </div>
17 17
   )

+ 1 - 1
frontend/src/component/Workspace/Folder.jsx View File

@@ -65,7 +65,7 @@ class Folder extends React.Component {
65 65
                 aria-expanded='false'
66 66
                 onClick={e => e.stopPropagation()}
67 67
               >
68
-                {t('Folder.create')} ...
68
+                {t('Create in folder...')}
69 69
               </button>
70 70
 
71 71
               <div className='addbtn__subdropdown dropdown-menu' aria-labelledby='dropdownMenuButton'>

+ 8 - 7
frontend/src/component/Workspace/OpenContentApp.jsx View File

@@ -3,13 +3,14 @@ import { connect } from 'react-redux'
3 3
 import { withRouter } from 'react-router'
4 4
 import appFactory from '../../appFactory.js'
5 5
 
6
+// @FIXME Côme - 2018/07/31 - should this be in a component like AppFeatureManager ?
6 7
 export class OpenContentApp extends React.Component {
7 8
   openContentApp = () => {
8
-    const { idWorkspace, appOpenedType, user, workspaceContent, contentType, renderAppFull, match } = this.props
9
+    const { idWorkspace, appOpenedType, user, workspaceContentList, contentType, renderAppFeature, dispatchCustomEvent, match } = this.props
9 10
 
10 11
     if (isNaN(idWorkspace) || idWorkspace === -1) return
11 12
 
12
-    if (['type', 'idcts'].every(p => p in match.params) && match.params.type !== 'contents' && workspaceContent.length) {
13
+    if (['type', 'idcts'].every(p => p in match.params) && match.params.type !== 'contents' && workspaceContentList.length) {
13 14
       if (isNaN(match.params.idcts) || !contentType.map(c => c.slug).includes(match.params.type)) return
14 15
 
15 16
       const contentToOpen = {
@@ -21,15 +22,15 @@ export class OpenContentApp extends React.Component {
21 22
       console.log('%c<OpenContentApp> contentToOpen', 'color: #dcae84', contentToOpen)
22 23
 
23 24
       if (appOpenedType === contentToOpen.type) { // app already open
24
-        GLOBAL_dispatchEvent({
25
-          type: `${contentToOpen.type}_reloadContent`, // handled by html-documents:src/container/HtmlDocument.jsx
25
+        dispatchCustomEvent({
26
+          type: `${contentToOpen.type}_reloadContent`, // handled by html-document:src/container/AdminWorkspaceUser.jsx
26 27
           data: contentToOpen
27 28
         })
28 29
       } else { // open another app
29 30
         // if another app is already visible, hide it
30
-        if (appOpenedType !== false) GLOBAL_dispatchEvent({type: `${appOpenedType}_hideApp`})
31
+        if (appOpenedType !== false) dispatchCustomEvent({type: `${appOpenedType}_hideApp`})
31 32
         // open app
32
-        renderAppFull(
33
+        renderAppFeature(
33 34
           contentType.find(ct => ct.slug === contentToOpen.type),
34 35
           user,
35 36
           contentToOpen
@@ -56,5 +57,5 @@ export class OpenContentApp extends React.Component {
56 57
   }
57 58
 }
58 59
 
59
-const mapStateToProps = ({ user, workspaceContent, contentType }) => ({ user, workspaceContent, contentType })
60
+const mapStateToProps = ({ user, workspaceContentList, contentType }) => ({ user, workspaceContentList, contentType })
60 61
 export default withRouter(connect(mapStateToProps)(appFactory(OpenContentApp)))

+ 2 - 1
frontend/src/component/Workspace/OpenCreateContentApp.jsx View File

@@ -5,6 +5,7 @@ import appFactory from '../../appFactory.js'
5 5
 
6 6
 const qs = require('query-string')
7 7
 
8
+// @FIXME Côme - 2018/07/31 - should this be in a component like AppFeatureManager ? (or AppCreateContentManager)
8 9
 export class OpenCreateContentApp extends React.Component {
9 10
   openCreateContentApp = () => {
10 11
     const { idWorkspace, user, contentType, renderAppPopupCreation, match, location } = this.props
@@ -38,5 +39,5 @@ export class OpenCreateContentApp extends React.Component {
38 39
   }
39 40
 }
40 41
 
41
-const mapStateToProps = ({ user, workspaceContent, contentType }) => ({ user, workspaceContent, contentType })
42
+const mapStateToProps = ({ user, contentType }) => ({ user, contentType })
42 43
 export default withRouter(connect(mapStateToProps)(appFactory(OpenCreateContentApp)))

+ 4 - 1
frontend/src/component/common/Input/SubDropdownCreateButton.jsx View File

@@ -10,7 +10,10 @@ const SubDropdownCreateButton = props => {
10 10
         <div className='subdropdown__link dropdown-item' onClick={e => props.onClickCreateContent(e, props.idFolder, app.slug)} key={app.slug}>
11 11
           <div className={`subdropdown__link__${app.slug} d-flex align-items-center`}>
12 12
             <div className={`subdropdown__link__${app.slug}__icon mr-3`}>
13
-              <i className={`fa fa-fw fa-${app.faIcon}`} />
13
+              <i
14
+                className={`fa fa-fw fa-${app.faIcon}`}
15
+                style={{color: app.hexcolor}}
16
+              />
14 17
             </div>
15 18
             <div className='subdropdown__link__folder__text'>
16 19
               {app.creationLabel}

+ 14 - 9
frontend/src/container/Account.jsx View File

@@ -1,9 +1,6 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3 3
 import Sidebar from './Sidebar.jsx'
4
-import PageWrapper from '../component/common/layout/PageWrapper.jsx'
5
-import PageTitle from '../component/common/layout/PageTitle.jsx'
6
-import PageContent from '../component/common/layout/PageContent.jsx'
7 4
 import UserInfo from '../component/Account/UserInfo.jsx'
8 5
 import MenuSubComponent from '../component/Account/MenuSubComponent.jsx'
9 6
 import PersonalData from '../component/Account/PersonalData.jsx'
@@ -11,12 +8,18 @@ import PersonalData from '../component/Account/PersonalData.jsx'
11 8
 import Notification from '../component/Account/Notification.jsx'
12 9
 import Password from '../component/Account/Password.jsx'
13 10
 import Timezone from '../component/Account/Timezone.jsx'
14
-import { Delimiter } from 'tracim_frontend_lib'
11
+import {
12
+  Delimiter,
13
+  PageWrapper,
14
+  PageTitle,
15
+  PageContent
16
+} from 'tracim_frontend_lib'
15 17
 import { updateUserWorkspaceSubscriptionNotif } from '../action-creator.sync.js'
16 18
 import {
17 19
   getTimezone,
18 20
   getUserRole
19 21
 } from '../action-creator.async.js'
22
+import { translate } from 'react-i18next'
20 23
 
21 24
 class Account extends React.Component {
22 25
   constructor (props) {
@@ -77,7 +80,8 @@ class Account extends React.Component {
77 80
     const subComponent = (() => {
78 81
       switch (this.state.subComponentMenu.find(({active}) => active).name) {
79 82
         case 'personalData':
80
-          return <PersonalData />
83
+          return <PersonalData
84
+          />
81 85
 
82 86
         // case 'calendar':
83 87
         //   return <Calendar user={this.props.user} />
@@ -88,11 +92,12 @@ class Account extends React.Component {
88 92
             onChangeSubscriptionNotif={this.handleChangeSubscriptionNotif}
89 93
           />
90 94
 
95
+        case 'password':
96
+          return <Password
97
+          />
98
+
91 99
         case 'timezone':
92 100
           return <Timezone timezone={this.props.timezone} onChangeTimezone={this.handleChangeTimezone} />
93
-
94
-        case 'password':
95
-          return <Password />
96 101
       }
97 102
     })()
98 103
 
@@ -127,4 +132,4 @@ class Account extends React.Component {
127 132
 }
128 133
 
129 134
 const mapStateToProps = ({ user, workspaceList, timezone }) => ({ user, workspaceList, timezone })
130
-export default connect(mapStateToProps)(Account)
135
+export default connect(mapStateToProps)(translate()(Account))

+ 47 - 0
frontend/src/container/AppFullscreenManager.jsx View File

@@ -0,0 +1,47 @@
1
+import React from 'react'
2
+import { connect } from 'react-redux'
3
+import { withRouter } from 'react-router'
4
+import { Route } from 'react-router-dom'
5
+import { PAGE } from '../helper.js'
6
+import appFactory from '../appFactory.js'
7
+import Sidebar from './Sidebar.jsx'
8
+
9
+class AppFullscreenManager extends React.Component {
10
+  constructor (props) {
11
+    super(props)
12
+    this.state = {
13
+      AmIMounted: false
14
+    }
15
+  }
16
+
17
+  componentDidMount = () => this.setState({AmIMounted: true})
18
+
19
+  render () {
20
+    const { props } = this
21
+
22
+    return (
23
+      <div className='sidebarpagecontainer'>
24
+        <Sidebar />
25
+
26
+        <div id='appFullscreenContainer' />
27
+
28
+        {this.state.AmIMounted && (// we must wait for the component to be fully mounted to be sure the div#appFullscreenContainer exists in DOM
29
+          <div className='emptyDiForRoute'>
30
+            <Route path={PAGE.ADMIN.WORKSPACE} render={() => {
31
+              props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'workspace'}, props.user, {})
32
+              return null
33
+            }} />
34
+
35
+            <Route path={PAGE.ADMIN.USER} render={() => {
36
+              props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'user'}, props.user, {})
37
+              return null
38
+            }} />
39
+          </div>
40
+        )}
41
+      </div>
42
+    )
43
+  }
44
+}
45
+
46
+const mapStateToProps = ({ user }) => ({ user })
47
+export default connect(mapStateToProps)(withRouter(appFactory(AppFullscreenManager)))

+ 189 - 377
frontend/src/container/Dashboard.jsx View File

@@ -2,13 +2,22 @@ import React from 'react'
2 2
 import { connect } from 'react-redux'
3 3
 import Sidebar from './Sidebar.jsx'
4 4
 import imgProfil from '../img/imgProfil.png'
5
+import { translate } from 'react-i18next'
6
+import Radium from 'radium'
7
+import color from 'color'
5 8
 import {
6
-  getAppList,
7
-  getContentTypeList, getWorkspaceList
9
+  PageWrapper,
10
+  PageTitle,
11
+  PageContent
12
+} from 'tracim_frontend_lib'
13
+import {
14
+  getWorkspaceDetail,
15
+  getWorkspaceMemberList
8 16
 } from '../action-creator.async.js'
9 17
 import {
10
-  setAppList,
11
-  setContentTypeList, setWorkspaceListIsOpenInSidebar, updateWorkspaceListData
18
+  addFlashMessage,
19
+  setWorkspaceDetail,
20
+  setWorkspaceMemberList
12 21
 } from '../action-creator.sync.js'
13 22
 
14 23
 class Dashboard extends React.Component {
@@ -24,28 +33,28 @@ class Dashboard extends React.Component {
24 33
   }
25 34
 
26 35
   async componentDidMount () {
27
-    const { workspaceIdInUrl } = this.state
28
-    const { user, workspaceList, app, contentType, dispatch } = this.props
29
-
30
-    console.log('<Dashboard> componentDidMount')
31
-
32
-    if (app.length === 0) {
33
-      const fetchGetAppList = await dispatch(getAppList(user))
34
-      if (fetchGetAppList.status === 200) dispatch(setAppList(fetchGetAppList.json))
35
-    }
36
-
37
-    if (contentType.length === 0) {
38
-      const fetchGetContentTypeList = await dispatch(getContentTypeList(user))
39
-      if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
36
+    const { props, state } = this
37
+
38
+    const fetchWorkspaceDetail = await props.dispatch(getWorkspaceDetail(props.user, state.workspaceIdInUrl))
39
+    switch (fetchWorkspaceDetail.status) {
40
+      case 200:
41
+        props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json))
42
+        break
43
+      case 400:
44
+      case 500:
45
+        props.dispatch(addFlashMessage(props.t('An error has happened'), 'warning'))
46
+        break
40 47
     }
41 48
 
42
-    if (user.user_id !== -1 && workspaceList.length === 0) {
43
-      const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
44
-
45
-      if (fetchGetWorkspaceList.status === 200) {
46
-        dispatch(updateWorkspaceListData(fetchGetWorkspaceList.json))
47
-        dispatch(setWorkspaceListIsOpenInSidebar(workspaceIdInUrl || fetchGetWorkspaceList.json[0].workspace_id, true))
48
-      }
49
+    const fetchWorkspaceMemberList = await props.dispatch(getWorkspaceMemberList(props.user, state.workspaceIdInUrl))
50
+    switch (fetchWorkspaceMemberList.status) {
51
+      case 200:
52
+        props.dispatch(setWorkspaceMemberList(fetchWorkspaceMemberList.json))
53
+        break
54
+      case 400:
55
+      case 500:
56
+        props.dispatch(addFlashMessage(props.t('An error has happened'), 'warning'))
57
+        break
49 58
     }
50 59
   }
51 60
 
@@ -66,219 +75,145 @@ class Dashboard extends React.Component {
66 75
   }))
67 76
 
68 77
   render () {
78
+    const { props, state } = this
79
+
69 80
     return (
70 81
       <div className='sidebarpagecontainer'>
71 82
         <Sidebar />
72 83
 
73
-        <div className='dashboard'>
74
-          <div className='container-fluid nopadding'>
75
-            <div className='dashboard__header mb-5'>
76
-              <div className='pageTitleGeneric dashboard__header__title d-flex align-items-center'>
77
-                <div className='pageTitleGeneric__title dashboard__header__title__text mr-3'>
78
-                  Dashboard
79
-                </div>
80
-                <div className='dashboard__header__acces' />
81
-              </div>
82
-              <div className='dashboard__header__advancedmode mr-3'>
83
-                <button type='button' className='btn btn-primary'>Activer édition avancé</button>
84
-              </div>
84
+        <PageWrapper customeClass='dashboard'>
85
+          <PageTitle
86
+            parentClass='dashboard__header'
87
+            title={props.t('Dashboard')}
88
+            subtitle={''}
89
+          >
90
+            <div className='dashboard__header__advancedmode mr-3'>
91
+              <button type='button' className='btn btn-primary'>
92
+                {props.t('Active advanced Dashboard')}
93
+              </button>
85 94
             </div>
95
+          </PageTitle>
86 96
 
87
-            <div className='dashboard__wkswrapper'>
97
+          <PageContent>
98
+            <div className='dashboard__workspace-wrapper'>
88 99
               <div className='dashboard__workspace'>
89 100
                 <div className='dashboard__workspace__title'>
90
-                  Développement tracim
101
+                  {props.curWs.label}
91 102
                 </div>
92 103
 
93 104
                 <div className='dashboard__workspace__detail'>
94
-                  Ligne directive pour le prochain design de Tracim et des futurs fonctionnalités.
105
+                  {props.curWs.description}
95 106
                 </div>
96 107
               </div>
97
-              <div className='dashboard__userstatut'>
98 108
 
109
+              <div className='dashboard__userstatut'>
99 110
                 <div className='dashboard__userstatut__role'>
100
-                  <div className='dashboard__userstatut__role__text'>
101
-                    Hi ! Alexi, vous êtes actuellement
111
+                  <div className='dashboard__userstatut__role__msg'>
112
+                    {props.t(`Hi ! ${props.user.public_name}, vous êtes actuellement`)}
102 113
                   </div>
103
-                  <div className='dashboard__userstatut__role__rank'>
104
-                    <div className='dashboard__userstatut__role__rank__icon'>
114
+
115
+                  <div className='dashboard__userstatut__role__definition'>
116
+                    <div className='dashboard__userstatut__role__definition__icon'>
105 117
                       <i className='fa fa-graduation-cap' />
106 118
                     </div>
107
-                    <div className='dashboard__userstatut__role__rank__rolename'>
108
-                      Gestionnaire de projet
119
+
120
+                    <div className='dashboard__userstatut__role__definition__text'>
121
+                      {(member => member ? member.role : '')(props.curWs.member.find(m => m.id === props.user.user_id))}
109 122
                     </div>
110 123
                   </div>
111 124
                 </div>
112 125
 
113 126
                 <div className='dashboard__userstatut__notification'>
114 127
                   <div className='dashboard__userstatut__notification__text'>
115
-                    Vous êtes abonné(e) aux notifications de ce workspace
128
+                    {props.t("You have subscribed to this workspace's notifications")} (nyi)
116 129
                   </div>
117
-                  {this.state.displayNotifBtn === false &&
118
-                    <div
119
-                      className='dashboard__userstatut__notification__btn btn btn-outline-primary'
120
-                      onClick={this.handleToggleNotifBtn}
121
-                    >
122
-                      Changer de statut
123
-                    </div>
124
-                  }
125 130
 
126
-                  {this.state.displayNotifBtn === true &&
127
-                    <div className='dashboard__userstatut__notification__subscribe dropdown'>
128
-                      <button className='dashboard__userstatut__notification__subscribe__btn btn btn-outline-primary dropdown-toggle' type='button' id='dropdownMenuButton' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
129
-                        Abonné(e)
130
-                      </button>
131
-                      <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
132
-                        <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>Abonné(e)
133
-                        </div>
134
-                        <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>Non Abonné(e)
131
+                  {state.displayNotifBtn
132
+                    ? (
133
+                      <div className='dashboard__userstatut__notification__subscribe dropdown'>
134
+                        <button
135
+                          className='dashboard__userstatut__notification__subscribe__btn btn btn-outline-primary dropdown-toggle'
136
+                          type='button'
137
+                          id='dropdownMenuButton'
138
+                          data-toggle='dropdown'
139
+                          aria-haspopup='true'
140
+                          aria-expanded='false'
141
+                        >
142
+                          Abonné(e)
143
+                        </button>
144
+
145
+                        <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
146
+                          <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>
147
+                            {props.t('subscriber')}
148
+                          </div>
149
+                          <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>
150
+                            {props.t('unsubscribed')}
151
+                          </div>
135 152
                         </div>
136 153
                       </div>
137
-                    </div>
154
+                    )
155
+                    : (
156
+                      <div
157
+                        className='dashboard__userstatut__notification__btn btn btn-outline-primary'
158
+                        onClick={this.handleToggleNotifBtn}
159
+                      >
160
+                        {props.t('Change your status')}
161
+                      </div>
162
+                    )
138 163
                   }
139 164
                 </div>
140 165
               </div>
141 166
             </div>
142 167
 
143 168
             <div className='dashboard__calltoaction justify-content-xl-center'>
144
-              <div className='dashboard__calltoaction__button btnaction btnthread'>
145
-                <div className='dashboard__calltoaction__button__text'>
146
-                  <div className='dashboard__calltoaction__button__text__icon'>
147
-                    <i className='fa fa-comments-o' />
148
-                  </div>
149
-                  <div className='dashboard__calltoaction__button__text__title'>
150
-                    Débuter une nouvelle discussion
151
-                  </div>
152
-                </div>
153
-              </div>
154
-
155
-              <div className='dashboard__calltoaction__button btnaction writefile'>
156
-                <div className='dashboard__calltoaction__button__text'>
157
-                  <div className='dashboard__calltoaction__button__text__icon'>
158
-                    <i className='fa fa-file-text-o' />
159
-                  </div>
160
-                  <div className='dashboard__calltoaction__button__text__title'>
161
-                    Rédiger un document
162
-                  </div>
163
-                </div>
164
-              </div>
165
-
166
-              <div className='dashboard__calltoaction__button btnaction importfile'>
167
-                <div className='dashboard__calltoaction__button__text'>
168
-                  <div className='dashboard__calltoaction__button__text__icon'>
169
-                    <i className='fa fa-paperclip' />
170
-                  </div>
171
-                  <div className='dashboard__calltoaction__button__text__title'>
172
-                    Importer un fichier
173
-                  </div>
174
-                </div>
175
-              </div>
176
-
177
-              {/*
178
-                <div className='dashboard__calltoaction__button btnaction visioconf'>
179
-                  <div className='dashboard__calltoaction__button__text'>
180
-                    <div className='dashboard__calltoaction__button__text__icon'>
181
-                      <i className='fa fa-video-camera' />
182
-                    </div>
183
-                    <div className='dashboard__calltoaction__button__text__title'>
184
-                      Débuter une visioconférence
185
-                    </div>
186
-                  </div>
187
-                </div>
188
-
189
-                <div className='dashboard__calltoaction__button btnaction calendar'>
169
+              {props.contentType.map(ct =>
170
+                <div
171
+                  className='dashboard__calltoaction__button btnaction'
172
+                  style={{
173
+                    backgroundColor: ct.hexcolor,
174
+                    ':hover': {
175
+                      backgroundColor: color(ct.hexcolor).darken(0.15).hexString()
176
+                    }
177
+                  }}
178
+                  key={ct.label}
179
+                >
190 180
                   <div className='dashboard__calltoaction__button__text'>
191 181
                     <div className='dashboard__calltoaction__button__text__icon'>
192
-                      <i className='fa fa-calendar' />
182
+                      <i className={`fa fa-${ct.faIcon}`} />
193 183
                     </div>
194 184
                     <div className='dashboard__calltoaction__button__text__title'>
195
-                      Voir le Calendrier
185
+                      {ct.creationLabel}
196 186
                     </div>
197 187
                   </div>
198 188
                 </div>
199
-              */ }
200
-
201
-              <div className='dashboard__calltoaction__button btnaction explore'>
202
-                <div className='dashboard__calltoaction__button__text'>
203
-                  <div className='dashboard__calltoaction__button__text__icon'>
204
-                    <i className='fa fa-folder-open-o' />
205
-                  </div>
206
-                  <div className='dashboard__calltoaction__button__text__title'>
207
-                    Explorer le Workspace
208
-                  </div>
209
-                </div>
210
-              </div>
189
+              )}
211 190
             </div>
212 191
 
213 192
             <div className='dashboard__wksinfo'>
214 193
               <div className='dashboard__activity'>
215 194
                 <div className='dashboard__activity__header'>
216 195
                   <div className='dashboard__activity__header__title subTitle'>
217
-                    Activité récente
196
+                    {this.props.t('Recent activity')}
218 197
                   </div>
219 198
 
220 199
                   <div className='dashboard__activity__header__allread btn btn-outline-primary'>
221
-                    Tout marquer comme lu
200
+                    {this.props.t('Mark everything as read')}
222 201
                   </div>
223 202
                 </div>
224 203
                 <div className='dashboard__activity__wrapper'>
225
-                  <div className='dashboard__activity__workspace'>
226
-                    <div className='dashboard__activity__workspace__icon'>
227
-                      <i className='fa fa-comments-o' />
228
-                    </div>
229
-                    <div className='dashboard__activity__workspace__name'>
230
-                      <span>Développement Tracim</span>
231
-                    </div>
232
-                  </div>
233
-
234
-                  <div className='dashboard__activity__workspace'>
235
-                    <div className='dashboard__activity__workspace__icon'>
236
-                      <i className='fa fa-list-ul' />
237
-                    </div>
238
-                    <div className='dashboard__activity__workspace__name'>
239
-                      Mission externe
240
-                    </div>
241
-                  </div>
242
-
243
-                  <div className='dashboard__activity__workspace'>
244
-                    <div className='dashboard__activity__workspace__icon'>
245
-                      <i className='fa fa-list-ul' />
246
-                    </div>
247
-                    <div className='dashboard__activity__workspace__name'>
248
-                      Recherche et developpement
249
-                    </div>
250
-                  </div>
251
-
252
-                  <div className='dashboard__activity__workspace'>
253
-                    <div className='dashboard__activity__workspace__icon'>
254
-                      <i className='fa fa-file-text-o' />
255
-                    </div>
256
-                    <div className='dashboard__activity__workspace__name'>
257
-                      <span>Marketing</span>
258
-                    </div>
259
-                  </div>
260 204
 
261 205
                   <div className='dashboard__activity__workspace'>
262 206
                     <div className='dashboard__activity__workspace__icon'>
263 207
                       <i className='fa fa-comments-o' />
264 208
                     </div>
265 209
                     <div className='dashboard__activity__workspace__name'>
266
-                      <span>Évolution</span>
267
-                    </div>
268
-                  </div>
269
-
270
-                  <div className='dashboard__activity__workspace'>
271
-                    <div className='dashboard__activity__workspace__icon'>
272
-                      <i className='fa fa-file-text-o' />
273
-                    </div>
274
-                    <div className='dashboard__activity__workspace__name'>
275
-                      Commercialisation
210
+                      <span>Développement Tracim</span>
276 211
                     </div>
277 212
                   </div>
278 213
 
279 214
                   <div className='dashboard__activity__more d-flex flex-row-reverse'>
280 215
                     <div className='dashboard__activity__more__btn btn btn-outline-primary'>
281
-                      Voir plus
216
+                      {this.props.t('See more')}
282 217
                     </div>
283 218
                   </div>
284 219
                 </div>
@@ -287,13 +222,14 @@ class Dashboard extends React.Component {
287 222
               <div className='dashboard__memberlist'>
288 223
 
289 224
                 <div className='dashboard__memberlist__title subTitle'>
290
-                  Liste des membres
225
+                  {this.props.t('Member List')}
291 226
                 </div>
292 227
 
293 228
                 <div className='dashboard__memberlist__wrapper'>
294 229
                   {this.state.displayNewMemberDashboard === false &&
295 230
                     <div>
296 231
                       <ul className='dashboard__memberlist__list'>
232
+
297 233
                         <li className='dashboard__memberlist__list__item'>
298 234
                           <div className='dashboard__memberlist__list__item__avatar'>
299 235
                             <img src={imgProfil} alt='avatar' />
@@ -311,110 +247,12 @@ class Dashboard extends React.Component {
311 247
                           </div>
312 248
                         </li>
313 249
 
314
-                        <li className='dashboard__memberlist__list__item'>
315
-                          <div className='dashboard__memberlist__list__item__avatar'>
316
-                            <img src={imgProfil} alt='avatar' />
317
-                          </div>
318
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
319
-                            <div className='dashboard__memberlist__list__item__info__name'>
320
-                              Aldwin Vinel
321
-                            </div>
322
-                            <div className='dashboard__memberlist__list__item__info__role'>
323
-                              Lecteur
324
-                            </div>
325
-                          </div>
326
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
327
-                            <i className='fa fa-trash-o' />
328
-                          </div>
329
-                        </li>
330
-
331
-                        <li className='dashboard__memberlist__list__item'>
332
-                          <div className='dashboard__memberlist__list__item__avatar'>
333
-                            <img src={imgProfil} alt='avatar' />
334
-                          </div>
335
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
336
-                            <div className='dashboard__memberlist__list__item__info__name'>
337
-                              William Himme
338
-                            </div>
339
-                            <div className='dashboard__memberlist__list__item__info__role'>
340
-                              Contributeur
341
-                            </div>
342
-                          </div>
343
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
344
-                            <i className='fa fa-trash-o' />
345
-                          </div>
346
-                        </li>
347
-
348
-                        <li className='dashboard__memberlist__list__item'>
349
-                          <div className='dashboard__memberlist__list__item__avatar'>
350
-                            <img src={imgProfil} alt='avatar' />
351
-                          </div>
352
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
353
-                            <div className='dashboard__memberlist__list__item__info__name'>
354
-                              Yacine Lite
355
-                            </div>
356
-                            <div className='dashboard__memberlist__list__item__info__role'>
357
-                              Contributeur
358
-                            </div>
359
-                          </div>
360
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
361
-                            <i className='fa fa-trash-o' />
362
-                          </div>
363
-                        </li>
364
-
365
-                        <li className='dashboard__memberlist__list__item'>
366
-                          <div className='dashboard__memberlist__list__item__avatar'>
367
-                            <img src={imgProfil} alt='avatar' />
368
-                          </div>
369
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
370
-                            <div className='dashboard__memberlist__list__item__info__name'>
371
-                              Alexi Falcin
372
-                            </div>
373
-                            <div className='dashboard__memberlist__list__item__info__role'>
374
-                              Gestionnaire
375
-                            </div>
376
-                          </div>
377
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
378
-                            <i className='fa fa-trash-o' />
379
-                          </div>
380
-                        </li>
381
-
382
-                        <li className='dashboard__memberlist__list__item'>
383
-                          <div className='dashboard__memberlist__list__item__avatar'>
384
-                            <img src={imgProfil} alt='avatar' />
385
-                          </div>
386
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
387
-                            <div className='dashboard__memberlist__list__item__info__name'>
388
-                              Mickaël Fonati
389
-                            </div>
390
-                            <div className='dashboard__memberlist__list__item__info__role'>
391
-                              Gestionnaire
392
-                            </div>
393
-                          </div>
394
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
395
-                            <i className='fa fa-trash-o' />
396
-                          </div>
397
-                        </li>
398
-
399
-                        <li className='dashboard__memberlist__list__item'>
400
-                          <div className='dashboard__memberlist__list__item__avatar'>
401
-                            <img src={imgProfil} alt='avatar' />
402
-                          </div>
403
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
404
-                            <div className='dashboard__memberlist__list__item__info__name'>
405
-                              Eva Lonbard
406
-                            </div>
407
-                            <div className='dashboard__memberlist__list__item__info__role'>
408
-                              Gestionnaire
409
-                            </div>
410
-                          </div>
411
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
412
-                            <i className='fa fa-trash-o' />
413
-                          </div>
414
-                        </li>
415 250
                       </ul>
416 251
 
417
-                      <div className='dashboard__memberlist__btnadd'>
252
+                      <div
253
+                        className='dashboard__memberlist__btnadd'
254
+                        onClick={this.handleToggleNewMemberDashboard}
255
+                      >
418 256
                         <div className='dashboard__memberlist__btnadd__button'>
419 257
                           <div className='dashboard__memberlist__btnadd__button__avatar'>
420 258
                             <div className='dashboard__memberlist__btnadd__button__avatar__icon'>
@@ -423,9 +261,8 @@ class Dashboard extends React.Component {
423 261
                           </div>
424 262
                           <div
425 263
                             className='dashboard__memberlist__btnadd__button__text'
426
-                            onClick={this.handleToggleNewMemberDashboard}
427 264
                           >
428
-                             Ajouter un membre
265
+                            {this.props.t('Add a member')}
429 266
                           </div>
430 267
                         </div>
431 268
                       </div>
@@ -433,89 +270,64 @@ class Dashboard extends React.Component {
433 270
                   }
434 271
 
435 272
                   {this.state.displayNewMemberDashboard === true &&
436
-                    <form className='dashboard__memberlist__form'>
437
-                      <div
438
-                        className='dashboard__memberlist__form__close d-flex justify-content-end'
439
-                      >
440
-                        <i className='fa fa-times' onClick={this.handleToggleNewMemberDashboard} />
273
+                  <form className='dashboard__memberlist__form'>
274
+                    <div
275
+                      className='dashboard__memberlist__form__close d-flex justify-content-end'
276
+                    >
277
+                      <i className='fa fa-times' onClick={this.handleToggleNewMemberDashboard} />
278
+                    </div>
279
+
280
+                    <div className='dashboard__memberlist__form__member'>
281
+                      <div className='dashboard__memberlist__form__member__name'>
282
+                        <label className='name__label' htmlFor='addmember'>
283
+                          {this.props.t('Enter the name or email of the member')}
284
+                        </label>
285
+                        <input type='text' id='addmember' className='name__input form-control' placeholder='Nom ou Email' />
441 286
                       </div>
442
-                      <div className='dashboard__memberlist__form__member'>
443
-                        <div className='dashboard__memberlist__form__member__name'>
444
-                          <label className='name__label' htmlFor='addmember'>Indiquer le nom ou l'email du membre</label>
445
-                          <input type='text' id='addmember' className='name__input form-control' placeholder='Nom ou Email' />
287
+
288
+                      <div className='dashboard__memberlist__form__member__create'>
289
+                        <div className='create__radiobtn mr-3'>
290
+                          <input type='radio' />
446 291
                         </div>
447
-                        <div className='dashboard__memberlist__form__member__create'>
448
-                          <div className='create__radiobtn mr-3'>
449
-                            <input type='radio' />
450
-                          </div>
451
-                          <div className='create__text'>
452
-                            Créer un compte
453
-                          </div>
292
+
293
+                        <div className='create__text'>
294
+                          {this.props.t('Create an account')}
454 295
                         </div>
455 296
                       </div>
456
-                      <div className='dashboard__memberlist__form__role'>
457
-                        <div className='dashboard__memberlist__form__role__text'>
458
-                          Choisissez le rôle du membre
459
-                        </div>
460
-                        <ul className='dashboard__memberlist__form__role__list'>
461
-                          <li className='dashboard__memberlist__form__role__list__item'>
462
-                            <div className='item__radiobtn mr-3'>
463
-                              <input type='radio' name='role' value='responsable' />
464
-                            </div>
465
-                            <div className='item__text'>
466
-                              <div className='item_text_icon mr-2'>
467
-                                <i className='fa fa-gavel' />
468
-                              </div>
469
-                              <div className='item__text__name'>
470
-                                Responsable
471
-                              </div>
472
-                            </div>
473
-                          </li>
474
-                          <li className='dashboard__memberlist__form__role__list__item'>
475
-                            <div className='item__radiobtn mr-3'>
476
-                              <input type='radio' name='role' value='gestionnaire' />
477
-                            </div>
478
-                            <div className='item__text'>
479
-                              <div className='item_text_icon mr-2'>
480
-                                <i className='fa fa-graduation-cap' />
481
-                              </div>
482
-                              <div className='item__text__name'>
483
-                                Gestionnaire de contenu
484
-                              </div>
485
-                            </div>
486
-                          </li>
487
-                          <li className='dashboard__memberlist__form__role__list__item'>
488
-                            <div className='item__radiobtn mr-3'>
489
-                              <input type='radio' name='role' value='contributeur' />
490
-                            </div>
491
-                            <div className='item__text'>
492
-                              <div className='item_text_icon mr-2'>
493
-                                <i className='fa fa-pencil' />
494
-                              </div>
495
-                              <div className='item__text__name'>
496
-                                Contributeur
497
-                              </div>
498
-                            </div>
499
-                          </li>
500
-                          <li className='dashboard__memberlist__form__role__list__item'>
501
-                            <div className='item__radiobtn mr-3'>
502
-                              <input type='radio' name='role' value='lecteur' />
297
+                    </div>
298
+
299
+                    <div className='dashboard__memberlist__form__role'>
300
+                      <div className='dashboard__memberlist__form__role__text'>
301
+                        {this.props.t('Choose the role of the member')}
302
+                      </div>
303
+
304
+                      <ul className='dashboard__memberlist__form__role__list'>
305
+
306
+                        <li className='dashboard__memberlist__form__role__list__item'>
307
+                          <div className='item__radiobtn mr-3'>
308
+                            <input type='radio' name='role' value='responsable' />
309
+                          </div>
310
+
311
+                          <div className='item__text'>
312
+                            <div className='item_text_icon mr-2'>
313
+                              <i className='fa fa-gavel' />
503 314
                             </div>
504
-                            <div className='item__text'>
505
-                              <div className='item_text_icon mr-2'>
506
-                                <i className='fa fa-eye' />
507
-                              </div>
508
-                              <div className='item__text__name'>
509
-                                Lecteur
510
-                              </div>
315
+
316
+                            <div className='item__text__name'>
317
+                              {this.props.t('Supervisor')}
511 318
                             </div>
512
-                          </li>
513
-                        </ul>
514
-                      </div>
515
-                      <div className='dashboard__memberlist__form__submitbtn'>
516
-                        <button className='btn btn-outline-primary'>Valider</button>
517
-                      </div>
518
-                    </form>
319
+                          </div>
320
+                        </li>
321
+
322
+                      </ul>
323
+                    </div>
324
+
325
+                    <div className='dashboard__memberlist__form__submitbtn'>
326
+                      <button className='btn btn-outline-primary'>
327
+                        {this.props.t('Validate')}
328
+                      </button>
329
+                    </div>
330
+                  </form>
519 331
                   }
520 332
                 </div>
521 333
               </div>
@@ -532,21 +344,21 @@ class Dashboard extends React.Component {
532 344
                   </div>
533 345
 
534 346
                   <div className='dashboard__moreinfo__webdav__btn__text genericBtnInfoDashboard__btn__text'>
535
-                    Implémenter Tracim dans votre explorateur
347
+                    {this.props.t('Implement Tracim in your explorer')}
536 348
                   </div>
537 349
                 </div>
538 350
                 {this.state.displayWebdavBtn === true &&
539
-                  <div>
540
-                    <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
541
-                      <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
542
-                        Retrouvez tous vos documents déposés en ligne directement sur votre ordinateur via le poste de travail, sans passer par le logiciel.
543
-                      </div>
351
+                <div>
352
+                  <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
353
+                    <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
354
+                      {this.props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
355
+                    </div>
544 356
 
545
-                      <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
546
-                        http://algoo.trac.im/webdav/
547
-                      </div>
357
+                    <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
358
+                      http://algoo.trac.im/webdav/
548 359
                     </div>
549 360
                   </div>
361
+                </div>
550 362
                 }
551 363
               </div>
552 364
               <div className='dashboard__moreinfo__calendar genericBtnInfoDashboard'>
@@ -560,33 +372,33 @@ class Dashboard extends React.Component {
560 372
                     </div>
561 373
 
562 374
                     <div className='dashboard__moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text'>
563
-                      Calendrier de l'espace de travail
375
+                      {this.props.t('Workspace Calendar')}
564 376
                     </div>
565 377
                   </div>
566 378
                 </div>
567 379
                 <div className='dashboard__moreinfo__calendar__wrapperText'>
568 380
                   {this.state.displayCalendarBtn === true &&
569
-                    <div>
570
-                      <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
571
-                        <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
572
-                          Chaque espace de travail possède son propre calendrier.
573
-                        </div>
381
+                  <div>
382
+                    <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
383
+                      <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
384
+                        {this.props.t('Each workspace has its own calendar.')}
385
+                      </div>
574 386
 
575
-                        <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
576
-                          http://algoo.trac.im/calendar/
577
-                        </div>
387
+                      <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
388
+                        http://algoo.trac.im/calendar/
578 389
                       </div>
579 390
                     </div>
391
+                  </div>
580 392
                   }
581 393
                 </div>
582 394
               </div>
583 395
             </div>
584
-          </div>
585
-        </div>
396
+          </PageContent>
397
+        </PageWrapper>
586 398
       </div>
587 399
     )
588 400
   }
589 401
 }
590 402
 
591
-const mapStateToProps = ({ user, app, contentType, workspaceList }) => ({ user, app, contentType, workspaceList })
592
-export default connect(mapStateToProps)(Dashboard)
403
+const mapStateToProps = ({ user, contentType, currentWorkspace }) => ({ user, contentType, curWs: currentWorkspace })
404
+export default connect(mapStateToProps)(translate()(Radium(Dashboard)))

+ 571 - 0
frontend/src/container/Dashboard_old.jsx View File

@@ -0,0 +1,571 @@
1
+import React from 'react'
2
+import { connect } from 'react-redux'
3
+import Sidebar from './Sidebar.jsx'
4
+import imgProfil from '../img/imgProfil.png'
5
+import { translate } from 'react-i18next'
6
+
7
+class Dashboard extends React.Component {
8
+  constructor (props) {
9
+    super(props)
10
+    this.state = {
11
+      workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null, // this is used to avoid handling the parseInt everytime
12
+      displayNewMemberDashboard: false,
13
+      displayNotifBtn: false,
14
+      displayWebdavBtn: false,
15
+      displayCalendarBtn: false
16
+    }
17
+  }
18
+
19
+  handleToggleNewMemberDashboard = () => this.setState(prevState => ({
20
+    displayNewMemberDashboard: !prevState.displayNewMemberDashboard
21
+  }))
22
+
23
+  handleToggleNotifBtn = () => this.setState(prevState => ({
24
+    displayNotifBtn: !prevState.displayNotifBtn
25
+  }))
26
+
27
+  handleToggleWebdavBtn = () => this.setState(prevState => ({
28
+    displayWebdavBtn: !prevState.displayWebdavBtn
29
+  }))
30
+
31
+  handleToggleCalendarBtn = () => this.setState(prevState => ({
32
+    displayCalendarBtn: !prevState.displayCalendarBtn
33
+  }))
34
+
35
+  render () {
36
+    return (
37
+      <div className='sidebarpagecontainer'>
38
+        <Sidebar />
39
+
40
+        <div className='dashboard'>
41
+          <div className='container-fluid nopadding'>
42
+            <div className='dashboard__header mb-5'>
43
+              <div className='pageTitleGeneric dashboard__header__title d-flex align-items-center'>
44
+                <div className='pageTitleGeneric__title dashboard__header__title__text mr-3'>
45
+                  {this.props.t('Dashboard')}
46
+                </div>
47
+                <div className='dashboard__header__acces' />
48
+              </div>
49
+
50
+              <div className='dashboard__header__advancedmode mr-3'>
51
+                <button type='button' className='btn btn-primary'>
52
+                  {this.props.t('Active advanced Dashboard')}
53
+                </button>
54
+              </div>
55
+            </div>
56
+
57
+            <div className='dashboard__workspace-wrapper'>
58
+              <div className='dashboard__workspace'>
59
+                <div className='dashboard__workspace__title'>
60
+                  Développement tracim
61
+                </div>
62
+
63
+                <div className='dashboard__workspace__detail'>
64
+                  Ligne directive pour le prochain design de Tracim et des futurs fonctionnalités.
65
+                </div>
66
+              </div>
67
+
68
+              <div className='dashboard__userstatut'>
69
+
70
+                <div className='dashboard__userstatut__role'>
71
+                  <div className='dashboard__userstatut__role__text'>
72
+                    Hi ! Alexi, vous êtes actuellement
73
+                  </div>
74
+                  <div className='dashboard__userstatut__role__rank'>
75
+                    <div className='dashboard__userstatut__role__rank__icon'>
76
+                      <i className='fa fa-graduation-cap' />
77
+                    </div>
78
+                    <div className='dashboard__userstatut__role__rank__rolename'>
79
+                      Gestionnaire de projet
80
+                    </div>
81
+                  </div>
82
+                </div>
83
+
84
+                <div className='dashboard__userstatut__notification'>
85
+                  <div className='dashboard__userstatut__notification__text'>
86
+                    Vous êtes abonné(e) aux notifications de ce workspace
87
+                  </div>
88
+                  {this.state.displayNotifBtn === false &&
89
+                  <div
90
+                    className='dashboard__userstatut__notification__btn btn btn-outline-primary'
91
+                    onClick={this.handleToggleNotifBtn}
92
+                  >
93
+                    {this.props.t('Change your status')}
94
+                  </div>
95
+                  }
96
+
97
+                  {this.state.displayNotifBtn === true &&
98
+                  <div className='dashboard__userstatut__notification__subscribe dropdown'>
99
+                    <button className='dashboard__userstatut__notification__subscribe__btn btn btn-outline-primary dropdown-toggle' type='button' id='dropdownMenuButton' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
100
+                      Abonné(e)
101
+                    </button>
102
+                    <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
103
+                      <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>
104
+                        {this.props.t('subscriber')}
105
+                      </div>
106
+                      <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>
107
+                        {this.props.t('unsubscribed')}
108
+                      </div>
109
+                    </div>
110
+                  </div>
111
+                  }
112
+                </div>
113
+              </div>
114
+            </div>
115
+
116
+            <div className='dashboard__calltoaction justify-content-xl-center'>
117
+              <div className='dashboard__calltoaction__button btnaction btnthread'>
118
+                <div className='dashboard__calltoaction__button__text'>
119
+                  <div className='dashboard__calltoaction__button__text__icon'>
120
+                    <i className='fa fa-comments-o' />
121
+                  </div>
122
+                  <div className='dashboard__calltoaction__button__text__title'>
123
+                    {this.props.t('Start a new Thread')}
124
+                  </div>
125
+                </div>
126
+              </div>
127
+
128
+              <div className='dashboard__calltoaction__button btnaction writefile'>
129
+                <div className='dashboard__calltoaction__button__text'>
130
+                  <div className='dashboard__calltoaction__button__text__icon'>
131
+                    <i className='fa fa-file-text-o' />
132
+                  </div>
133
+                  <div className='dashboard__calltoaction__button__text__title'>
134
+                    {this.props.t('Writing a document')}
135
+                  </div>
136
+                </div>
137
+              </div>
138
+
139
+              <div className='dashboard__calltoaction__button btnaction importfile'>
140
+                <div className='dashboard__calltoaction__button__text'>
141
+                  <div className='dashboard__calltoaction__button__text__icon'>
142
+                    <i className='fa fa-paperclip' />
143
+                  </div>
144
+                  <div className='dashboard__calltoaction__button__text__title'>
145
+                    {this.props.t('Upload a file')}
146
+                  </div>
147
+                </div>
148
+              </div>
149
+
150
+              {/*
151
+                <div className='dashboard__calltoaction__button btnaction visioconf'>
152
+                  <div className='dashboard__calltoaction__button__text'>
153
+                    <div className='dashboard__calltoaction__button__text__icon'>
154
+                      <i className='fa fa-video-camera' />
155
+                    </div>
156
+                    <div className='dashboard__calltoaction__button__text__title'>
157
+                      {this.props.t('Start a videoconference')}
158
+                    </div>
159
+                  </div>
160
+                </div>
161
+
162
+                <div className='dashboard__calltoaction__button btnaction calendar'>
163
+                  <div className='dashboard__calltoaction__button__text'>
164
+                    <div className='dashboard__calltoaction__button__text__icon'>
165
+                      <i className='fa fa-calendar' />
166
+                    </div>
167
+                    <div className='dashboard__calltoaction__button__text__title'>
168
+                      {this.props.t('View the Calendar')}
169
+                    </div>
170
+                  </div>
171
+                </div>
172
+              */ }
173
+
174
+              <div className='dashboard__calltoaction__button btnaction explore'>
175
+                <div className='dashboard__calltoaction__button__text'>
176
+                  <div className='dashboard__calltoaction__button__text__icon'>
177
+                    <i className='fa fa-folder-open-o' />
178
+                  </div>
179
+                  <div className='dashboard__calltoaction__button__text__title'>
180
+                    {this.props.t('Explore the workspace')}
181
+                  </div>
182
+                </div>
183
+              </div>
184
+            </div>
185
+
186
+            <div className='dashboard__wksinfo'>
187
+              <div className='dashboard__activity'>
188
+                <div className='dashboard__activity__header'>
189
+                  <div className='dashboard__activity__header__title subTitle'>
190
+                    {this.props.t('Recent activity')}
191
+                  </div>
192
+
193
+                  <div className='dashboard__activity__header__allread btn btn-outline-primary'>
194
+                    {this.props.t('Mark everything as read')}
195
+                  </div>
196
+                </div>
197
+                <div className='dashboard__activity__wrapper'>
198
+                  <div className='dashboard__activity__workspace'>
199
+                    <div className='dashboard__activity__workspace__icon'>
200
+                      <i className='fa fa-comments-o' />
201
+                    </div>
202
+                    <div className='dashboard__activity__workspace__name'>
203
+                      <span>Développement Tracim</span>
204
+                    </div>
205
+                  </div>
206
+
207
+                  <div className='dashboard__activity__workspace'>
208
+                    <div className='dashboard__activity__workspace__icon'>
209
+                      <i className='fa fa-list-ul' />
210
+                    </div>
211
+                    <div className='dashboard__activity__workspace__name'>
212
+                      Mission externe
213
+                    </div>
214
+                  </div>
215
+
216
+                  <div className='dashboard__activity__workspace'>
217
+                    <div className='dashboard__activity__workspace__icon'>
218
+                      <i className='fa fa-list-ul' />
219
+                    </div>
220
+                    <div className='dashboard__activity__workspace__name'>
221
+                      Recherche et developpement
222
+                    </div>
223
+                  </div>
224
+
225
+                  <div className='dashboard__activity__workspace'>
226
+                    <div className='dashboard__activity__workspace__icon'>
227
+                      <i className='fa fa-file-text-o' />
228
+                    </div>
229
+                    <div className='dashboard__activity__workspace__name'>
230
+                      <span>Marketing</span>
231
+                    </div>
232
+                  </div>
233
+
234
+                  <div className='dashboard__activity__workspace'>
235
+                    <div className='dashboard__activity__workspace__icon'>
236
+                      <i className='fa fa-comments-o' />
237
+                    </div>
238
+                    <div className='dashboard__activity__workspace__name'>
239
+                      <span>Évolution</span>
240
+                    </div>
241
+                  </div>
242
+
243
+                  <div className='dashboard__activity__workspace'>
244
+                    <div className='dashboard__activity__workspace__icon'>
245
+                      <i className='fa fa-file-text-o' />
246
+                    </div>
247
+                    <div className='dashboard__activity__workspace__name'>
248
+                      Commercialisation
249
+                    </div>
250
+                  </div>
251
+
252
+                  <div className='dashboard__activity__more d-flex flex-row-reverse'>
253
+                    <div className='dashboard__activity__more__btn btn btn-outline-primary'>
254
+                      {this.props.t('See more')}
255
+                    </div>
256
+                  </div>
257
+                </div>
258
+              </div>
259
+
260
+              <div className='dashboard__memberlist'>
261
+
262
+                <div className='dashboard__memberlist__title subTitle'>
263
+                  {this.props.t('Member List')}
264
+                </div>
265
+
266
+                <div className='dashboard__memberlist__wrapper'>
267
+                  {this.state.displayNewMemberDashboard === false &&
268
+                  <div>
269
+                    <ul className='dashboard__memberlist__list'>
270
+                      <li className='dashboard__memberlist__list__item'>
271
+                        <div className='dashboard__memberlist__list__item__avatar'>
272
+                          <img src={imgProfil} alt='avatar' />
273
+                        </div>
274
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
275
+                          <div className='dashboard__memberlist__list__item__info__name'>
276
+                            Jean Dupont
277
+                          </div>
278
+                          <div className='dashboard__memberlist__list__item__info__role'>
279
+                            Responsable
280
+                          </div>
281
+                        </div>
282
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
283
+                          <i className='fa fa-trash-o' />
284
+                        </div>
285
+                      </li>
286
+
287
+                      <li className='dashboard__memberlist__list__item'>
288
+                        <div className='dashboard__memberlist__list__item__avatar'>
289
+                          <img src={imgProfil} alt='avatar' />
290
+                        </div>
291
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
292
+                          <div className='dashboard__memberlist__list__item__info__name'>
293
+                            Aldwin Vinel
294
+                          </div>
295
+                          <div className='dashboard__memberlist__list__item__info__role'>
296
+                            Lecteur
297
+                          </div>
298
+                        </div>
299
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
300
+                          <i className='fa fa-trash-o' />
301
+                        </div>
302
+                      </li>
303
+
304
+                      <li className='dashboard__memberlist__list__item'>
305
+                        <div className='dashboard__memberlist__list__item__avatar'>
306
+                          <img src={imgProfil} alt='avatar' />
307
+                        </div>
308
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
309
+                          <div className='dashboard__memberlist__list__item__info__name'>
310
+                            William Himme
311
+                          </div>
312
+                          <div className='dashboard__memberlist__list__item__info__role'>
313
+                            Contributeur
314
+                          </div>
315
+                        </div>
316
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
317
+                          <i className='fa fa-trash-o' />
318
+                        </div>
319
+                      </li>
320
+
321
+                      <li className='dashboard__memberlist__list__item'>
322
+                        <div className='dashboard__memberlist__list__item__avatar'>
323
+                          <img src={imgProfil} alt='avatar' />
324
+                        </div>
325
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
326
+                          <div className='dashboard__memberlist__list__item__info__name'>
327
+                            Yacine Lite
328
+                          </div>
329
+                          <div className='dashboard__memberlist__list__item__info__role'>
330
+                            Contributeur
331
+                          </div>
332
+                        </div>
333
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
334
+                          <i className='fa fa-trash-o' />
335
+                        </div>
336
+                      </li>
337
+
338
+                      <li className='dashboard__memberlist__list__item'>
339
+                        <div className='dashboard__memberlist__list__item__avatar'>
340
+                          <img src={imgProfil} alt='avatar' />
341
+                        </div>
342
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
343
+                          <div className='dashboard__memberlist__list__item__info__name'>
344
+                            Alexi Falcin
345
+                          </div>
346
+                          <div className='dashboard__memberlist__list__item__info__role'>
347
+                            Gestionnaire
348
+                          </div>
349
+                        </div>
350
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
351
+                          <i className='fa fa-trash-o' />
352
+                        </div>
353
+                      </li>
354
+
355
+                      <li className='dashboard__memberlist__list__item'>
356
+                        <div className='dashboard__memberlist__list__item__avatar'>
357
+                          <img src={imgProfil} alt='avatar' />
358
+                        </div>
359
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
360
+                          <div className='dashboard__memberlist__list__item__info__name'>
361
+                            Mickaël Fonati
362
+                          </div>
363
+                          <div className='dashboard__memberlist__list__item__info__role'>
364
+                            Gestionnaire
365
+                          </div>
366
+                        </div>
367
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
368
+                          <i className='fa fa-trash-o' />
369
+                        </div>
370
+                      </li>
371
+
372
+                      <li className='dashboard__memberlist__list__item'>
373
+                        <div className='dashboard__memberlist__list__item__avatar'>
374
+                          <img src={imgProfil} alt='avatar' />
375
+                        </div>
376
+                        <div className='dashboard__memberlist__list__item__info mr-auto'>
377
+                          <div className='dashboard__memberlist__list__item__info__name'>
378
+                            Eva Lonbard
379
+                          </div>
380
+                          <div className='dashboard__memberlist__list__item__info__role'>
381
+                            Gestionnaire
382
+                          </div>
383
+                        </div>
384
+                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
385
+                          <i className='fa fa-trash-o' />
386
+                        </div>
387
+                      </li>
388
+                    </ul>
389
+
390
+                    <div
391
+                      className='dashboard__memberlist__btnadd'
392
+                      onClick={this.handleToggleNewMemberDashboard}
393
+                    >
394
+                      <div className='dashboard__memberlist__btnadd__button'>
395
+                        <div className='dashboard__memberlist__btnadd__button__avatar'>
396
+                          <div className='dashboard__memberlist__btnadd__button__avatar__icon'>
397
+                            <i className='fa fa-plus' />
398
+                          </div>
399
+                        </div>
400
+                        <div
401
+                          className='dashboard__memberlist__btnadd__button__text'
402
+                        >
403
+                          {this.props.t('Add a member')}
404
+                        </div>
405
+                      </div>
406
+                    </div>
407
+                  </div>
408
+                  }
409
+
410
+                  {this.state.displayNewMemberDashboard === true &&
411
+                  <form className='dashboard__memberlist__form'>
412
+                    <div
413
+                      className='dashboard__memberlist__form__close d-flex justify-content-end'
414
+                    >
415
+                      <i className='fa fa-times' onClick={this.handleToggleNewMemberDashboard} />
416
+                    </div>
417
+                    <div className='dashboard__memberlist__form__member'>
418
+                      <div className='dashboard__memberlist__form__member__name'>
419
+                        <label className='name__label' htmlFor='addmember'>
420
+                          {this.props.t('Enter the name or email of the member')}
421
+                        </label>
422
+                        <input type='text' id='addmember' className='name__input form-control' placeholder='Nom ou Email' />
423
+                      </div>
424
+                      <div className='dashboard__memberlist__form__member__create'>
425
+                        <div className='create__radiobtn mr-3'>
426
+                          <input type='radio' />
427
+                        </div>
428
+                        <div className='create__text'>
429
+                          {this.props.t('Create an account')}
430
+                        </div>
431
+                      </div>
432
+                    </div>
433
+                    <div className='dashboard__memberlist__form__role'>
434
+                      <div className='dashboard__memberlist__form__role__text'>
435
+                        {this.props.t('Choose the role of the member')}
436
+                      </div>
437
+                      <ul className='dashboard__memberlist__form__role__list'>
438
+                        <li className='dashboard__memberlist__form__role__list__item'>
439
+                          <div className='item__radiobtn mr-3'>
440
+                            <input type='radio' name='role' value='responsable' />
441
+                          </div>
442
+                          <div className='item__text'>
443
+                            <div className='item_text_icon mr-2'>
444
+                              <i className='fa fa-gavel' />
445
+                            </div>
446
+                            <div className='item__text__name'>
447
+                              {this.props.t('Supervisor')}
448
+                            </div>
449
+                          </div>
450
+                        </li>
451
+                        <li className='dashboard__memberlist__form__role__list__item'>
452
+                          <div className='item__radiobtn mr-3'>
453
+                            <input type='radio' name='role' value='gestionnaire' />
454
+                          </div>
455
+                          <div className='item__text'>
456
+                            <div className='item_text_icon mr-2'>
457
+                              <i className='fa fa-graduation-cap' />
458
+                            </div>
459
+                            <div className='item__text__name'>
460
+                              {this.props.t('Content Manager')}
461
+                            </div>
462
+                          </div>
463
+                        </li>
464
+                        <li className='dashboard__memberlist__form__role__list__item'>
465
+                          <div className='item__radiobtn mr-3'>
466
+                            <input type='radio' name='role' value='contributeur' />
467
+                          </div>
468
+                          <div className='item__text'>
469
+                            <div className='item_text_icon mr-2'>
470
+                              <i className='fa fa-pencil' />
471
+                            </div>
472
+                            <div className='item__text__name'>
473
+                              {this.props.t('Contributor')}
474
+                            </div>
475
+                          </div>
476
+                        </li>
477
+                        <li className='dashboard__memberlist__form__role__list__item'>
478
+                          <div className='item__radiobtn mr-3'>
479
+                            <input type='radio' name='role' value='lecteur' />
480
+                          </div>
481
+                          <div className='item__text'>
482
+                            <div className='item_text_icon mr-2'>
483
+                              <i className='fa fa-eye' />
484
+                            </div>
485
+                            <div className='item__text__name'>
486
+                              {this.props.t('Reader')}
487
+                            </div>
488
+                          </div>
489
+                        </li>
490
+                      </ul>
491
+                    </div>
492
+                    <div className='dashboard__memberlist__form__submitbtn'>
493
+                      <button className='btn btn-outline-primary'>
494
+                        {this.props.t('Validate')}
495
+                      </button>
496
+                    </div>
497
+                  </form>
498
+                  }
499
+                </div>
500
+              </div>
501
+            </div>
502
+
503
+            <div className='dashboard__moreinfo'>
504
+              <div className='dashboard__moreinfo__webdav genericBtnInfoDashboard'>
505
+                <div
506
+                  className='dashboard__moreinfo__webdav__btn genericBtnInfoDashboard__btn'
507
+                  onClick={this.handleToggleWebdavBtn}
508
+                >
509
+                  <div className='dashboard__moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
510
+                    <i className='fa fa-windows' />
511
+                  </div>
512
+
513
+                  <div className='dashboard__moreinfo__webdav__btn__text genericBtnInfoDashboard__btn__text'>
514
+                    {this.props.t('Implement Tracim in your explorer')}
515
+                  </div>
516
+                </div>
517
+                {this.state.displayWebdavBtn === true &&
518
+                <div>
519
+                  <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
520
+                    <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
521
+                      {this.props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
522
+                    </div>
523
+
524
+                    <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
525
+                      http://algoo.trac.im/webdav/
526
+                    </div>
527
+                  </div>
528
+                </div>
529
+                }
530
+              </div>
531
+              <div className='dashboard__moreinfo__calendar genericBtnInfoDashboard'>
532
+                <div className='dashboard__moreinfo__calendar__wrapperBtn'>
533
+                  <div
534
+                    className='dashboard__moreinfo__calendar__btn genericBtnInfoDashboard__btn'
535
+                    onClick={this.handleToggleCalendarBtn}
536
+                  >
537
+                    <div className='dashboard__moreinfo__calendar__btn__icon genericBtnInfoDashboard__btn__icon'>
538
+                      <i className='fa fa-calendar' />
539
+                    </div>
540
+
541
+                    <div className='dashboard__moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text'>
542
+                      {this.props.t('Workspace Calendar')}
543
+                    </div>
544
+                  </div>
545
+                </div>
546
+                <div className='dashboard__moreinfo__calendar__wrapperText'>
547
+                  {this.state.displayCalendarBtn === true &&
548
+                  <div>
549
+                    <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
550
+                      <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
551
+                        {this.props.t('Each workspace has its own calendar.')}
552
+                      </div>
553
+
554
+                      <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
555
+                        http://algoo.trac.im/calendar/
556
+                      </div>
557
+                    </div>
558
+                  </div>
559
+                  }
560
+                </div>
561
+              </div>
562
+            </div>
563
+          </div>
564
+        </div>
565
+      </div>
566
+    )
567
+  }
568
+}
569
+
570
+const mapStateToProps = ({ user, app, contentType, workspaceList }) => ({ user, app, contentType, workspaceList })
571
+export default connect(mapStateToProps)(translate()(Dashboard))

+ 13 - 8
frontend/src/container/Header.jsx View File

@@ -1,6 +1,8 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3
+import { withRouter } from 'react-router'
3 4
 import i18n from '../i18n.js'
5
+import appFactory from '../appFactory.js'
4 6
 import { translate } from 'react-i18next'
5 7
 import Cookies from 'js-cookie'
6 8
 import Logo from '../component/Header/Logo.jsx'
@@ -14,13 +16,13 @@ import MenuActionListItemNotification from '../component/Header/MenuActionListIt
14 16
 import logoHeader from '../img/logo-tracim.png'
15 17
 import {
16 18
   newFlashMessage,
17
-  setLangActive,
19
+  setUserLang,
18 20
   setUserDisconnected
19 21
 } from '../action-creator.sync.js'
20 22
 import {
21 23
   postUserLogout
22 24
 } from '../action-creator.async.js'
23
-import { COOKIE } from '../helper.js'
25
+import { COOKIE, PAGE } from '../helper.js'
24 26
 
25 27
 class Header extends React.Component {
26 28
   handleClickLogo = () => {}
@@ -32,15 +34,16 @@ class Header extends React.Component {
32 34
   handleChangeInput = e => this.setState({inputSearchValue: e.target.value})
33 35
   handleClickSubmit = () => {}
34 36
 
35
-  handleChangeLang = langId => {
36
-    this.props.dispatch(setLangActive(langId))
37
-    i18n.changeLanguage(langId)
37
+  handleChangeLang = idLang => {
38
+    this.props.dispatch(setUserLang(idLang))
39
+    i18n.changeLanguage(idLang)
40
+    this.props.dispatchCustomEvent('allApp_changeLang', idLang)
38 41
   }
39 42
 
40 43
   handleClickHelp = () => {}
41 44
 
42 45
   handleClickLogout = async () => {
43
-    const { dispatch, t } = this.props
46
+    const { history, dispatch, t } = this.props
44 47
 
45 48
     const fetchPostUserLogout = await dispatch(postUserLogout())
46 49
     if (fetchPostUserLogout.status === 204) {
@@ -48,8 +51,9 @@ class Header extends React.Component {
48 51
       Cookies.remove(COOKIE.USER_AUTH)
49 52
 
50 53
       dispatch(setUserDisconnected())
54
+      history.push(PAGE.LOGIN)
51 55
     } else {
52
-      dispatch(newFlashMessage(t('Login.logout_error', 'danger')))
56
+      dispatch(newFlashMessage(t('Disconnection error', 'danger')))
53 57
     }
54 58
   }
55 59
 
@@ -82,6 +86,7 @@ class Header extends React.Component {
82 86
 
83 87
               <MenuActionListItemDropdownLang
84 88
                 langList={lang}
89
+                idLangActive={user.lang}
85 90
                 onChangeLang={this.handleChangeLang}
86 91
               />
87 92
 
@@ -104,4 +109,4 @@ class Header extends React.Component {
104 109
 }
105 110
 
106 111
 const mapStateToProps = ({ lang, user }) => ({ lang, user })
107
-export default connect(mapStateToProps)(translate()(Header))
112
+export default withRouter(connect(mapStateToProps)(translate()(appFactory(Header))))

+ 3 - 3
frontend/src/container/Login.jsx View File

@@ -57,7 +57,7 @@ class Login extends React.Component {
57 57
 
58 58
       history.push(PAGE.HOME)
59 59
     } else if (fetchPostUserLogin.status === 400) {
60
-      dispatch(newFlashMessage(t('Login.fail'), 'danger'))
60
+      dispatch(newFlashMessage(t('Email or password invalid'), 'danger'))
61 61
     }
62 62
   }
63 63
 
@@ -83,7 +83,7 @@ class Login extends React.Component {
83 83
                         customClass='mb-3 mt-4'
84 84
                         icon='fa-envelope-open-o'
85 85
                         type='email'
86
-                        placeHolder='Adresse Email'
86
+                        placeHolder={this.props.t('Email Adress')}
87 87
                         invalidMsg='Email invalide.'
88 88
                         isInvalid={this.state.inputLogin.isInvalid}
89 89
                         value={this.state.inputLogin.value}
@@ -95,7 +95,7 @@ class Login extends React.Component {
95 95
                         customClass=''
96 96
                         icon='fa-lock'
97 97
                         type='password'
98
-                        placeHolder='Mot de passe'
98
+                        placeHolder={this.props.t('Password')}
99 99
                         invalidMsg='Mot de passe invalide.'
100 100
                         isInvalid={this.state.inputPassword.isInvalid}
101 101
                         value={this.state.inputPassword.value}

+ 47 - 9
frontend/src/container/Sidebar.jsx View File

@@ -3,12 +3,17 @@ import { connect } from 'react-redux'
3 3
 import { withRouter } from 'react-router'
4 4
 import classnames from 'classnames'
5 5
 import { translate } from 'react-i18next'
6
+import appFactory from '../appFactory.js'
6 7
 import WorkspaceListItem from '../component/Sidebar/WorkspaceListItem.jsx'
7 8
 import {
8 9
   setWorkspaceListIsOpenInSidebar,
9
-  updateWorkspaceFilter
10
+  updateWorkspaceFilter,
11
+  updateWorkspaceListData
10 12
 } from '../action-creator.sync.js'
11
-import { PAGE } from '../helper.js'
13
+import {
14
+  getWorkspaceList
15
+} from '../action-creator.async.js'
16
+import { PAGE, workspaceConfig } from '../helper.js'
12 17
 
13 18
 const qs = require('query-string')
14 19
 
@@ -19,6 +24,21 @@ class Sidebar extends React.Component {
19 24
       sidebarClose: false,
20 25
       workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null
21 26
     }
27
+
28
+    document.addEventListener('appCustomEvent', this.customEventReducer)
29
+  }
30
+
31
+  customEventReducer = async ({ detail: { type, data } }) => {
32
+    switch (type) {
33
+      case 'refreshWorkspaceList':
34
+        console.log('%c<Sidebar> Custom event', 'color: #28a745', type, data)
35
+        this.loadWorkspaceList()
36
+        break
37
+    }
38
+  }
39
+
40
+  componentDidMount () {
41
+    this.loadWorkspaceList()
22 42
   }
23 43
 
24 44
   componentDidUpdate (prevProps, prevState) {
@@ -29,6 +49,20 @@ class Sidebar extends React.Component {
29 49
     if (prevState.workspaceIdInUrl !== newWorkspaceId) this.setState({workspaceIdInUrl: newWorkspaceId})
30 50
   }
31 51
 
52
+  loadWorkspaceList = async () => {
53
+    const { workspaceIdInUrl } = this.state
54
+    const { user, dispatch } = this.props
55
+
56
+    if (user.user_id !== -1) {
57
+      const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
58
+
59
+      if (fetchGetWorkspaceList.status === 200) {
60
+        dispatch(updateWorkspaceListData(fetchGetWorkspaceList.json))
61
+        dispatch(setWorkspaceListIsOpenInSidebar(workspaceIdInUrl || fetchGetWorkspaceList.json[0].workspace_id, true))
62
+      }
63
+    }
64
+  }
65
+
32 66
   handleClickWorkspace = (idWs, newIsOpenInSidebar) => this.props.dispatch(setWorkspaceListIsOpenInSidebar(idWs, newIsOpenInSidebar))
33 67
 
34 68
   handleClickAllContent = idWs => {
@@ -45,13 +79,15 @@ class Sidebar extends React.Component {
45 79
 
46 80
     history.push(`${PAGE.WORKSPACE.CONTENT_LIST(idWs)}?type=${newFilter.join(';')}`) // workspace.filter gets updated on react redraw from match.params
47 81
 
48
-    // obviously, it's ugly to use custom event to tell WorkspaceContent to refresh, but since WorkspaceContent
82
+    // obviously, it's ugly to use custom event to tell WorkspaceContentList to refresh, but since WorkspaceContentList
49 83
     // will end up being an App, it'll have to be that way. So it's fine
50 84
     GLOBAL_dispatchEvent({ type: 'refreshContentList', data: {} })
51 85
   }
52 86
 
53 87
   handleClickToggleSidebar = () => this.setState(prev => ({sidebarClose: !prev.sidebarClose}))
54 88
 
89
+  handleClickNewWorkspace = () => this.props.renderAppPopupCreation(workspaceConfig, this.props.user, null, null)
90
+
55 91
   render () {
56 92
     const { sidebarClose, workspaceIdInUrl } = this.state
57 93
     const { activeLang, workspaceList, t } = this.props
@@ -85,8 +121,11 @@ class Sidebar extends React.Component {
85 121
             </nav>
86 122
 
87 123
             <div className='sidebar__btnnewworkspace'>
88
-              <button className='sidebar__btnnewworkspace__btn btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover mb-5'>
89
-                {t('Sidebar.create_new_workspace')}
124
+              <button
125
+                className='sidebar__btnnewworkspace__btn btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover mb-5'
126
+                onClick={this.handleClickNewWorkspace}
127
+              >
128
+                {t('Create a workspace')}
90 129
               </button>
91 130
             </div>
92 131
 
@@ -106,11 +145,10 @@ class Sidebar extends React.Component {
106 145
   }
107 146
 }
108 147
 
109
-const mapStateToProps = ({ lang, user, workspace, workspaceList, app }) => ({
148
+const mapStateToProps = ({ lang, user, workspace, workspaceList }) => ({
110 149
   activeLang: lang.find(l => l.active) || {id: 'en'},
111 150
   user,
112 151
   workspace,
113
-  workspaceList,
114
-  app
152
+  workspaceList
115 153
 })
116
-export default withRouter(connect(mapStateToProps)(translate()(Sidebar)))
154
+export default withRouter(connect(mapStateToProps)(appFactory(translate()(Sidebar))))

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

@@ -5,6 +5,7 @@ import Header from './Header.jsx'
5 5
 import Login from './Login.jsx'
6 6
 import Dashboard from './Dashboard.jsx'
7 7
 import Account from './Account.jsx'
8
+import AppFullscreenManager from './AppFullscreenManager.jsx'
8 9
 import FlashMessage from '../component/FlashMessage.jsx'
9 10
 import WorkspaceContent from './WorkspaceContent.jsx'
10 11
 import WIPcomponent from './WIPcomponent.jsx'
@@ -14,15 +15,34 @@ import {
14 15
 import PrivateRoute from './PrivateRoute.jsx'
15 16
 import { COOKIE, PAGE } from '../helper.js'
16 17
 import {
17
-  getUserIsConnected
18
+  getAppList,
19
+  getUserIsConnected,
20
+  getContentTypeList
18 21
 } from '../action-creator.async.js'
19 22
 import {
20 23
   removeFlashMessage,
21
-  setUserConnected
24
+  setAppList,
25
+  setUserConnected,
26
+  setContentTypeList
22 27
 } from '../action-creator.sync.js'
23 28
 import Cookies from 'js-cookie'
24 29
 
25 30
 class Tracim extends React.Component {
31
+  constructor (props) {
32
+    super(props)
33
+
34
+    document.addEventListener('appCustomEvent', this.customEventReducer)
35
+  }
36
+
37
+  customEventReducer = async ({ detail: { type, data } }) => {
38
+    switch (type) {
39
+      case 'redirect':
40
+        console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
41
+        this.props.history.push(data.url)
42
+        break
43
+    }
44
+  }
45
+
26 46
   async componentDidMount () {
27 47
     const { dispatch } = this.props
28 48
 
@@ -34,14 +54,24 @@ class Tracim extends React.Component {
34 54
     const fetchGetUserIsConnected = await dispatch(getUserIsConnected(userFromCookies))
35 55
     switch (fetchGetUserIsConnected.status) {
36 56
       case 200:
37
-        dispatch(setUserConnected({
57
+        const userLogged = {
38 58
           ...fetchGetUserIsConnected.json,
39 59
           auth: userFromCookies.auth,
40 60
           logged: true
41
-        }))
61
+        }
62
+
63
+        dispatch(setUserConnected(userLogged))
64
+
65
+        const fetchGetAppList = await dispatch(getAppList(userLogged))
66
+        if (fetchGetAppList.status === 200) dispatch(setAppList(fetchGetAppList.json))
67
+
68
+        const fetchGetContentTypeList = await dispatch(getContentTypeList(userLogged))
69
+        if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
42 70
         break
71
+
43 72
       case 401:
44 73
         dispatch(setUserConnected({logged: false})); break
74
+
45 75
       default:
46 76
         dispatch(setUserConnected({logged: null})); break
47 77
     }
@@ -69,7 +99,10 @@ class Tracim extends React.Component {
69 99
           </Switch>
70 100
 
71 101
           <PrivateRoute path={PAGE.ACCOUNT} component={Account} />
102
+          <PrivateRoute path={PAGE.ADMIN.ROOT} component={AppFullscreenManager} />
72 103
           <PrivateRoute path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
104
+
105
+          <div id='appFeatureContainer' />
73 106
         </div>
74 107
 
75 108
       </div>
@@ -77,5 +110,5 @@ class Tracim extends React.Component {
77 110
   }
78 111
 }
79 112
 
80
-const mapStateToProps = ({ flashMessage, user }) => ({ flashMessage, user })
113
+const mapStateToProps = ({ flashMessage }) => ({ flashMessage })
81 114
 export default withRouter(connect(mapStateToProps)(translate()(Tracim)))

+ 24 - 58
frontend/src/container/WorkspaceContent.jsx View File

@@ -7,26 +7,21 @@ import Sidebar from './Sidebar.jsx'
7 7
 import Folder from '../component/Workspace/Folder.jsx'
8 8
 import ContentItem from '../component/Workspace/ContentItem.jsx'
9 9
 import ContentItemHeader from '../component/Workspace/ContentItemHeader.jsx'
10
-import PageWrapper from '../component/common/layout/PageWrapper.jsx'
11
-import PageTitle from '../component/common/layout/PageTitle.jsx'
12
-import PageContent from '../component/common/layout/PageContent.jsx'
13 10
 import DropdownCreateButton from '../component/common/Input/DropdownCreateButton.jsx'
14 11
 import OpenContentApp from '../component/Workspace/OpenContentApp.jsx'
15 12
 import OpenCreateContentApp from '../component/Workspace/OpenCreateContentApp.jsx'
16 13
 import {
17
-  getAppList,
18
-  getContentTypeList,
14
+  PageWrapper,
15
+  PageTitle,
16
+  PageContent
17
+} from 'tracim_frontend_lib'
18
+import {
19 19
   getWorkspaceContentList,
20
-  getFolderContent,
21
-  getWorkspaceList
20
+  getFolderContent
22 21
 } from '../action-creator.async.js'
23 22
 import {
24 23
   newFlashMessage,
25
-  setAppList,
26
-  setContentTypeList,
27
-  setWorkspaceContent,
28
-  setWorkspaceListIsOpenInSidebar,
29
-  updateWorkspaceListData
24
+  setWorkspaceContentList
30 25
 } from '../action-creator.sync.js'
31 26
 
32 27
 const qs = require('query-string')
@@ -35,11 +30,6 @@ class WorkspaceContent extends React.Component {
35 30
   constructor (props) {
36 31
     super(props)
37 32
     this.state = {
38
-      popupCreateContent: {
39
-        display: false,
40
-        type: undefined,
41
-        folder: undefined
42
-      },
43 33
       workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null, // this is used to avoid handling the parseInt every time
44 34
       appOpenedType: false
45 35
     }
@@ -69,38 +59,16 @@ class WorkspaceContent extends React.Component {
69 59
   }
70 60
 
71 61
   async componentDidMount () {
72
-    const { workspaceIdInUrl } = this.state
73
-    const { user, workspaceList, app, contentType, match, dispatch } = this.props
62
+    const { workspaceList, match } = this.props
74 63
 
75 64
     console.log('%c<WorkspaceContent> componentDidMount', 'color: #c17838')
76 65
 
77
-    if (app.length === 0) { // @fixme shouldn't this be done by <Sidebar> ?
78
-      const fetchGetAppList = await dispatch(getAppList(user))
79
-      if (fetchGetAppList.status === 200) dispatch(setAppList(fetchGetAppList.json))
80
-    }
81
-
82
-    if (contentType.length === 0) {
83
-      const fetchGetContentTypeList = await dispatch(getContentTypeList(user))
84
-      if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
85
-    }
86
-
87 66
     let wsToLoad = null
88
-    if (match.params.idws !== undefined) wsToLoad = match.params.idws
89
-
90
-    if (user.user_id !== -1 && workspaceList.length === 0) {
91
-      const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
92 67
 
93
-      if (fetchGetWorkspaceList.status === 200) {
94
-        dispatch(updateWorkspaceListData(fetchGetWorkspaceList.json))
95
-        dispatch(setWorkspaceListIsOpenInSidebar(workspaceIdInUrl || fetchGetWorkspaceList.json[0].workspace_id, true))
96
-
97
-        if (match.params.idws === undefined && fetchGetWorkspaceList.json.length > 0) {
98
-          wsToLoad = fetchGetWorkspaceList.json[0].workspace_id // load first ws if none specified
99
-        }
100
-      }
101
-    }
102
-
103
-    if (wsToLoad === null) return // ws already loaded
68
+    if (match.params.idws === undefined) {
69
+      if (workspaceList.length > 0) wsToLoad = workspaceList[0].id
70
+      else return
71
+    } else wsToLoad = match.params.idws
104 72
 
105 73
     this.loadContentList(wsToLoad)
106 74
   }
@@ -111,7 +79,6 @@ class WorkspaceContent extends React.Component {
111 79
     if (this.state.workspaceIdInUrl === null) return
112 80
 
113 81
     const idWorkspace = parseInt(this.props.match.params.idws)
114
-
115 82
     if (isNaN(idWorkspace)) return
116 83
 
117 84
     const prevFilter = qs.parse(prevProps.location.search).type
@@ -126,7 +93,7 @@ class WorkspaceContent extends React.Component {
126 93
   }
127 94
 
128 95
   componentWillUnmount () {
129
-    this.props.emitEventApp('unmount_app')
96
+    this.props.dispatchCustomEvent('unmount_app')
130 97
     document.removeEventListener('appCustomEvent', this.customEventReducer)
131 98
   }
132 99
 
@@ -135,7 +102,7 @@ class WorkspaceContent extends React.Component {
135 102
 
136 103
     const wsContent = await dispatch(getWorkspaceContentList(user, idWorkspace, 0))
137 104
 
138
-    if (wsContent.status === 200) dispatch(setWorkspaceContent(wsContent.json, qs.parse(location.search).type))
105
+    if (wsContent.status === 200) dispatch(setWorkspaceContentList(wsContent.json, qs.parse(location.search).type))
139 106
     else dispatch(newFlashMessage('Error while loading workspace', 'danger'))
140 107
   }
141 108
 
@@ -181,7 +148,7 @@ class WorkspaceContent extends React.Component {
181 148
   handleUpdateAppOpenedType = openedAppType => this.setState({appOpenedType: openedAppType})
182 149
 
183 150
   render () {
184
-    const { workspaceContent, contentType } = this.props
151
+    const { workspaceContentList, contentType } = this.props
185 152
 
186 153
     const filterWorkspaceContent = (contentList, filter) => {
187 154
       return filter.length === 0
@@ -194,8 +161,8 @@ class WorkspaceContent extends React.Component {
194 161
 
195 162
     const urlFilter = qs.parse(this.props.location.search).type
196 163
 
197
-    const filteredWorkspaceContent = workspaceContent.length > 0
198
-      ? filterWorkspaceContent(workspaceContent, urlFilter ? [urlFilter] : [])
164
+    const filteredWorkspaceContentList = workspaceContentList.length > 0
165
+      ? filterWorkspaceContent(workspaceContentList, urlFilter ? [urlFilter] : [])
199 166
       : []
200 167
 
201 168
     return (
@@ -221,7 +188,8 @@ class WorkspaceContent extends React.Component {
221 188
           <PageTitle
222 189
             parentClass='workspace__header'
223 190
             customClass='justify-content-between'
224
-            title={workspaceContent.label ? workspaceContent.label : ''}
191
+            title='Liste des Contenus'
192
+            subtitle={workspaceContentList.label ? workspaceContentList.label : ''}
225 193
           >
226 194
             <DropdownCreateButton
227 195
               parentClass='workspace__header__btnaddworkspace'
@@ -237,7 +205,7 @@ class WorkspaceContent extends React.Component {
237 205
             <div className='workspace__content__fileandfolder folder__content active'>
238 206
               <ContentItemHeader />
239 207
 
240
-              { filteredWorkspaceContent.map((c, i) => c.type === 'folder'
208
+              { filteredWorkspaceContentList.map((c, i) => c.type === 'folder'
241 209
                 ? (
242 210
                   <Folder
243 211
                     availableApp={contentType}
@@ -252,7 +220,7 @@ class WorkspaceContent extends React.Component {
252 220
                     }}
253 221
                     onClickFolder={this.handleClickFolder}
254 222
                     onClickCreateContent={this.handleClickCreateContent}
255
-                    isLast={i === filteredWorkspaceContent.length - 1}
223
+                    isLast={i === filteredWorkspaceContentList.length - 1}
256 224
                     key={c.id}
257 225
                   />
258 226
                 )
@@ -262,7 +230,7 @@ class WorkspaceContent extends React.Component {
262 230
                     type={c.type}
263 231
                     faIcon={contentType.length ? contentType.find(a => a.slug === c.type).faIcon : ''}
264 232
                     statusSlug={c.statusSlug}
265
-                    contentType={contentType.find(ct => ct.slug === c.type)}
233
+                    contentType={contentType.length ? contentType.find(ct => ct.slug === c.type) : null}
266 234
                     onClickItem={() => this.handleClickContentItem(c)}
267 235
                     onClickExtendedAction={{
268 236
                       edit: e => this.handleClickEditContentItem(e, c),
@@ -272,7 +240,7 @@ class WorkspaceContent extends React.Component {
272 240
                       delete: e => this.handleClickDeleteContentItem(e, c)
273 241
                     }}
274 242
                     onClickCreateContent={this.handleClickCreateContent}
275
-                    isLast={i === filteredWorkspaceContent.length - 1}
243
+                    isLast={i === filteredWorkspaceContentList.length - 1}
276 244
                     key={c.id}
277 245
                   />
278 246
                 )
@@ -285,8 +253,6 @@ class WorkspaceContent extends React.Component {
285 253
               onClickCreateContent={this.handleClickCreateContent}
286 254
               availableApp={contentType}
287 255
             />
288
-
289
-            <div id='appContainer' />
290 256
           </PageContent>
291 257
 
292 258
         </PageWrapper>
@@ -295,5 +261,5 @@ class WorkspaceContent extends React.Component {
295 261
   }
296 262
 }
297 263
 
298
-const mapStateToProps = ({ user, workspaceContent, workspaceList, app, contentType }) => ({ user, workspaceContent, workspaceList, app, contentType })
264
+const mapStateToProps = ({ user, workspaceContentList, workspaceList, contentType }) => ({ user, workspaceContentList, workspaceList, contentType })
299 265
 export default withRouter(connect(mapStateToProps)(appFactory(WorkspaceContent)))

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

@@ -43,7 +43,7 @@ coloricon()
43 43
         margin 0
44 44
     &__advancedmode
45 45
       cursor pointer
46
-  &__wkswrapper
46
+  &__workspace-wrapper
47 47
     flexwrap()
48 48
   &__workspace
49 49
     margin-right 20px
@@ -62,9 +62,9 @@ coloricon()
62 62
       flexwrap()
63 63
       margin 20px 0
64 64
       font-size 18px
65
-      &__text
65
+      &__msg
66 66
         margin-right 15px
67
-      &__rank
67
+      &__definition
68 68
         display flex
69 69
         &__icon
70 70
           margin-right 15px
@@ -88,25 +88,11 @@ coloricon()
88 88
       &:active
89 89
         box-shadow inset 0px 0px 5px 2px #656565
90 90
       &__text
91
+        color white
91 92
         &__icon
92 93
           font-size 30px
93
-          .fa-comments-o, .fa-paperclip, .fa-calendar, .fa-file-text-o, .fa-folder-open-o, .fa-video-camera
94
-              color white
95 94
         &__title
96 95
           font-size 18px
97
-          color white
98
-    .btnthread
99
-      background-color threadColor
100
-    .writefile
101
-      background-color writefile
102
-    .importfile
103
-      background-color importfile
104
-    .visioconf
105
-      background-color grey-hover
106
-    .calendar
107
-      background-color calendar
108
-    .explore
109
-      background-color explore
110 96
   &__wksinfo
111 97
     flexwrap()
112 98
     margin-top 150px
@@ -270,19 +256,19 @@ coloricon()
270 256
     justify-content space-between
271 257
     flexwrap wrap
272 258
     &__webdav
273
-      margin-bottom 40px
259
+      margin 0 15px 40px 0
274 260
       &__btn
275 261
         width 300px
276 262
       &__information
277
-        width 550px
263
+        width 300px
278 264
     &__calendar
279 265
       margin-bottom 100px
280 266
       &__wrapperBtn
281
-        margin-right 300px
267
+        margin-right 290px
282 268
       &__btn
283 269
         width 300px
284 270
       &__information
285
-        width 550px
271
+        width 300px
286 272
 
287 273
 /**** MEDIAQUERIES *****/
288 274
 

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

@@ -202,7 +202,7 @@ a
202 202
   display flex
203 203
   flex-direction column
204 204
   justify-content center
205
-  margin 20px 30px 20px 0
205
+  margin 0 15px
206 206
   border-radius 10px
207 207
   padding 15px
208 208
   width 230px
@@ -210,6 +210,10 @@ a
210 210
   box-shadow shadow-all
211 211
   text-align center
212 212
   cursor pointer
213
+  &:nth-child(1)
214
+    margin-left 0
215
+  &:nth-last-child
216
+    margin-right 0
213 217
 
214 218
 .genericBtnInfoDashboard
215 219
   &__btn
@@ -237,4 +241,4 @@ a
237 241
       margin-bottom 15px
238 242
       border 1px solid grey
239 243
       border-radius 10px
240
-      padding 15px 25px
244
+      padding 15px

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

@@ -40,7 +40,6 @@
40 40
         background-color transparent
41 41
         cursor pointer
42 42
       &__itemlanguage
43
-        display none
44 43
         &__languagedropdown
45 44
           .languagedropdown
46 45
             &__btnlanguage

+ 3 - 3
frontend/src/css/Login.styl View File

@@ -65,10 +65,10 @@
65 65
         width 130px
66 66
   &__footer
67 67
     position fixed
68
-    top 95%
69
-    left calc(50% - 152px) // 152px => width of the text / 2
68
+    bottom 2%
69
+    left calc(50% - 155px) // 155px => width of the text / 2
70 70
     &__text
71
-      width 305px
71
+      width 310px
72 72
       font-size 17px
73 73
 
74 74
 @media (min-width: min-lg) and (max-width: max-lg)

+ 1 - 1
frontend/src/css/Workspace.styl View File

@@ -8,7 +8,7 @@
8 8
     &__button
9 9
       display flex
10 10
       justify-content flex-end
11
-      margin 50px 15px 0 0
11
+      margin 45px 15px 45px 0
12 12
       width 100%
13 13
       &__btnaddworkspace
14 14
         margin-bottom 50px

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

@@ -12,6 +12,15 @@ export const COOKIE = {
12 12
   USER_AUTH: 'user_auth'
13 13
 }
14 14
 
15
+// Côme - 2018/08/02 - shouldn't this come from api ?
16
+export const workspaceConfig = {
17
+  slug: 'workspace',
18
+  faIcon: 'space-shuttle',
19
+  hexcolor: '#7d4e24',
20
+  creationLabel: 'Create a workspace',
21
+  domContainer: 'appFeatureContainer'
22
+}
23
+
15 24
 export const PAGE = {
16 25
   HOME: '/',
17 26
   WORKSPACE: {
@@ -26,7 +35,12 @@ export const PAGE = {
26 35
     ADMIN: (idws = ':idws') => `/workspaces/${idws}/admin`
27 36
   },
28 37
   LOGIN: '/login',
29
-  ACCOUNT: '/account'
38
+  ACCOUNT: '/account',
39
+  ADMIN: {
40
+    ROOT: '/admin',
41
+    WORKSPACE: '/admin/workspace',
42
+    USEr: '/admin/user'
43
+  }
30 44
 }
31 45
 
32 46
 export const ROLE = [{
@@ -50,3 +64,5 @@ export const ROLE = [{
50 64
   icon: 'fa-gavel',
51 65
   translationKey: 'role.manager'
52 66
 }]
67
+
68
+export const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route

+ 19 - 9
frontend/src/i18n.js View File

@@ -1,29 +1,39 @@
1 1
 import i18n from 'i18next'
2 2
 import { reactI18nextModule } from 'react-i18next'
3
-import { langFr, langEn } from 'tracim_frontend_lib'
4
-import fr from './translate/fr.js'
5
-import en from './translate/en.js'
3
+import { frLib, enLib } from 'tracim_frontend_lib'
4
+import en from '../i18next.scanner/en/translation.json'
5
+import fr from '../i18next.scanner/fr/translation.json'
6
+
7
+// get translation files of apps
8
+// theses files are generated by build_appname.sh
9
+const htmlDocEnTranslation = require('../dist/app/html-document_en_translation.json')
10
+const htmlDocFrTranslation = require('../dist/app/html-document_fr_translation.json')
6 11
 
7 12
 i18n
8 13
   .use(reactI18nextModule)
9 14
   .init({
10
-    fallbackLng: 'fr',
15
+    fallbackLng: 'en',
11 16
     // have a common namespace used around the full app
12 17
     ns: ['translation'], // namespace
13 18
     defaultNS: 'translation',
14 19
     debug: true,
15
-    // interpolation: {
16
-    //   escapeValue: false, // not needed for react!!
17
-    // },
18 20
     react: {
19 21
       wait: true
20 22
     },
21 23
     resources: {
22 24
       en: {
23
-        translation: {...langEn.translation, ...en.translation}
25
+        translation: {
26
+          ...enLib, // fronted_lib
27
+          ...en, // frontend
28
+          ...htmlDocEnTranslation // html-document
29
+        }
24 30
       },
25 31
       fr: {
26
-        translation: {...langFr.translation, ...fr.translation}
32
+        translation: {
33
+          ...frLib, // fronted_lib
34
+          ...fr, // frontend
35
+          ...htmlDocFrTranslation // html-document
36
+        }
27 37
       }
28 38
     }
29 39
   })

frontend/src/img/drapeau-en.png → frontend/src/img/flag_en.png View File


frontend/src/img/drapeau-fr.png → frontend/src/img/flag_fr.png View File


frontend/src/reducer/app.js → frontend/src/reducer/appList.js View File

@@ -1,8 +1,8 @@
1
-import { APP_LIST } from '../action-creator.sync.js'
1
+import { SET, APP_LIST } from '../action-creator.sync.js'
2 2
 
3 3
 export default function app (state = [], action) {
4 4
   switch (action.type) {
5
-    case `Set/${APP_LIST}`:
5
+    case `${SET}/${APP_LIST}`:
6 6
       return action.appList.map(a => ({
7 7
         label: a.label,
8 8
         slug: a.slug,

+ 2 - 2
frontend/src/reducer/contentType.js View File

@@ -1,8 +1,8 @@
1
-import { CONTENT_TYPE_LIST } from '../action-creator.sync.js'
1
+import { SET, CONTENT_TYPE_LIST } from '../action-creator.sync.js'
2 2
 
3 3
 export function contentType (state = [], action) {
4 4
   switch (action.type) {
5
-    case `Set/${CONTENT_TYPE_LIST}`:
5
+    case `${SET}/${CONTENT_TYPE_LIST}`:
6 6
       return action.contentTypeList.map(ct => ({
7 7
         label: ct.label,
8 8
         slug: ct.slug,

+ 46 - 0
frontend/src/reducer/currentWorkspace.js View File

@@ -0,0 +1,46 @@
1
+import {SET, WORKSPACE_DETAIL, WORKSPACE_MEMBER_LIST} from '../action-creator.sync.js'
2
+import { handleRouteFromApi } from '../helper.js'
3
+
4
+const defaultWorkspace = {
5
+  id: 0,
6
+  slug: '',
7
+  label: '',
8
+  description: '',
9
+  sidebarEntries: [],
10
+  member: []
11
+}
12
+
13
+export default function currentWorkspace (state = defaultWorkspace, action) {
14
+  switch (action.type) {
15
+    case `${SET}/${WORKSPACE_DETAIL}`:
16
+      return {
17
+        ...state,
18
+        id: action.workspaceDetail.workspace_id,
19
+        slug: action.workspaceDetail.slug,
20
+        label: action.workspaceDetail.label,
21
+        description: action.workspaceDetail.description,
22
+        sidebarEntries: action.workspaceDetail.sidebar_entries.map(sbe => ({
23
+          slug: sbe.slug,
24
+          route: handleRouteFromApi(sbe.route),
25
+          faIcon: sbe.fa_icon,
26
+          hexcolor: sbe.hexcolor,
27
+          label: sbe.label
28
+        }))
29
+      }
30
+
31
+    case `${SET}/${WORKSPACE_MEMBER_LIST}`:
32
+      return {
33
+        ...state,
34
+        member: action.workspaceMemberList.map(m => ({
35
+          id: m.user_id,
36
+          publicName: m.user.public_name,
37
+          avatarUrl: m.user.avatar_url,
38
+          role: m.role,
39
+          isActive: m.is_active,
40
+        }))
41
+      }
42
+
43
+    default:
44
+      return state
45
+  }
46
+}

+ 3 - 5
frontend/src/reducer/flashMessage.js View File

@@ -1,16 +1,14 @@
1
-import {
2
-  FLASH_MESSAGE
3
-} from '../action-creator.sync.js'
1
+import { ADD, REMOVE, FLASH_MESSAGE } from '../action-creator.sync.js'
4 2
 
5 3
 export default function flashMessage (state = [], action) {
6 4
   switch (action.type) {
7
-    case `Add/${FLASH_MESSAGE}`:
5
+    case `${ADD}/${FLASH_MESSAGE}`:
8 6
       return [...state, {
9 7
         message: action.msg.message,
10 8
         type: action.msg.type || 'info' // may be info, success, danger
11 9
       }]
12 10
 
13
-    case `Remove/${FLASH_MESSAGE}`:
11
+    case `${REMOVE}/${FLASH_MESSAGE}`:
14 12
       return state.filter(fm => fm.message === action.message)
15 13
 
16 14
     default:

+ 16 - 5
frontend/src/reducer/lang.js View File

@@ -1,12 +1,23 @@
1
-import { LANG } from '../action-creator.sync.js'
1
+import { UPDATE, LANG } from '../action-creator.sync.js'
2
+import flagEn from '../img/flag_en.png'
3
+import flagFr from '../img/flag_fr.png'
2 4
 
3
-export function lang (state = [], action) {
5
+const defaultLang = [{
6
+  id: 'en',
7
+  icon: flagEn
8
+}, {
9
+  id: 'fr',
10
+  icon: flagFr
11
+}]
12
+
13
+export function lang (state = defaultLang, action) {
4 14
   switch (action.type) {
5
-    case `Update/${LANG}`:
15
+    case `${UPDATE}/${LANG}`:
6 16
       return action.langList
7 17
 
8
-    case `Set/${LANG}/Active`:
9
-      return state.map(l => ({...l, active: l.id === action.langId}))
18
+    // Côme - 2018/07/30 - deprecated, lang active is saved in user reducer
19
+    // case `Set/${LANG}/Active`:
20
+    //   return state.map(l => ({...l, active: l.id === action.langId}))
10 21
 
11 22
     default:
12 23
       return state

+ 4 - 3
frontend/src/reducer/root.js View File

@@ -2,12 +2,13 @@ import { combineReducers } from 'redux'
2 2
 import lang from './lang.js'
3 3
 import flashMessage from './flashMessage.js'
4 4
 import user from './user.js'
5
-import workspaceContent from './workspaceContent.js'
5
+import currentWorkspace from './currentWorkspace.js'
6
+import workspaceContentList from './workspaceContentList.js'
6 7
 import workspaceList from './workspaceList.js'
7
-import app from './app.js'
8
+import appList from './appList.js'
8 9
 import contentType from './contentType.js'
9 10
 import timezone from './timezone.js'
10 11
 
11
-const rootReducer = combineReducers({ lang, flashMessage, user, workspaceContent, workspaceList, app, contentType, timezone })
12
+const rootReducer = combineReducers({ lang, flashMessage, user, currentWorkspace, workspaceContentList, workspaceList, appList, contentType, timezone })
12 13
 
13 14
 export default rootReducer

+ 2 - 2
frontend/src/reducer/timezone.js View File

@@ -1,8 +1,8 @@
1
-import { TIMEZONE } from '../action-creator.sync.js'
1
+import { SET, TIMEZONE } from '../action-creator.sync.js'
2 2
 
3 3
 export function timezone (state = [], action) {
4 4
   switch (action.type) {
5
-    case `Set/${TIMEZONE}`:
5
+    case `${SET}/${TIMEZONE}`:
6 6
       return action.timezone
7 7
 
8 8
     default:

+ 14 - 5
frontend/src/reducer/user.js View File

@@ -1,12 +1,16 @@
1 1
 import {
2
+  SET,
3
+  UPDATE,
2 4
   USER_CONNECTED,
3 5
   USER_DISCONNECTED,
4
-  USER_DATA
6
+  USER_DATA,
7
+  USER_LANG
5 8
 } from '../action-creator.sync.js'
6 9
 
7 10
 const defaultUser = {
8 11
   user_id: -1,
9 12
   logged: null, // null avoid to be redirected to /login while whoami ep has not responded yet
13
+  auth: '',
10 14
   timezone: '',
11 15
   profile: {
12 16
     id: 1,
@@ -17,23 +21,28 @@ const defaultUser = {
17 21
   caldav_url: null,
18 22
   avatar_url: null,
19 23
   created: '',
20
-  display_name: ''
24
+  public_name: '',
25
+  lang: 'en' // @FIXME Côme - 2018/07/30 - remove this line when api returns the lang (https://github.com/tracim/tracim/issues/734)
21 26
 }
22 27
 
23 28
 export default function user (state = defaultUser, action) {
24 29
   switch (action.type) {
25
-    case `Set/${USER_CONNECTED}`:
30
+    case `${SET}/${USER_CONNECTED}`:
26 31
       return {
32
+        ...state,
27 33
         ...action.user,
28 34
         avatar_url: 'https://www.algoo.fr/static/images/people_images/PERSO_SEUL.png' // @FIXME use avatar from api when db handles it
29 35
       }
30 36
 
31
-    case `Set/${USER_DISCONNECTED}`:
37
+    case `${SET}/${USER_DISCONNECTED}`:
32 38
       return defaultUser
33 39
 
34
-    case `Update/${USER_DATA}`:
40
+    case `${UPDATE}/${USER_DATA}`:
35 41
       return {...state, ...action.data}
36 42
 
43
+    case `${SET}/${USER_LANG}`:
44
+      return {...state, lang: action.lang}
45
+
37 46
     default:
38 47
       return state
39 48
   }

frontend/src/reducer/workspaceContent.js → frontend/src/reducer/workspaceContentList.js View File

@@ -1,12 +1,15 @@
1 1
 import {
2
+  SET,
3
+  UPDATE,
2 4
   WORKSPACE,
5
+  WORKSPACE_CONTENT,
3 6
   FOLDER
4 7
 } from '../action-creator.sync.js'
5 8
 
6
-export default function workspace (state = [], action) {
9
+export default function workspaceContentList (state = [], action) {
7 10
   switch (action.type) {
8
-    case `Set/${WORKSPACE}/Content`:
9
-      return action.workspaceContent.map(wsc => ({
11
+    case `${SET}/${WORKSPACE_CONTENT}`:
12
+      return action.workspaceContentList.map(wsc => ({
10 13
         id: wsc.content_id,
11 14
         label: wsc.label,
12 15
         slug: wsc.slug,
@@ -20,10 +23,10 @@ export default function workspace (state = [], action) {
20 23
         subContentTypeSlug: wsc.sub_content_type_slug
21 24
       }))
22 25
 
23
-    case `Update/${WORKSPACE}/Filter`: // not used anymore ?
26
+    case `${UPDATE}/${WORKSPACE}/Filter`: // not used anymore ?
24 27
       return {...state, filter: action.filterList}
25 28
 
26
-    case `Set/${WORKSPACE}/${FOLDER}/Content`:
29
+    case `${SET}/${WORKSPACE}/${FOLDER}/Content`:
27 30
       const setFolderContent = (contentItem, action) => {
28 31
         if (contentItem.id === action.folderId) return {...contentItem, content: action.content}
29 32
 

+ 8 - 7
frontend/src/reducer/workspaceList.js View File

@@ -1,18 +1,19 @@
1 1
 import {
2
+  SET,
3
+  UPDATE,
2 4
   WORKSPACE_LIST,
3 5
   USER_ROLE
4 6
 } from '../action-creator.sync.js'
5
-
6
-const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route
7
+import { handleRouteFromApi } from '../helper.js'
7 8
 
8 9
 export function workspaceList (state = [], action) {
9 10
   switch (action.type) {
10
-    case `Update/${WORKSPACE_LIST}`:
11
+    case `${UPDATE}/${WORKSPACE_LIST}`:
11 12
       return action.workspaceList.map(ws => ({
12 13
         id: ws.workspace_id,
13 14
         label: ws.label,
14 15
         slug: ws.slug,
15
-        description: ws.description,
16
+        // description: ws.description, // not returned by /api/v2/users/:idUser/workspaces
16 17
         sidebarEntry: ws.sidebar_entries.map(sbe => ({
17 18
           slug: sbe.slug,
18 19
           route: handleRouteFromApi(sbe.route),
@@ -23,13 +24,13 @@ export function workspaceList (state = [], action) {
23 24
         isOpenInSidebar: false
24 25
       }))
25 26
 
26
-    case `Set/${WORKSPACE_LIST}/isOpenInSidebar`:
27
+    case `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`:
27 28
       return state.map(ws => ws.id === action.workspaceId
28 29
         ? {...ws, isOpenInSidebar: action.isOpenInSidebar}
29 30
         : ws
30 31
       )
31 32
 
32
-    case `Set/${USER_ROLE}`: // not used yet
33
+    case `${SET}/${USER_ROLE}`: // not used yet
33 34
       return state.map(ws => {
34 35
         const foundWorkspace = action.userRole.find(r => ws.id === r.workspace.id) || {role: '', subscribed_to_notif: ''}
35 36
         return {
@@ -39,7 +40,7 @@ export function workspaceList (state = [], action) {
39 40
         }
40 41
       })
41 42
 
42
-    case `Update/${USER_ROLE}/SubscriptionNotif`: // not used yet
43
+    case `${UPDATE}/${USER_ROLE}/SubscriptionNotif`: // not used yet
43 44
       return state.map(ws => ws.id === action.workspaceId
44 45
         ? {...ws, notif: action.subscriptionNotif}
45 46
         : ws

+ 0 - 41
frontend/src/translate/en.js View File

@@ -1,41 +0,0 @@
1
-const en = {
2
-  translation: { // 'en' in the namespace 'translation'
3
-    Header: {
4
-      Search: 'Search'
5
-    },
6
-    Footer: {
7
-      marketing_msg: 'Create your own collaborative workspaces on trac.im',
8
-      copyright: 'Copyright 2013 - 2017'
9
-    },
10
-    FlashMessage: {
11
-      error: 'Error'
12
-    },
13
-    Login: {
14
-      fail: 'Unknown email or password',
15
-      logout_error: 'Disconnection error'
16
-    },
17
-    FileItemHeader: {
18
-      type: 'Type',
19
-      document_name: "Document's name",
20
-      status: 'Status'
21
-    },
22
-    Folder: {
23
-      create: 'Create in this folder',
24
-      content_type: 'Content type'
25
-    },
26
-    Sidebar: {
27
-      create_new_workspace: 'Create new workspace'
28
-    },
29
-    Account: {
30
-      title: 'My account'
31
-    },
32
-    role: {
33
-      reader: 'Reader',
34
-      contributor: 'Contributor',
35
-      content_manager: 'Content manager',
36
-      manager: 'Manager'
37
-    }
38
-  }
39
-}
40
-
41
-export default en

+ 0 - 41
frontend/src/translate/fr.js View File

@@ -1,41 +0,0 @@
1
-const fr = {
2
-  translation: { // 'fr' in the namespace 'translation'
3
-    Header: {
4
-      Search: 'Rechercher'
5
-    },
6
-    Footer: {
7
-      marketing_msg: 'Créer votre propre espace de travail collaboratif sur trac.im',
8
-      copyright: 'Copyright 2013 - 2017'
9
-    },
10
-    FlashMessage: {
11
-      error: 'Erreur'
12
-    },
13
-    Login: {
14
-      fail: 'Email ou mot de passe inconnu.',
15
-      logout_error: 'Erreur de déconnexion.'
16
-    },
17
-    FileItemHeader: {
18
-      type: 'Type',
19
-      document_name: 'Nom du document',
20
-      status: 'Statut'
21
-    },
22
-    Folder: {
23
-      create: 'Créer dans ce dossier',
24
-      content_type: 'Type de contenu'
25
-    },
26
-    Sidebar: {
27
-      create_new_workspace: 'Créer un workspace'
28
-    },
29
-    Account: {
30
-      title: 'Mon compte'
31
-    },
32
-    role: {
33
-      reader: 'Lecteur',
34
-      contributor: 'Contributeur',
35
-      content_manager: 'Gestionnaire de contenu',
36
-      manager: 'Responsable'
37
-    }
38
-  }
39
-}
40
-
41
-export default fr

+ 13 - 0
frontend_app_admin_workspace_user/.editorconfig View File

@@ -0,0 +1,13 @@
1
+# doc here : https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+insert_final_newline = true
10
+trim_trailing_whitespace = true
11
+
12
+[*.py]
13
+indent_size = 4

+ 5 - 0
frontend_app_admin_workspace_user/.gitignore View File

@@ -0,0 +1,5 @@
1
+# Created by .ignore support plugin (hsz.mobi)
2
+.idea/
3
+.git/
4
+dist/admin_workspace_user.app.js
5
+node_modules/

+ 1 - 0
frontend_app_admin_workspace_user/README.md View File

@@ -0,0 +1 @@
1
+# app Admin Workspace User

+ 17 - 0
frontend_app_admin_workspace_user/build_admin_workspace_user.sh View File

@@ -0,0 +1,17 @@
1
+#!/bin/bash
2
+
3
+. ../bash_library.sh # source bash_library.sh
4
+
5
+windoz=""
6
+if [[ $1 = "-w" ]]; then
7
+    windoz="windoz"
8
+fi
9
+
10
+log "npm run build$windoz"
11
+npm run build$windoz
12
+log "cp dist/admin_workspace_user.app.js ../frontend/dist/app"
13
+cp dist/admin_workspace_user.app.js ../frontend/dist/app
14
+log "cp i18next.scanner/en/translation.json ../frontend/dist/app/admin_workspace_user_en_translation.json"
15
+cp i18next.scanner/en/translation.json ../frontend/dist/app/admin_workspace_user_en_translation.json
16
+log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/admin_workspace_user_fr_translation.json"
17
+cp i18next.scanner/fr/translation.json ../frontend/dist/app/admin_workspace_user_fr_translation.json

+ 1 - 0
frontend_app_admin_workspace_user/dist/asset View File

@@ -0,0 +1 @@
1
+../../frontend/dist/asset/

+ 0 - 0
frontend_app_admin_workspace_user/dist/dev View File


Some files were not shown because too many files changed in this diff