Browse Source

Merge pull request #16 from tracim/feature/638_refactor_applications+support_for_color.json

Bastien Sevajol 5 years ago
parent
commit
320149c8e9
No account linked to committer's email
55 changed files with 748 additions and 496 deletions
  1. 2 0
      .gitignore
  2. 4 1
      backend/README.md
  3. 6 0
      backend/color-test.json
  4. 9 0
      backend/development.ini.sample
  5. 0 1
      backend/setup.py
  6. 19 0
      backend/tests_configs.ini
  7. 76 0
      backend/tracim_backend/app_models/applications.py
  8. 53 86
      backend/tracim_backend/app_models/contents.py
  9. 11 0
      backend/tracim_backend/app_models/validator.py
  10. 33 0
      backend/tracim_backend/app_models/workspace_menu_entries.py
  11. 149 4
      backend/tracim_backend/config.py
  12. 6 0
      backend/tracim_backend/exceptions.py
  13. 11 0
      backend/tracim_backend/extensions.py
  14. 1 1
      backend/tracim_backend/fixtures/content.py
  15. 70 0
      backend/tracim_backend/lib/core/application.py
  16. 4 4
      backend/tracim_backend/lib/core/content.py
  17. 1 1
      backend/tracim_backend/lib/mail_notifier/notifier.py
  18. 1 1
      backend/tracim_backend/lib/utils/authentification.py
  19. 5 7
      backend/tracim_backend/lib/utils/authorization.py
  20. 1 1
      backend/tracim_backend/lib/utils/request.py
  21. 67 6
      backend/tracim_backend/lib/utils/utils.py
  22. 1 1
      backend/tracim_backend/lib/webdav/dav_provider.py
  23. 1 1
      backend/tracim_backend/lib/webdav/design.py
  24. 1 1
      backend/tracim_backend/lib/webdav/resources.py
  25. 1 1
      backend/tracim_backend/lib/webdav/utils.py
  26. 1 1
      backend/tracim_backend/models/__init__.py
  27. 0 117
      backend/tracim_backend/models/applications.py
  28. 8 4
      backend/tracim_backend/models/context_models.py
  29. 5 5
      backend/tracim_backend/models/data.py
  30. 0 71
      backend/tracim_backend/models/workspace_menu_entries.py
  31. 1 1
      backend/tracim_backend/tests/__init__.py
  32. 1 1
      backend/tracim_backend/tests/functional/test_contents.py
  33. 1 1
      backend/tracim_backend/tests/functional/test_mail_notification.py
  34. 10 2
      backend/tracim_backend/tests/functional/test_system.py
  35. 26 39
      backend/tracim_backend/tests/functional/test_user.py
  36. 46 42
      backend/tracim_backend/tests/functional/test_workspaces.py
  37. 1 1
      backend/tracim_backend/tests/library/test_content_api.py
  38. 3 10
      backend/tracim_backend/tests/library/test_webdav.py
  39. 1 1
      backend/tracim_backend/tests/models/test_content.py
  40. 1 1
      backend/tracim_backend/tests/models/test_content_revision.py
  41. 2 2
      backend/tracim_backend/views/contents_api/comment_controller.py
  42. 17 17
      backend/tracim_backend/views/contents_api/file_controller.py
  43. 6 6
      backend/tracim_backend/views/contents_api/folder_controller.py
  44. 7 7
      backend/tracim_backend/views/contents_api/html_document_controller.py
  45. 7 7
      backend/tracim_backend/views/contents_api/threads_controller.py
  46. 12 10
      backend/tracim_backend/views/core_api/schemas.py
  47. 1 1
      backend/tracim_backend/views/core_api/session_controller.py
  48. 9 4
      backend/tracim_backend/views/core_api/system_controller.py
  49. 2 2
      backend/tracim_backend/views/core_api/user_controller.py
  50. 2 2
      backend/tracim_backend/views/core_api/workspace_controller.py
  51. 9 3
      backend/tracim_backend/views/frontend.py
  52. 10 0
      backend/wsgidav-test.conf
  53. 5 0
      backend_lib.sh
  54. 3 3
      color.json.sample
  55. 18 18
      frontend/dist/index.mak

+ 2 - 0
.gitignore View File

@@ -6,9 +6,11 @@ frontend_app_html-document/dist/html-document.app.js
6 6
 frontend_lib/dist/tracim_frontend_lib.js
7 7
 npm-debug.log
8 8
 package-lock.json
9
+color.json
9 10
 
10 11
 #ignore file or folder about cypress tests
11 12
 functionnal_tests/package.json
12 13
 functionnal_tests/cypress.json
13 14
 functionnal_tests/node_modules/
14 15
 functionnal_tests/cypress/
16
+

+ 4 - 1
backend/README.md View File

@@ -85,8 +85,11 @@ create wsgidav configuration file for webdav:
85 85
 
86 86
     cp wsgidav.conf.sample wsgidav.conf
87 87
 
88
-## Run Tracim_backend With Uwsgi : great for production ##
88
+if not did before, you need to create a color.json file at root of tracim_v2 :
89
+   
90
+    cp ../color.json.sample ../color.json
89 91
 
92
+## Run Tracim_backend With Uwsgi : great for production ##
90 93
 
91 94
 #### Install Uwsgi
92 95
 

+ 6 - 0
backend/color-test.json View File

@@ -0,0 +1,6 @@
1
+{
2
+  "primary": "#7d4e24",
3
+  "contents/html-document": "#3f52e3",
4
+  "contents/thread": "#ad4cf9",
5
+  "contents/file": "#ff9900"
6
+}

+ 9 - 0
backend/development.ini.sample View File

@@ -35,6 +35,8 @@ retry.attempts = 3
35 35
 
36 36
 ### Global
37 37
 
38
+# Enable debug mode
39
+# debug = True
38 40
 cache_dir = %(here)s/data
39 41
 # preview generator cache directory
40 42
 preview_cache_dir = /tmp/tracim/preview/
@@ -193,6 +195,13 @@ frontend.serve = True
193 195
 # organisation.
194 196
 # frontend.dist_folder_path = /home/user/tracim_v2/frontend/dist
195 197
 
198
+### Color
199
+# check for color.json file in tracim_v2, check by default in tracim_v2 parent
200
+# dir of backend.
201
+# you can set a specific file path here
202
+# color.config_file_path = /home/user/tracim_v2/color.json
203
+
204
+
196 205
 ###
197 206
 # wsgi server configuration
198 207
 ###

+ 0 - 1
backend/setup.py View File

@@ -43,7 +43,6 @@ requires = [
43 43
     'rq',
44 44
     # frontend file serve
45 45
     'pyramid_mako',
46
-    'spectra',
47 46
 ]
48 47
 
