Browse Source

Merge pull request #83 from tracim/feature/583_Api_for_generic_content

Bastien Sevajol 6 years ago
parent
commit
b834be7ee8
No account linked to committer's email

+ 1 - 1
README.md View File

@@ -5,7 +5,7 @@
5 5
 tracim_backend
6 6
 ==============
7 7
 
8
-This code is Work in progress. Not usable at all for production.
8
+This code is "work in progress". Not usable at all for production.
9 9
 
10 10
 Backend source code of tracim v2, using Pyramid Framework.
11 11
 

+ 9 - 1
tracim/exceptions.py View File

@@ -97,9 +97,17 @@ class GroupDoesNotExist(TracimError):
97 97
     pass
98 98
 
99 99
 
100
+class ContentStatusNotExist(TracimError):
101
+    pass
102
+
103
+
104
+class ContentTypeNotExist(TracimError):
105
+    pass
106
+
107
+
100 108
 class UserDoesNotExist(TracimException):
101 109
     pass
102 110
 
103 111
 
104 112
 class UserNotFoundInTracimRequest(TracimException):
105
-    pass
113
+    pass

+ 6 - 1
tracim/fixtures/__init__.py View File

@@ -1,3 +1,4 @@
1
+import copy
1 2
 import transaction
2 3
 
3 4
 
@@ -23,7 +24,11 @@ class FixturesLoader(object):
23 24
         loaded = [] if loaded is None else loaded
24 25
         self._loaded = loaded
25 26
         self._session = session
26
-        self._config = config
27
+        # FIXME - G.M - 2018-06-169 - Fixture failed with email_notification
28
+        # activated, disable it there now. Find better way to fix this
29
+        # later
30
+        self._config = copy.copy(config)
31
+        self._config.EMAIL_NOTIFICATION_ACTIVATED = False
27 32
 
28 33
     def loads(self, fixtures_classes):
29 34
         for fixture_class in fixtures_classes:

+ 141 - 66
tracim/fixtures/content.py View File

@@ -1,5 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from depot.io.utils import FileIntent
3
+import transaction
3 4
 
4 5
 from tracim import models
5 6
 from tracim.fixtures import Fixture
@@ -9,6 +10,7 @@ from tracim.lib.core.userworkspace import RoleApi
9 10
 from tracim.lib.core.workspace import WorkspaceApi
10 11
 from tracim.models.data import ContentType
11 12
 from tracim.models.data import UserRoleInWorkspace
13
+from tracim.models.revision_protection import new_revision
12 14
 
13 15
 
14 16
 class Content(Fixture):
@@ -43,133 +45,206 @@ class Content(Fixture):
43 45
         )
44 46
 
45 47
         # Workspaces