49 48
 tests_require = [

+ 19 - 0
backend/tests_configs.ini View File

@@ -5,6 +5,8 @@ depot_storage_dir = /tmp/test/depot
5 5
 user.auth_token.validity = 604800
6 6
 preview_cache_dir = /tmp/test/preview_cache
7 7
 website.base_url = http://localhost:6543
8
+color.config_file_path = %(here)s/color-test.json
9
+
8 10
 [app:command_test]
9 11
 use = egg:tracim_backend
10 12
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -13,6 +15,7 @@ depot_storage_dir = /tmp/test/depot
13 15
 user.auth_token.validity = 604800
14 16
 preview_cache_dir = /tmp/test/preview_cache
15 17
 website.base_url = http://localhost:6543
18
+color.config_file_path = %(here)s/color-test.json
16 19
 
17 20
 [mail_test]
18 21
 sqlalchemy.url = sqlite:///:memory:
@@ -39,6 +42,7 @@ email.notification.smtp.port = 1025
39 42
 email.notification.smtp.user = test_user
40 43
 email.notification.smtp.password = just_a_password
41 44
 website.base_url = http://localhost:6543
45
+color.config_file_path = %(here)s/color-test.json
42 46
 
43 47
 [mail_test_async]
44 48
 sqlalchemy.url = sqlite:///:memory:
@@ -66,6 +70,7 @@ email.notification.smtp.port = 1025
66 70
 email.notification.smtp.user = test_user
67 71
 email.notification.smtp.password = just_a_password
68 72
 website.base_url = http://localhost:6543
73
+color.config_file_path = %(here)s/color-test.json
69 74
 
70 75
 [functional_test]
71 76
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -76,6 +81,7 @@ preview_cache_dir = /tmp/test/preview_cache
76 81
 preview.jpg.restricted_dims = True
77 82
 email.notification.activated = false
78 83
 website.base_url = http://localhost:6543
84
+color.config_file_path = %(here)s/color-test.json
79 85
 
80 86
 [functional_test_no_db]
81 87
 sqlalchemy.url = sqlite://
@@ -86,6 +92,7 @@ preview_cache_dir = /tmp/test/preview_cache
86 92
 preview.jpg.restricted_dims = True
87 93
 email.notification.activated = false
88 94
 website.base_url = http://localhost:6543
95
+color.config_file_path = %(here)s/color-test.json
89 96
 
90 97
 [functional_test_with_mail_test_sync]
91 98
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -111,6 +118,7 @@ email.notification.smtp.port = 1025
111 118
 email.notification.smtp.user = test_user
112 119
 email.notification.smtp.password = just_a_password
113 120
 website.base_url = http://localhost:6543
121
+color.config_file_path = %(here)s/color-test.json
114 122
 
115 123
 [functional_test_with_mail_test_async]
116 124
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -136,3 +144,14 @@ email.notification.smtp.port = 1025
136 144
 email.notification.smtp.user = test_user
137 145
 email.notification.smtp.password = just_a_password
138 146
 website.base_url = http://localhost:6543
147
+color.config_file_path = %(here)s/color-test.json
148
+
149
+[webdav_test]
150
+website.base_url = http://localhost:6543
151
+sqlalchemy.url = sqlite:///:memory:
152
+user.auth_token.validity = 604800
153
+depot_storage_dir = /tmp/test/depot
154
+depot_storage_name = test
155
+preview_cache_dir = /tmp/test/preview_cache
156
+color.config_file_path = %(here)s/color-test.json
157
+wsgidav.config_path = %(here)s/wsgidav-test.conf

+ 76 - 0
backend/tracim_backend/app_models/applications.py View File

@@ -0,0 +1,76 @@
1
+# coding=utf-8
2
+import typing
3
+
4
+from tracim_backend.app_models.contents import ContentType
5
+if typing.TYPE_CHECKING:
6
+    from tracim_backend.config import CFG
7
+    from tracim_backend.app_models.contents import ContentStatus
8
+
9
+
10
+class Application(object):
11
+    """
12
+    Application class with data needed for frontend
13
+    """
14
+    def __init__(
15
+            self,
16
+            label: str,
17
+            slug: str,
18
+            fa_icon: str,
19
+            is_active: bool,
20
+            config: typing.Dict[str, str],
21
+            main_route: str,
22
+            app_config: 'CFG',
23
+    ) -> None:
24
+        """
25
+        @param label: public label of application
26
+        @param slug: identifier of application
27
+        @param fa_icon: font awesome icon class
28
+        @param is_active: True if application enable, False if inactive
29
+        @param config: a dict with eventual application config
30
+        @param main_route: the route of the frontend "home" screen of
31
+        the application. For exemple, if you have an application
32
+        called "calendar", the main route will be something
33
+        like /#/workspace/{wid}/calendar.
34
+        """
35
+        self.label = label
36
+        self.slug = slug
37
+        self.fa_icon = fa_icon
38
+        self.hexcolor = self._get_hexcolor_or_default(slug, app_config)
39
+        self.is_active = is_active
40
+        self.config = config
41
+        self.main_route = main_route
42
+        self.content_types = []
43
+
44
+    # TODO - G.M - 2018-08-07 - Refactor slug coherence issue like this one.
45
+    # we probably should not have 2 kind of slug
46
+    @property
47
+    def minislug(self):
48
+        return self.slug.replace('contents/', '')
49
+
50
+    def add_content_type(
51
+            self,
52
+            label: str,
53
+            slug: str,
54
+            creation_label: str,
55
+            available_statuses: typing.List['ContentStatus'],
56
+            slug_alias: typing.List[str] = None,
57
+            allow_sub_content: bool = False,
58
+    ):
59
+        content_type = ContentType(
60
+            slug=slug,
61
+            fa_icon=self.fa_icon,
62
+            label=label,
63
+            hexcolor=self.hexcolor,
64
+            creation_label=creation_label,
65
+            available_statuses=available_statuses,
66
+            slug_alias=slug_alias,
67
+            allow_sub_content=allow_sub_content,
68
+        )
69
+        self.content_types.append(content_type)
70
+
71
+    def _get_hexcolor_or_default(self, slug: str, app_config: 'CFG') -> str:
72
+        assert app_config.APPS_COLORS
73
+        assert 'primary' in app_config.APPS_COLORS
74
+        if slug in app_config.APPS_COLORS:
75
+            return app_config.APPS_COLORS[slug]
76
+        return app_config.APPS_COLORS['primary']

backend/tracim_backend/models/contents.py → backend/tracim_backend/app_models/contents.py View File

@@ -2,17 +2,15 @@
2 2
 import typing
3 3
 from enum import Enum
4 4
 
5
+from tracim_backend.extensions import app_list
5 6
 from tracim_backend.exceptions import ContentTypeNotExist
6 7
 from tracim_backend.exceptions import ContentStatusNotExist
7
-from tracim_backend.models.applications import html_documents
8
-from tracim_backend.models.applications import _file
9
-from tracim_backend.models.applications import folder
10
-from tracim_backend.models.applications import thread
11
-from tracim_backend.models.applications import markdownpluspage
12
-
13 8
 
14 9
 ####
15 10
 # Content Status
11
+from tracim_backend.lib.core.application import ApplicationApi
12
+if typing.TYPE_CHECKING:
13
+    from tracim_backend.app_models.applications import Application
16 14
 
17 15
 
18 16
 class GlobalStatus(Enum):
@@ -134,60 +132,17 @@ class ContentType(object):
134 132
         self.allow_sub_content = allow_sub_content
135 133
 
136 134
 
137
-thread_type = ContentType(
138
-    slug='thread',
139
-    fa_icon=thread.fa_icon,
140
-    hexcolor=thread.hexcolor,
141
-    label='Thread',
142
-    creation_label='Discuss about a topic',
143
-    available_statuses=CONTENT_STATUS.get_all(),
144
-)
145
-
146
-file_type = ContentType(
147
-    slug='file',
148
-    fa_icon=_file.fa_icon,
149
-    hexcolor=_file.hexcolor,
150
-    label='File',
151
-    creation_label='Upload a file',
152
-    available_statuses=CONTENT_STATUS.get_all(),
153
-)
154
-
155
-markdownpluspage_type = ContentType(
156
-    slug='markdownpage',
157
-    fa_icon=markdownpluspage.fa_icon,
158
-    hexcolor=markdownpluspage.hexcolor,
159
-    label='Rich Markdown File',
160
-    creation_label='Create a Markdown document',
161
-    available_statuses=CONTENT_STATUS.get_all(),
162
-)
163
-
164
-html_documents_type = ContentType(
165
-    slug='html-document',
166
-    fa_icon=html_documents.fa_icon,
167
-    hexcolor=html_documents.hexcolor,
168
-    label='Text Document',
169
-    creation_label='Write a document',
170
-    available_statuses=CONTENT_STATUS.get_all(),
171
-    slug_alias=['page']
172
-)
173
-
174
-# TODO - G.M - 31-05-2018 - Set Better folder params
175
-folder_type = ContentType(
176
-    slug='folder',
177
-    fa_icon=folder.fa_icon,
178
-    hexcolor=folder.hexcolor,
179
-    label='Folder',
180
-    creation_label='Create a folder',
181
-    available_statuses=CONTENT_STATUS.get_all(),
182
-    allow_sub_content=True,
183
-)
184
-
135
+THREAD_TYPE = 'thread'
136
+FILE_TYPE = 'file'
137
+MARKDOWNPLUSPAGE_TYPE = 'markdownpage'
138
+HTML_DOCUMENTS_TYPE = 'html-document'
139
+FOLDER_TYPE = 'folder'
185 140
 
186 141
 # TODO - G.M - 31-05-2018 - Set Better Event params
187 142
 event_type = ContentType(
188 143
     slug='event',
189
-    fa_icon=thread.fa_icon,
190
-    hexcolor=thread.hexcolor,
144
+    fa_icon='',
145
+    hexcolor='',
191 146
     label='Event',
192 147
     creation_label='Event',
193 148
     available_statuses=CONTENT_STATUS.get_all(),
@@ -196,8 +151,8 @@ event_type = ContentType(
196 151
 # TODO - G.M - 31-05-2018 - Set Better Event params
197 152
 comment_type = ContentType(
198 153
     slug='comment',
199
-    fa_icon=thread.fa_icon,
200
-    hexcolor=thread.hexcolor,
154
+    fa_icon='',
155
+    hexcolor='',
201 156
     label='Comment',
202 157
     creation_label='Comment',
203 158
     available_statuses=CONTENT_STATUS.get_all(),
@@ -209,19 +164,36 @@ class ContentTypeList(object):
209 164
     ContentType List
210 165
     """
211 166
     Any_SLUG = 'any'
212
-    Folder = folder_type
213 167
     Comment = comment_type
214 168
     Event = event_type
215
-    File = file_type
216
-    Page = html_documents_type
217
-    Thread = thread_type
218 169
 
219
-    def __init__(self, extend_content_status: typing.List[ContentType]):
220
-        self._content_types = [self.Folder]
221
-        self._content_types.extend(extend_content_status)
170
+    @property
171
+    def Folder(self):
172
+        return self.get_one_by_slug(FOLDER_TYPE)
173
+
174
+    @property
175
+    def File(self):
176
+        return self.get_one_by_slug(FILE_TYPE)
177
+
178
+    @property
179
+    def Page(self):
180
+        return self.get_one_by_slug(HTML_DOCUMENTS_TYPE)
181
+
182
+    @property
183
+    def Thread(self):
184
+        return self.get_one_by_slug(THREAD_TYPE)
185
+
186
+    def __init__(self, app_list: typing.List['Application']):
187
+        self.app_list = app_list
222 188
         self._special_contents_types = [self.Comment]
223 189
         self._extra_slugs = [self.Any_SLUG]
224 190
 
191
+    @property
192
+    def _content_types(self):
193
+        app_api = ApplicationApi(self.app_list)
194
+        content_types = app_api.get_content_types()
195
+        return content_types
196
+
225 197
     def get_one_by_slug(self, slug: str) -> ContentType:
226 198
         """
227 199
         Get ContentType object according to slug
@@ -235,21 +207,24 @@ class ContentTypeList(object):
235 207
                 return item
236 208
         raise ContentTypeNotExist()
237 209
 
238
-    def endpoint_allowed_types_slug(self) -> typing.List[str]:
210
+    def restricted_allowed_types_slug(self) -> typing.List[str]:
239 211
         """
240
-        Return restricted list of content_type:
241
-        dont return special content_type like  comment, don't return
212
+        Return restricted list of content_type: don't return
242 213
         "any" slug, dont return content type slug alias , don't return event.
243 214
         Useful to restrict slug param in schema.
244 215
         """
245 216
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
246 217
         return allowed_type_slug
247 218
 
248
-    def extended_endpoint_allowed_types_slug(self) -> typing.List[str]:
249
-        allowed_types_slug = self.endpoint_allowed_types_slug().copy()
250
-        for content_type in self._special_contents_types:
251
-            allowed_types_slug.append(content_type.slug)
252
-        return allowed_types_slug
219
+    def endpoint_allowed_types_slug(self) -> typing.List[str]:
220
+        """
221
+        Same as restricted_allowed_types_slug but with special content_type
222
+        included like comments.
223
+        """
224
+        content_types = self._content_types
225
+        content_types.extend(self._special_contents_types)
226
+        allowed_type_slug = [contents_type.slug for contents_type in content_types]  # nopep8
227
+        return allowed_type_slug
253 228
 
254 229
     def query_allowed_types_slugs(self) -> typing.List[str]:
255 230
         """
@@ -258,19 +233,19 @@ class ContentTypeList(object):
258 233
         Usefull allowed value to perform query to database.
259 234
         """
260 235
         allowed_types_slug = []
261
-        for content_type in self._content_types:
236
+        content_types = self._content_types
237
+        content_types.extend(self._special_contents_types)
238
+        for content_type in content_types:
262 239
             allowed_types_slug.append(content_type.slug)
263 240
             if content_type.slug_alias:
264 241
                 allowed_types_slug.extend(content_type.slug_alias)
265
-        for content_type in self._special_contents_types:
266
-            allowed_types_slug.append(content_type.slug)
267 242
         allowed_types_slug.extend(self._extra_slugs)
268 243
         return allowed_types_slug
269 244
 
270 245
     def default_allowed_content_properties(self, slug) -> dict:
271 246
         content_type = self.get_one_by_slug(slug)
272 247
         if content_type.allow_sub_content:
273
-            sub_content_allowed = self.extended_endpoint_allowed_types_slug()
248
+            sub_content_allowed = self.endpoint_allowed_types_slug()
274 249
         else:
275 250
             sub_content_allowed = [self.Comment.slug]
276 251
 
@@ -280,12 +255,4 @@ class ContentTypeList(object):
280 255
         return properties_dict
281 256
 
282 257
 
283
-CONTENT_TYPES = ContentTypeList(
284
-    [
285
-        thread_type,
286
-        file_type,
287
-        # TODO - G.M - 2018-08-02 - Restore markdown page content
288
-        #    markdownpluspage_type,
289
-        html_documents_type,
290
-    ]
291
-)
258
+CONTENT_TYPES = ContentTypeList(app_list)

+ 11 - 0
backend/tracim_backend/app_models/validator.py View File

@@ -0,0 +1,11 @@
1
+from marshmallow.validate import OneOf
2
+from tracim_backend.app_models.contents import CONTENT_TYPES
3
+
4
+# TODO - G.M - 2018-08-08 - [GlobalVar] Refactor Global var
5
+# of tracim_backend, Be careful all_content_types_validator is a global_var !
6
+
7
+all_content_types_validator = OneOf(choices=[])
8
+
9
+
10
+def update_validators():
11
+    all_content_types_validator.choices = CONTENT_TYPES.endpoint_allowed_types_slug()  # nopep8

+ 33 - 0
backend/tracim_backend/app_models/workspace_menu_entries.py View File

@@ -0,0 +1,33 @@
1
+class WorkspaceMenuEntry(object):
2
+    """
3
+    Application class with data needed for frontend
4
+    """
5
+    def __init__(
6
+            self,
7
+            label: str,
8
+            slug: str,
9
+            fa_icon: str,
10
+            hexcolor: str,
11
+            route: str,
12
+    ) -> None:
13
+        self.slug = slug
14
+        self.label = label
15
+        self.route = route
16
+        self.hexcolor = hexcolor
17
+        self.fa_icon = fa_icon
18
+
19
+
20
+dashboard_menu_entry = WorkspaceMenuEntry(
21
+  slug='dashboard',
22
+  label='Dashboard',
23
+  route='/#/workspaces/{workspace_id}/dashboard',
24
+  hexcolor='#252525',
25
+  fa_icon="signal",
26
+)
27
+all_content_menu_entry = WorkspaceMenuEntry(
28
+  slug="contents/all",
29
+  label="All Contents",
30
+  route="/#/workspaces/{workspace_id}/contents",
31
+  hexcolor="#fdfdfd",
32
+  fa_icon="th",
33
+)

+ 149 - 4
backend/tracim_backend/config.py View File

@@ -1,15 +1,19 @@
1 1
 # -*- coding: utf-8 -*-
2
+import json
2 3
 from urllib.parse import urlparse
3 4
 
4 5
 import os
5 6
 from paste.deploy.converters import asbool
7
+from tracim_backend.app_models.validator import update_validators
8
+from tracim_backend.extensions import app_list
6 9
 from tracim_backend.lib.utils.logger import logger
7 10
 from depot.manager import DepotManager
8
-from tracim_backend.models.contents import CONTENT_TYPES
11
+from tracim_backend.app_models.applications import Application
12
+from tracim_backend.app_models.contents import CONTENT_TYPES
13
+from tracim_backend.app_models.contents import CONTENT_STATUS
9 14
 from tracim_backend.models.data import ActionDescription
10 15
 
11 16
 
12
-
13 17
 class CFG(object):
14 18
     """Object used for easy access to config file parameters."""
15 19
 
@@ -41,7 +45,36 @@ class CFG(object):
41 45
         ###
42 46
         # General
43 47
         ###
48
+        backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # nopep8
49
+        tracim_v2_folder = os.path.dirname(backend_folder)
50
+        default_color_config_file_path = os.path.join(tracim_v2_folder, 'color.json')  # nopep8
51
+        self.COLOR_CONFIG_FILE_PATH = settings.get(
52
+            'color.config_file_path', default_color_config_file_path
53
+        )
54
+        if not os.path.exists(self.COLOR_CONFIG_FILE_PATH):
55
+            raise Exception(
56
+                'ERROR: {} file does not exist. '
57
+                'please create it or set color.config_file_path'
58
+                'with a correct value'.format(self.COLOR_CONFIG_FILE_PATH)
59
+            )
60
+
61
+        try:
62
+            with open(self.COLOR_CONFIG_FILE_PATH) as json_file:
63
+                self.APPS_COLORS = json.load(json_file)
64
+        except Exception as e:
65
+            raise Exception(
66
+                'Error: {} file could not be load as json'.format(self.COLOR_CONFIG_FILE_PATH) # nopep8
67
+            ) from e
44 68
 
69
+        try:
70
+            self.APPS_COLORS['primary']
71
+        except KeyError as e:
72
+            raise Exception(
73
+                'Error: primary color is required in {} file'.format(
74
+                    self.COLOR_CONFIG_FILE_PATH)  # nopep8
75
+            ) from e
76
+
77
+        self._set_default_app()
45 78
         mandatory_msg = \
46 79
             'ERROR: {} configuration is mandatory. Set it before continuing.'
47 80
         self.DEPOT_STORAGE_DIR = settings.get(
@@ -450,8 +483,6 @@ class CFG(object):
450 483
         # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder
451 484
         # is probably in frontend subfolder
452 485
         # of tracim_v2 parent of both backend and frontend
453
-        backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # nopep8
454
-        tracim_v2_folder = os.path.dirname(backend_folder)
455 486
         frontend_dist_folder = os.path.join(tracim_v2_folder, 'frontend', 'dist')  # nopep8
456 487
 
457 488
         self.FRONTEND_DIST_FOLDER_PATH = settings.get(
@@ -467,6 +498,10 @@ class CFG(object):
467 498
             )
468 499
 
469 500
     def configure_filedepot(self):
501
+
502
+        # TODO - G.M - 2018-08-08 - [GlobalVar] Refactor Global var
503
+        # of tracim_backend, Be careful DepotManager is a Singleton !
504
+
470 505
         depot_storage_name = self.DEPOT_STORAGE_NAME
471 506
         depot_storage_path = self.DEPOT_STORAGE_DIR
472 507
         depot_storage_settings = {'depot.storage_path': depot_storage_path}
@@ -475,6 +510,116 @@ class CFG(object):
475 510
             depot_storage_settings,
476 511
         )
477 512
 
513
+    def _set_default_app(self):
514
+        calendar = Application(
515
+            label='Calendar',
516
+            slug='calendar',
517
+            fa_icon='calendar',
518
+            is_active=False,
519
+            config={},
520
+            main_route='/#/workspaces/{workspace_id}/calendar',
521
+            app_config=self
522
+        )
523
+
524
+        thread = Application(
525
+            label='Threads',
526
+            slug='contents/thread',
527
+            fa_icon='comments-o',
528
+            is_active=True,
529
+            config={},
530
+            main_route='/#/workspaces/{workspace_id}/contents?type=thread',
531
+            app_config=self
532
+        )
533
+        thread.add_content_type(
534
+            slug='thread',
535
+            label='Thread',
536
+            creation_label='Discuss about a topic',
537
+            available_statuses=CONTENT_STATUS.get_all(),
538
+        )
539
+
540
+        folder = Application(
541
+            label='Folder',
542
+            slug='contents/folder',
543
+            fa_icon='folder-open-o',
544
+            is_active=True,
545
+            config={},
546
+            main_route='',
547
+            app_config=self
548
+        )
549
+        folder.add_content_type(
550
+            slug='folder',
551
+            label='Folder',
552
+            creation_label='Create a folder',
553
+            available_statuses=CONTENT_STATUS.get_all(),
554
+            allow_sub_content=True,
555
+        )
556
+
557
+        _file = Application(
558
+            label='Files',
559
+            slug='contents/file',
560
+            fa_icon='paperclip',
561
+            is_active=True,
562
+            config={},
563
+            main_route='/#/workspaces/{workspace_id}/contents?type=file',
564
+            app_config=self,
565
+        )
566
+        _file.add_content_type(
567
+            slug='file',
568
+            label='File',
569
+            creation_label='Upload a file',
570
+            available_statuses=CONTENT_STATUS.get_all(),
571
+        )
572
+
573
+        markdownpluspage = Application(
574
+            label='Markdown Plus Documents',
575
+            # TODO - G.M - 24-05-2018 - Check label
576
+            slug='content/markdownpluspage',
577
+            fa_icon='file-code-o',
578
+            is_active=False,
579
+            config={},
580
+            main_route='/#/workspaces/{workspace_id}/contents?type=markdownpluspage',
581
+            # nopep8
582
+            app_config=self,
583
+        )
584
+        markdownpluspage.add_content_type(
585
+            slug='markdownpage',
586
+            label='Rich Markdown File',
587
+            creation_label='Create a Markdown document',
588
+            available_statuses=CONTENT_STATUS.get_all(),
589
+        )
590
+
591
+        html_documents = Application(
592
+            label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
593
+            slug='contents/html-document',
594
+            fa_icon='file-text-o',
595
+            is_active=True,
596
+            config={},
597
+            main_route='/#/workspaces/{workspace_id}/contents?type=html-document',
598
+            app_config=self
599
+        )
600
+        html_documents.add_content_type(
601
+            slug='html-document',
602
+            label='Text Document',
603
+            creation_label='Write a document',
604
+            available_statuses=CONTENT_STATUS.get_all(),
605
+            slug_alias=['page']
606
+        )
607
+
608
+        # TODO - G.M - 2018-08-08 - [GlobalVar] Refactor Global var
609
+        # of tracim_backend, Be careful app_list is a global_var
610
+        app_list.clear()
611
+        app_list.extend([
612
+            html_documents,
613
+            markdownpluspage,
614
+            _file,
615
+            thread,
616
+            folder,
617
+            calendar,
618
+        ])
619
+        # TODO - G.M - 2018-08-08 - We need to update validators each time
620
+        # app_list is updated.
621
+        update_validators()
622
+
478 623
     class CST(object):
479 624
         ASYNC = 'ASYNC'
480 625
         SYNC = 'SYNC'

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

@@ -204,9 +204,11 @@ class PageOfPreviewNotFound(NotFound):
204 204
 class PreviewDimNotAllowed(TracimException):
205 205
     pass
206 206
 
207
+
207 208
 class UnallowedSubContent(TracimException):
208 209
     pass
209 210
 
211
+
210 212
 class TooShortAutocompleteString(TracimException):
211 213
     pass
212 214
 
@@ -215,5 +217,9 @@ class PageNotFound(TracimException):
215 217
     pass
216 218
 
217 219
 
220
+class AppDoesNotExist(TracimException):
221
+    pass
222
+
223
+
218 224
 class EmailAlreadyExistInDb(TracimException):
219 225
     pass

+ 11 - 0
backend/tracim_backend/extensions.py View File

@@ -1,3 +1,14 @@
1 1
 from hapic import Hapic
2 2
 
3 3
 hapic = Hapic()
4
+
5
+# TODO - G.M - 2018-08-08 - [GlobalVar] Refactor Global var of tracim_backend
6
+
7
+# INFO - G.M - 2018-08-08 - app_list
8
+# app_list is one of the few "global_val" in tracim_backend, with hapic
9
+# and all_content_types_validator.
10
+# The goal of this is to be able to get current list of loaded app.
11
+# List is empty until config load apps.
12
+# If you need to update app_list, think about updating Content validator like
13
+# all_content_types_validator , see  update_validators() method.
14
+app_list = []

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

+ 70 - 0
backend/tracim_backend/lib/core/application.py View File

@@ -0,0 +1,70 @@
1
+import typing
2
+from copy import copy
3
+
4
+from tracim_backend.exceptions import AppDoesNotExist
5
+from tracim_backend.app_models.workspace_menu_entries import WorkspaceMenuEntry
6
+from tracim_backend.app_models.workspace_menu_entries import dashboard_menu_entry
7
+from tracim_backend.app_models.workspace_menu_entries import all_content_menu_entry
8
+
9
+
10
+class ApplicationApi(object):
11
+
12
+    def __init__(
13
+        self,
14
+        app_list,
15
+        show_all: bool = False,
16
+    ) ->  None:
17
+        self.apps = app_list
18
+        self.show_all = show_all
19
+
20
+    def get_one(self, slug):
21
+        for app in self.apps:
22
+            if app.slug == slug:
23
+                return app
24
+        raise AppDoesNotExist('Application {app} does not exist'.format(app=slug))  # nopep8
25
+
26
+    def get_all(self):
27
+        active_apps = []
28
+        for app in self.apps:
29
+            if self.show_all or app.is_active:
30
+                active_apps.append(app)
31
+
32
+        return active_apps
33
+
34
+    def get_content_types(self):
35
+        active_content_types = []
36
+        for app in self.get_all():
37
+            if app.content_types:
38
+                for content_type in app.content_types:
39
+                    active_content_types.append(content_type)
40
+        return active_content_types
41
+
42
+    def get_default_workspace_menu_entry(
43
+            self,
44
+            workspace: 'Workspace',
45
+    ) -> typing.List[WorkspaceMenuEntry]:
46
+        """
47
+        Get default menu entry for a workspace
48
+        """
49
+        menu_entries = [
50
+            copy(dashboard_menu_entry),
51
+            copy(all_content_menu_entry),
52
+        ]
53
+        for app in self.get_all():
54
+            if app.main_route:
55
+                new_entry = WorkspaceMenuEntry(
56
+                    slug=app.slug,
57
+                    label=app.label,
58
+                    hexcolor=app.hexcolor,
59
+                    fa_icon=app.fa_icon,
60
+                    route=app.main_route
61
+                )
62
+                menu_entries.append(new_entry)
63
+
64
+        for entry in menu_entries:
65
+            entry.route = entry.route.replace(
66
+                '{workspace_id}',
67
+                str(workspace.workspace_id)
68
+            )
69
+
70
+        return menu_entries

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

@@ -36,9 +36,9 @@ from tracim_backend.exceptions import EmptyLabelNotAllowed
36 36
 from tracim_backend.exceptions import ContentNotFound
37 37
 from tracim_backend.exceptions import WorkspacesDoNotMatch
38 38
 from tracim_backend.lib.utils.utils import current_date_for_filename
39
-from tracim_backend.models.contents import CONTENT_STATUS
40
-from tracim_backend.models.contents import ContentType
41
-from tracim_backend.models.contents import CONTENT_TYPES
39
+from tracim_backend.app_models.contents import CONTENT_STATUS
40
+from tracim_backend.app_models.contents import ContentType
41
+from tracim_backend.app_models.contents import CONTENT_TYPES
42 42
 from tracim_backend.models.revision_protection import new_revision
43 43
 from tracim_backend.models.auth import User
44 44
 from tracim_backend.models.data import ActionDescription
@@ -1137,7 +1137,7 @@ class ContentApi(object):
1137 1137
         """
1138 1138
         allowed_content_dict = {}
1139 1139
         for allowed_content_type_slug in allowed_content_type_slug_list:
1140
-            if allowed_content_type_slug not in CONTENT_TYPES.extended_endpoint_allowed_types_slug():
1140
+            if allowed_content_type_slug not in CONTENT_TYPES.endpoint_allowed_types_slug():
1141 1141
                 raise ContentTypeNotExist('Content_type {} does not exist'.format(allowed_content_type_slug))  # nopep8
1142 1142
             allowed_content_dict[allowed_content_type_slug] = True
1143 1143
 

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

@@ -20,7 +20,7 @@ from tracim_backend.lib.utils.logger import logger
20 20
 from tracim_backend.lib.utils.utils import get_login_frontend_url
21 21
 from tracim_backend.lib.utils.utils import get_email_logo_frontend_url
22 22
 from tracim_backend.models.auth import User
23
-from tracim_backend.models.contents import CONTENT_TYPES
23
+from tracim_backend.app_models.contents import CONTENT_TYPES
24 24
 from tracim_backend.models.context_models import ContentInContext
25 25
 from tracim_backend.models.context_models import WorkspaceInContext
26 26
 from tracim_backend.models.data import ActionDescription

+ 1 - 1
backend/tracim_backend/lib/utils/authentification.py View File

@@ -3,7 +3,7 @@ import typing
3 3
 from pyramid.request import Request
4 4
 from sqlalchemy.orm.exc import NoResultFound
5 5
 
6
-from tracim_backend import TracimRequest
6
+from tracim_backend.lib.utils.request import TracimRequest
7 7
 from tracim_backend.exceptions import UserDoesNotExist
8 8
 from tracim_backend.lib.core.user import UserApi
9 9
 from tracim_backend.models import User

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

@@ -5,15 +5,14 @@ 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 ContentType
9
-from tracim_backend.models.contents import CONTENT_TYPES
8
+from tracim_backend.app_models.contents import ContentType
9
+from tracim_backend.app_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 ContentType
17 16
 from tracim_backend.exceptions import InsufficientUserRoleInWorkspace
18 17
 from tracim_backend.exceptions import ContentTypeNotAllowed
19 18
 from tracim_backend.exceptions import InsufficientUserProfile
@@ -167,19 +166,18 @@ def require_candidate_workspace_role(minimal_required_role: int) -> typing.Calla
167 166
     return decorator
168 167
 
169 168
 
170
-def require_content_types(content_types: typing.List['ContentType']) -> typing.Callable:  # nopep8
169
+def require_content_types(content_types_slug: typing.List[str]) -> typing.Callable:  # nopep8
171 170
     """
172 171
     Restricts access to specific file type or raise an exception.
173 172
     Check role for candidate_workspace.
174
-    :param content_types: list of ContentType object
173
+    :param content_types_slug: list of slug of content_types
175 174
     :return: decorator
176 175
     """
177 176
     def decorator(func: typing.Callable) -> typing.Callable:
178 177
         @functools.wraps(func)
179 178
         def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
180 179
             content = request.current_content
181
-            current_content_type_slug = CONTENT_TYPES.get_one_by_slug(content.type).slug
182
-            content_types_slug = [content_type.slug for content_type in content_types]  # nopep8
180
+            current_content_type_slug = CONTENT_TYPES.get_one_by_slug(content.type).slug  # nopep8
183 181
             if current_content_type_slug in content_types_slug:
184 182
                 return func(self, context, request)
185 183
             raise ContentTypeNotAllowed()

+ 1 - 1
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 CONTENT_TYPES
18
+from tracim_backend.app_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

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

@@ -3,11 +3,13 @@ import datetime
3 3
 import random
4 4
 import string
5 5
 from enum import Enum
6
+import colorsys
6 7
 
7 8
 from redis import Redis
8 9
 from rq import Queue
9
-
10
-from tracim_backend.config import CFG
10
+import typing
11
+if typing.TYPE_CHECKING:
12
+    from tracim_backend.config import CFG
11 13
 
12 14
 DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
13 15
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
@@ -16,7 +18,7 @@ CONTENT_FRONTEND_URL_SCHEMA = 'workspaces/{workspace_id}/contents/{content_type}
16 18
 WORKSPACE_FRONTEND_URL_SCHEMA = 'workspaces/{workspace_id}'  # nopep8
17 19
 
18 20
 
19
-def get_root_frontend_url(config: CFG) -> str:
21
+def get_root_frontend_url(config: 'CFG') -> str:
20 22
     """
21 23
     Return website base url with always '/' at the end
22 24
     """
@@ -28,19 +30,19 @@ def get_root_frontend_url(config: CFG) -> str:
28 30
     return base_url
29 31
 
30 32
 
31
-def get_login_frontend_url(config: CFG):
33
+def get_login_frontend_url(config: 'CFG'):
32 34
     """
33 35
     Return login page url
34 36
     """
35 37
     return get_root_frontend_url(config) + 'login'
36 38
 
37 39
 
38
-def get_email_logo_frontend_url(config: CFG):
40
+def get_email_logo_frontend_url(config: 'CFG'):
39 41
     # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for email_logo_frontend_url  # nopep8
40 42
     return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH4QUTDjMSlsws9AAAB89JREFUaN7tmWtwlFcZx3/PeXeXXLmKQJEGsFZApIUBnaqjUm4VgdYyCUib0cJoKLmHW5EWmlGoQkMICURgEPuhtiYDU7XcyVgKVugULZVCQAwUYyENYAmEXHb3PH7IFjYQNgnZHfnA+bKz+7573v//PPf/C/fWvdWhJWHfMbO4P479OpCAigeowWglPq3ARWfU9AJ7Cb/3MAXZn95dBLKKHsfIQmAUIq7rv6t6gU+BKCAGaEBkN9h88tLe7sgjTdjAJ5Y4OGYMIo80Aw8g4kakJyLxiDiIxABPoMwj5+XP3R0ESpP8WC1H1YuqBj4bUWwIB5gA0d/6/xFQFUpKHBJLHADyU9chTEc1BdEZqJ0O+ofb4xcPyuBIxoBQuHc4ov3w2zrwH0Nd0aBDccwgVPsjEg/4Uc4jegKfvkfMmQ9JSfECkFP0HGJeuu0TrJZhSacg9Xh4Caze2wsXC1B9GugO+BAqUIkC/QIinhYsYoEqYDt+fY2s8WXkFE5CnBIg+vaWtJuQukzy5te2l4Bz2ysTkxchshCRWERMU2BKT4RuiLT0vwagHsENPIyR7zAx2eGybws09AUZEQLHcNS5wMEdB8NjgaKyBNTuQuTLoWOAs6AHUd5DpRzxX0KMBe0M+gA4D4Hdw5mT72LJQORxkM6IfgxSC3wjyHpHEN8M8jKPtYeAq2VgvhHg9A4RvPUIKzCyBVe346SM9LZw1y5KShzOxfUhL60SmMfcNUXgisXYS/iNA/IzhNmBgH4I3E8Cxzpmgby9ffFoMSKTQxA4jZjRpI35qENZbF5Rb1SWgcwM7HsQr28KhZnVd55GPfqTkOCbaB+i2lR1uHa8nHYeda0EygJWGILHGXzndaBgz3CQ5Fb8/hSqr5E7uj4sBXBVSjlqN6FaixKHlS8BxCYtHhs/bUly9JT597U9C30/eRYiU4Nc5Q1gH+AG/gvsxJBH2vg3w9pDPTjqFDGxX0FkKOih2PtHOcbIBiPMMi5XT9eDo9/xlu+rDW2BpX+MAQnOCudRWcWFA3OwncYhrke5VpuK1SrW7+sTVgKv5NYjsr2p6TMY1QEGvhi4mmw89unWXaiX6QQEN1YHuHjpELm5lsxvV2MbhxAdWwCSiNfnCXsb7mc/Ih8hXED8J1T1k6awEAG+R+LCLqEJOL2vInwQOP46VHaSm9QYiI1nMU4xwv2oa3OHs0+LiaHuHKr/wOo/r1yuOQwcDEqVw2KdqITQBFJGelFWojITPz8kY+wmAAp3pWLkJVT7oGwkY3R5REar/Ln1qO7AyIfsLGxQ1eB60E3UDmi9kKWPOwmcvP69cM8kROYCXVB5FW18I4LToeLSbTGnKh2T9MIChLHBOI2lT9srMUDB9s7AM8CApj7H/yaZExsiOd9GnbngcjzOciPyVPOuW0RFB7ZvHnDck4FJgbOpwPoPRHpAdxs782bwQSwei3ty0aC2EVh61ANMutEy63Gi/l0VUfSJC7sIjAnhYYON4x7eNgI9KgeifDPIOyuuDygRWnGuKDcQF6roWrGxbSNgzDAgqBuV6ki7z9WT5y4DR0LdoqoVbSOgZhAi7hv4bXzEFarDG7yqbPmsgN2aovhdLdf+2jqBorIE4JGbIugZCvcsijQHr5ojN1tbVaustUWNNP6S0vy60AR+ut4Ndj7CYzdlgL7AixTtmRnRLOToVNAhQeDfR+yPr+iJrIbf/+pM62l0WP8RKNNCSCCTySuJjgT42MTnv4rqjEDvg6J+VH5T8/qynZSW+ts40EgCQtcQDcsDEN2d9IKe4U2hiR5jmG1ERt4QKrS4pkqK2zmRSQMaUsj6D5cuu3C5ppCY6IQLf2dn8CIjZk4Q+N961VnJvlxf+4Z6v/cojvsDYEQL4BXRHVTXfkysjqPfd3tA6YqOnXx293gnLgvV5xFBVa2iv/b6dEX91p+fbf9ImTnxX6gWAadbILARX+0rbEjxohwCFpBVlH1nyJea2KTFY+OduHUGeaFJoNN3gcwr1smu37qsze16y7pQ4a7pYFYj0iuAvho0jbTxJQBkrB6Ky70VGIDqamA9+WmnWtwrZ20/fN4uQA1rss52Slw8wIOZKg4/QIlTKBfY32DttobSZafDJS0Ka3YXYEx6UCXZid8/n6wJRwHIXrsEI7kB6+xHtQTDn8hLPQuipBb2oJOkgSQBPREuAlupq98cfa6mXsXnOFpfV5uQMBRxlMpP/kJpbmP4tNHC3SNBNiLycJAbleLlOXLGVTB33edRLUBkepAuegyRd1D7Pphh10Wr5mJuMT5fNo67L45NQSUlIBqsYlXqkqaiGy51es3eWQh5CDfmUeufTcaE9QBkFY7AyIuImXxT3bfI7doUvQayFbUDMSZYRHiLq64n2JByOXzvBzLGbkJ1GarnP4OPmBt60Or0v+E381Fdj+ILOhYT4p1ADOhTzcA3EdjeXvCtEwDIGNc0J6vNB/0FDXXbmk9uc05wMSYLq+nAgYArtWJ3keYW0eVUVheE9/3ArbYXkND+mbO2H8gk0AnAlGZAb3WlWpQ/I/o6q9JevWMxIyJdWWZ+V4xnOUaebQH4MVS2I/59eO3bFGbWdEiNiVhrmVncH+NfhMg0IA4RH1bfQnQlq9LKwiYnRbTB/9HmKLrWfg2j94HUoI1/Z3XOOe6te+vuWf8DkM0cb7DOQZgAAAAASUVORK5CYII='  # nopep8'
41 43
 
42 44
 
43
-def get_redis_connection(config: CFG) -> Redis:
45
+def get_redis_connection(config: 'CFG') -> Redis:
44 46
     """
45 47
     :param config: current app_config
46 48
     :return: redis connection
@@ -125,3 +127,62 @@ def password_generator(
125 127
     """
126 128
     return ''.join(random.choice(chars) for char_number in range(length))
127 129
 
130
+
131
+def clamp(val: float, minimum: float = 0.0, maximum: float= 255.0) -> int:
132
+    """ Fix value between min an max"""
133
+    if val < minimum:
134
+        return minimum
135
+    if val > maximum:
136
+        return maximum
137
+    return int(val)
138
+
139
+
140
+COLOR_DARKEN_SCALE_FACTOR = 0.85
141
+COLOR_LIGHTEN_SCALE_FACTOR = 1.15
142
+
143
+
144
+class Color(object):
145
+    def __init__(self, base_hex_code: str):
146
+        """
147
+        :param base_hex_code: hex color like '#FFFFFF'
148
+        """
149
+
150
+        assert len(base_hex_code) == 7
151
+        self._base_hex_code = base_hex_code
152
+
153
+    # INFO - G.M - 2018-08-10 - get_hexcolor, inspired by
154
+    # https://thadeusb.com/weblog/2010/10/10/python_scale_hex_color/
155
+
156
+    def get_hexcolor(self, scalefactor: float) -> str:
157
+        """
158
+
159
+        :param scalefactor: factor of scaling,
160
+        value between 0 and 1 darken the color,
161
+        value >1 lighten the color.
162
+        :return: new hex_color
163
+        """
164
+
165
+        hex_color = self._base_hex_code.strip('#')
166
+        assert scalefactor > 0
167
+
168
+        r = int(hex_color[:2], 16)
169
+        g = int(hex_color[2:4], 16)
170
+        b = int(hex_color[4:], 16)
171
+
172
+        h, l, s = colorsys.rgb_to_hls(r, g, b)
173
+        l = scalefactor * l
174
+        r, g, b = colorsys.hls_to_rgb(h, l, s)
175
+
176
+        return "#%02x%02x%02x" % (clamp(r), clamp(g), clamp(b))
177
+
178
+    @property
179
+    def normal(self):
180
+        return self._base_hex_code
181
+
182
+    @property
183
+    def darken(self):
184
+        return self.get_hexcolor(COLOR_DARKEN_SCALE_FACTOR)
185
+
186
+    @property
187
+    def lighten(self):
188
+        return self.get_hexcolor(COLOR_LIGHTEN_SCALE_FACTOR)

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

@@ -8,7 +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
+from tracim_backend.app_models.contents import CONTENT_TYPES
12 12
 
13 13
 from wsgidav.dav_provider import DAVProvider
14 14
 from wsgidav.lock_manager import LockManager

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

@@ -1,7 +1,7 @@
1 1
 #coding: utf8
2 2
 from datetime import datetime
3 3
 
4
-from tracim_backend.models.contents import CONTENT_TYPES
4
+from tracim_backend.app_models.contents import CONTENT_TYPES
5 5
 from tracim_backend.models.data import VirtualEvent
6 6
 from tracim_backend.models import data
7 7
 

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

@@ -19,7 +19,7 @@ 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
+from tracim_backend.app_models.contents import CONTENT_TYPES
23 23
 from tracim_backend.models.data import User, ContentRevisionRO
24 24
 from tracim_backend.models.data import Workspace
25 25
 from tracim_backend.models.data import Content

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

@@ -4,7 +4,7 @@ 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
+from tracim_backend.app_models.contents import CONTENT_TYPES
8 8
 from wsgidav import util
9 9
 from wsgidav import compat
10 10
 

+ 1 - 1
backend/tracim_backend/models/__init__.py View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import sessionmaker
5 5
 from sqlalchemy.orm import configure_mappers
6 6
 import zope.sqlalchemy
7 7
 from .meta import DeclarativeBase
8
-from .revision_protection import prevent_content_revision_delete
8
+from tracim_backend.models.revision_protection import prevent_content_revision_delete
9 9
 # import or define all models here to ensure they are attached to the
10 10
 # Base.metadata prior to any initialization routines
11 11
 from tracim_backend.models.auth import User, Group, Permission

+ 0 - 117
backend/tracim_backend/models/applications.py View File

@@ -1,117 +0,0 @@
1
-# coding=utf-8
2
-import typing
3
-
4
-
5
-class Application(object):
6
-    """
7
-    Application class with data needed for frontend
8
-    """
9
-    def __init__(
10
-            self,
11
-            label: str,
12
-            slug: str,
13
-            fa_icon: str,
14
-            hexcolor: str,
15
-            is_active: bool,
16
-            config: typing.Dict[str, str],
17
-            main_route: str,
18
-    ) -> None:
19
-        """
20
-        @param label: public label of application
21
-        @param slug: identifier of application
22
-        @param icon: font awesome icon class
23
-        @param hexcolor: hexa color of application main color
24
-        @param is_active: True if application enable, False if inactive
25
-        @param config: a dict with eventual application config
26
-        @param main_route: the route of the frontend "home" screen of
27
-        the application. For exemple, if you have an application
28
-        called "calendar", the main route will be something
29
-        like /#/workspace/{wid}/calendar.
30
-        """
31
-        self.label = label
32
-        self.slug = slug
33
-        self.fa_icon = fa_icon
34
-        self.hexcolor = hexcolor
35
-        self.is_active = is_active
36
-        self.config = config
37
-        self.main_route = main_route
38
-
39
-    # TODO - G.M - 2018-08-07 - Refactor slug coherence issue like this one.
40
-    # we probably should not have 2 kind of slug
41
-    @property
42
-    def minislug(self):
43
-        return self.slug.replace('contents/', '')
44
-
45
-
46
-# default apps
47
-calendar = Application(
48
-    label='Calendar',
49
-    slug='calendar',
50
-    fa_icon='calendar',
51
-    hexcolor='#757575',
52
-    is_active=True,
53
-    config={},
54
-    main_route='/#/workspaces/{workspace_id}/calendar',
55
-)
56
-
57
-thread = Application(
58
-    label='Threads',
59
-    slug='contents/thread',
60
-    fa_icon='comments-o',
61
-    hexcolor='#ad4cf9',
62
-    is_active=True,
63
-    config={},
64
-    main_route='/#/workspaces/{workspace_id}/contents?type=thread',
65
-
66
-)
67
-
68
-folder = Application(
69
-    label='Folder',
70
-    slug='contents/folder',
71
-    fa_icon='folder-open-o',
72
-    hexcolor='#252525',
73
-    is_active=True,
74
-    config={},
75
-    main_route='',
76
-)
77
-
78
-_file = Application(
79
-    label='Files',
80
-    slug='contents/file',
81
-    fa_icon='paperclip',
82
-    hexcolor='#FF9900',
83
-    is_active=True,
84
-    config={},
85
-    main_route='/#/workspaces/{workspace_id}/contents?type=file',
86
-)
87
-
88
-markdownpluspage = Application(
89
-    label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
90
-    slug='contents/markdownpluspage',
91
-    fa_icon='file-code-o',
92
-    hexcolor='#f12d2d',
93
-    is_active=True,
94
-    config={},
95
-    main_route='/#/workspaces/{workspace_id}/contents?type=markdownpluspage',
96
-)
97
-
98
-html_documents = Application(
99
-    label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
100
-    slug='contents/html-document',
101
-    fa_icon='file-text-o',
102
-    hexcolor='#3f52e3',
103
-    is_active=True,
104
-    config={},
105
-    main_route='/#/workspaces/{workspace_id}/contents?type=html-document',
106
-)
107
-# TODO - G.M - 08-06-2018 - This is hardcoded lists of app, make this dynamic.
108
-# List of applications
109
-applications = [
110
-    html_documents,
111
-    # TODO - G.M - 2018-08-02 - Restore markdownpage app
112
-    # markdownpluspage,
113
-    _file,
114
-    thread,
115
-    folder,
116
-    # calendar,
117
-]

+ 8 - 4
backend/tracim_backend/models/context_models.py View File

@@ -7,6 +7,8 @@ from slugify import slugify
7 7
 from sqlalchemy.orm import Session
8 8
 from tracim_backend.config import CFG
9 9
 from tracim_backend.config import PreviewDim
10
+from tracim_backend.extensions import app_list
11
+from tracim_backend.lib.core.application import ApplicationApi
10 12
 from tracim_backend.lib.utils.utils import get_root_frontend_url
11 13
 from tracim_backend.lib.utils.utils import password_generator
12 14
 from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
@@ -19,9 +21,8 @@ from tracim_backend.models.data import ContentRevisionRO
19 21
 from tracim_backend.models.data import Workspace
20 22
 from tracim_backend.models.data import UserRoleInWorkspace
21 23
 from tracim_backend.models.roles import WorkspaceRoles
22
-from tracim_backend.models.workspace_menu_entries import default_workspace_menu_entry  # nopep8
23
-from tracim_backend.models.workspace_menu_entries import WorkspaceMenuEntry
24
-from tracim_backend.models.contents import CONTENT_TYPES
24
+from tracim_backend.app_models.workspace_menu_entries import WorkspaceMenuEntry
25
+from tracim_backend.app_models.contents import CONTENT_TYPES
25 26
 
26 27
 
27 28
 class PreviewAllowedDim(object):
@@ -493,7 +494,10 @@ class WorkspaceInContext(object):
493 494
         # order to not use hardcoded list
494 495
         # list should be able to change (depending on activated/disabled
495 496
         # apps)
496
-        return default_workspace_menu_entry(self.workspace)
497
+        app_api = ApplicationApi(
498
+            app_list
499
+        )
500
+        return app_api.get_default_workspace_menu_entry(self.workspace)
497 501
 
498 502
     @property
499 503
     def frontend_url(self):

+ 5 - 5
backend/tracim_backend/models/data.py View File

@@ -87,7 +87,7 @@ class Workspace(DeclarativeBase):
87 87
 
88 88
     def get_allowed_content_types(self):
89 89
         # @see Content.get_allowed_content_types()
90
-        return CONTENT_TYPES.extended_endpoint_allowed_types_slug()
90
+        return CONTENT_TYPES.endpoint_allowed_types_slug()
91 91
 
92 92
     def get_valid_children(
93 93
             self,
@@ -282,9 +282,9 @@ class ActionDescription(object):
282 282
                 ]
283 283
 
284 284
 
285
-from tracim_backend.models.contents import CONTENT_STATUS
286
-from tracim_backend.models.contents import ContentStatus
287
-from tracim_backend.models.contents import CONTENT_TYPES
285
+from tracim_backend.app_models.contents import CONTENT_STATUS
286
+from tracim_backend.app_models.contents import ContentStatus
287
+from tracim_backend.app_models.contents import CONTENT_TYPES
288 288
 # TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
289 289
 # is lost .
290 290
 
@@ -552,7 +552,7 @@ class ContentChecker(object):
552 552
                 for content_slug, value in properties['allowed_content'].items():  # nopep8
553 553
                     if not isinstance(value, bool):
554 554
                         return False
555
-                    if not content_slug in CONTENT_TYPES.extended_endpoint_allowed_types_slug():  # nopep8
555
+                    if not content_slug in CONTENT_TYPES.endpoint_allowed_types_slug():  # nopep8
556 556
                         return False
557 557
             if 'origin' in properties.keys():
558 558
                 pass

+ 0 - 71
backend/tracim_backend/models/workspace_menu_entries.py View File

@@ -1,71 +0,0 @@
1
-# coding=utf-8
2
-import typing
3
-from copy import copy
4
-
5
-from tracim_backend.models.applications import applications
6
-from tracim_backend.models.data import Workspace
7
-
8
-
9
-class WorkspaceMenuEntry(object):
10
-    """
11
-    Application class with data needed for frontend
12
-    """
13
-    def __init__(
14
-            self,
15
-            label: str,
16
-            slug: str,
17
-            fa_icon: str,
18
-            hexcolor: str,
19
-            route: str,
20
-    ) -> None:
21
-        self.slug = slug
22
-        self.label = label
23
-        self.route = route
24
-        self.hexcolor = hexcolor
25
-        self.fa_icon = fa_icon
26
-
27
-dashboard_menu_entry = WorkspaceMenuEntry(
28
-  slug='dashboard',
29
-  label='Dashboard',
30
-  route='/#/workspaces/{workspace_id}/dashboard',
31
-  hexcolor='#252525',
32
-  fa_icon="signal",
33
-)
34
-all_content_menu_entry = WorkspaceMenuEntry(
35
-  slug="contents/all",
36
-  label="All Contents",
37
-  route="/#/workspaces/{workspace_id}/contents",
38
-  hexcolor="#fdfdfd",
39
-  fa_icon="th",
40
-)
41
-
42
-# TODO - G.M - 08-06-2018 - This is hardcoded default menu entry,
43
-#  of app, make this dynamic (and loaded from application system)
44
-def default_workspace_menu_entry(
45
-    workspace: Workspace,
46
-)-> typing.List[WorkspaceMenuEntry]:
47
-    """
48
-    Get default menu entry for a workspace
49
-    """
50
-    menu_entries = [
51
-        copy(dashboard_menu_entry),
52
-        copy(all_content_menu_entry),
53
-    ]
54
-    for app in applications:
55
-        if app.main_route:
56
-            new_entry = WorkspaceMenuEntry(
57
-                slug=app.slug,
58
-                label=app.label,
59
-                hexcolor=app.hexcolor,
60
-                fa_icon=app.fa_icon,
61
-                route=app.main_route
62
-            )
63
-            menu_entries.append(new_entry)
64
-
65
-    for entry in menu_entries:
66
-        entry.route = entry.route.replace(
67
-            '{workspace_id}',
68
-            str(workspace.workspace_id)
69
-        )
70
-
71
-    return menu_entries

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

@@ -14,7 +14,7 @@ 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
+from tracim_backend.app_models.contents import CONTENT_TYPES
18 18
 from tracim_backend.models.data import Workspace
19 19
 from tracim_backend.models.data import ContentRevisionRO
20 20
 from tracim_backend.models.data import Content

+ 1 - 1
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 CONTENT_TYPES
8
+from tracim_backend.app_models.contents import CONTENT_TYPES
9 9
 from tracim_backend.models.revision_protection import new_revision
10 10
 import io
11 11
 

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

+ 10 - 2
backend/tracim_backend/tests/functional/test_system.py View File

@@ -1,7 +1,10 @@
1 1
 # coding=utf-8
2
-from tracim_backend.models.contents import CONTENT_TYPES
2
+import transaction
3
+from tracim_backend.extensions import app_list
4
+from tracim_backend.lib.core.application import ApplicationApi
5
+from tracim_backend.models import get_tm_session
6
+from tracim_backend.app_models.contents import CONTENT_TYPES
3 7
 from tracim_backend.tests import FunctionalTest
4
-from tracim_backend.models.applications import applications
5 8
 
6 9
 """
7 10
 Tests for /api/v2/system subpath endpoints.
@@ -26,6 +29,11 @@ class TestApplicationEndpoint(FunctionalTest):
26 29
         )
27 30
         res = self.testapp.get('/api/v2/system/applications', status=200)
28 31
         res = res.json_body
32
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
33
+        app_api = ApplicationApi(
34
+            app_list=app_list,
35
+        )
36
+        applications = app_api.get_all()
29 37
         assert len(res) == len(applications)
30 38
         for counter, application in enumerate(applications):
31 39
             assert res[counter]['label'] == application.label

+ 26 - 39
backend/tracim_backend/tests/functional/test_user.py View File

@@ -8,13 +8,15 @@ import requests
8 8
 import transaction
9 9
 
10 10
 from tracim_backend import models
11
+from tracim_backend.extensions import app_list
12
+from tracim_backend.lib.core.application import ApplicationApi
11 13
 from tracim_backend.lib.core.content import ContentApi
12 14
 from tracim_backend.lib.core.user import UserApi
13 15
 from tracim_backend.lib.core.group import GroupApi
14 16
 from tracim_backend.lib.core.userworkspace import RoleApi
15 17
 from tracim_backend.lib.core.workspace import WorkspaceApi
16 18
 from tracim_backend.models import get_tm_session
17
-from tracim_backend.models.contents import CONTENT_TYPES
19
+from tracim_backend.app_models.contents import CONTENT_TYPES
18 20
 from tracim_backend.models.data import UserRoleInWorkspace
19 21
 from tracim_backend.models.revision_protection import new_revision
20 22
 from tracim_backend.tests import FunctionalTest
@@ -2370,6 +2372,22 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
2370 2372
         """
2371 2373
         Check obtain all workspaces reachables for user with user auth.
2372 2374
         """
2375
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2376
+        admin = dbsession.query(models.User) \
2377
+            .filter(models.User.email == 'admin@admin.admin') \
2378
+            .one()
2379
+
2380
+        workspace_api = WorkspaceApi(
2381
+            session=dbsession,
2382
+            current_user=admin,
2383
+            config=self.app_config,
2384
+        )
2385
+        workspace = workspace_api.get_one(1)
2386
+        app_api = ApplicationApi(
2387
+            app_list
2388
+        )
2389
+
2390
+        default_sidebar_entry = app_api.get_default_workspace_menu_entry(workspace=workspace)  # nope8
2373 2391
         self.testapp.authorization = (
2374 2392
             'Basic',
2375 2393
             (
@@ -2384,45 +2402,14 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
2384 2402
         assert workspace['label'] == 'Business'
2385 2403
         assert workspace['slug'] == 'business'
2386 2404
         assert workspace['is_deleted'] is False
2387
-        assert len(workspace['sidebar_entries']) == 5
2388
-
2389
-        # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
2390
-        # not fixed on active application/content-file
2391
-        sidebar_entry = workspace['sidebar_entries'][0]
2392
-        assert sidebar_entry['slug'] == 'dashboard'
2393
-        assert sidebar_entry['label'] == 'Dashboard'
2394
-        assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
2395
-        assert sidebar_entry['hexcolor'] == "#252525"
2396
-        assert sidebar_entry['fa_icon'] == "signal"
2397
-
2398
-        sidebar_entry = workspace['sidebar_entries'][1]
2399
-        assert sidebar_entry['slug'] == 'contents/all'
2400
-        assert sidebar_entry['label'] == 'All Contents'
2401
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
2402
-        assert sidebar_entry['hexcolor'] == "#fdfdfd"
2403
-        assert sidebar_entry['fa_icon'] == "th"
2404
-
2405
-        sidebar_entry = workspace['sidebar_entries'][2]
2406
-        assert sidebar_entry['slug'] == 'contents/html-document'
2407
-        assert sidebar_entry['label'] == 'Text Documents'
2408
-        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=html-document'  # nopep8
2409
-        assert sidebar_entry['hexcolor'] == "#3f52e3"
2410
-        assert sidebar_entry['fa_icon'] == "file-text-o"
2411
-
2412
-        sidebar_entry = workspace['sidebar_entries'][3]
2413
-        assert sidebar_entry['slug'] == 'contents/file'
2414
-        assert sidebar_entry['label'] == 'Files'
2415
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
2416
-        assert sidebar_entry['hexcolor'] == "#FF9900"
2417
-        assert sidebar_entry['fa_icon'] == "paperclip"
2418
-
2419
-        sidebar_entry = workspace['sidebar_entries'][4]
2420
-        assert sidebar_entry['slug'] == 'contents/thread'
2421
-        assert sidebar_entry['label'] == 'Threads'
2422
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
2423
-        assert sidebar_entry['hexcolor'] == "#ad4cf9"
2424
-        assert sidebar_entry['fa_icon'] == "comments-o"
2425 2405
 
2406
+        assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
2407
+        for counter, sidebar_entry in enumerate(default_sidebar_entry):
2408
+            workspace['sidebar_entries'][counter]['slug'] = sidebar_entry.slug
2409
+            workspace['sidebar_entries'][counter]['label'] = sidebar_entry.label
2410
+            workspace['sidebar_entries'][counter]['route'] = sidebar_entry.route
2411
+            workspace['sidebar_entries'][counter]['hexcolor'] = sidebar_entry.hexcolor  # nopep8
2412
+            workspace['sidebar_entries'][counter]['fa_icon'] = sidebar_entry.fa_icon  # nopep8
2426 2413
 
2427 2414
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
2428 2415
         """

+ 46 - 42
backend/tracim_backend/tests/functional/test_workspaces.py View File

@@ -7,13 +7,15 @@ import transaction
7 7
 from depot.io.utils import FileIntent
8 8
 
9 9
 from tracim_backend import models
10
+from tracim_backend.extensions import app_list
11
+from tracim_backend.lib.core.application import ApplicationApi
10 12
 from tracim_backend.lib.core.content import ContentApi
11 13
 from tracim_backend.lib.core.group import GroupApi
12 14
 from tracim_backend.lib.core.user import UserApi
13 15
 from tracim_backend.lib.core.userworkspace import RoleApi
14 16
 from tracim_backend.lib.core.workspace import WorkspaceApi
15 17
 from tracim_backend.models import get_tm_session
16
-from tracim_backend.models.contents import CONTENT_TYPES
18
+from tracim_backend.app_models.contents import CONTENT_TYPES
17 19
 from tracim_backend.models.data import UserRoleInWorkspace
18 20
 from tracim_backend.tests import FunctionalTest
19 21
 from tracim_backend.tests import set_html_document_slug_to_legacy
@@ -32,6 +34,22 @@ class TestWorkspaceEndpoint(FunctionalTest):
32 34
         """
33 35
         Check obtain workspace reachable for user.
34 36
         """
37
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
38
+        admin = dbsession.query(models.User) \
39
+            .filter(models.User.email == 'admin@admin.admin') \
40
+            .one()
41
+
42
+        workspace_api = WorkspaceApi(
43
+            session=dbsession,
44
+            current_user=admin,
45
+            config=self.app_config,
46
+        )
47
+        workspace = workspace_api.get_one(1)
48
+        app_api = ApplicationApi(
49
+            app_list
50
+        )
51
+        default_sidebar_entry = app_api.get_default_workspace_menu_entry(workspace=workspace)  # nope8
52
+
35 53
         self.testapp.authorization = (
36 54
             'Basic',
37 55
             (
@@ -46,49 +64,35 @@ class TestWorkspaceEndpoint(FunctionalTest):
46 64
         assert workspace['label'] == 'Business'
47 65
         assert workspace['description'] == 'All importants documents'
48 66
         assert workspace['is_deleted'] is False
49
-        assert len(workspace['sidebar_entries']) == 5
50
-
51
-        # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
52
-        # not fixed on active application/content-file
53
-        sidebar_entry = workspace['sidebar_entries'][0]
54
-        assert sidebar_entry['slug'] == 'dashboard'
55
-        assert sidebar_entry['label'] == 'Dashboard'
56
-        assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
57
-        assert sidebar_entry['hexcolor'] == "#252525"
58
-        assert sidebar_entry['fa_icon'] == "signal"
59
-
60
-        sidebar_entry = workspace['sidebar_entries'][1]
61
-        assert sidebar_entry['slug'] == 'contents/all'
62
-        assert sidebar_entry['label'] == 'All Contents'
63
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
64
-        assert sidebar_entry['hexcolor'] == "#fdfdfd"
65
-        assert sidebar_entry['fa_icon'] == "th"
66
-
67
-        sidebar_entry = workspace['sidebar_entries'][2]
68
-        assert sidebar_entry['slug'] == 'contents/html-document'
69
-        assert sidebar_entry['label'] == 'Text Documents'
70
-        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=html-document'  # nopep8
71
-        assert sidebar_entry['hexcolor'] == "#3f52e3"
72
-        assert sidebar_entry['fa_icon'] == "file-text-o"
73
-
74
-        sidebar_entry = workspace['sidebar_entries'][3]
75
-        assert sidebar_entry['slug'] == 'contents/file'
76
-        assert sidebar_entry['label'] == 'Files'
77
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
78
-        assert sidebar_entry['hexcolor'] == "#FF9900"
79
-        assert sidebar_entry['fa_icon'] == "paperclip"
80
-
81
-        sidebar_entry = workspace['sidebar_entries'][4]
82
-        assert sidebar_entry['slug'] == 'contents/thread'
83
-        assert sidebar_entry['label'] == 'Threads'
84
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
85
-        assert sidebar_entry['hexcolor'] == "#ad4cf9"
86
-        assert sidebar_entry['fa_icon'] == "comments-o"
67
+
68
+        assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
69
+        for counter, sidebar_entry in enumerate(default_sidebar_entry):
70
+            workspace['sidebar_entries'][counter]['slug'] = sidebar_entry.slug
71
+            workspace['sidebar_entries'][counter]['label'] = sidebar_entry.label
72
+            workspace['sidebar_entries'][counter]['route'] = sidebar_entry.route
73
+            workspace['sidebar_entries'][counter]['hexcolor'] = sidebar_entry.hexcolor  # nopep8
74
+            workspace['sidebar_entries'][counter]['fa_icon'] = sidebar_entry.fa_icon  # nopep8
87 75
 
88 76
     def test_api__update_workspace__ok_200__nominal_case(self) -> None:
89 77
         """
90 78
         Test update workspace
91 79
         """
80
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
81
+        admin = dbsession.query(models.User) \
82
+            .filter(models.User.email == 'admin@admin.admin') \
83
+            .one()
84
+
85
+        workspace_api = WorkspaceApi(
86
+            session=dbsession,
87
+            current_user=admin,
88
+            config=self.app_config,
89
+        )
90
+        workspace = workspace_api.get_one(1)
91
+        app_api = ApplicationApi(
92
+            app_list
93
+        )
94
+        default_sidebar_entry = app_api.get_default_workspace_menu_entry(workspace=workspace)  # nope8
95
+
92 96
         self.testapp.authorization = (
93 97
             'Basic',
94 98
             (
@@ -111,8 +115,8 @@ class TestWorkspaceEndpoint(FunctionalTest):
111 115
         assert workspace['slug'] == 'business'
112 116
         assert workspace['label'] == 'Business'
113 117
         assert workspace['description'] == 'All importants documents'
118
+        assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
114 119
         assert workspace['is_deleted'] is False
115
-        assert len(workspace['sidebar_entries']) == 5
116 120
 
117 121
         # modify workspace
118 122
         res = self.testapp.put_json(
@@ -126,8 +130,8 @@ class TestWorkspaceEndpoint(FunctionalTest):
126 130
         assert workspace['slug'] == 'superworkspace'
127 131
         assert workspace['label'] == 'superworkspace'
128 132
         assert workspace['description'] == 'mysuperdescription'
133
+        assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
129 134
         assert workspace['is_deleted'] is False
130
-        assert len(workspace['sidebar_entries']) == 5
131 135
 
132 136
         # after
133 137
         res = self.testapp.get(
@@ -140,8 +144,8 @@ class TestWorkspaceEndpoint(FunctionalTest):
140 144
         assert workspace['slug'] == 'superworkspace'
141 145
         assert workspace['label'] == 'superworkspace'
142 146
         assert workspace['description'] == 'mysuperdescription'
147
+        assert len(workspace['sidebar_entries']) == len(default_sidebar_entry)
143 148
         assert workspace['is_deleted'] is False
144
-        assert len(workspace['sidebar_entries']) == 5
145 149
 
146 150
     def test_api__update_workspace__err_400__empty_label(self) -> None:
147 151
         """

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

@@ -16,7 +16,7 @@ from tracim_backend.exceptions import UnallowedSubContent
16 16
 from tracim_backend.lib.core.workspace import RoleApi
17 17
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
18 18
 from tracim_backend.lib.core.workspace import WorkspaceApi
19
-from tracim_backend.models.contents import CONTENT_TYPES
19
+from tracim_backend.app_models.contents import CONTENT_TYPES
20 20
 from tracim_backend.models.revision_protection import new_revision
21 21
 from tracim_backend.models.auth import User
22 22
 from tracim_backend.models.auth import Group

+ 3 - 10
backend/tracim_backend/tests/library/test_webdav.py View File

@@ -22,22 +22,15 @@ from unittest.mock import MagicMock
22 22
 
23 23
 class TestWebdavFactory(StandardTest):
24 24
 
25
+    config_section = 'webdav_test'
26
+
25 27
     def test_unit__initConfig__ok__nominal_case(self):
26 28
         """
27 29
         Check if config is correctly modify for wsgidav using mocked
28 30
         wsgidav and tracim conf (as dict)
29 31
         :return:
30 32
         """
31
-        tracim_settings = {
32
-            'website.base_url': 'http://localhost:6543',
33
-            'sqlalchemy.url': 'sqlite:///:memory:',
34
-            'user.auth_token.validity': '604800',
35
-            'depot_storage_dir': '/tmp/test/depot',
36
-            'depot_storage_name': 'test',
37
-            'preview_cache_dir': '/tmp/test/preview_cache',
38
-            'wsgidav.config_path': 'development.ini'
39
-
40
-        }
33
+        tracim_settings = self.settings
41 34
         wsgidav_setting = DEFAULT_CONFIG.copy()
42 35
         wsgidav_setting.update(
43 36
             {

+ 1 - 1
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.contents import CONTENT_TYPES
18
+from tracim_backend.app_models.contents import CONTENT_TYPES
19 19
 from tracim_backend.models.data import Workspace
20 20
 from tracim_backend.tests import StandardTest
21 21
 

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

@@ -5,7 +5,7 @@ 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.contents import CONTENT_TYPES
8
+from tracim_backend.app_models.contents import CONTENT_TYPES
9 9
 from tracim_backend.tests import DefaultTest
10 10
 from tracim_backend.tests import eq_
11 11
 

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

@@ -7,7 +7,7 @@ try:  # Python 3.5+
7 7
 except ImportError:
8 8
     from http import client as HTTPStatus
9 9
 
10
-from tracim_backend import TracimRequest
10
+from tracim_backend.lib.utils.request import TracimRequest
11 11
 from tracim_backend.extensions import hapic
12 12
 from tracim_backend.lib.core.content import ContentApi
13 13
 from tracim_backend.lib.core.workspace import WorkspaceApi
@@ -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 CONTENT_TYPES
23
+from tracim_backend.app_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
 

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

@@ -12,7 +12,7 @@ try:  # Python 3.5+
12 12
 except ImportError:
13 13
     from http import client as HTTPStatus
14 14
 
15
-from tracim_backend import TracimRequest
15
+from tracim_backend.lib.utils.request import TracimRequest
16 16
 from tracim_backend.extensions import hapic
17 17
 from tracim_backend.lib.core.content import ContentApi
18 18
 from tracim_backend.views.controllers import Controller
@@ -32,8 +32,8 @@ 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 CONTENT_TYPES
36
-from tracim_backend.models.contents import file_type
35
+from tracim_backend.app_models.contents import CONTENT_TYPES
36
+from tracim_backend.app_models.contents import FILE_TYPE
37 37
 from tracim_backend.models.revision_protection import new_revision
38 38
 from tracim_backend.exceptions import EmptyLabelNotAllowed
39 39
 from tracim_backend.exceptions import PageOfPreviewNotFound
@@ -50,7 +50,7 @@ class FileController(Controller):
50 50
     # File data
51 51
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
52 52
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
53
-    @require_content_types([file_type])
53
+    @require_content_types([FILE_TYPE])
54 54
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
55 55
     # TODO - G.M - 2018-07-24 - Use hapic for input file
56 56
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
@@ -88,7 +88,7 @@ class FileController(Controller):
88 88
 
89 89
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
90 90
     @require_workspace_role(UserRoleInWorkspace.READER)
91
-    @require_content_types([file_type])
91
+    @require_content_types([FILE_TYPE])
92 92
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
93 93
     @hapic.output_file([])
94 94
     def download_file(self, context, request: TracimRequest, hapic_data=None):
@@ -115,7 +115,7 @@ class FileController(Controller):
115 115
 
116 116
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
117 117
     @require_workspace_role(UserRoleInWorkspace.READER)
118
-    @require_content_types([file_type])
118
+    @require_content_types([FILE_TYPE])
119 119
     @hapic.input_path(WorkspaceAndContentRevisionIdPathSchema())
120 120
     @hapic.output_file([])
121 121
     def download_revisions_file(self, context, request: TracimRequest, hapic_data=None):  # nopep8
@@ -148,7 +148,7 @@ class FileController(Controller):
148 148
     # pdf
149 149
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
150 150
     @require_workspace_role(UserRoleInWorkspace.READER)
151
-    @require_content_types([file_type])
151
+    @require_content_types([FILE_TYPE])
152 152
     @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
153 153
     @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
154 154
     @hapic.input_query(PageQuerySchema())
@@ -179,7 +179,7 @@ class FileController(Controller):
179 179
 
180 180
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
181 181
     @require_workspace_role(UserRoleInWorkspace.READER)
182
-    @require_content_types([file_type])
182
+    @require_content_types([FILE_TYPE])
183 183
     @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
184 184
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
185 185
     @hapic.output_file([])
@@ -204,7 +204,7 @@ class FileController(Controller):
204 204
 
205 205
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
206 206
     @require_workspace_role(UserRoleInWorkspace.READER)
207
-    @require_content_types([file_type])
207
+    @require_content_types([FILE_TYPE])
208 208
     @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
209 209
     @hapic.input_path(WorkspaceAndContentRevisionIdPathSchema())
210 210
     @hapic.input_query(PageQuerySchema())
@@ -239,7 +239,7 @@ class FileController(Controller):
239 239
     # jpg
240 240
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
241 241
     @require_workspace_role(UserRoleInWorkspace.READER)
242
-    @require_content_types([file_type])
242
+    @require_content_types([FILE_TYPE])
243 243
     @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
244 244
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
245 245
     @hapic.input_query(PageQuerySchema())
@@ -272,7 +272,7 @@ class FileController(Controller):
272 272
 
273 273
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
274 274
     @require_workspace_role(UserRoleInWorkspace.READER)
275
-    @require_content_types([file_type])
275
+    @require_content_types([FILE_TYPE])
276 276
     @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
277 277
     @hapic.handle_exception(PreviewDimNotAllowed, HTTPStatus.BAD_REQUEST)
278 278
     @hapic.input_query(PageQuerySchema())
@@ -305,7 +305,7 @@ class FileController(Controller):
305 305
 
306 306
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
307 307
     @require_workspace_role(UserRoleInWorkspace.READER)
308
-    @require_content_types([file_type])
308
+    @require_content_types([FILE_TYPE])
309 309
     @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
310 310
     @hapic.handle_exception(PreviewDimNotAllowed, HTTPStatus.BAD_REQUEST)
311 311
     @hapic.input_path(RevisionPreviewSizedPathSchema())
@@ -342,7 +342,7 @@ class FileController(Controller):
342 342
 
343 343
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
344 344
     @require_workspace_role(UserRoleInWorkspace.READER)
345
-    @require_content_types([file_type])
345
+    @require_content_types([FILE_TYPE])
346 346
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
347 347
     @hapic.output_body(AllowedJpgPreviewDimSchema())
348 348
     def allowed_dim_preview_jpg(self, context, request: TracimRequest, hapic_data=None):  # nopep8
@@ -363,7 +363,7 @@ class FileController(Controller):
363 363
     # File infos
364 364
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
365 365
     @require_workspace_role(UserRoleInWorkspace.READER)
366
-    @require_content_types([file_type])
366
+    @require_content_types([FILE_TYPE])
367 367
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
368 368
     @hapic.output_body(FileContentSchema())
369 369
     def get_file_infos(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
@@ -387,7 +387,7 @@ class FileController(Controller):
387 387
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
388 388
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
389 389
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
390
-    @require_content_types([file_type])
390
+    @require_content_types([FILE_TYPE])
391 391
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
392 392
     @hapic.input_body(FileContentModifySchema())
393 393
     @hapic.output_body(FileContentSchema())
@@ -423,7 +423,7 @@ class FileController(Controller):
423 423
 
424 424
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
425 425
     @require_workspace_role(UserRoleInWorkspace.READER)
426
-    @require_content_types([file_type])
426
+    @require_content_types([FILE_TYPE])
427 427
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
428 428
     @hapic.output_body(FileRevisionSchema(many=True))
429 429
     def get_file_revisions(
@@ -456,7 +456,7 @@ class FileController(Controller):
456 456
     @hapic.with_api_doc(tags=[SWAGGER_TAG__FILE_ENDPOINTS])
457 457
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
458 458
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
459
-    @require_content_types([file_type])
459
+    @require_content_types([FILE_TYPE])
460 460
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
461 461
     @hapic.input_body(SetContentStatusSchema())
462 462
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8

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

@@ -25,8 +25,8 @@ 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 CONTENT_TYPES
29
-from tracim_backend.models.contents import folder_type
28
+from tracim_backend.app_models.contents import CONTENT_TYPES
29
+from tracim_backend.app_models.contents import FOLDER_TYPE
30 30
 from tracim_backend.models.revision_protection import new_revision
31 31
 
32 32
 SWAGGER_TAG__Folders_ENDPOINTS = 'Folders'
@@ -36,7 +36,7 @@ class FolderController(Controller):
36 36
 
37 37
     @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
38 38
     @require_workspace_role(UserRoleInWorkspace.READER)
39
-    @require_content_types([folder_type])
39
+    @require_content_types([FOLDER_TYPE])
40 40
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
41 41
     @hapic.output_body(TextBasedContentSchema())
42 42
     def get_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
@@ -60,7 +60,7 @@ class FolderController(Controller):
60 60
     @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
61 61
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
62 62
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
63
-    @require_content_types([folder_type])
63
+    @require_content_types([FOLDER_TYPE])
64 64
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
65 65
     @hapic.input_body(FolderContentModifySchema())
66 66
     @hapic.output_body(TextBasedContentSchema())
@@ -100,7 +100,7 @@ class FolderController(Controller):
100 100
 
101 101
     @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
102 102
     @require_workspace_role(UserRoleInWorkspace.READER)
103
-    @require_content_types([folder_type])
103
+    @require_content_types([FOLDER_TYPE])
104 104
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
105 105
     @hapic.output_body(TextBasedRevisionSchema(many=True))
106 106
     def get_folder_revisions(
@@ -132,7 +132,7 @@ class FolderController(Controller):
132 132
 
133 133
     @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
134 134
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
135
-    @require_content_types([folder_type])
135
+    @require_content_types([FOLDER_TYPE])
136 136
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
137 137
     @hapic.input_body(SetContentStatusSchema())
138 138
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8

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

@@ -11,7 +11,7 @@ try:  # Python 3.5+
11 11
 except ImportError:
12 12
     from http import client as HTTPStatus
13 13
 
14
-from tracim_backend import TracimRequest
14
+from tracim_backend.lib.utils.request import TracimRequest
15 15
 from tracim_backend.extensions import hapic
16 16
 from tracim_backend.lib.core.content import ContentApi
17 17
 from tracim_backend.views.controllers import Controller
@@ -26,8 +26,8 @@ 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 CONTENT_TYPES
30
-from tracim_backend.models.contents import html_documents_type
29
+from tracim_backend.app_models.contents import CONTENT_TYPES
30
+from tracim_backend.app_models.contents import HTML_DOCUMENTS_TYPE
31 31
 from tracim_backend.models.revision_protection import new_revision
32 32
 
33 33
 SWAGGER_TAG__HTML_DOCUMENT_ENDPOINTS = 'HTML documents'
@@ -37,7 +37,7 @@ class HTMLDocumentController(Controller):
37 37
 
38 38
     @hapic.with_api_doc(tags=[SWAGGER_TAG__HTML_DOCUMENT_ENDPOINTS])
39 39
     @require_workspace_role(UserRoleInWorkspace.READER)
40
-    @require_content_types([html_documents_type])
40
+    @require_content_types([HTML_DOCUMENTS_TYPE])
41 41
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
42 42
     @hapic.output_body(TextBasedContentSchema())
43 43
     def get_html_document(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
@@ -61,7 +61,7 @@ class HTMLDocumentController(Controller):
61 61
     @hapic.with_api_doc(tags=[SWAGGER_TAG__HTML_DOCUMENT_ENDPOINTS])
62 62
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
63 63
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
64
-    @require_content_types([html_documents_type])
64
+    @require_content_types([HTML_DOCUMENTS_TYPE])
65 65
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
66 66
     @hapic.input_body(TextBasedContentModifySchema())
67 67
     @hapic.output_body(TextBasedContentSchema())
@@ -97,7 +97,7 @@ class HTMLDocumentController(Controller):
97 97
 
98 98
     @hapic.with_api_doc(tags=[SWAGGER_TAG__HTML_DOCUMENT_ENDPOINTS])
99 99
     @require_workspace_role(UserRoleInWorkspace.READER)
100
-    @require_content_types([html_documents_type])
100
+    @require_content_types([HTML_DOCUMENTS_TYPE])
101 101
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
102 102
     @hapic.output_body(TextBasedRevisionSchema(many=True))
103 103
     def get_html_document_revisions(
@@ -129,7 +129,7 @@ class HTMLDocumentController(Controller):
129 129
 
130 130
     @hapic.with_api_doc(tags=[SWAGGER_TAG__HTML_DOCUMENT_ENDPOINTS])
131 131
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
132
-    @require_content_types([html_documents_type])
132
+    @require_content_types([HTML_DOCUMENTS_TYPE])
133 133
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
134 134
     @hapic.input_body(SetContentStatusSchema())
135 135
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8

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

@@ -10,7 +10,7 @@ try:  # Python 3.5+
10 10
 except ImportError:
11 11
     from http import client as HTTPStatus
12 12
 
13
-from tracim_backend import TracimRequest
13
+from tracim_backend.lib.utils.request import TracimRequest
14 14
 from tracim_backend.extensions import hapic
15 15
 from tracim_backend.lib.core.content import ContentApi
16 16
 from tracim_backend.views.controllers import Controller
@@ -25,8 +25,8 @@ 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 CONTENT_TYPES
29
-from tracim_backend.models.contents import thread_type
28
+from tracim_backend.app_models.contents import CONTENT_TYPES
29
+from tracim_backend.app_models.contents import THREAD_TYPE
30 30
 from tracim_backend.models.revision_protection import new_revision
31 31
 
32 32
 SWAGGER_TAG__THREAD_ENDPOINTS = 'Threads'
@@ -36,7 +36,7 @@ class ThreadController(Controller):
36 36
 
37 37
     @hapic.with_api_doc(tags=[SWAGGER_TAG__THREAD_ENDPOINTS])
38 38
     @require_workspace_role(UserRoleInWorkspace.READER)
39
-    @require_content_types([thread_type])
39
+    @require_content_types([THREAD_TYPE])
40 40
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
41 41
     @hapic.output_body(TextBasedContentSchema())
42 42
     def get_thread(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
@@ -60,7 +60,7 @@ class ThreadController(Controller):
60 60
     @hapic.with_api_doc(tags=[SWAGGER_TAG__THREAD_ENDPOINTS])
61 61
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
62 62
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
63
-    @require_content_types([thread_type])
63
+    @require_content_types([THREAD_TYPE])
64 64
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
65 65
     @hapic.input_body(TextBasedContentModifySchema())
66 66
     @hapic.output_body(TextBasedContentSchema())
@@ -96,7 +96,7 @@ class ThreadController(Controller):
96 96
 
97 97
     @hapic.with_api_doc(tags=[SWAGGER_TAG__THREAD_ENDPOINTS])
98 98
     @require_workspace_role(UserRoleInWorkspace.READER)
99
-    @require_content_types([thread_type])
99
+    @require_content_types([THREAD_TYPE])
100 100
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
101 101
     @hapic.output_body(TextBasedRevisionSchema(many=True))
102 102
     def get_thread_revisions(
@@ -128,7 +128,7 @@ class ThreadController(Controller):
128 128
 
129 129
     @hapic.with_api_doc(tags=[SWAGGER_TAG__THREAD_ENDPOINTS])
130 130
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
131
-    @require_content_types([thread_type])
131
+    @require_content_types([THREAD_TYPE])
132 132
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
133 133
     @hapic.input_body(SetContentStatusSchema())
134 134
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8

+ 12 - 10
backend/tracim_backend/views/core_api/schemas.py View File

@@ -7,11 +7,12 @@ from marshmallow.validate import Range
7 7
 
8 8
 from tracim_backend.lib.utils.utils import DATETIME_FORMAT
9 9
 from tracim_backend.models.auth import Profile
10
+
11
+from tracim_backend.app_models.contents import GlobalStatus
12
+from tracim_backend.app_models.contents import CONTENT_STATUS
13
+from tracim_backend.app_models.contents import CONTENT_TYPES
14
+from tracim_backend.app_models.contents import open_status
10 15
 from tracim_backend.models.auth import Group
11
-from tracim_backend.models.contents import GlobalStatus
12
-from tracim_backend.models.contents import CONTENT_STATUS
13
-from tracim_backend.models.contents import CONTENT_TYPES
14
-from tracim_backend.models.contents import open_status
15 16
 from tracim_backend.models.context_models import ActiveContentFilter
16 17
 from tracim_backend.models.context_models import FolderContentUpdate
17 18
 from tracim_backend.models.context_models import AutocompleteQuery
@@ -41,6 +42,7 @@ from tracim_backend.models.context_models import ContentFilter
41 42
 from tracim_backend.models.context_models import LoginCredentials
42 43
 from tracim_backend.models.data import UserRoleInWorkspace
43 44
 from tracim_backend.models.data import ActionDescription
45
+from tracim_backend.app_models.validator import all_content_types_validator
44 46
 
45 47
 
46 48
 class UserDigestSchema(marshmallow.Schema):
@@ -390,7 +392,7 @@ class FilterContentQuerySchema(marshmallow.Schema):
390 392
     content_type = marshmallow.fields.String(
391 393
         example=CONTENT_TYPES.Any_SLUG,
392 394
         default=CONTENT_TYPES.Any_SLUG,
393
-        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug())
395
+        validate=all_content_types_validator
394 396
     )
395 397
 
396 398
     @post_load
@@ -641,7 +643,7 @@ class StatusSchema(marshmallow.Schema):
641 643
 class ContentTypeSchema(marshmallow.Schema):
642 644
     slug = marshmallow.fields.String(
643 645
         example='pagehtml',
644
-        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug()),
646
+        validate=all_content_types_validator,
645 647
     )
646 648
     fa_icon = marshmallow.fields.String(
647 649
         example='fa-file-text-o',
@@ -695,7 +697,7 @@ class ContentCreationSchema(marshmallow.Schema):
695 697
     )
696 698
     content_type = marshmallow.fields.String(
697 699
         example='html-document',
698
-        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug()),
700
+        validate=all_content_types_validator,
699 701
     )
700 702
     parent_id = marshmallow.fields.Integer(
701 703
         example=35,
@@ -730,12 +732,12 @@ class ContentDigestSchema(marshmallow.Schema):
730 732
     label = marshmallow.fields.Str(example='Intervention Report 12')
731 733
     content_type = marshmallow.fields.Str(
732 734
         example='html-document',
733
-        validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug()),
735
+        validate=all_content_types_validator,
734 736
     )
735 737
     sub_content_types = marshmallow.fields.List(
736 738
         marshmallow.fields.String(
737 739
             example='html-content',
738
-            validate=OneOf(CONTENT_TYPES.extended_endpoint_allowed_types_slug())
740
+            validate=all_content_types_validator
739 741
         ),
740 742
         description='list of content types allowed as sub contents. '
741 743
                     'This field is required for folder contents, '
@@ -885,7 +887,7 @@ class FolderContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbstra
885 887
     sub_content_types = marshmallow.fields.List(
886 888
         marshmallow.fields.String(
887 889
             example='html-document',
888
-            validate=OneOf(CONTENT_TYPES.extended_endpoint_allowed_types_slug())
890
+            validate=all_content_types_validator,
889 891
         ),
890 892
         description='list of content types allowed as sub contents. '
891 893
                     'This field is required for folder contents, '

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

@@ -5,7 +5,7 @@ try:  # Python 3.5+
5 5
 except ImportError:
6 6
     from http import client as HTTPStatus
7 7
 
8
-from tracim_backend import TracimRequest
8
+from tracim_backend.lib.utils.request import TracimRequest
9 9
 from tracim_backend.extensions import hapic
10 10
 from tracim_backend.lib.core.user import UserApi
11 11
 from tracim_backend.views.controllers import Controller

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

@@ -2,18 +2,19 @@
2 2
 from pyramid.config import Configurator
3 3
 from tracim_backend.exceptions import NotAuthenticated
4 4
 from tracim_backend.exceptions import InsufficientUserProfile
5
+from tracim_backend.lib.core.application import ApplicationApi
5 6
 from tracim_backend.lib.utils.authorization import require_profile
6 7
 from tracim_backend.models import Group
7
-from tracim_backend.models.applications import applications
8
-from tracim_backend.models.contents import CONTENT_TYPES
8
+from tracim_backend.app_models.contents import CONTENT_TYPES
9 9
 
10 10
 try:  # Python 3.5+
11 11
     from http import HTTPStatus
12 12
 except ImportError:
13 13
     from http import client as HTTPStatus
14 14
 
15
-from tracim_backend import TracimRequest
15
+from tracim_backend.lib.utils.request import TracimRequest
16 16
 from tracim_backend.extensions import hapic
17
+from tracim_backend.extensions import app_list
17 18
 from tracim_backend.views.controllers import Controller
18 19
 from tracim_backend.views.core_api.schemas import ApplicationSchema
19 20
 from tracim_backend.views.core_api.schemas import ContentTypeSchema
@@ -30,7 +31,11 @@ class SystemController(Controller):
30 31
         """
31 32
         Get list of alls applications installed in this tracim instance.
32 33
         """
33
-        return applications
34
+        app_config = request.registry.settings['CFG']
35
+        app_api = ApplicationApi(
36
+            app_list=app_list,
37
+        )
38
+        return app_api.get_all()
34 39
 
35 40
     @hapic.with_api_doc(tags=[SWAGGER_TAG_SYSTEM_ENDPOINTS])
36 41
     @require_profile(Group.TIM_USER)

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

@@ -7,7 +7,7 @@ except ImportError:
7 7
     from http import client as HTTPStatus
8 8
 
9 9
 from tracim_backend import hapic
10
-from tracim_backend import TracimRequest
10
+from tracim_backend.lib.utils.request import TracimRequest
11 11
 from tracim_backend.models import Group
12 12
 from tracim_backend.lib.core.group import GroupApi
13 13
 from tracim_backend.lib.core.user import UserApi
@@ -36,7 +36,7 @@ from tracim_backend.views.core_api.schemas import UserWorkspaceAndContentIdPathS
36 36
 from tracim_backend.views.core_api.schemas import ContentDigestSchema
37 37
 from tracim_backend.views.core_api.schemas import ActiveContentFilterQuerySchema
38 38
 from tracim_backend.views.core_api.schemas import WorkspaceDigestSchema
39
-from tracim_backend.models.contents import CONTENT_TYPES
39
+from tracim_backend.app_models.contents import CONTENT_TYPES
40 40
 
41 41
 SWAGGER_TAG__USER_ENDPOINTS = 'Users'
42 42
 

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

@@ -12,8 +12,8 @@ except ImportError:
12 12
     from http import client as HTTPStatus
13 13
 
14 14
 from tracim_backend import hapic
15
+from tracim_backend.lib.utils.request import TracimRequest
15 16
 from tracim_backend import BASE_API_V2
16
-from tracim_backend import TracimRequest
17 17
 from tracim_backend.lib.core.workspace import WorkspaceApi
18 18
 from tracim_backend.lib.core.content import ContentApi
19 19
 from tracim_backend.lib.core.userworkspace import RoleApi
@@ -52,7 +52,7 @@ from tracim_backend.views.core_api.schemas import ContentDigestSchema
52 52
 from tracim_backend.views.core_api.schemas import WorkspaceSchema
53 53
 from tracim_backend.views.core_api.schemas import WorkspaceIdPathSchema
54 54
 from tracim_backend.views.core_api.schemas import WorkspaceMemberSchema
55
-from tracim_backend.models.contents import CONTENT_TYPES
55
+from tracim_backend.app_models.contents import CONTENT_TYPES
56 56
 from tracim_backend.models.revision_protection import new_revision
57 57
 
58 58
 SWAGGER_TAG_WORKSPACE_ENDPOINTS = 'Workspaces'

+ 9 - 3
backend/tracim_backend/views/frontend.py View File

@@ -2,12 +2,13 @@ import os
2 2
 
3 3
 from pyramid.renderers import render_to_response
4 4
 from pyramid.config import Configurator
5
+from tracim_backend.extensions import app_list
5 6
 from tracim_backend.exceptions import PageNotFound
6
-from tracim_backend.models.applications import applications
7
+from tracim_backend.lib.core.application import ApplicationApi
8
+from tracim_backend.lib.utils.utils import Color
7 9
 from tracim_backend.views import BASE_API_V2
8 10
 from tracim_backend.lib.utils.request import TracimRequest
9 11
 from tracim_backend.views.controllers import Controller
10
-import spectra
11 12
 
12 13
 INDEX_PAGE_NAME = 'index.mak'
13 14
 APP_FRONTEND_PATH = 'app/{minislug}.app.js'
@@ -34,6 +35,10 @@ class FrontendController(Controller):
34 35
         app_config = request.registry.settings['CFG']
35 36
         # TODO - G.M - 2018-08-07 - Refactor autogen valid app list for frontend
36 37
         frontend_apps = []
38
+        app_api = ApplicationApi(
39
+            app_list=app_list,
40
+        )
41
+        applications = app_api.get_all()
37 42
         for app in applications:
38 43
             app_frontend_path = APP_FRONTEND_PATH.replace('{minislug}',
39 44
                                                           app.minislug)  # nopep8
@@ -41,11 +46,12 @@ class FrontendController(Controller):
41 46
                                     app_frontend_path)  # nopep8
42 47
             if os.path.exists(app_path):
43 48
                 frontend_apps.append(app)
49
+
44 50
         return render_to_response(
45 51
             self._get_index_file_path(),
46 52
             {
47 53
                 'colors': {
48
-                    'primary': spectra.html('#7d4e24'),
54
+                    'primary': Color(app_config.APPS_COLORS['primary']),
49 55
                 },
50 56
                 'applications': frontend_apps,
51 57
             }

+ 10 - 0
backend/wsgidav-test.conf View File

@@ -0,0 +1,10 @@
1
+host  = "0.0.0.0"
2
+port = 3030
3
+show_history = True
4
+show_deleted = True
5
+show_archived = True
6
+manager_locks = True
7
+root_path = ''
8
+acceptbasic = True
9
+acceptdigest = False
10
+defaultdigest = False

+ 5 - 0
backend_lib.sh View File

@@ -37,6 +37,11 @@ function setup_config_file {
37 37
        log "generate missing wsgidav.conf ..."
38 38
        cp wsgidav.conf.sample wsgidav.conf
39 39
     fi
40
+
41
+    if [ ! -f ../color.json ]; then
42
+       log "generate missing color.json ..."
43
+       cp ../color.json.sample ../color.json
44
+    fi
40 45
 }
41 46
 
42 47
 function setup_db {

+ 3 - 3
color.json.sample View File

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "primary": "#7d4e24",
3
-  "html-document": "#3f52e3",
4
-  "thread": "#ad4cf9",
5
-  "file": "#ff9900"
3
+  "contents/html-document": "#3f52e3",
4
+  "contents/thread": "#ad4cf9",
5
+  "contents/file": "#ff9900"
6 6
 }

+ 18 - 18
frontend/dist/index.mak View File

@@ -21,37 +21,37 @@
21 21
         param = 'color'
22 22
         color_change_value = 15
23 23
       %>
24
-      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
25
-      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
26
-      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
24
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.normal}; }
25
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken}; }
26
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.lighten}; }
27 27
       <% html_class = '.primaryColorFont{state}Hover:hover' %>
28
-      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
29
-      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
30
-      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
28
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.normal}; }
29
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken}; }
30
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.lighten}; }
31 31
 
32 32
       <%
33 33
         html_class = '.primaryColorBg{state}'
34 34
         param = 'background-color'
35 35
       %>
36
-      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
37
-      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
38
-      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
36
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.normal}; }
37
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken}; }
38
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.lighten}; }
39 39
       <% html_class = '.primaryColorBg{state}Hover:hover'%>
40
-      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
41
-      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
42
-      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
40
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.normal}; }
41
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken}; }
42
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.lighten}; }
43 43
 
44 44
       <%
45 45
         param = 'border-color'
46 46
         html_class = '.primaryColorBorder{state}'
47 47
       %>
48
-      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
49
-      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
50
-      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
48
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.normal}; }
49
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken}; }
50
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.lighten}; }
51 51
       <% html_class = '.primaryColorBorder{state}Hover:hover' %>
52
-      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
53
-      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
54
-      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
52
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.normal}; }
53
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken}; }
54
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.lighten}; }
55 55
     </style>
56 56
   </head>
57 57