46
-        w1 = admin_workspace_api.create_workspace(
47
-            'w1',
48
-            description='This is a workspace',
49
-            save_now=True
48
+        business_workspace = admin_workspace_api.create_workspace(
49
+            'Business',
50
+            description='All importants documents',
51
+            save_now=True,
50 52
         )
51
-        w2 = bob_workspace_api.create_workspace(
52
-            'w2',
53
-            description='A great workspace',
54
-            save_now=True
53
+        recipe_workspace = admin_workspace_api.create_workspace(
54
+            'Recipes',
55
+            description='Our best recipes',
56
+            save_now=True,
55 57
         )
56
-        w3 = admin_workspace_api.create_workspace(
57
-            'w3',
58
-            description='Just another workspace',
59
-            save_now=True
58
+        other_workspace = bob_workspace_api.create_workspace(
59
+            'Others',
60
+            description='Other Workspace',
61
+            save_now=True,
60 62
         )
61 63
 
62 64
         # Workspaces roles
63 65
         role_api.create_one(
64 66
             user=bob,
65
-            workspace=w1,
67
+            workspace=recipe_workspace,
66 68
             role_level=UserRoleInWorkspace.CONTENT_MANAGER,
67 69
             with_notif=False,
68 70
         )
69
-
70 71
         # Folders
71
-        w1f1 = content_api.create(
72
+
73
+        tool_workspace = content_api.create(
72 74
             content_type=ContentType.Folder,
73
-            workspace=w1,
74
-            label='w1f1',
75
+            workspace=business_workspace,
76
+            label='Tools',
75 77
             do_save=True,
76 78
             do_notify=False,
77 79
         )
78
-        w1f2 = content_api.create(
80
+        menu_workspace = content_api.create(
79 81
             content_type=ContentType.Folder,
80
-            workspace=w1,
81
-            label='w1f2',
82
+            workspace=business_workspace,
83
+            label='Menus',
82 84
             do_save=True,
83 85
             do_notify=False,
84 86
         )
85 87
 
86
-        w2f1 = content_api.create(
88
+        dessert_folder = content_api.create(
87 89
             content_type=ContentType.Folder,
88
-            workspace=w2,
89
-            label='w2f1',
90
+            workspace=recipe_workspace,
91
+            label='Desserts',
90 92
             do_save=True,
91 93
             do_notify=False,
92 94
         )
93
-        w2f2 = content_api.create(
95
+        salads_folder = content_api.create(
94 96
             content_type=ContentType.Folder,
95
-            workspace=w2,
96
-            label='w2f2',
97
+            workspace=recipe_workspace,
98
+            label='Salads',
97 99
             do_save=True,
98 100
             do_notify=False,
99 101
         )
100
-
101
-        w3f1 = content_api.create(
102
+        other_folder = content_api.create(
102 103
             content_type=ContentType.Folder,
103
-            workspace=w3,
104
-            label='w3f3',
104
+            workspace=other_workspace,
105
+            label='Infos',
105 106
             do_save=True,
106 107
             do_notify=False,
107 108
         )
108 109
 
109 110
         # Pages, threads, ..
110
-        w1f1p1 = content_api.create(
111
+        tiramisu_page = content_api.create(
111 112
             content_type=ContentType.Page,
112
-            workspace=w1,
113
-            parent=w1f1,
114
-            label='w1f1p1',
113
+            workspace=recipe_workspace,
114
+            parent=dessert_folder,
115
+            label='Tiramisu Recipe',
115 116
             do_save=True,
116 117
             do_notify=False,
117 118
         )
118
-        w1f1t1 = content_api.create(
119
+        best_cake_thread = content_api.create(
119 120
             content_type=ContentType.Thread,
120
-            workspace=w1,
121
-            parent=w1f1,
122
-            label='w1f1t1',
121
+            workspace=recipe_workspace,
122
+            parent=dessert_folder,
123
+            label='Best Cakes ?',
123 124
             do_save=False,
124 125
             do_notify=False,
125 126
         )
126
-        w1f1t1.description = 'w1f1t1 description'
127
-        self._session.add(w1f1t1)
128
-        w1f1d1_txt = content_api.create(
127
+        best_cake_thread.description = 'What is the best cake ?'
128
+        self._session.add(best_cake_thread)
129
+        apple_pie_recipe = content_api.create(
129 130
             content_type=ContentType.File,
130
-            workspace=w1,
131
-            parent=w1f1,
132
-            label='w1f1d1',
131
+            workspace=recipe_workspace,
132
+            parent=dessert_folder,
133
+            label='Apple_Pie',
133 134
             do_save=False,
134 135
             do_notify=False,
135 136
         )
136
-        w1f1d1_txt.file_extension = '.txt'
137
-        w1f1d1_txt.depot_file = FileIntent(
138
-            b'w1f1d1 content',
139
-            'w1f1d1.txt',
137
+        apple_pie_recipe.file_extension = '.txt'
138
+        apple_pie_recipe.depot_file = FileIntent(
139
+            b'Apple pie Recipe',
140
+            'apple_Pie.txt',
140 141
             'text/plain',
141 142
         )
142
-        self._session.add(w1f1d1_txt)
143
-        w1f1d2_html = content_api.create(
143
+        self._session.add(apple_pie_recipe)
144
+        Brownie_recipe = content_api.create(
144 145
             content_type=ContentType.File,
145
-            workspace=w1,
146
-            parent=w1f1,
147
-            label='w1f1d2',
146
+            workspace=recipe_workspace,
147
+            parent=dessert_folder,
148
+            label='Brownie Recipe',
148 149
             do_save=False,
149 150
             do_notify=False,
150 151
         )
151
-        w1f1d2_html.file_extension = '.html'
152
-        w1f1d2_html.depot_file = FileIntent(
153
-            b'<p>w1f1d2 content</p>',
154
-            'w1f1d2.html',
152
+        Brownie_recipe.file_extension = '.html'
153
+        Brownie_recipe.depot_file = FileIntent(
154
+            b'<p>Brownie Recipe</p>',
155
+            'brownie_recipe.html',
155 156
             'text/html',
156 157
         )
157
-        self._session.add(w1f1d2_html)
158
-        w1f1f1 = content_api.create(
158
+        self._session.add(Brownie_recipe)
159
+        fruits_desserts_folder = content_api.create(
159 160
             content_type=ContentType.Folder,
160
-            workspace=w1,
161
-            label='w1f1f1',
162
-            parent=w1f1,
161
+            workspace=recipe_workspace,
162
+            label='Fruits Desserts',
163
+            parent=dessert_folder,
164
+            do_save=True,
165
+        )
166
+
167
+        menu_page = content_api.create(
168
+            content_type=ContentType.Page,
169
+            workspace=business_workspace,
170
+            parent=menu_workspace,
171
+            label='Current Menu',
172
+            do_save=True,
173
+        )
174
+
175
+        new_fruit_salad = content_api.create(
176
+            content_type=ContentType.Page,
177
+            workspace=recipe_workspace,
178
+            parent=fruits_desserts_folder,
179
+            label='New Fruit Salad',
180
+            do_save=True,
181
+        )
182
+        old_fruit_salad = content_api.create(
183
+            content_type=ContentType.Page,
184
+            workspace=recipe_workspace,
185
+            parent=fruits_desserts_folder,
186
+            label='Fruit Salad',
163 187
             do_save=True,
164 188
             do_notify=False,
165 189
         )
190
+        with new_revision(
191
+                session=self._session,
192
+                tm=transaction.manager,
193
+                content=old_fruit_salad,
194
+        ):
195
+            content_api.archive(old_fruit_salad)
196
+        content_api.save(old_fruit_salad)
166 197
 
167
-        w2f1p1 = content_api.create(
198
+        bad_fruit_salad = content_api.create(
168 199
             content_type=ContentType.Page,
169
-            workspace=w2,
170
-            parent=w2f1,
171
-            label='w2f1p1',
200
+            workspace=recipe_workspace,
201
+            parent=fruits_desserts_folder,
202
+            label='Bad Fruit Salad',
172 203
             do_save=True,
173 204
             do_notify=False,
174 205
         )
206
+        with new_revision(
207
+                session=self._session,
208
+                tm=transaction.manager,
209
+                content=bad_fruit_salad,
210
+        ):
211
+            content_api.delete(bad_fruit_salad)
212
+        content_api.save(bad_fruit_salad)
213
+
214
+        # File at the root for test
215
+        new_fruit_salad = content_api.create(
216
+            content_type=ContentType.Page,
217
+            workspace=other_workspace,
218
+            label='New Fruit Salad',
219
+            do_save=True,
220
+        )
221
+        old_fruit_salad = content_api.create(
222
+            content_type=ContentType.Page,
223
+            workspace=other_workspace,
224
+            label='Fruit Salad',
225
+            do_save=True,
226
+        )
227
+        with new_revision(
228
+                session=self._session,
229
+                tm=transaction.manager,
230
+                content=old_fruit_salad,
231
+        ):
232
+            content_api.archive(old_fruit_salad)
233
+        content_api.save(old_fruit_salad)
234
+
235
+        bad_fruit_salad = content_api.create(
236
+            content_type=ContentType.Page,
237
+            workspace=other_workspace,
238
+            label='Bad Fruit Salad',
239
+            do_save=True,
240
+        )
241
+        with new_revision(
242
+                session=self._session,
243
+                tm=transaction.manager,
244
+                content=bad_fruit_salad,
245
+        ):
246
+            content_api.delete(bad_fruit_salad)
247
+        content_api.save(bad_fruit_salad)
248
+
249
+
175 250
         self._session.flush()

+ 23 - 13
tracim/lib/core/content.py View File

@@ -1,25 +1,17 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from contextlib import contextmanager
3
-
4 3
 import os
5
-
4
+import datetime
5
+import re
6
+import typing
6 7
 from operator import itemgetter
8
+from operator import not_
7 9
 
8 10
 import transaction
9 11
 from sqlalchemy import func
10 12
 from sqlalchemy.orm import Query
11
-
12
-__author__ = 'damien'
13
-
14
-import datetime
15
-import re
16
-import typing
17
-
18
-from tracim.lib.utils.translation import fake_translator as _
19
-
20 13
 from depot.manager import DepotManager
21 14
 from depot.io.utils import FileIntent
22
-
23 15
 import sqlalchemy
24 16
 from sqlalchemy.orm import aliased
25 17
 from sqlalchemy.orm import joinedload
@@ -29,6 +21,7 @@ from sqlalchemy import desc
29 21
 from sqlalchemy import distinct
30 22
 from sqlalchemy import or_
31 23
 from sqlalchemy.sql.elements import and_
24
+
32 25
 from tracim.lib.utils.utils import cmp_to_key
33 26
 from tracim.lib.core.notifications import NotifierFactory
34 27
 from tracim.exceptions import SameValueError
@@ -44,6 +37,11 @@ from tracim.models.data import NodeTreeItem
44 37
 from tracim.models.data import RevisionReadStatus
45 38
 from tracim.models.data import UserRoleInWorkspace
46 39
 from tracim.models.data import Workspace
40
+from tracim.lib.utils.translation import fake_translator as _
41
+from tracim.models.context_models import ContentInContext
42
+
43
+
44
+__author__ = 'damien'
47 45
 
48 46
 
49 47
 def compare_content_for_sorting_by_type_and_name(
@@ -103,6 +101,7 @@ class ContentApi(object):
103 101
         ContentType.Comment,
104 102
         ContentType.Thread,
105 103
         ContentType.Page,
104
+        ContentType.MarkdownPage,
106 105
     )
107 106
 
108 107
     def __init__(
@@ -113,6 +112,7 @@ class ContentApi(object):
113 112
             show_archived: bool = False,
114 113
             show_deleted: bool = False,
115 114
             show_temporary: bool = False,
115
+            show_active: bool = True,
116 116
             all_content_in_treeview: bool = True,
117 117
             force_show_all_types: bool = False,
118 118
             disable_user_workspaces_filter: bool = False,
@@ -124,6 +124,7 @@ class ContentApi(object):
124 124
         self._show_archived = show_archived
125 125
         self._show_deleted = show_deleted
126 126
         self._show_temporary = show_temporary
127
+        self._show_active = show_active
127 128
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
128 129
         self._force_show_all_types = force_show_all_types
129 130
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
@@ -156,6 +157,9 @@ class ContentApi(object):
156 157
             self._show_deleted = previous_show_deleted
157 158
             self._show_temporary = previous_show_temporary
158 159
 
160
+    def get_content_in_context(self, content: Content):
161
+        return ContentInContext(content, self._session, self._config)
162
+
159 163
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
160 164
         """
161 165
         Return the Content/ContentRevision query join condition
@@ -234,6 +238,11 @@ class ContentApi(object):
234 238
     def _base_query(self, workspace: Workspace=None) -> Query:
235 239
         result = self.__real_base_query(workspace)
236 240
 
241
+        if not self._show_active:
242
+            result = result.filter(or_(
243
+                Content.is_deleted==True,
244
+                Content.is_archived==True,
245
+            ))
237 246
         if not self._show_deleted:
238 247
             result = result.filter(Content.is_deleted==False)
239 248
 
@@ -686,8 +695,9 @@ class ContentApi(object):
686 695
 
687 696
         if parent_id:
688 697
             resultset = resultset.filter(Content.parent_id==parent_id)
689
-        if parent_id is False:
698
+        if parent_id == 0 or parent_id is False:
690 699
             resultset = resultset.filter(Content.parent_id == None)
700
+        # parent_id == None give all contents
691 701
 
692 702
         return resultset.all()
693 703
 

+ 248 - 0
tracim/models/contents.py View File

@@ -0,0 +1,248 @@
1
+# -*- coding: utf-8 -*-
2
+import typing
3
+from enum import Enum
4
+
5
+from tracim.exceptions import ContentStatusNotExist, ContentTypeNotExist
6
+from tracim.models.applications import htmlpage, _file, thread, markdownpluspage
7
+
8
+
9
+####
10
+# Content Status
11
+
12
+
13
+class GlobalStatus(Enum):
14
+    OPEN = 'open'
15
+    CLOSED = 'closed'
16
+
17
+
18
+class NewContentStatus(object):
19
+    """
20
+    Future ContentStatus object class
21
+    """
22
+    def __init__(
23
+            self,
24
+            slug: str,
25
+            global_status: str,
26
+            label: str,
27
+            fa_icon: str,
28
+            hexcolor: str,
29
+    ):
30
+        self.slug = slug
31
+        self.global_status = global_status
32
+        self.label = label
33
+        self.fa_icon = fa_icon
34
+        self.hexcolor = hexcolor
35
+
36
+
37
+open_status = NewContentStatus(
38
+    slug='open',
39
+    global_status=GlobalStatus.OPEN.value,
40
+    label='Open',
41
+    fa_icon='fa-square-o',
42
+    hexcolor='#000FF',
43
+)
44
+
45
+closed_validated_status = NewContentStatus(
46
+    slug='closed-validated',
47
+    global_status=GlobalStatus.CLOSED.value,
48
+    label='Validated',
49
+    fa_icon='fa-check-square-o',
50
+    hexcolor='#000FF',
51
+)
52
+
53
+closed_unvalidated_status = NewContentStatus(
54
+    slug='closed-unvalidated',
55
+    global_status=GlobalStatus.CLOSED.value,
56
+    label='Cancelled',
57
+    fa_icon='fa-close',
58
+    hexcolor='#000FF',
59
+)
60
+
61
+closed_deprecated_status = NewContentStatus(
62
+    slug='closed-deprecated',
63
+    global_status=GlobalStatus.CLOSED.value,
64
+    label='Deprecated',
65
+    fa_icon='fa-warning',
66
+    hexcolor='#000FF',
67
+)
68
+
69
+
70
+CONTENT_DEFAULT_STATUS = [
71
+    open_status,
72
+    closed_validated_status,
73
+    closed_unvalidated_status,
74
+    closed_deprecated_status,
75
+]
76
+
77
+
78
+class ContentStatusLegacy(NewContentStatus):
79
+    """
80
+    Temporary remplacement object for Legacy ContentStatus Object
81
+    """
82
+    OPEN = open_status.slug
83
+    CLOSED_VALIDATED = closed_validated_status.slug
84
+    CLOSED_UNVALIDATED = closed_unvalidated_status.slug
85
+    CLOSED_DEPRECATED = closed_deprecated_status.slug
86
+
87
+    def __init__(self, slug: str):
88
+        for status in CONTENT_DEFAULT_STATUS:
89
+            if slug == status.slug:
90
+                super(ContentStatusLegacy, self).__init__(
91
+                    slug=status.slug,
92
+                    global_status=status.global_status,
93
+                    label=status.label,
94
+                    fa_icon=status.fa_icon,
95
+                    hexcolor=status.hexcolor,
96
+                )
97
+                return
98
+        raise ContentStatusNotExist()
99
+
100
+    @classmethod
101
+    def all(cls, type='') -> ['NewContentStatus']:
102
+        return CONTENT_DEFAULT_STATUS
103
+
104
+    @classmethod
105
+    def allowed_values(cls):
106
+        return [status.slug for status in CONTENT_DEFAULT_STATUS]
107
+
108
+
109
+####
110
+# ContentType
111
+
112
+
113
+class NewContentType(object):
114
+    """
115
+    Future ContentType object class
116
+    """
117
+    def __init__(
118
+            self,
119
+            slug: str,
120
+            fa_icon: str,
121
+            hexcolor: str,
122
+            label: str,
123
+            creation_label: str,
124
+            available_statuses: typing.List[NewContentStatus],
125
+
126
+    ):
127
+        self.slug = slug
128
+        self.fa_icon = fa_icon
129
+        self.hexcolor = hexcolor
130
+        self.label = label
131
+        self.creation_label = creation_label
132
+        self.available_statuses = available_statuses
133
+
134
+
135
+thread_type = NewContentType(
136
+    slug='thread',
137
+    fa_icon=thread.fa_icon,
138
+    hexcolor=thread.hexcolor,
139
+    label='Thread',
140
+    creation_label='Discuss about a topic',
141
+    available_statuses=CONTENT_DEFAULT_STATUS,
142
+)
143
+
144
+file_type = NewContentType(
145
+    slug='file',
146
+    fa_icon=_file.fa_icon,
147
+    hexcolor=_file.hexcolor,
148
+    label='File',
149
+    creation_label='Upload a file',
150
+    available_statuses=CONTENT_DEFAULT_STATUS,
151
+)
152
+
153
+markdownpluspage_type = NewContentType(
154
+    slug='markdownpage',
155
+    fa_icon=markdownpluspage.fa_icon,
156
+    hexcolor=markdownpluspage.hexcolor,
157
+    label='Rich Markdown File',
158
+    creation_label='Create a Markdown document',
159
+    available_statuses=CONTENT_DEFAULT_STATUS,
160
+)
161
+
162
+htmlpage_type = NewContentType(
163
+    slug='page',
164
+    fa_icon=htmlpage.fa_icon,
165
+    hexcolor=htmlpage.hexcolor,
166
+    label='Text Document',
167
+    creation_label='Write a document',
168
+    available_statuses=CONTENT_DEFAULT_STATUS,
169
+)
170
+
171
+# TODO - G.M - 31-05-2018 - Set Better folder params
172
+folder_type = NewContentType(
173
+    slug='folder',
174
+    fa_icon=thread.fa_icon,
175
+    hexcolor=thread.hexcolor,
176
+    label='Folder',
177
+    creation_label='Create collection of any documents',
178
+    available_statuses=CONTENT_DEFAULT_STATUS,
179
+)
180
+
181
+CONTENT_DEFAULT_TYPE = [
182
+    thread_type,
183
+    file_type,
184
+    markdownpluspage_type,
185
+    htmlpage_type,
186
+    folder_type,
187
+]
188
+
189
+
190
+class ContentTypeLegacy(NewContentType):
191
+    """
192
+    Temporary remplacement object for Legacy ContentType Object
193
+    """
194
+
195
+    # special type
196
+    Any = 'any'
197
+    Folder = 'folder'
198
+    Event = 'event'
199
+    Comment = 'comment'
200
+
201
+    File = file_type.slug
202
+    Thread = thread_type.slug
203
+    Page = htmlpage_type.slug
204
+    MarkdownPage = markdownpluspage_type.slug
205
+
206
+    def __init__(self, slug: str):
207
+        for content_type in CONTENT_DEFAULT_TYPE:
208
+            if slug == content_type.slug:
209
+                super(ContentTypeLegacy, self).__init__(
210
+                    slug=content_type.slug,
211
+                    fa_icon=content_type.fa_icon,
212
+                    hexcolor=content_type.hexcolor,
213
+                    label=content_type.label,
214
+                    creation_label=content_type.creation_label,
215
+                    available_statuses=content_type.available_statuses
216
+                )
217
+                return
218
+        raise ContentTypeNotExist()
219
+
220
+    @classmethod
221
+    def all(cls) -> typing.List[str]:
222
+        return cls.allowed_types()
223
+
224
+    @classmethod
225
+    def allowed_types(cls) -> typing.List[str]:
226
+        contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
227
+        contents_types.extend([cls.Folder, cls.Event, cls.Comment])
228
+        return contents_types
229
+
230
+    @classmethod
231
+    def allowed_types_for_folding(cls):
232
+        # This method is used for showing only "main"
233
+        # types in the left-side treeview
234
+        contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
235
+        contents_types.extend([cls.Folder])
236
+        return contents_types
237
+
238
+    # TODO - G.M - 30-05-2018 - This method don't do anything.
239
+    @classmethod
240
+    def sorted(cls, types: ['ContentType']) -> ['ContentType']:
241
+        return types
242
+
243
+    @property
244
+    def id(self):
245
+        return self.slug
246
+
247
+    def toDict(self):
248
+        raise NotImplementedError()

+ 112 - 0
tracim/models/context_models.py View File

@@ -7,11 +7,20 @@ from sqlalchemy.orm import Session
7 7
 from tracim import CFG
8 8
 from tracim.models import User
9 9
 from tracim.models.auth import Profile
10
+from tracim.models.data import Content
10 11
 from tracim.models.data import Workspace, UserRoleInWorkspace
11 12
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
12 13
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
13 14
 
14 15
 
16
+class MoveParams(object):
17
+    """
18
+    Json body params for move action
19
+    """
20
+    def __init__(self, new_parent_id: str):
21
+        self.new_parent_id = new_parent_id
22
+
23
+
15 24
 class LoginCredentials(object):
16 25
     """
17 26
     Login credentials model for login
@@ -22,6 +31,45 @@ class LoginCredentials(object):
22 31
         self.password = password
23 32
 
24 33
 
34
+class WorkspaceAndContentPath(object):
35
+    """
36
+    Paths params with workspace id and content_id
37
+    """
38
+    def __init__(self, workspace_id: int, content_id: int):
39
+        self.content_id = content_id
40
+        self.workspace_id = workspace_id
41
+
42
+
43
+class ContentFilter(object):
44
+    """
45
+    Content filter model
46
+    """
47
+    def __init__(
48
+            self,
49
+            parent_id: int = None,
50
+            show_archived: int = 0,
51
+            show_deleted: int = 0,
52
+            show_active: int = 1,
53
+    ):
54
+        self.parent_id = parent_id
55
+        self.show_archived = bool(show_archived)
56
+        self.show_deleted = bool(show_deleted)
57
+        self.show_active = bool(show_active)
58
+
59
+
60
+class ContentCreation(object):
61
+    """
62
+    Content creation model
63
+    """
64
+    def __init__(
65
+            self,
66
+            label: str,
67
+            content_type_slug: str,
68
+    ):
69
+        self.label = label
70
+        self.content_type_slug = content_type_slug
71
+
72
+
25 73
 class UserInContext(object):
26 74
     """
27 75
     Interface to get User data and User data related to context.
@@ -211,3 +259,67 @@ class UserRoleWorkspaceInContext(object):
211 259
             self.dbsession,
212 260
             self.config
213 261
         )
262
+
263
+
264
+class ContentInContext(object):
265
+    """
266
+    Interface to get Content data and Content data related to context.
267
+    """
268
+
269
+    def __init__(self, content: Content, dbsession: Session, config: CFG):
270
+        self.content = content
271
+        self.dbsession = dbsession
272
+        self.config = config
273
+
274
+    # Default
275
+
276
+    @property
277
+    def id(self) -> int:
278
+        return self.content.content_id
279
+
280
+    @property
281
+    def parent_id(self) -> int:
282
+        return self.content.parent_id
283
+
284
+    @property
285
+    def workspace_id(self) -> int:
286
+        return self.content.workspace_id
287
+
288
+    @property
289
+    def label(self) -> str:
290
+        return self.content.label
291
+
292
+    @property
293
+    def content_type_slug(self) -> str:
294
+        return self.content.type
295
+
296
+    @property
297
+    def sub_content_type_slug(self) -> typing.List[str]:
298
+        return [type.slug for type in self.content.get_allowed_content_types()]
299
+
300
+    @property
301
+    def status_slug(self) -> str:
302
+        return self.content.status
303
+
304
+    @property
305
+    def is_archived(self):
306
+        return self.content.is_archived
307
+
308
+    @property
309
+    def is_deleted(self):
310
+        return self.content.is_deleted
311
+
312
+    # Context-related
313
+
314
+    @property
315
+    def show_in_ui(self):
316
+        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
317
+        # if false, then do not show content in the treeview.
318
+        # This may his maybe used for specific contents or for sub-contents.
319
+        # Default is True.
320
+        # In first version of the API, this field is always True
321
+        return True
322
+
323
+    @property
324
+    def slug(self):
325
+        return slugify(self.content.label)

+ 252 - 244
tracim/models/data.py View File

@@ -292,250 +292,254 @@ class ActionDescription(object):
292 292
                 ]
293 293
 
294 294
 
295
-class ContentStatus(object):
296
-    """
297
-    Allowed status are:
298
-    - open
299
-    - closed-validated
300
-    - closed-invalidated
301
-    - closed-deprecated
302
-    """
303
-
304
-    OPEN = 'open'
305
-    CLOSED_VALIDATED = 'closed-validated'
306
-    CLOSED_UNVALIDATED = 'closed-unvalidated'
307
-    CLOSED_DEPRECATED = 'closed-deprecated'
308
-
309
-    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
310
-    # _LABELS = {'open': l_('work in progress'),
311
-    #            'closed-validated': l_('closed — validated'),
312
-    #            'closed-unvalidated': l_('closed — cancelled'),
313
-    #            'closed-deprecated': l_('deprecated')}
314
-    #
315
-    # _LABELS_THREAD = {'open': l_('subject in progress'),
316
-    #                   'closed-validated': l_('subject closed — resolved'),
317
-    #                   'closed-unvalidated': l_('subject closed — cancelled'),
318
-    #                   'closed-deprecated': l_('deprecated')}
319
-    #
320
-    # _LABELS_FILE = {'open': l_('work in progress'),
321
-    #                 'closed-validated': l_('closed — validated'),
322
-    #                 'closed-unvalidated': l_('closed — cancelled'),
323
-    #                 'closed-deprecated': l_('deprecated')}
324
-    #
325
-    # _ICONS = {
326
-    #     'open': 'fa fa-square-o',
327
-    #     'closed-validated': 'fa fa-check-square-o',
328
-    #     'closed-unvalidated': 'fa fa-close',
329
-    #     'closed-deprecated': 'fa fa-warning',
330
-    # }
331
-    #
332
-    # _CSS = {
333
-    #     'open': 'tracim-status-open',
334
-    #     'closed-validated': 'tracim-status-closed-validated',
335
-    #     'closed-unvalidated': 'tracim-status-closed-unvalidated',
336
-    #     'closed-deprecated': 'tracim-status-closed-deprecated',
337
-    # }
338
-
339
-    def __init__(self,
340
-                 id,
341
-                 # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
342
-                 # type=''
343
-    ):
344
-        self.id = id
345
-        self.label = self.id
346
-        # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
347
-        # self.fa_icon = ContentStatus._ICONS[id]
348
-        # self.css = ContentStatus._CSS[id]
349
-        #
350
-        # if type==ContentType.Thread:
351
-        #     self.label = ContentStatus._LABELS_THREAD[id]
352
-        # elif type==ContentType.File:
353
-        #     self.label = ContentStatus._LABELS_FILE[id]
354
-        # else:
355
-        #     self.label = ContentStatus._LABELS[id]
356
-
357
-
358
-    @classmethod
359
-    def all(cls, type='') -> ['ContentStatus']:
360
-        # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
361
-        # all = []
362
-        # all.append(ContentStatus('open', type))
363
-        # all.append(ContentStatus('closed-validated', type))
364
-        # all.append(ContentStatus('closed-unvalidated', type))
365
-        # all.append(ContentStatus('closed-deprecated', type))
366
-        # return all
367
-        status_list = list()
368
-        for elem in cls.allowed_values():
369
-            status_list.append(ContentStatus(elem))
370
-        return status_list
371
-
372
-    @classmethod
373
-    def allowed_values(cls):
374
-        # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
375
-        # return ContentStatus._LABELS.keys()
376
-        return [
377
-            ContentStatus.OPEN,
378
-            ContentStatus.CLOSED_UNVALIDATED,
379
-            ContentStatus.CLOSED_VALIDATED,
380
-            ContentStatus.CLOSED_DEPRECATED
381
-        ]
382
-
383
-
384
-class ContentType(object):
385
-    Any = 'any'
386
-
387
-    Folder = 'folder'
388
-    File = 'file'
389
-    Comment = 'comment'
390
-    Thread = 'thread'
391
-    Page = 'page'
392
-    Event = 'event'
393
-
394
-    # TODO - G.M - 10-04-2018 - [Cleanup] Do we really need this ?
395
-    # _STRING_LIST_SEPARATOR = ','
396
-
397
-    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
398
-    # _ICONS = {  # Deprecated
399
-    #     'dashboard': 'fa-home',
400
-    #     'workspace': 'fa-bank',
401
-    #     'folder': 'fa fa-folder-open-o',
402
-    #     'file': 'fa fa-paperclip',
403
-    #     'page': 'fa fa-file-text-o',
404
-    #     'thread': 'fa fa-comments-o',
405
-    #     'comment': 'fa fa-comment-o',
406
-    #     'event': 'fa fa-calendar-o',
407
-    # }
408
-    #
409
-    # _CSS_ICONS = {
410
-    #     'dashboard': 'fa fa-home',
411
-    #     'workspace': 'fa fa-bank',
412
-    #     'folder': 'fa fa-folder-open-o',
413
-    #     'file': 'fa fa-paperclip',
414
-    #     'page': 'fa fa-file-text-o',
415
-    #     'thread': 'fa fa-comments-o',
416
-    #     'comment': 'fa fa-comment-o',
417
-    #     'event': 'fa fa-calendar-o',
418
-    # }
419
-    #
420
-    # _CSS_COLORS = {
421
-    #     'dashboard': 't-dashboard-color',
422
-    #     'workspace': 't-less-visible',
423
-    #     'folder': 't-folder-color',
424
-    #     'file': 't-file-color',
425
-    #     'page': 't-page-color',
426
-    #     'thread': 't-thread-color',
427
-    #     'comment': 't-thread-color',
428
-    #     'event': 't-event-color',
429
-    # }
430
-
431
-    _ORDER_WEIGHT = {
432
-        'folder': 0,
433
-        'page': 1,
434
-        'thread': 2,
435
-        'file': 3,
436
-        'comment': 4,
437
-        'event': 5,
438
-    }
439
-
440
-    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
441
-    # _LABEL = {
442
-    #     'dashboard': '',
443
-    #     'workspace': l_('workspace'),
444
-    #     'folder': l_('folder'),
445
-    #     'file': l_('file'),
446
-    #     'page': l_('page'),
447
-    #     'thread': l_('thread'),
448
-    #     'comment': l_('comment'),
449
-    #     'event': l_('event'),
450
-    # }
451
-    #
452
-    # _DELETE_LABEL = {
453
-    #     'dashboard': '',
454
-    #     'workspace': l_('Delete this workspace'),
455
-    #     'folder': l_('Delete this folder'),
456
-    #     'file': l_('Delete this file'),
457
-    #     'page': l_('Delete this page'),
458
-    #     'thread': l_('Delete this thread'),
459
-    #     'comment': l_('Delete this comment'),
460
-    #     'event': l_('Delete this event'),
461
-    # }
462
-    #
463
-    # @classmethod
464
-    # def get_icon(cls, type: str):
465
-    #     assert(type in ContentType._ICONS) # DYN_REMOVE
466
-    #     return ContentType._ICONS[type]
467
-
468
-    @classmethod
469
-    def all(cls):
470
-        return cls.allowed_types()
471
-
472
-    @classmethod
473
-    def allowed_types(cls):
474
-        return [cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page,
475
-                cls.Event]
476
-
477
-    @classmethod
478
-    def allowed_types_for_folding(cls):
479
-        # This method is used for showing only "main"
480
-        # types in the left-side treeview
481
-        return [cls.Folder, cls.File, cls.Thread, cls.Page]
482
-
483
-    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
484
-    # @classmethod
485
-    # def allowed_types_from_str(cls, allowed_types_as_string: str):
486
-    #     allowed_types = []
487
-    #     # HACK - THIS
488
-    #     for item in allowed_types_as_string.split(ContentType._STRING_LIST_SEPARATOR):
489
-    #         if item and item in ContentType.allowed_types_for_folding():
490
-    #             allowed_types.append(item)
491
-    #     return allowed_types
492
-    #
493
-    # @classmethod
494
-    # def fill_url(cls, content: 'Content'):
495
-    #     # TODO - DYNDATATYPE - D.A. - 2014-12-02
496
-    #     # Make this code dynamic loading data types
497
-    #
498
-    #     if content.type==ContentType.Folder:
499
-    #         return '/workspaces/{}/folders/{}'.format(content.workspace_id, content.content_id)
500
-    #     elif content.type==ContentType.File:
501
-    #         return '/workspaces/{}/folders/{}/files/{}'.format(content.workspace_id, content.parent_id, content.content_id)
502
-    #     elif content.type==ContentType.Thread:
503
-    #         return '/workspaces/{}/folders/{}/threads/{}'.format(content.workspace_id, content.parent_id, content.content_id)
504
-    #     elif content.type==ContentType.Page:
505
-    #         return '/workspaces/{}/folders/{}/pages/{}'.format(content.workspace_id, content.parent_id, content.content_id)
506
-    #
507
-    # @classmethod
508
-    # def fill_url_for_workspace(cls, workspace: Workspace):
509
-    #     # TODO - DYNDATATYPE - D.A. - 2014-12-02
510
-    #     # Make this code dynamic loading data types
511
-    #     return '/workspaces/{}'.format(workspace.workspace_id)
512
-
513
-    @classmethod
514
-    def sorted(cls, types: ['ContentType']) -> ['ContentType']:
515
-        return sorted(types, key=lambda content_type: content_type.priority)
516
-
517
-    @property
518
-    def type(self):
519
-        return self.id
520
-
521
-    def __init__(self, type):
522
-        self.id = type
523
-        # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
524
-        # self.fa_icon = ContentType._CSS_ICONS[type]
525
-        # self.color = ContentType._CSS_COLORS[type]  # deprecated
526
-        # self.css = ContentType._CSS_COLORS[type]
527
-        # self.label = ContentType._LABEL[type]
528
-        self.priority = ContentType._ORDER_WEIGHT[type]
529
-
530
-    def toDict(self):
531
-        return dict(id=self.type,
532
-                    type=self.type,
533
-                    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
534
-                    # fa_icon=self.fa_icon,
535
-                    # color=self.color,
536
-                    # label=self.label,
537
-                    priority=self.priority)
538
-
295
+from tracim.models.contents import ContentStatusLegacy as ContentStatus
296
+from tracim.models.contents import ContentTypeLegacy as ContentType
297
+# TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
298
+# is lost .
299
+
300
+
301
+# class ContentStatus(object):
302
+#     """
303
+#     Allowed status are:
304
+#     - open
305
+#     - closed-validated
306
+#     - closed-invalidated
307
+#     - closed-deprecated
308
+#     """
309
+#
310
+#     OPEN = 'open'
311
+#     CLOSED_VALIDATED = 'closed-validated'
312
+#     CLOSED_UNVALIDATED = 'closed-unvalidated'
313
+#     CLOSED_DEPRECATED = 'closed-deprecated'
314
+#
315
+#     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
316
+#     # _LABELS = {'open': l_('work in progress'),
317
+#     #            'closed-validated': l_('closed — validated'),
318
+#     #            'closed-unvalidated': l_('closed — cancelled'),
319
+#     #            'closed-deprecated': l_('deprecated')}
320
+#     #
321
+#     # _LABELS_THREAD = {'open': l_('subject in progress'),
322
+#     #                   'closed-validated': l_('subject closed — resolved'),
323
+#     #                   'closed-unvalidated': l_('subject closed — cancelled'),
324
+#     #                   'closed-deprecated': l_('deprecated')}
325
+#     #
326
+#     # _LABELS_FILE = {'open': l_('work in progress'),
327
+#     #                 'closed-validated': l_('closed — validated'),
328
+#     #                 'closed-unvalidated': l_('closed — cancelled'),
329
+#     #                 'closed-deprecated': l_('deprecated')}
330
+#     #
331
+#     # _ICONS = {
332
+#     #     'open': 'fa fa-square-o',
333
+#     #     'closed-validated': 'fa fa-check-square-o',
334
+#     #     'closed-unvalidated': 'fa fa-close',
335
+#     #     'closed-deprecated': 'fa fa-warning',
336
+#     # }
337
+#     #
338
+#     # _CSS = {
339
+#     #     'open': 'tracim-status-open',
340
+#     #     'closed-validated': 'tracim-status-closed-validated',
341
+#     #     'closed-unvalidated': 'tracim-status-closed-unvalidated',
342
+#     #     'closed-deprecated': 'tracim-status-closed-deprecated',
343
+#     # }
344
+#
345
+#     def __init__(self,
346
+#                  id,
347
+#                  # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
348
+#                  # type=''
349
+#     ):
350
+#         self.id = id
351
+#         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
352
+#         # self.icon = ContentStatus._ICONS[id]
353
+#         # self.css = ContentStatus._CSS[id]
354
+#         #
355
+#         # if type==ContentType.Thread:
356
+#         #     self.label = ContentStatus._LABELS_THREAD[id]
357
+#         # elif type==ContentType.File:
358
+#         #     self.label = ContentStatus._LABELS_FILE[id]
359
+#         # else:
360
+#         #     self.label = ContentStatus._LABELS[id]
361
+#
362
+#
363
+#     @classmethod
364
+#     def all(cls, type='') -> ['ContentStatus']:
365
+#         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
366
+#         # all = []
367
+#         # all.append(ContentStatus('open', type))
368
+#         # all.append(ContentStatus('closed-validated', type))
369
+#         # all.append(ContentStatus('closed-unvalidated', type))
370
+#         # all.append(ContentStatus('closed-deprecated', type))
371
+#         # return all
372
+#         status_list = list()
373
+#         for elem in cls.allowed_values():
374
+#             status_list.append(ContentStatus(elem))
375
+#         return status_list
376
+#
377
+#     @classmethod
378
+#     def allowed_values(cls):
379
+#         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
380
+#         # return ContentStatus._LABELS.keys()
381
+#         return [
382
+#             ContentStatus.OPEN,
383
+#             ContentStatus.CLOSED_UNVALIDATED,
384
+#             ContentStatus.CLOSED_VALIDATED,
385
+#             ContentStatus.CLOSED_DEPRECATED
386
+#         ]
387
+
388
+
389
+# class ContentType(object):
390
+#     Any = 'any'
391
+#
392
+#     Folder = 'folder'
393
+#     File = 'file'
394
+#     Comment = 'comment'
395
+#     Thread = 'thread'
396
+#     Page = 'page'
397
+#     Event = 'event'
398
+#
399
+#     # TODO - G.M - 10-04-2018 - [Cleanup] Do we really need this ?
400
+#     # _STRING_LIST_SEPARATOR = ','
401
+#
402
+#     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
403
+#     # _ICONS = {  # Deprecated
404
+#     #     'dashboard': 'fa-home',
405
+#     #     'workspace': 'fa-bank',
406
+#     #     'folder': 'fa fa-folder-open-o',
407
+#     #     'file': 'fa fa-paperclip',
408
+#     #     'page': 'fa fa-file-text-o',
409
+#     #     'thread': 'fa fa-comments-o',
410
+#     #     'comment': 'fa fa-comment-o',
411
+#     #     'event': 'fa fa-calendar-o',
412
+#     # }
413
+#     #
414
+#     # _CSS_ICONS = {
415
+#     #     'dashboard': 'fa fa-home',
416
+#     #     'workspace': 'fa fa-bank',
417
+#     #     'folder': 'fa fa-folder-open-o',
418
+#     #     'file': 'fa fa-paperclip',
419
+#     #     'page': 'fa fa-file-text-o',
420
+#     #     'thread': 'fa fa-comments-o',
421
+#     #     'comment': 'fa fa-comment-o',
422
+#     #     'event': 'fa fa-calendar-o',
423
+#     # }
424
+#     #
425
+#     # _CSS_COLORS = {
426
+#     #     'dashboard': 't-dashboard-color',
427
+#     #     'workspace': 't-less-visible',
428
+#     #     'folder': 't-folder-color',
429
+#     #     'file': 't-file-color',
430
+#     #     'page': 't-page-color',
431
+#     #     'thread': 't-thread-color',
432
+#     #     'comment': 't-thread-color',
433
+#     #     'event': 't-event-color',
434
+#     # }
435
+#
436
+#     _ORDER_WEIGHT = {
437
+#         'folder': 0,
438
+#         'page': 1,
439
+#         'thread': 2,
440
+#         'file': 3,
441
+#         'comment': 4,
442
+#         'event': 5,
443
+#     }
444
+#
445
+#     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
446
+#     # _LABEL = {
447
+#     #     'dashboard': '',
448
+#     #     'workspace': l_('workspace'),
449
+#     #     'folder': l_('folder'),
450
+#     #     'file': l_('file'),
451
+#     #     'page': l_('page'),
452
+#     #     'thread': l_('thread'),
453
+#     #     'comment': l_('comment'),
454
+#     #     'event': l_('event'),
455
+#     # }
456
+#     #
457
+#     # _DELETE_LABEL = {
458
+#     #     'dashboard': '',
459
+#     #     'workspace': l_('Delete this workspace'),
460
+#     #     'folder': l_('Delete this folder'),
461
+#     #     'file': l_('Delete this file'),
462
+#     #     'page': l_('Delete this page'),
463
+#     #     'thread': l_('Delete this thread'),
464
+#     #     'comment': l_('Delete this comment'),
465
+#     #     'event': l_('Delete this event'),
466
+#     # }
467
+#     #
468
+#     # @classmethod
469
+#     # def get_icon(cls, type: str):
470
+#     #     assert(type in ContentType._ICONS) # DYN_REMOVE
471
+#     #     return ContentType._ICONS[type]
472
+#
473
+#     @classmethod
474
+#     def all(cls):
475
+#         return cls.allowed_types()
476
+#
477
+#     @classmethod
478
+#     def allowed_types(cls):
479
+#         return [cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page,
480
+#                 cls.Event]
481
+#
482
+#     @classmethod
483
+#     def allowed_types_for_folding(cls):
484
+#         # This method is used for showing only "main"
485
+#         # types in the left-side treeview
486
+#         return [cls.Folder, cls.File, cls.Thread, cls.Page]
487
+#
488
+#     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
489
+#     # @classmethod
490
+#     # def allowed_types_from_str(cls, allowed_types_as_string: str):
491
+#     #     allowed_types = []
492
+#     #     # HACK - THIS
493
+#     #     for item in allowed_types_as_string.split(ContentType._STRING_LIST_SEPARATOR):
494
+#     #         if item and item in ContentType.allowed_types_for_folding():
495
+#     #             allowed_types.append(item)
496
+#     #     return allowed_types
497
+#     #
498
+#     # @classmethod
499
+#     # def fill_url(cls, content: 'Content'):
500
+#     #     # TODO - DYNDATATYPE - D.A. - 2014-12-02
501
+#     #     # Make this code dynamic loading data types
502
+#     #
503
+#     #     if content.type==ContentType.Folder:
504
+#     #         return '/workspaces/{}/folders/{}'.format(content.workspace_id, content.content_id)
505
+#     #     elif content.type==ContentType.File:
506
+#     #         return '/workspaces/{}/folders/{}/files/{}'.format(content.workspace_id, content.parent_id, content.content_id)
507
+#     #     elif content.type==ContentType.Thread:
508
+#     #         return '/workspaces/{}/folders/{}/threads/{}'.format(content.workspace_id, content.parent_id, content.content_id)
509
+#     #     elif content.type==ContentType.Page:
510
+#     #         return '/workspaces/{}/folders/{}/pages/{}'.format(content.workspace_id, content.parent_id, content.content_id)
511
+#     #
512
+#     # @classmethod
513
+#     # def fill_url_for_workspace(cls, workspace: Workspace):
514
+#     #     # TODO - DYNDATATYPE - D.A. - 2014-12-02
515
+#     #     # Make this code dynamic loading data types
516
+#     #     return '/workspaces/{}'.format(workspace.workspace_id)
517
+#
518
+#     @classmethod
519
+#     def sorted(cls, types: ['ContentType']) -> ['ContentType']:
520
+#         return sorted(types, key=lambda content_type: content_type.priority)
521
+#
522
+#     @property
523
+#     def type(self):
524
+#         return self.id
525
+#
526
+#     def __init__(self, type):
527
+#         self.id = type
528
+#         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
529
+#         # self.icon = ContentType._CSS_ICONS[type]
530
+#         # self.color = ContentType._CSS_COLORS[type]  # deprecated
531
+#         # self.css = ContentType._CSS_COLORS[type]
532
+#         # self.label = ContentType._LABEL[type]
533
+#         self.priority = ContentType._ORDER_WEIGHT[type]
534
+#
535
+#     def toDict(self):
536
+#         return dict(id=self.type,
537
+#                     type=self.type,
538
+#                     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
539
+#                     # icon=self.icon,
540
+#                     # color=self.color,
541
+#                     # label=self.label,
542
+#                     priority=self.priority)
539 543
 
540 544
 class ContentChecker(object):
541 545
 
@@ -1147,6 +1151,10 @@ class Content(DeclarativeBase):
1147 1151
         return not self.is_archived and not self.is_deleted
1148 1152
 
1149 1153
     @property
1154
+    def is_active(self) -> bool:
1155
+        return self.is_editable
1156
+
1157
+    @property
1150 1158
     def depot_file(self) -> UploadedFile:
1151 1159
         return self.revision.depot_file
1152 1160
 

+ 8 - 8
tracim/tests/functional/test_mail_notification.py View File

@@ -129,7 +129,7 @@ class TestNotificationsSync(MailHogTest):
129 129
             session=self.session,
130 130
             config=self.app_config,
131 131
         )
132
-        workspace = wapi.get_one_by_label('w1')
132
+        workspace = wapi.get_one_by_label('Recipes')
133 133
         user = uapi.get_one_by_email('bob@fsf.local')
134 134
         wapi.enable_notifications(user, workspace)
135 135
 
@@ -161,9 +161,9 @@ class TestNotificationsSync(MailHogTest):
161 161
         headers = response[0]['Content']['Headers']
162 162
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
163 163
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
164
-        assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)'
165
-        assert headers['References'][0] == 'test_user_refs+13@localhost'
166
-        assert headers['Reply-to'][0] == '"Bob i. & all members of w1" <test_user_reply+13@localhost>'  # nopep8
164
+        assert headers['Subject'][0] == '[TRACIM] [Recipes] file1 (Open)'
165
+        assert headers['References'][0] == 'test_user_refs+19@localhost'
166
+        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+19@localhost>'  # nopep8
167 167
 
168 168
 
169 169
 class TestNotificationsAsync(MailHogTest):
@@ -221,7 +221,7 @@ class TestNotificationsAsync(MailHogTest):
221 221
             session=self.session,
222 222
             config=self.app_config,
223 223
         )
224
-        workspace = wapi.get_one_by_label('w1')
224
+        workspace = wapi.get_one_by_label('Recipes')
225 225
         user = uapi.get_one_by_email('bob@fsf.local')
226 226
         wapi.enable_notifications(user, workspace)
227 227
 
@@ -262,6 +262,6 @@ class TestNotificationsAsync(MailHogTest):
262 262
         headers = response[0]['Content']['Headers']
263 263
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
264 264
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
265
-        assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)'
266
-        assert headers['References'][0] == 'test_user_refs+13@localhost'
267
-        assert headers['Reply-to'][0] == '"Bob i. & all members of w1" <test_user_reply+13@localhost>'  # nopep8
265
+        assert headers['Subject'][0] == '[TRACIM] [Recipes] file1 (Open)'
266
+        assert headers['References'][0] == 'test_user_refs+19@localhost'
267
+        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+19@localhost>'  # nopep8

+ 76 - 1
tracim/tests/functional/test_system.py View File

@@ -59,7 +59,7 @@ class TestApplicationEndpoint(FunctionalTest):
59 59
         assert application['is_active'] is True
60 60
         assert 'config' in application
61 61
 
62
-    def test_api__get_workspace__err_401__unregistered_user(self):
62
+    def test_api__get_applications__err_401__unregistered_user(self):
63 63
         """
64 64
         Get applications list with an unregistered user (bad auth)
65 65
         """
@@ -75,3 +75,78 @@ class TestApplicationEndpoint(FunctionalTest):
75 75
         assert 'code' in res.json.keys()
76 76
         assert 'message' in res.json.keys()
77 77
         assert 'details' in res.json.keys()
78
+
79
+
80
+class TestContentsTypesEndpoint(FunctionalTest):
81
+    """
82
+    Tests for /api/v2/system/content_types
83
+    """
84
+
85
+    def test_api__get_content_types__ok_200__nominal_case(self):
86
+        """
87
+        Get system content_types list with a registered user.
88
+        """
89
+        self.testapp.authorization = (
90
+            'Basic',
91
+            (
92
+                'admin@admin.admin',
93
+                'admin@admin.admin'
94
+            )
95
+        )
96
+        res = self.testapp.get('/api/v2/system/content_types', status=200)
97
+        res = res.json_body
98
+
99
+        content_type = res[0]
100
+        assert content_type['slug'] == 'thread'
101
+        assert content_type['fa_icon'] == 'comments-o'
102
+        assert content_type['hexcolor'] == '#ad4cf9'
103
+        assert content_type['label'] == 'Thread'
104
+        assert content_type['creation_label'] == 'Discuss about a topic'
105
+        assert 'available_statuses' in content_type
106
+        assert len(content_type['available_statuses']) == 4
107
+
108
+        content_type = res[1]
109
+        assert content_type['slug'] == 'file'
110
+        assert content_type['fa_icon'] == 'paperclip'
111
+        assert content_type['hexcolor'] == '#FF9900'
112
+        assert content_type['label'] == 'File'
113
+        assert content_type['creation_label'] == 'Upload a file'
114
+        assert 'available_statuses' in content_type
115
+        assert len(content_type['available_statuses']) == 4
116
+
117
+        content_type = res[2]
118
+        assert content_type['slug'] == 'markdownpage'
119
+        assert content_type['fa_icon'] == 'file-code'
120
+        assert content_type['hexcolor'] == '#f12d2d'
121
+        assert content_type['label'] == 'Rich Markdown File'
122
+        assert content_type['creation_label'] == 'Create a Markdown document'
123
+        assert 'available_statuses' in content_type
124
+        assert len(content_type['available_statuses']) == 4
125
+
126
+        content_type = res[3]
127
+        assert content_type['slug'] == 'page'
128
+        assert content_type['fa_icon'] == 'file-text-o'
129
+        assert content_type['hexcolor'] == '#3f52e3'
130
+        assert content_type['label'] == 'Text Document'
131
+        assert content_type['creation_label'] == 'Write a document'
132
+        assert 'available_statuses' in content_type
133
+        assert len(content_type['available_statuses']) == 4
134
+        # TODO - G.M - 31-05-2018 - Check Folder type
135
+        # TODO - G.M - 29-05-2018 - Better check for available_statuses
136
+
137
+    def test_api__get_content_types__err_401__unregistered_user(self):
138
+        """
139
+        Get system content_types list with an unregistered user (bad auth)
140
+        """
141
+        self.testapp.authorization = (
142
+            'Basic',
143
+            (
144
+                'john@doe.doe',
145
+                'lapin'
146
+            )
147
+        )
148
+        res = self.testapp.get('/api/v2/system/content_types', status=401)
149
+        assert isinstance(res.json, dict)
150
+        assert 'code' in res.json.keys()
151
+        assert 'message' in res.json.keys()
152
+        assert 'details' in res.json.keys()

+ 1 - 1
tracim/tests/functional/test_user.py View File

@@ -29,7 +29,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
29 29
         res = res.json_body
30 30
         workspace = res[0]
31 31
         assert workspace['id'] == 1
32
-        assert workspace['label'] == 'w1'
32
+        assert workspace['label'] == 'Business'
33 33
         assert len(workspace['sidebar_entries']) == 7
34 34
 
35 35
         sidebar_entry = workspace['sidebar_entries'][0]

+ 752 - 4
tracim/tests/functional/test_workspaces.py View File

@@ -28,9 +28,9 @@ class TestWorkspaceEndpoint(FunctionalTest):
28 28
         res = self.testapp.get('/api/v2/workspaces/1', status=200)
29 29
         workspace = res.json_body
30 30
         assert workspace['id'] == 1
31
-        assert workspace['slug'] == 'w1'
32
-        assert workspace['label'] == 'w1'
33
-        assert workspace['description'] == 'This is a workspace'
31
+        assert workspace['slug'] == 'business'
32
+        assert workspace['label'] == 'Business'
33
+        assert workspace['description'] == 'All importants documents'
34 34
         assert len(workspace['sidebar_entries']) == 7
35 35
 
36 36
         sidebar_entry = workspace['sidebar_entries'][0]
@@ -153,7 +153,7 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
153 153
             )
154 154
         )
155 155
         res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
156
-        assert len(res) == 2
156
+        assert len(res) == 1
157 157
         user_role = res[0]
158 158
         assert user_role['role_slug'] == 'workspace-manager'
159 159
         assert user_role['user_id'] == 1
@@ -215,3 +215,751 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
215 215
         assert 'code' in res.json.keys()
216 216
         assert 'message' in res.json.keys()
217 217
         assert 'details' in res.json.keys()
218
+
219
+
220
+class TestWorkspaceContents(FunctionalTest):
221
+    """
222
+    Tests for /api/v2/workspaces/{workspace_id}/contents endpoint
223
+    """
224
+
225
+    fixtures = [BaseFixture, ContentFixtures]
226
+
227
+    def test_api__get_workspace_content__ok_200__get_default(self):
228
+        """
229
+        Check obtain workspace contents with defaults filters
230
+        """
231
+        self.testapp.authorization = (
232
+            'Basic',
233
+            (
234
+                'admin@admin.admin',
235
+                'admin@admin.admin'
236
+            )
237
+        )
238
+        res = self.testapp.get('/api/v2/workspaces/1/contents', status=200).json_body   # nopep8
239
+        # TODO - G.M - 30-05-2018 - Check this test
240
+        assert len(res) == 3
241
+        content = res[0]
242
+        assert content['id'] == 1
243
+        assert content['is_archived'] is False
244
+        assert content['is_deleted'] is False
245
+        assert content['label'] == 'Tools'
246
+        assert content['parent_id'] is None
247
+        assert content['show_in_ui'] is True
248
+        assert content['slug'] == 'tools'
249
+        assert content['status_slug'] == 'open'
250
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
251
+        assert content['workspace_id'] == 1
252
+        content = res[1]
253
+        assert content['id'] == 2
254
+        assert content['is_archived'] is False
255
+        assert content['is_deleted'] is False
256
+        assert content['label'] == 'Menus'
257
+        assert content['parent_id'] is None
258
+        assert content['show_in_ui'] is True
259
+        assert content['slug'] == 'menus'
260
+        assert content['status_slug'] == 'open'
261
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
262
+        assert content['workspace_id'] == 1
263
+        content = res[2]
264
+        assert content['id'] == 11
265
+        assert content['is_archived'] is False
266
+        assert content['is_deleted'] is False
267
+        assert content['label'] == 'Current Menu'
268
+        assert content['parent_id'] == 2
269
+        assert content['show_in_ui'] is True
270
+        assert content['slug'] == 'current-menu'
271
+        assert content['status_slug'] == 'open'
272
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
273
+        assert content['workspace_id'] == 1
274
+
275
+    # Root related
276
+
277
+    def test_api__get_workspace_content__ok_200__get_all_root_content(self):
278
+        """
279
+        Check obtain workspace all root contents
280
+        """
281
+        params = {
282
+            'parent_id': 0,
283
+            'show_archived': 1,
284
+            'show_deleted': 1,
285
+            'show_active': 1,
286
+        }
287
+        self.testapp.authorization = (
288
+            'Basic',
289
+            (
290
+                'bob@fsf.local',
291
+                'foobarbaz'
292
+            )
293
+        )
294
+        res = self.testapp.get(
295
+            '/api/v2/workspaces/3/contents',
296
+            status=200,
297
+            params=params,
298
+        ).json_body  # nopep8
299
+        # TODO - G.M - 30-05-2018 - Check this test
300
+        assert len(res) == 4
301
+        content = res[1]
302
+        assert content['content_type_slug'] == 'page'
303
+        assert content['id'] == 15
304
+        assert content['is_archived'] is False
305
+        assert content['is_deleted'] is False
306
+        assert content['label'] == 'New Fruit Salad'
307
+        assert content['parent_id'] is None
308
+        assert content['show_in_ui'] is True
309
+        assert content['slug'] == 'new-fruit-salad'
310
+        assert content['status_slug'] == 'open'
311
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
312
+        assert content['workspace_id'] == 3
313
+
314
+        content = res[2]
315
+        assert content['content_type_slug'] == 'page'
316
+        assert content['id'] == 16
317
+        assert content['is_archived'] is True
318
+        assert content['is_deleted'] is False
319
+        assert content['label'].startswith('Fruit Salad')
320
+        assert content['parent_id'] is None
321
+        assert content['show_in_ui'] is True
322
+        assert content['slug'].startswith('fruit-salad')
323
+        assert content['status_slug'] == 'open'
324
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
325
+        assert content['workspace_id'] == 3
326
+
327
+        content = res[3]
328
+        assert content['content_type_slug'] == 'page'
329
+        assert content['id'] == 17
330
+        assert content['is_archived'] is False
331
+        assert content['is_deleted'] is True
332
+        assert content['label'].startswith('Bad Fruit Salad')
333
+        assert content['parent_id'] is None
334
+        assert content['show_in_ui'] is True
335
+        assert content['slug'].startswith('bad-fruit-salad')
336
+        assert content['status_slug'] == 'open'
337
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
338
+        assert content['workspace_id'] == 3
339
+
340
+    def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
341
+        """
342
+        Check obtain workspace root active contents
343
+        """
344
+        params = {
345
+            'parent_id': 0,
346
+            'show_archived': 0,
347
+            'show_deleted': 0,
348
+            'show_active': 1,
349
+        }
350
+        self.testapp.authorization = (
351
+            'Basic',
352
+            (
353
+                'bob@fsf.local',
354
+                'foobarbaz'
355
+            )
356
+        )
357
+        res = self.testapp.get(
358
+            '/api/v2/workspaces/3/contents',
359
+            status=200,
360
+            params=params,
361
+        ).json_body   # nopep8
362
+        # TODO - G.M - 30-05-2018 - Check this test
363
+        assert len(res) == 2
364
+        content = res[1]
365
+        assert content['content_type_slug'] == 'page'
366
+        assert content['id'] == 15
367
+        assert content['is_archived'] is False
368
+        assert content['is_deleted'] is False
369
+        assert content['label'] == 'New Fruit Salad'
370
+        assert content['parent_id'] is None
371
+        assert content['show_in_ui'] is True
372
+        assert content['slug'] == 'new-fruit-salad'
373
+        assert content['status_slug'] == 'open'
374
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
375
+        assert content['workspace_id'] == 3
376
+
377
+    def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
378
+        """
379
+        Check obtain workspace root archived contents
380
+        """
381
+        params = {
382
+            'parent_id': 0,
383
+            'show_archived': 1,
384
+            'show_deleted': 0,
385
+            'show_active': 0,
386
+        }
387
+        self.testapp.authorization = (
388
+            'Basic',
389
+            (
390
+                'bob@fsf.local',
391
+                'foobarbaz'
392
+            )
393
+        )
394
+        res = self.testapp.get(
395
+            '/api/v2/workspaces/3/contents',
396
+            status=200,
397
+            params=params,
398
+        ).json_body   # nopep8
399
+        assert len(res) == 1
400
+        content = res[0]
401
+        assert content['content_type_slug'] == 'page'
402
+        assert content['id'] == 16
403
+        assert content['is_archived'] is True
404
+        assert content['is_deleted'] is False
405
+        assert content['label'].startswith('Fruit Salad')
406
+        assert content['parent_id'] is None
407
+        assert content['show_in_ui'] is True
408
+        assert content['slug'].startswith('fruit-salad')
409
+        assert content['status_slug'] == 'open'
410
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
411
+        assert content['workspace_id'] == 3
412
+
413
+    def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
414
+        """
415
+         Check obtain workspace root deleted contents
416
+         """
417
+        params = {
418
+            'parent_id': 0,
419
+            'show_archived': 0,
420
+            'show_deleted': 1,
421
+            'show_active': 0,
422
+        }
423
+        self.testapp.authorization = (
424
+            'Basic',
425
+            (
426
+                'bob@fsf.local',
427
+                'foobarbaz'
428
+            )
429
+        )
430
+        res = self.testapp.get(
431
+            '/api/v2/workspaces/3/contents',
432
+            status=200,
433
+            params=params,
434
+        ).json_body   # nopep8
435
+        # TODO - G.M - 30-05-2018 - Check this test
436
+
437
+        assert len(res) == 1
438
+        content = res[0]
439
+        assert content['content_type_slug'] == 'page'
440
+        assert content['id'] == 17
441
+        assert content['is_archived'] is False
442
+        assert content['is_deleted'] is True
443
+        assert content['label'].startswith('Bad Fruit Salad')
444
+        assert content['parent_id'] is None
445
+        assert content['show_in_ui'] is True
446
+        assert content['slug'].startswith('bad-fruit-salad')
447
+        assert content['status_slug'] == 'open'
448
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
449
+        assert content['workspace_id'] == 3
450
+
451
+    def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
452
+        """
453
+        Check obtain workspace root content who does not match any type
454
+        (archived, deleted, active) result should be empty list.
455
+        """
456
+        params = {
457
+            'parent_id': 0,
458
+            'show_archived': 0,
459
+            'show_deleted': 0,
460
+            'show_active': 0,
461
+        }
462
+        self.testapp.authorization = (
463
+            'Basic',
464
+            (
465
+                'bob@fsf.local',
466
+                'foobarbaz'
467
+            )
468
+        )
469
+        res = self.testapp.get(
470
+            '/api/v2/workspaces/3/contents',
471
+            status=200,
472
+            params=params,
473
+        ).json_body  # nopep8
474
+        # TODO - G.M - 30-05-2018 - Check this test
475
+        assert res == []
476
+
477
+    # Folder related
478
+
479
+    def test_api__get_workspace_content__ok_200__get_all_folder_content(self):
480
+        """
481
+         Check obtain workspace folder all contents
482
+         """
483
+        params = {
484
+            'parent_id': 10,  # TODO - G.M - 30-05-2018 - Find a real id
485
+            'show_archived': 1,
486
+            'show_deleted': 1,
487
+            'show_active': 1,
488
+        }
489
+        self.testapp.authorization = (
490
+            'Basic',
491
+            (
492
+                'admin@admin.admin',
493
+                'admin@admin.admin'
494
+            )
495
+        )
496
+        res = self.testapp.get(
497
+            '/api/v2/workspaces/2/contents',
498
+            status=200,
499
+            params=params,
500
+        ).json_body   # nopep8
501
+        assert len(res) == 3
502
+        content = res[0]
503
+        assert content['content_type_slug'] == 'page'
504
+        assert content['id'] == 12
505
+        assert content['is_archived'] is False
506
+        assert content['is_deleted'] is False
507
+        assert content['label'] == 'New Fruit Salad'
508
+        assert content['parent_id'] == 10
509
+        assert content['show_in_ui'] is True
510
+        assert content['slug'] == 'new-fruit-salad'
511
+        assert content['status_slug'] == 'open'
512
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
513
+        assert content['workspace_id'] == 2
514
+
515
+        content = res[1]
516
+        assert content['content_type_slug'] == 'page'
517
+        assert content['id'] == 13
518
+        assert content['is_archived'] is True
519
+        assert content['is_deleted'] is False
520
+        assert content['label'].startswith('Fruit Salad')
521
+        assert content['parent_id'] == 10
522
+        assert content['show_in_ui'] is True
523
+        assert content['slug'].startswith('fruit-salad')
524
+        assert content['status_slug'] == 'open'
525
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
526
+        assert content['workspace_id'] == 2
527
+
528
+        content = res[2]
529
+        assert content['content_type_slug'] == 'page'
530
+        assert content['id'] == 14
531
+        assert content['is_archived'] is False
532
+        assert content['is_deleted'] is True
533
+        assert content['label'].startswith('Bad Fruit Salad')
534
+        assert content['parent_id'] == 10
535
+        assert content['show_in_ui'] is True
536
+        assert content['slug'].startswith('bad-fruit-salad')
537
+        assert content['status_slug'] == 'open'
538
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
539
+        assert content['workspace_id'] == 2
540
+
541
+    def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
542
+        """
543
+         Check obtain workspace folder active contents
544
+         """
545
+        params = {
546
+            'parent_id': 10,
547
+            'show_archived': 0,
548
+            'show_deleted': 0,
549
+            'show_active': 1,
550
+        }
551
+        self.testapp.authorization = (
552
+            'Basic',
553
+            (
554
+                'admin@admin.admin',
555
+                'admin@admin.admin'
556
+            )
557
+        )
558
+        res = self.testapp.get(
559
+            '/api/v2/workspaces/2/contents',
560
+            status=200,
561
+            params=params,
562
+        ).json_body   # nopep8
563
+        assert len(res) == 1
564
+        content = res[0]
565
+        assert content['content_type_slug']
566
+        assert content['id'] == 12
567
+        assert content['is_archived'] is False
568
+        assert content['is_deleted'] is False
569
+        assert content['label'] == 'New Fruit Salad'
570
+        assert content['parent_id'] == 10
571
+        assert content['show_in_ui'] is True
572
+        assert content['slug'] == 'new-fruit-salad'
573
+        assert content['status_slug'] == 'open'
574
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
575
+        assert content['workspace_id'] == 2
576
+
577
+    def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
578
+        """
579
+         Check obtain workspace folder archived contents
580
+         """
581
+        params = {
582
+            'parent_id': 10,
583
+            'show_archived': 1,
584
+            'show_deleted': 0,
585
+            'show_active': 0,
586
+        }
587
+        self.testapp.authorization = (
588
+            'Basic',
589
+            (
590
+                'admin@admin.admin',
591
+                'admin@admin.admin'
592
+            )
593
+        )
594
+        res = self.testapp.get(
595
+            '/api/v2/workspaces/2/contents',
596
+            status=200,
597
+            params=params,
598
+        ).json_body   # nopep8
599
+        assert len(res) == 1
600
+        content = res[0]
601
+        assert content['content_type_slug'] == 'page'
602
+        assert content['id'] == 13
603
+        assert content['is_archived'] is True
604
+        assert content['is_deleted'] is False
605
+        assert content['label'].startswith('Fruit Salad')
606
+        assert content['parent_id'] == 10
607
+        assert content['show_in_ui'] is True
608
+        assert content['slug'].startswith('fruit-salad')
609
+        assert content['status_slug'] == 'open'
610
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
611
+        assert content['workspace_id'] == 2
612
+
613
+    def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
614
+        """
615
+         Check obtain workspace folder deleted contents
616
+         """
617
+        params = {
618
+            'parent_id': 10,
619
+            'show_archived': 0,
620
+            'show_deleted': 1,
621
+            'show_active': 0,
622
+        }
623
+        self.testapp.authorization = (
624
+            'Basic',
625
+            (
626
+                'admin@admin.admin',
627
+                'admin@admin.admin'
628
+            )
629
+        )
630
+        res = self.testapp.get(
631
+            '/api/v2/workspaces/2/contents',
632
+            status=200,
633
+            params=params,
634
+        ).json_body   # nopep8
635
+
636
+        assert len(res) == 1
637
+        content = res[0]
638
+        assert content['content_type_slug'] == 'page'
639
+        assert content['id'] == 14
640
+        assert content['is_archived'] is False
641
+        assert content['is_deleted'] is True
642
+        assert content['label'].startswith('Bad Fruit Salad')
643
+        assert content['parent_id'] == 10
644
+        assert content['show_in_ui'] is True
645
+        assert content['slug'].startswith('bad-fruit-salad')
646
+        assert content['status_slug'] == 'open'
647
+        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
648
+        assert content['workspace_id'] == 2
649
+
650
+    def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
651
+        """
652
+        Check obtain workspace folder content who does not match any type
653
+        (archived, deleted, active) result should be empty list.
654
+        """
655
+        params = {
656
+            'parent_id': 10,
657
+            'show_archived': 0,
658
+            'show_deleted': 0,
659
+            'show_active': 0,
660
+        }
661
+        self.testapp.authorization = (
662
+            'Basic',
663
+            (
664
+                'admin@admin.admin',
665
+                'admin@admin.admin'
666
+            )
667
+        )
668
+        res = self.testapp.get(
669
+            '/api/v2/workspaces/2/contents',
670
+            status=200,
671
+            params=params,
672
+        ).json_body   # nopep8
673
+        # TODO - G.M - 30-05-2018 - Check this test
674
+        assert res == []
675
+
676
+    # Error case
677
+
678
+    def test_api__get_workspace_content__err_403__unallowed_user(self):
679
+        """
680
+        Check obtain workspace content list with an unreachable workspace for
681
+        user
682
+        """
683
+        self.testapp.authorization = (
684
+            'Basic',
685
+            (
686
+                'lawrence-not-real-email@fsf.local',
687
+                'foobarbaz'
688
+            )
689
+        )
690
+        res = self.testapp.get('/api/v2/workspaces/3/contents', status=403)
691
+        assert isinstance(res.json, dict)
692
+        assert 'code' in res.json.keys()
693
+        assert 'message' in res.json.keys()
694
+        assert 'details' in res.json.keys()
695
+
696
+    def test_api__get_workspace_content__err_401__unregistered_user(self):
697
+        """
698
+        Check obtain workspace content list with an unregistered user
699
+        """
700
+        self.testapp.authorization = (
701
+            'Basic',
702
+            (
703
+                'john@doe.doe',
704
+                'lapin'
705
+            )
706
+        )
707
+        res = self.testapp.get('/api/v2/workspaces/1/contents', status=401)
708
+        assert isinstance(res.json, dict)
709
+        assert 'code' in res.json.keys()
710
+        assert 'message' in res.json.keys()
711
+        assert 'details' in res.json.keys()
712
+
713
+    def test_api__get_workspace_content__err_403__workspace_does_not_exist(self):  # nopep8
714
+        """
715
+        Check obtain workspace contents list with an existing user but
716
+        an unexisting workspace
717
+        """
718
+        self.testapp.authorization = (
719
+            'Basic',
720
+            (
721
+                'admin@admin.admin',
722
+                'admin@admin.admin'
723
+            )
724
+        )
725
+        res = self.testapp.get('/api/v2/workspaces/5/contents', status=403)
726
+        assert isinstance(res.json, dict)
727
+        assert 'code' in res.json.keys()
728
+        assert 'message' in res.json.keys()
729
+        assert 'details' in res.json.keys()
730
+
731
+    def test_api__post_content_create_generic_content__ok_200__nominal_case(self) -> None:  # nopep8
732
+        """
733
+        Create generic content
734
+        """
735
+        self.testapp.authorization = (
736
+            'Basic',
737
+            (
738
+                'admin@admin.admin',
739
+                'admin@admin.admin'
740
+            )
741
+        )
742
+        params = {
743
+            'label': 'GenericCreatedContent',
744
+            'content_type_slug': 'markdownpage',
745
+        }
746
+        res = self.testapp.post_json(
747
+            '/api/v2/workspaces/1/contents',
748
+            params=params,
749
+            status=200
750
+        )
751
+        assert res
752
+        assert res.json_body
753
+        assert res.json_body['status_slug'] == 'open'
754
+        assert res.json_body['id']
755
+        assert res.json_body['content_type_slug'] == 'markdownpage'
756
+        assert res.json_body['is_archived'] is False
757
+        assert res.json_body['is_deleted'] is False
758
+        assert res.json_body['workspace_id'] == 1
759
+        assert res.json_body['slug'] == 'genericcreatedcontent'
760
+        assert res.json_body['parent_id'] is None
761
+        assert res.json_body['show_in_ui'] is True
762
+        assert res.json_body['sub_content_type_slug']
763
+        params_active = {
764
+            'parent_id': 0,
765
+            'show_archived': 0,
766
+            'show_deleted': 0,
767
+            'show_active': 1,
768
+        }
769
+        # INFO - G.M - 2018-06-165 - Verify if new content is correctly created
770
+        active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
771
+        assert res.json_body in active_contents
772
+
773
+    def test_api_put_move_content__ok_200__nominal_case(self):
774
+        """
775
+        Move content
776
+        move Apple_Pie (content_id: 8)
777
+        from Desserts folder(content_id: 3) to Salads subfolder (content_id: 4)
778
+        of workspace Recipes.
779
+        """
780
+        self.testapp.authorization = (
781
+            'Basic',
782
+            (
783
+                'admin@admin.admin',
784
+                'admin@admin.admin'
785
+            )
786
+        )
787
+        params = {
788
+            'new_parent_id': '4',  # Salads
789
+        }
790
+        params_folder1 = {
791
+            'parent_id': 3,
792
+            'show_archived': 0,
793
+            'show_deleted': 0,
794
+            'show_active': 1,
795
+        }
796
+        params_folder2 = {
797
+            'parent_id': 4,
798
+            'show_archived': 0,
799
+            'show_deleted': 0,
800
+            'show_active': 1,
801
+        }
802
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
803
+        folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
804
+        assert [content for content in folder1_contents if content['id'] == 8]  # nopep8
805
+        assert not [content for content in folder2_contents if content['id'] == 8]  # nopep8
806
+        # TODO - G.M - 2018-06-163 - Check content
807
+        res = self.testapp.put_json(
808
+            '/api/v2/workspaces/2/contents/8/move',
809
+            params=params,
810
+            status=200
811
+        )
812
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
813
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
814
+        assert not [content for content in new_folder1_contents if content['id'] == 8]  # nopep8
815
+        assert [content for content in new_folder2_contents if content['id'] == 8]  # nopep8
816
+
817
+    def test_api_put_delete_content__ok_200__nominal_case(self):
818
+        """
819
+        delete content
820
+        delete Apple_pie ( content_id: 8, parent_id: 3)
821
+        """
822
+        self.testapp.authorization = (
823
+            'Basic',
824
+            (
825
+                'admin@admin.admin',
826
+                'admin@admin.admin'
827
+            )
828
+        )
829
+        params_active = {
830
+            'parent_id': 3,
831
+            'show_archived': 0,
832
+            'show_deleted': 0,
833
+            'show_active': 1,
834
+        }
835
+        params_deleted = {
836
+            'parent_id': 3,
837
+            'show_archived': 0,
838
+            'show_deleted': 1,
839
+            'show_active': 0,
840
+        }
841
+        active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
842
+        deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
843
+        assert [content for content in active_contents if content['id'] == 8]  # nopep8
844
+        assert not [content for content in deleted_contents if content['id'] == 8]  # nopep8
845
+        # TODO - G.M - 2018-06-163 - Check content
846
+        res = self.testapp.put_json(
847
+            # INFO - G.M - 2018-06-163 - delete Apple_Pie
848
+            '/api/v2/workspaces/2/contents/8/delete',
849
+            status=200
850
+        )
851
+        new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
852
+        new_deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
853
+        assert not [content for content in new_active_contents if content['id'] == 8]  # nopep8
854
+        assert [content for content in new_deleted_contents if content['id'] == 8]  # nopep8
855
+
856
+    def test_api_put_archive_content__ok_200__nominal_case(self):
857
+        """
858
+        archive content
859
+        archive Apple_pie ( content_id: 8, parent_id: 3)
860
+        """
861
+        self.testapp.authorization = (
862
+            'Basic',
863
+            (
864
+                'admin@admin.admin',
865
+                'admin@admin.admin'
866
+            )
867
+        )
868
+        params_active = {
869
+            'parent_id': 3,
870
+            'show_archived': 0,
871
+            'show_deleted': 0,
872
+            'show_active': 1,
873
+        }
874
+        params_archived = {
875
+            'parent_id': 3,
876
+            'show_archived': 1,
877
+            'show_deleted': 0,
878
+            'show_active': 0,
879
+        }
880
+        active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
881
+        archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
882
+        assert [content for content in active_contents if content['id'] == 8]  # nopep8
883
+        assert not [content for content in archived_contents if content['id'] == 8]  # nopep8
884
+        res = self.testapp.put_json(
885
+            '/api/v2/workspaces/2/contents/8/archive',
886
+            status=200
887
+        )
888
+        new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
889
+        new_archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
890
+        assert not [content for content in new_active_contents if content['id'] == 8]  # nopep8
891
+        assert [content for content in new_archived_contents if content['id'] == 8]  # nopep8
892
+
893
+    def test_api_put_undelete_content__ok_200__nominal_case(self):
894
+        """
895
+        Undelete content
896
+        undelete Bad_Fruit_Salad ( content_id: 14, parent_id: 10)
897
+        """
898
+        self.testapp.authorization = (
899
+            'Basic',
900
+            (
901
+                'bob@fsf.local',
902
+                'foobarbaz'
903
+            )
904
+        )
905
+        params_active = {
906
+            'parent_id': 10,
907
+            'show_archived': 0,
908
+            'show_deleted': 0,
909
+            'show_active': 1,
910
+        }
911
+        params_deleted = {
912
+            'parent_id': 10,
913
+            'show_archived': 0,
914
+            'show_deleted': 1,
915
+            'show_active': 0,
916
+        }
917
+        active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
918
+        deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
919
+        assert not [content for content in active_contents if content['id'] == 14]  # nopep8
920
+        assert [content for content in deleted_contents if content['id'] == 14]  # nopep8
921
+        res = self.testapp.put_json(
922
+            '/api/v2/workspaces/2/contents/14/undelete',
923
+            status=200
924
+        )
925
+        new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
926
+        new_deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
927
+        assert [content for content in new_active_contents if content['id'] == 14]  # nopep8
928
+        assert not [content for content in new_deleted_contents if content['id'] == 14]  # nopep8
929
+
930
+    def test_api_put_unarchive_content__ok_200__nominal_case(self):
931
+        """
932
+        unarchive content,
933
+        unarchive Fruit_salads ( content_id: 13, parent_id: 10)
934
+        """
935
+        self.testapp.authorization = (
936
+            'Basic',
937
+            (
938
+                'bob@fsf.local',
939
+                'foobarbaz'
940
+            )
941
+        )
942
+        params_active = {
943
+            'parent_id': 10,
944
+            'show_archived': 0,
945
+            'show_deleted': 0,
946
+            'show_active': 1,
947
+        }
948
+        params_archived = {
949
+            'parent_id': 10,
950
+            'show_archived': 1,
951
+            'show_deleted': 0,
952
+            'show_active': 0,
953
+        }
954
+        active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
955
+        archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
956
+        assert not [content for content in active_contents if content['id'] == 13]  # nopep8
957
+        assert [content for content in archived_contents if content['id'] == 13]  # nopep8
958
+        res = self.testapp.put_json(
959
+            '/api/v2/workspaces/2/contents/13/unarchive',
960
+            status=200
961
+        )
962
+        new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
963
+        new_archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
964
+        assert [content for content in new_active_contents if content['id'] == 13]  # nopep8
965
+        assert not [content for content in new_archived_contents if content['id'] == 13]  # nopep8

+ 119 - 119
tracim/tests/library/test_webdav.py View File

@@ -162,11 +162,11 @@ class TestWebDav(StandardTest):
162 162
         )
163 163
 
164 164
         workspaces_names = [w.name for w in children]
165
-        assert 'w1' in workspaces_names, \
166
-            'w1 should be in names ({0})'.format(
165
+        assert 'Recipes' in workspaces_names, \
166
+            'Recipes should be in names ({0})'.format(
167 167
                 workspaces_names,
168 168
         )
169
-        assert 'w2' in workspaces_names, 'w2 should be in names ({0})'.format(
169
+        assert 'Others' in workspaces_names, 'Others should be in names ({0})'.format(
170 170
             workspaces_names,
171 171
         )
172 172
 
@@ -186,138 +186,138 @@ class TestWebDav(StandardTest):
186 186
         eq_(
187 187
             2,
188 188
             len(children),
189
-            msg='RootResource should return 2 workspaces instead {0}'.format(
189
+            msg='RootResource should return 3 workspaces instead {0}'.format(
190 190
                 len(children),
191 191
             )
192 192
         )
193 193
 
194 194
         workspaces_names = [w.name for w in children]
195
-        assert 'w1' in workspaces_names, 'w1 should be in names ({0})'.format(
195
+        assert 'Recipes' in workspaces_names, 'Recipes should be in names ({0})'.format(
196 196
             workspaces_names,
197 197
         )
198
-        assert 'w3' in workspaces_names, 'w3 should be in names ({0})'.format(
198
+        assert 'Business' in workspaces_names, 'Business should be in names ({0})'.format(
199 199
             workspaces_names,
200 200
         )
201 201
 
202 202
     def test_unit__list_workspace_folders__ok(self):
203 203
         provider = self._get_provider(self.app_config)
204
-        w1 = provider.getResourceInst(
205
-            '/w1/',
204
+        Recipes = provider.getResourceInst(
205
+            '/Recipes/',
206 206
             self._get_environ(
207 207
                 provider,
208 208
                 'bob@fsf.local',
209 209
             )
210 210
         )
211
-        assert w1, 'Path /w1 should return a Wrkspace instance'
211
+        assert Recipes, 'Path /Recipes should return a Wrkspace instance'
212 212
 
213
-        children = w1.getMemberList()
213
+        children = Recipes.getMemberList()
214 214
         eq_(
215 215
             2,
216 216
             len(children),
217
-            msg='w1 should list 2 folders instead {0}'.format(
217
+            msg='Recipes should list 2 folders instead {0}'.format(
218 218
                 len(children),
219 219
             ),
220 220
         )
221 221
 
222 222
         folders_names = [f.name for f in children]
223
-        assert 'w1f1' in folders_names, 'w1f1 should be in names ({0})'.format(
223
+        assert 'Salads' in folders_names, 'Salads should be in names ({0})'.format(
224 224
                 folders_names,
225 225
         )
226
-        assert 'w1f2' in folders_names, 'w1f2 should be in names ({0})'.format(
226
+        assert 'Desserts' in folders_names, 'Desserts should be in names ({0})'.format(
227 227
                 folders_names,
228 228
         )
229 229
 
230 230
     def test_unit__list_content__ok(self):
231 231
         provider = self._get_provider(self.app_config)
232
-        w1f1 = provider.getResourceInst(
233
-            '/w1/w1f1',
232
+        Salads = provider.getResourceInst(
233
+            '/Recipes/Desserts',
234 234
             self._get_environ(
235 235
                 provider,
236 236
                 'bob@fsf.local',
237 237
             )
238 238
         )
239
-        assert w1f1, 'Path /w1f1 should return a Wrkspace instance'
239
+        assert Salads, 'Path /Salads should return a Wrkspace instance'
240 240
 
241
-        children = w1f1.getMemberList()
241
+        children = Salads.getMemberList()
242 242
         eq_(
243 243
             5,
244 244
             len(children),
245
-            msg='w1f1 should list 5 folders instead {0}'.format(
245
+            msg='Salads should list 5 Files instead {0}'.format(
246 246
                 len(children),
247 247
             ),
248 248
         )
249 249
 
250 250
         content_names = [c.name for c in children]
251
-        assert 'w1f1p1.html' in content_names, \
252
-            'w1f1.html should be in names ({0})'.format(
251
+        assert 'Brownie Recipe.html' in content_names, \
252
+            'Brownie Recipe.html should be in names ({0})'.format(
253 253
                 content_names,
254 254
         )
255 255
 
256
-        assert 'w1f1t1.html' in content_names,\
257
-            'w1f1t1.html should be in names ({0})'.format(
256
+        assert 'Best Cakes ʔ.html' in content_names,\
257
+            'Best Cakes ʔ.html should be in names ({0})'.format(
258 258
                 content_names,
259 259
         )
260
-        assert 'w1f1d1.txt' in content_names,\
261
-            'w1f1d1.txt should be in names ({0})'.format(content_names,)
260
+        assert 'Apple_Pie.txt' in content_names,\
261
+            'Apple_Pie.txt should be in names ({0})'.format(content_names,)
262 262
 
263
-        assert 'w1f1f1' in content_names, \
264
-            'w1f1f1 should be in names ({0})'.format(
263
+        assert 'Fruits Desserts' in content_names, \
264
+            'Fruits Desserts should be in names ({0})'.format(
265 265
                 content_names,
266 266
         )
267 267
 
268
-        assert 'w1f1d2.html' in content_names,\
269
-            'w1f1d2.html should be in names ({0})'.format(
268
+        assert 'Tiramisu Recipe.html' in content_names,\
269
+            'Tiramisu Recipe.html should be in names ({0})'.format(
270 270
                 content_names,
271 271
         )
272 272
 
273 273
     def test_unit__get_content__ok(self):
274 274
         provider = self._get_provider(self.app_config)
275
-        w1f1d1 = provider.getResourceInst(
276
-            '/w1/w1f1/w1f1d1.txt',
275
+        pie = provider.getResourceInst(
276
+            '/Recipes/Desserts/Apple_Pie.txt',
277 277
             self._get_environ(
278 278
                 provider,
279 279
                 'bob@fsf.local',
280 280
             )
281 281
         )
282 282
 
283
-        assert w1f1d1, 'w1f1d1 should be found'
284
-        eq_('w1f1d1.txt', w1f1d1.name)
283
+        assert pie, 'Apple_Pie should be found'
284
+        eq_('Apple_Pie.txt', pie.name)
285 285
 
286 286
     def test_unit__delete_content__ok(self):
287 287
         provider = self._get_provider(self.app_config)
288
-        w1f1d1 = provider.getResourceInst(
289
-            '/w1/w1f1/w1f1d1.txt',
288
+        pie = provider.getResourceInst(
289
+            '/Recipes/Desserts/Apple_Pie.txt',
290 290
             self._get_environ(
291 291
                 provider,
292 292
                 'bob@fsf.local',
293 293
             )
294 294
         )
295 295
         
296
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
297
-            .filter(Content.label == 'w1f1d1') \
296
+        content_pie = self.session.query(ContentRevisionRO) \
297
+            .filter(Content.label == 'Apple_Pie') \
298 298
             .one()  # It must exist only one revision, cf fixtures
299 299
         eq_(
300 300
             False,
301
-            content_w1f1d1.is_deleted,
301
+            content_pie.is_deleted,
302 302
             msg='Content should not be deleted !'
303 303
         )
304
-        content_w1f1d1_id = content_w1f1d1.content_id
304
+        content_pie_id = content_pie.content_id
305 305
 
306
-        w1f1d1.delete()
306
+        pie.delete()
307 307
 
308 308
         self.session.flush()
309
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
310
-            .filter(Content.content_id == content_w1f1d1_id) \
309
+        content_pie = self.session.query(ContentRevisionRO) \
310
+            .filter(Content.content_id == content_pie_id) \
311 311
             .order_by(Content.revision_id.desc()) \
312 312
             .first()
313 313
         eq_(
314 314
             True,
315
-            content_w1f1d1.is_deleted,
315
+            content_pie.is_deleted,
316 316
             msg='Content should be deleted !'
317 317
         )
318 318
 
319 319
         result = provider.getResourceInst(
320
-            '/w1/w1f1/w1f1d1.txt',
320
+            '/Recipes/Desserts/Apple_Pie.txt',
321 321
             self._get_environ(
322 322
                 provider,
323 323
                 'bob@fsf.local',
@@ -334,7 +334,7 @@ class TestWebDav(StandardTest):
334 334
             'bob@fsf.local',
335 335
         )
336 336
         result = provider.getResourceInst(
337
-            '/w1/w1f1/new_file.txt',
337
+            '/Recipes/Salads/greek_salad.txt',
338 338
             environ,
339 339
         )
340 340
 
@@ -345,17 +345,17 @@ class TestWebDav(StandardTest):
345 345
         result = self._put_new_text_file(
346 346
             provider,
347 347
             environ,
348
-            '/w1/w1f1/new_file.txt',
349
-            b'hello\n',
348
+            '/Recipes/Salads/greek_salad.txt',
349
+            b'Greek Salad\n',
350 350
         )
351 351
 
352 352
         assert result, 'Result should not be None instead {0}'.format(
353 353
             result
354 354
         )
355 355
         eq_(
356
-            b'hello\n',
356
+            b'Greek Salad\n',
357 357
             result.content.depot_file.file.read(),
358
-            msg='fiel content should be "hello\n" but it is {0}'.format(
358
+            msg='fiel content should be "Greek Salad\n" but it is {0}'.format(
359 359
                 result.content.depot_file.file.read()
360 360
             )
361 361
         )
@@ -367,7 +367,7 @@ class TestWebDav(StandardTest):
367 367
             'bob@fsf.local',
368 368
         )
369 369
         new_file = provider.getResourceInst(
370
-            '/w1/w1f1/new_file.txt',
370
+            '/Recipes/Salads/greek_salad.txt',
371 371
             environ,
372 372
         )
373 373
 
@@ -379,15 +379,15 @@ class TestWebDav(StandardTest):
379 379
         new_file = self._put_new_text_file(
380 380
             provider,
381 381
             environ,
382
-            '/w1/w1f1/new_file.txt',
383
-            b'hello\n',
382
+            '/Recipes/Salads/greek_salad.txt',
383
+            b'Greek Salad\n',
384 384
         )
385 385
         assert new_file, 'Result should not be None instead {0}'.format(
386 386
             new_file
387 387
         )
388 388
 
389 389
         content_new_file = self.session.query(ContentRevisionRO) \
390
-            .filter(Content.label == 'new_file') \
390
+            .filter(Content.label == 'greek_salad') \
391 391
             .one()  # It must exist only one revision
392 392
         eq_(
393 393
             False,
@@ -400,18 +400,18 @@ class TestWebDav(StandardTest):
400 400
         new_file.delete()
401 401
 
402 402
         self.session.flush()
403
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
403
+        content_pie = self.session.query(ContentRevisionRO) \
404 404
             .filter(Content.content_id == content_new_file_id) \
405 405
             .order_by(Content.revision_id.desc()) \
406 406
             .first()
407 407
         eq_(
408 408
             True,
409
-            content_w1f1d1.is_deleted,
409
+            content_pie.is_deleted,
410 410
             msg='Content should be deleted !'
411 411
         )
412 412
 
413 413
         result = provider.getResourceInst(
414
-            '/w1/w1f1/new_file.txt',
414
+            '/Recipes/Salads/greek_salad.txt',
415 415
             self._get_environ(
416 416
                 provider,
417 417
                 'bob@fsf.local',
@@ -425,8 +425,8 @@ class TestWebDav(StandardTest):
425 425
         new_file = self._put_new_text_file(
426 426
             provider,
427 427
             environ,
428
-            '/w1/w1f1/new_file.txt',
429
-            b'hello\n',
428
+            '/Recipes/Salads/greek_salad.txt',
429
+            b'greek_salad\n',
430 430
         )
431 431
         assert new_file, 'Result should not be None instead {0}'.format(
432 432
             new_file
@@ -434,19 +434,19 @@ class TestWebDav(StandardTest):
434 434
 
435 435
         # Previous file is still dleeted
436 436
         self.session.flush()
437
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
437
+        content_pie = self.session.query(ContentRevisionRO) \
438 438
             .filter(Content.content_id == content_new_file_id) \
439 439
             .order_by(Content.revision_id.desc()) \
440 440
             .first()
441 441
         eq_(
442 442
             True,
443
-            content_w1f1d1.is_deleted,
443
+            content_pie.is_deleted,
444 444
             msg='Content should be deleted !'
445 445
         )
446 446
 
447 447
         # And an other file exist for this name
448 448
         content_new_new_file = self.session.query(ContentRevisionRO) \
449
-            .filter(Content.label == 'new_file') \
449
+            .filter(Content.label == 'greek_salad') \
450 450
             .order_by(Content.revision_id.desc()) \
451 451
             .first()
452 452
         assert content_new_new_file.content_id != content_new_file_id,\
@@ -464,29 +464,29 @@ class TestWebDav(StandardTest):
464 464
             provider,
465 465
             'bob@fsf.local',
466 466
         )
467
-        w1f1d1 = provider.getResourceInst(
468
-            '/w1/w1f1/w1f1d1.txt',
467
+        pie = provider.getResourceInst(
468
+            '/Recipes/Desserts/Apple_Pie.txt',
469 469
             environ,
470 470
         )
471 471
 
472
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
473
-            .filter(Content.label == 'w1f1d1') \
472
+        content_pie = self.session.query(ContentRevisionRO) \
473
+            .filter(Content.label == 'Apple_Pie') \
474 474
             .one()  # It must exist only one revision, cf fixtures
475
-        assert content_w1f1d1, 'w1f1d1 should be exist'
476
-        content_w1f1d1_id = content_w1f1d1.content_id
475
+        assert content_pie, 'Apple_Pie should be exist'
476
+        content_pie_id = content_pie.content_id
477 477
 
478
-        w1f1d1.moveRecursive('/w1/w1f1/w1f1d1_RENAMED.txt')
478
+        pie.moveRecursive('/Recipes/Desserts/Apple_Pie_RENAMED.txt')
479 479
 
480 480
         # Database content is renamed
481
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
482
-            .filter(ContentRevisionRO.content_id == content_w1f1d1_id) \
481
+        content_pie = self.session.query(ContentRevisionRO) \
482
+            .filter(ContentRevisionRO.content_id == content_pie_id) \
483 483
             .order_by(ContentRevisionRO.revision_id.desc()) \
484 484
             .first()
485 485
         eq_(
486
-            'w1f1d1_RENAMED',
487
-            content_w1f1d1.label,
488
-            msg='File should be labeled w1f1d1_RENAMED, not {0}'.format(
489
-                content_w1f1d1.label
486
+            'Apple_Pie_RENAMED',
487
+            content_pie.label,
488
+            msg='File should be labeled Apple_Pie_RENAMED, not {0}'.format(
489
+                content_pie.label
490 490
             )
491 491
         )
492 492
 
@@ -496,34 +496,34 @@ class TestWebDav(StandardTest):
496 496
             provider,
497 497
             'bob@fsf.local',
498 498
         )
499
-        w1f1d1 = provider.getResourceInst(
500
-            '/w1/w1f1/w1f1d1.txt',
499
+        pie = provider.getResourceInst(
500
+            '/Recipes/Desserts/Apple_Pie.txt',
501 501
             environ,
502 502
         )
503 503
 
504
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
505
-            .filter(Content.label == 'w1f1d1') \
504
+        content_pie = self.session.query(ContentRevisionRO) \
505
+            .filter(Content.label == 'Apple_Pie') \
506 506
             .one()  # It must exist only one revision, cf fixtures
507
-        assert content_w1f1d1, 'w1f1d1 should be exist'
508
-        content_w1f1d1_id = content_w1f1d1.content_id
509
-        content_w1f1d1_parent = content_w1f1d1.parent
507
+        assert content_pie, 'Apple_Pie should be exist'
508
+        content_pie_id = content_pie.content_id
509
+        content_pie_parent = content_pie.parent
510 510
         eq_(
511
-            content_w1f1d1_parent.label,
512
-            'w1f1',
513
-            msg='field parent should be w1f1',
511
+            content_pie_parent.label,
512
+            'Desserts',
513
+            msg='field parent should be Desserts',
514 514
         )
515 515
 
516
-        w1f1d1.moveRecursive('/w1/w1f2/w1f1d1.txt')  # move in f2
516
+        pie.moveRecursive('/Recipes/Salads/Apple_Pie.txt')  # move in f2
517 517
 
518 518
         # Database content is moved
519
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
520
-            .filter(ContentRevisionRO.content_id == content_w1f1d1_id) \
519
+        content_pie = self.session.query(ContentRevisionRO) \
520
+            .filter(ContentRevisionRO.content_id == content_pie_id) \
521 521
             .order_by(ContentRevisionRO.revision_id.desc()) \
522 522
             .first()
523 523
 
524
-        assert content_w1f1d1.parent.label != content_w1f1d1_parent.label,\
525
-            'file should be moved in w1f2 but is in {0}'.format(
526
-                content_w1f1d1.parent.label
524
+        assert content_pie.parent.label != content_pie_parent.label,\
525
+            'file should be moved in Salads but is in {0}'.format(
526
+                content_pie.parent.label
527 527
         )
528 528
 
529 529
     def test_unit__move_and_rename_content__ok(self):
@@ -532,39 +532,39 @@ class TestWebDav(StandardTest):
532 532
             provider,
533 533
             'bob@fsf.local',
534 534
         )
535
-        w1f1d1 = provider.getResourceInst(
536
-            '/w1/w1f1/w1f1d1.txt',
535
+        pie = provider.getResourceInst(
536
+            '/Recipes/Desserts/Apple_Pie.txt',
537 537
             environ,
538 538
         )
539 539
 
540
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
541
-            .filter(Content.label == 'w1f1d1') \
540
+        content_pie = self.session.query(ContentRevisionRO) \
541
+            .filter(Content.label == 'Apple_Pie') \
542 542
             .one()  # It must exist only one revision, cf fixtures
543
-        assert content_w1f1d1, 'w1f1d1 should be exist'
544
-        content_w1f1d1_id = content_w1f1d1.content_id
545
-        content_w1f1d1_parent = content_w1f1d1.parent
543
+        assert content_pie, 'Apple_Pie should be exist'
544
+        content_pie_id = content_pie.content_id
545
+        content_pie_parent = content_pie.parent
546 546
         eq_(
547
-            content_w1f1d1_parent.label,
548
-            'w1f1',
549
-            msg='field parent should be w1f1',
547
+            content_pie_parent.label,
548
+            'Desserts',
549
+            msg='field parent should be Desserts',
550 550
         )
551 551
 
552
-        w1f1d1.moveRecursive('/w1/w1f2/w1f1d1_RENAMED.txt')
552
+        pie.moveRecursive('/Others/Infos/Apple_Pie_RENAMED.txt')
553 553
 
554 554
         # Database content is moved
555
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
556
-            .filter(ContentRevisionRO.content_id == content_w1f1d1_id) \
555
+        content_pie = self.session.query(ContentRevisionRO) \
556
+            .filter(ContentRevisionRO.content_id == content_pie_id) \
557 557
             .order_by(ContentRevisionRO.revision_id.desc()) \
558 558
             .first()
559
-        assert content_w1f1d1.parent.label != content_w1f1d1_parent.label,\
560
-            'file should be moved in w1f2 but is in {0}'.format(
561
-                content_w1f1d1.parent.label
559
+        assert content_pie.parent.label != content_pie_parent.label,\
560
+            'file should be moved in Recipesf2 but is in {0}'.format(
561
+                content_pie.parent.label
562 562
         )
563 563
         eq_(
564
-            'w1f1d1_RENAMED',
565
-            content_w1f1d1.label,
566
-            msg='File should be labeled w1f1d1_RENAMED, not {0}'.format(
567
-                content_w1f1d1.label
564
+            'Apple_Pie_RENAMED',
565
+            content_pie.label,
566
+            msg='File should be labeled Apple_Pie_RENAMED, not {0}'.format(
567
+                content_pie.label
568 568
             )
569 569
         )
570 570
 
@@ -575,23 +575,23 @@ class TestWebDav(StandardTest):
575 575
             'bob@fsf.local',
576 576
         )
577 577
         content_to_move_res = provider.getResourceInst(
578
-            '/w1/w1f1/w1f1d1.txt',
578
+            '/Recipes/Desserts/Apple_Pie.txt',
579 579
             environ,
580 580
         )
581 581
 
582 582
         content_to_move = self.session.query(ContentRevisionRO) \
583
-            .filter(Content.label == 'w1f1d1') \
583
+            .filter(Content.label == 'Apple_Pie') \
584 584
             .one()  # It must exist only one revision, cf fixtures
585
-        assert content_to_move, 'w1f1d1 should be exist'
585
+        assert content_to_move, 'Apple_Pie should be exist'
586 586
         content_to_move_id = content_to_move.content_id
587 587
         content_to_move_parent = content_to_move.parent
588 588
         eq_(
589 589
             content_to_move_parent.label,
590
-            'w1f1',
591
-            msg='field parent should be w1f1',
590
+            'Desserts',
591
+            msg='field parent should be Desserts',
592 592
         )
593 593
 
594
-        content_to_move_res.moveRecursive('/w2/w2f1/w1f1d1.txt')  # move in w2, f1
594
+        content_to_move_res.moveRecursive('/Others/Infos/Apple_Pie.txt')  # move in Business, f1
595 595
 
596 596
         # Database content is moved
597 597
         content_to_move = self.session.query(ContentRevisionRO) \
@@ -601,8 +601,8 @@ class TestWebDav(StandardTest):
601 601
 
602 602
         assert content_to_move.parent, 'Content should have a parent'
603 603
 
604
-        assert content_to_move.parent.label == 'w2f1',\
605
-            'file should be moved in w2f1 but is in {0}'.format(
604
+        assert content_to_move.parent.label == 'Infos',\
605
+            'file should be moved in Infos but is in {0}'.format(
606 606
                 content_to_move.parent.label
607 607
         )
608 608
 
@@ -613,7 +613,7 @@ class TestWebDav(StandardTest):
613 613
             'bob@fsf.local',
614 614
         )
615 615
         result = provider.getResourceInst(
616
-            '/w1/w1f1/new_file.txt',
616
+            '/Recipes/Salads/greek_salad.txt',
617 617
             environ,
618 618
         )
619 619
 
@@ -624,7 +624,7 @@ class TestWebDav(StandardTest):
624 624
         result = self._put_new_text_file(
625 625
             provider,
626 626
             environ,
627
-            '/w1/w1f1/new_file.txt',
627
+            '/Recipes/Salads/greek_salad.txt',
628 628
             b'hello\n',
629 629
         )
630 630
 

+ 172 - 2
tracim/views/core_api/schemas.py View File

@@ -4,6 +4,14 @@ from marshmallow import post_load
4 4
 from marshmallow.validate import OneOf
5 5
 
6 6
 from tracim.models.auth import Profile
7
+from tracim.models.contents import CONTENT_DEFAULT_TYPE
8
+from tracim.models.contents import CONTENT_DEFAULT_STATUS
9
+from tracim.models.contents import GlobalStatus
10
+from tracim.models.contents import open_status
11
+from tracim.models.context_models import ContentCreation
12
+from tracim.models.context_models import MoveParams
13
+from tracim.models.context_models import WorkspaceAndContentPath
14
+from tracim.models.context_models import ContentFilter
7 15
 from tracim.models.context_models import LoginCredentials
8 16
 from tracim.models.data import UserRoleInWorkspace
9 17
 
@@ -65,12 +73,63 @@ class UserSchema(marshmallow.Schema):
65 73
         description = 'User account of Tracim'
66 74
 
67 75
 
76
+# Path Schemas
77
+
78
+
68 79
 class UserIdPathSchema(marshmallow.Schema):
69
-    user_id = marshmallow.fields.Int(example=3)
80
+    user_id = marshmallow.fields.Int(example=3, required=True)
70 81
 
71 82
 
72 83
 class WorkspaceIdPathSchema(marshmallow.Schema):
73
-    workspace_id = marshmallow.fields.Int(example=4)
84
+    workspace_id = marshmallow.fields.Int(example=4, required=True)
85
+
86
+
87
+class ContentIdPathSchema(marshmallow.Schema):
88
+    content_id = marshmallow.fields.Int(example=6, required=True)
89
+
90
+
91
+class WorkspaceAndContentIdPathSchema(WorkspaceIdPathSchema, ContentIdPathSchema):
92
+    @post_load
93
+    def make_path_object(self, data):
94
+        return WorkspaceAndContentPath(**data)
95
+
96
+
97
+class FilterContentQuerySchema(marshmallow.Schema):
98
+    parent_id = workspace_id = marshmallow.fields.Int(
99
+        example=2,
100
+        default=None,
101
+        description='allow to filter items in a folder.'
102
+                    ' If not set, then return all contents.'
103
+                    ' If set to 0, then return root contents.'
104
+                    ' If set to another value, return all contents'
105
+                    ' directly included in the folder parent_id'
106
+    )
107
+    show_archived = marshmallow.fields.Int(
108
+        example=0,
109
+        default=0,
110
+        description='if set to 1, then show archived contents.'
111
+                    ' Default is 0 - hide archived content'
112
+    )
113
+    show_deleted = marshmallow.fields.Int(
114
+        example=0,
115
+        default=0,
116
+        description='if set to 1, then show deleted contents.'
117
+                    ' Default is 0 - hide deleted content'
118
+    )
119
+    show_active = marshmallow.fields.Int(
120
+        example=1,
121
+        default=1,
122
+        description='f set to 1, then show active contents. '
123
+                    'Default is 1 - show active content.'
124
+                    ' Note: active content are content '
125
+                    'that is neither archived nor deleted. '
126
+                    'The reason for this parameter to exist is for example '
127
+                    'to allow to show only archived documents'
128
+    )
129
+    @post_load
130
+    def make_content_filter(self, data):
131
+        return ContentFilter(**data)
132
+###
74 133
 
75 134
 
76 135
 class BasicAuthSchema(marshmallow.Schema):
@@ -188,3 +247,114 @@ class ApplicationSchema(marshmallow.Schema):
188 247
 
189 248
     class Meta:
190 249
         description = 'Tracim Application informations'
250
+
251
+
252
+class StatusSchema(marshmallow.Schema):
253
+    slug = marshmallow.fields.String(
254
+        example='open',
255
+        description='the slug represents the type of status. '
256
+                    'Statuses are open, closed-validated, closed-invalidated, closed-deprecated'  # nopep8
257
+    )
258
+    global_status = marshmallow.fields.String(
259
+        example='Open',
260
+        description='global_status: open, closed',
261
+        validate=OneOf([status.value for status in GlobalStatus]),
262
+    )
263
+    label = marshmallow.fields.String(example='Open')
264
+    fa_icon = marshmallow.fields.String(example='fa-check')
265
+    hexcolor = marshmallow.fields.String(example='#0000FF')
266
+
267
+
268
+class ContentTypeSchema(marshmallow.Schema):
269
+    slug = marshmallow.fields.String(
270
+        example='pagehtml',
271
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
272
+    )
273
+    fa_icon = marshmallow.fields.String(
274
+        example='fa-file-text-o',
275
+        description='CSS class of the icon. Example: file-o for using Fontawesome file-o icon',  # nopep8
276
+    )
277
+    hexcolor = marshmallow.fields.String(
278
+        example="#FF0000",
279
+        description='HTML encoded color associated to the application. Example:#FF0000 for red'  # nopep8
280
+    )
281
+    label = marshmallow.fields.String(
282
+        example='Text Documents'
283
+    )
284
+    creation_label = marshmallow.fields.String(
285
+        example='Write a document'
286
+    )
287
+    available_statuses = marshmallow.fields.Nested(
288
+        StatusSchema,
289
+        many=True
290
+    )
291
+
292
+
293
+class ContentMoveSchema(marshmallow.Schema):
294
+    # TODO - G.M - 30-05-2018 - Read and apply this note
295
+    # Note:
296
+    # if the new workspace is different, then the backend
297
+    # must check if the user is allowed to move to this workspace
298
+    # (the user must be content manager of both workspaces)
299
+    new_parent_id = marshmallow.fields.Int(
300
+        example=42,
301
+        description='id of the new parent content id.'
302
+    )
303
+
304
+    @post_load
305
+    def make_move_params(self, data):
306
+        return MoveParams(**data)
307
+
308
+
309
+class ContentCreationSchema(marshmallow.Schema):
310
+    label = marshmallow.fields.String(
311
+        example='contract for client XXX',
312
+        description='Title of the content to create'
313
+    )
314
+    content_type_slug = marshmallow.fields.String(
315
+        example='htmlpage',
316
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
317
+    )
318
+
319
+    @post_load
320
+    def make_content_filter(self, data):
321
+        return ContentCreation(**data)
322
+
323
+
324
+class ContentDigestSchema(marshmallow.Schema):
325
+    id = marshmallow.fields.Int(example=6)
326
+    slug = marshmallow.fields.Str(example='intervention-report-12')
327
+    parent_id = marshmallow.fields.Int(
328
+        example=34,
329
+        allow_none=True,
330
+        default=None
331
+    )
332
+    workspace_id = marshmallow.fields.Int(
333
+        example=19,
334
+    )
335
+    label = marshmallow.fields.Str(example='Intervention Report 12')
336
+    content_type_slug = marshmallow.fields.Str(
337
+        example='htmlpage',
338
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
339
+    )
340
+    sub_content_type_slug = marshmallow.fields.List(
341
+        marshmallow.fields.Str,
342
+        description='list of content types allowed as sub contents. '
343
+                    'This field is required for folder contents, '
344
+                    'set it to empty list in other cases'
345
+    )
346
+    status_slug = marshmallow.fields.Str(
347
+        example='closed-deprecated',
348
+        validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
349
+        description='this slug is found in content_type available statuses',
350
+        default=open_status
351
+    )
352
+    is_archived = marshmallow.fields.Bool(example=False, default=False)
353
+    is_deleted = marshmallow.fields.Bool(example=False, default=False)
354
+    show_in_ui = marshmallow.fields.Bool(
355
+        example=True,
356
+        description='if false, then do not show content in the treeview. '
357
+                    'This may his maybe used for specific contents or '
358
+                    'for sub-contents. Default is True. '
359
+                    'In first version of the API, this field is always True',
360
+    )

+ 21 - 2
tracim/views/core_api/system_controller.py View File

@@ -1,10 +1,11 @@
1 1
 # coding=utf-8
2 2
 from pyramid.config import Configurator
3
-
4
-from tracim.exceptions import NotAuthenticated, InsufficientUserProfile
3
+from tracim.exceptions import NotAuthenticated
4
+from tracim.exceptions import InsufficientUserProfile
5 5
 from tracim.lib.utils.authorization import require_profile
6 6
 from tracim.models import Group
7 7
 from tracim.models.applications import applications
8
+from tracim.models.contents import CONTENT_DEFAULT_TYPE
8 9
 
9 10
 try:  # Python 3.5+
10 11
     from http import HTTPStatus
@@ -15,6 +16,7 @@ from tracim import TracimRequest
15 16
 from tracim.extensions import hapic
16 17
 from tracim.views.controllers import Controller
17 18
 from tracim.views.core_api.schemas import ApplicationSchema
19
+from tracim.views.core_api.schemas import ContentTypeSchema
18 20
 
19 21
 
20 22
 class SystemController(Controller):
@@ -30,6 +32,18 @@ class SystemController(Controller):
30 32
         """
31 33
         return applications
32 34
 
35
+    @hapic.with_api_doc()
36
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
37
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
38
+    @require_profile(Group.TIM_USER)
39
+    @hapic.output_body(ContentTypeSchema(many=True),)
40
+    def content_types(self, context, request: TracimRequest, hapic_data=None):
41
+        """
42
+        Get list of alls applications installed in this tracim instance.
43
+        """
44
+
45
+        return CONTENT_DEFAULT_TYPE
46
+
33 47
     def bind(self, configurator: Configurator) -> None:
34 48
         """
35 49
         Create all routes and views using pyramid configurator
@@ -40,3 +54,8 @@ class SystemController(Controller):
40 54
         configurator.add_route('applications', '/system/applications', request_method='GET')  # nopep8
41 55
         configurator.add_view(self.applications, route_name='applications')
42 56
 
57
+        # Content_types
58
+        configurator.add_route('content_types', '/system/content_types', request_method='GET')  # nopep8
59
+        configurator.add_view(self.content_types, route_name='content_types')
60
+
61
+

+ 286 - 12
tracim/views/core_api/workspace_controller.py View File

@@ -1,30 +1,36 @@
1 1
 import typing
2 2
 
3
+import transaction
3 4
 from pyramid.config import Configurator
4
-from sqlalchemy.orm.exc import NoResultFound
5
-
6
-from tracim.lib.core.userworkspace import RoleApi
7
-from tracim.lib.utils.authorization import require_workspace_role
8
-from tracim.models.context_models import WorkspaceInContext
9
-from tracim.models.context_models import UserRoleWorkspaceInContext
10
-from tracim.models.data import UserRoleInWorkspace
11
-
12 5
 try:  # Python 3.5+
13 6
     from http import HTTPStatus
14 7
 except ImportError:
15 8
     from http import client as HTTPStatus
16 9
 
17 10
 from tracim import hapic, TracimRequest
11
+from tracim.lib.core.workspace import WorkspaceApi
12
+from tracim.lib.core.content import ContentApi
13
+from tracim.lib.core.userworkspace import RoleApi
14
+from tracim.lib.utils.authorization import require_workspace_role
15
+from tracim.models.data import UserRoleInWorkspace, ActionDescription
16
+from tracim.models.context_models import UserRoleWorkspaceInContext
17
+from tracim.models.context_models import ContentInContext
18 18
 from tracim.exceptions import NotAuthenticated
19 19
 from tracim.exceptions import InsufficientUserProfile
20 20
 from tracim.exceptions import WorkspaceNotFound
21
-from tracim.lib.core.user import UserApi
22
-from tracim.lib.core.workspace import WorkspaceApi
23 21
 from tracim.views.controllers import Controller
22
+from tracim.views.core_api.schemas import FilterContentQuerySchema
23
+from tracim.views.core_api.schemas import ContentMoveSchema
24
+from tracim.views.core_api.schemas import NoContentSchema
25
+from tracim.views.core_api.schemas import ContentCreationSchema
26
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
27
+from tracim.views.core_api.schemas import ContentDigestSchema
24 28
 from tracim.views.core_api.schemas import WorkspaceSchema
25
-from tracim.views.core_api.schemas import UserSchema
26 29
 from tracim.views.core_api.schemas import WorkspaceIdPathSchema
27 30
 from tracim.views.core_api.schemas import WorkspaceMemberSchema
31
+from tracim.models.contents import ContentTypeLegacy as ContentType
32
+from tracim.models.revision_protection import new_revision
33
+
28 34
 
29 35
 class WorkspaceController(Controller):
30 36
 
@@ -77,14 +83,282 @@ class WorkspaceController(Controller):
77 83
             for user_role in roles
78 84
         ]
79 85
 
86
+    @hapic.with_api_doc()
87
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
88
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
89
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
90
+    @require_workspace_role(UserRoleInWorkspace.READER)
91
+    @hapic.input_path(WorkspaceIdPathSchema())
92
+    @hapic.input_query(FilterContentQuerySchema())
93
+    @hapic.output_body(ContentDigestSchema(many=True))
94
+    def workspace_content(
95
+            self,
96
+            context,
97
+            request: TracimRequest,
98
+            hapic_data=None,
99
+    ) -> typing.List[ContentInContext]:
100
+        """
101
+        return list of contents found in the workspace
102
+        """
103
+        app_config = request.registry.settings['CFG']
104
+        content_filter = hapic_data.query
105
+        api = ContentApi(
106
+            current_user=request.current_user,
107
+            session=request.dbsession,
108
+            config=app_config,
109
+            show_archived=content_filter.show_archived,
110
+            show_deleted=content_filter.show_deleted,
111
+            show_active=content_filter.show_active,
112
+        )
113
+        contents = api.get_all(
114
+            parent_id=content_filter.parent_id,
115
+            workspace=request.current_workspace,
116
+        )
117
+        contents = [
118
+            api.get_content_in_context(content) for content in contents
119
+        ]
120
+        return contents
121
+
122
+    @hapic.with_api_doc()
123
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
124
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
125
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
126
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
127
+    @hapic.input_path(WorkspaceIdPathSchema())
128
+    @hapic.input_body(ContentCreationSchema())
129
+    @hapic.output_body(ContentDigestSchema())
130
+    def create_generic_empty_content(
131
+            self,
132
+            context,
133
+            request: TracimRequest,
134
+            hapic_data=None,
135
+    ) -> typing.List[ContentInContext]:
136
+        """
137
+        create a generic empty content
138
+        """
139
+        app_config = request.registry.settings['CFG']
140
+        creation_data = hapic_data.body
141
+        api = ContentApi(
142
+            current_user=request.current_user,
143
+            session=request.dbsession,
144
+            config=app_config,
145
+        )
146
+        content = api.create(
147
+            label=creation_data.label,
148
+            content_type=creation_data.content_type_slug,
149
+            workspace=request.current_workspace,
150
+        )
151
+        api.save(content, ActionDescription.CREATION)
152
+        content = api.get_content_in_context(content)
153
+        return content
154
+
155
+    @hapic.with_api_doc()
156
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
157
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
158
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
159
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
160
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
161
+    @hapic.input_body(ContentMoveSchema())
162
+    @hapic.output_body(NoContentSchema())
163
+    def move_content(
164
+            self,
165
+            context,
166
+            request: TracimRequest,
167
+            hapic_data=None,
168
+    ) -> typing.List[ContentInContext]:
169
+        """
170
+        move a content
171
+        """
172
+        app_config = request.registry.settings['CFG']
173
+        path_data = hapic_data.path
174
+        move_data = hapic_data.body
175
+        api = ContentApi(
176
+            current_user=request.current_user,
177
+            session=request.dbsession,
178
+            config=app_config,
179
+        )
180
+        content = api.get_one(
181
+            path_data.content_id,
182
+            content_type=ContentType.Any
183
+        )
184
+        new_parent = api.get_one(
185
+            move_data.new_parent_id, content_type=ContentType.Any
186
+        )
187
+        with new_revision(
188
+                session=request.dbsession,
189
+                tm=transaction.manager,
190
+                content=content
191
+        ):
192
+            api.move(content, new_parent=new_parent)
193
+        return
194
+
195
+    @hapic.with_api_doc()
196
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
197
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
198
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
199
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
200
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
201
+    @hapic.output_body(NoContentSchema())
202
+    def delete_content(
203
+            self,
204
+            context,
205
+            request: TracimRequest,
206
+            hapic_data=None,
207
+    ) -> typing.List[ContentInContext]:
208
+        """
209
+        delete a content
210
+        """
211
+        app_config = request.registry.settings['CFG']
212
+        path_data = hapic_data.path
213
+        api = ContentApi(
214
+            current_user=request.current_user,
215
+            session=request.dbsession,
216
+            config=app_config,
217
+        )
218
+        content = api.get_one(
219
+            path_data.content_id,
220
+            content_type=ContentType.Any
221
+        )
222
+        with new_revision(
223
+                session=request.dbsession,
224
+                tm=transaction.manager,
225
+                content=content
226
+        ):
227
+            api.delete(content)
228
+        return
229
+
230
+    @hapic.with_api_doc()
231
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
232
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
233
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
234
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
235
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
236
+    @hapic.output_body(NoContentSchema())
237
+    def undelete_content(
238
+            self,
239
+            context,
240
+            request: TracimRequest,
241
+            hapic_data=None,
242
+    ) -> typing.List[ContentInContext]:
243
+        """
244
+        undelete a content
245
+        """
246
+        app_config = request.registry.settings['CFG']
247
+        path_data = hapic_data.path
248
+        api = ContentApi(
249
+            current_user=request.current_user,
250
+            session=request.dbsession,
251
+            config=app_config,
252
+            show_deleted=True,
253
+        )
254
+        content = api.get_one(
255
+            path_data.content_id,
256
+            content_type=ContentType.Any
257
+        )
258
+        with new_revision(
259
+                session=request.dbsession,
260
+                tm=transaction.manager,
261
+                content=content
262
+        ):
263
+            api.undelete(content)
264
+        return
265
+
266
+    @hapic.with_api_doc()
267
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
268
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
269
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
270
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
271
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
272
+    @hapic.output_body(NoContentSchema())
273
+    def archive_content(
274
+            self,
275
+            context,
276
+            request: TracimRequest,
277
+            hapic_data=None,
278
+    ) -> typing.List[ContentInContext]:
279
+        """
280
+        archive a content
281
+        """
282
+        app_config = request.registry.settings['CFG']
283
+        path_data = hapic_data.path
284
+        api = ContentApi(
285
+            current_user=request.current_user,
286
+            session=request.dbsession,
287
+            config=app_config,
288
+        )
289
+        content = api.get_one(path_data.content_id, content_type=ContentType.Any)
290
+        with new_revision(
291
+                session=request.dbsession,
292
+                tm=transaction.manager,
293
+                content=content
294
+        ):
295
+            api.archive(content)
296
+        return
297
+
298
+    @hapic.with_api_doc()
299
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
300
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
301
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
302
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
303
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
304
+    @hapic.output_body(NoContentSchema())
305
+    def unarchive_content(
306
+            self,
307
+            context,
308
+            request: TracimRequest,
309
+            hapic_data=None,
310
+    ) -> typing.List[ContentInContext]:
311
+        """
312
+        unarchive a content
313
+        """
314
+        app_config = request.registry.settings['CFG']
315
+        path_data = hapic_data.path
316
+        api = ContentApi(
317
+            current_user=request.current_user,
318
+            session=request.dbsession,
319
+            config=app_config,
320
+            show_archived=True,
321
+        )
322
+        content = api.get_one(
323
+            path_data.content_id,
324
+            content_type=ContentType.Any
325
+        )
326
+        with new_revision(
327
+                session=request.dbsession,
328
+                tm=transaction.manager,
329
+                content=content
330
+        ):
331
+            api.unarchive(content)
332
+        return
333
+
80 334
     def bind(self, configurator: Configurator) -> None:
81 335
         """
82 336
         Create all routes and views using
83 337
         pyramid configurator for this controller
84 338
         """
85 339
 
86
-        # Applications
340
+        # Workspace
87 341
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
88 342
         configurator.add_view(self.workspace, route_name='workspace')
343
+        # Workspace Members (Roles)
89 344
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
90 345
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
346
+        # Workspace Content
347
+        configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
348
+        configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
349
+        # Create Generic Content
350
+        configurator.add_route('create_generic_content', '/workspaces/{workspace_id}/contents', request_method='POST')  # nopep8
351
+        configurator.add_view(self.create_generic_empty_content, route_name='create_generic_content')  # nopep8
352
+        # Move Content
353
+        configurator.add_route('move_content', '/workspaces/{workspace_id}/contents/{content_id}/move', request_method='PUT')  # nopep8
354
+        configurator.add_view(self.move_content, route_name='move_content')  # nopep8
355
+        # Delete/Undelete Content
356
+        configurator.add_route('delete_content', '/workspaces/{workspace_id}/contents/{content_id}/delete', request_method='PUT')  # nopep8
357
+        configurator.add_view(self.delete_content, route_name='delete_content')  # nopep8
358
+        configurator.add_route('undelete_content', '/workspaces/{workspace_id}/contents/{content_id}/undelete', request_method='PUT')  # nopep8
359
+        configurator.add_view(self.undelete_content, route_name='undelete_content')  # nopep8
360
+        # # Archive/Unarchive Content
361
+        configurator.add_route('archive_content', '/workspaces/{workspace_id}/contents/{content_id}/archive', request_method='PUT')  # nopep8
362
+        configurator.add_view(self.archive_content, route_name='archive_content')  # nopep8
363
+        configurator.add_route('unarchive_content', '/workspaces/{workspace_id}/contents/{content_id}/unarchive', request_method='PUT')  # nopep8
364
+        configurator.add_view(self.unarchive_content, route_name='unarchive_content')  # nopep8