Browse Source

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

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

+ 1 - 1
README.md View File

5
 tracim_backend
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
 Backend source code of tracim v2, using Pyramid Framework.
10
 Backend source code of tracim v2, using Pyramid Framework.
11
 
11
 

+ 9 - 1
tracim/exceptions.py View File

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

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

1
+import copy
1
 import transaction
2
 import transaction
2
 
3
 
3
 
4
 
23
         loaded = [] if loaded is None else loaded
24
         loaded = [] if loaded is None else loaded
24
         self._loaded = loaded
25
         self._loaded = loaded
25
         self._session = session
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
     def loads(self, fixtures_classes):
33
     def loads(self, fixtures_classes):
29
         for fixture_class in fixtures_classes:
34
         for fixture_class in fixtures_classes:

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from depot.io.utils import FileIntent
2
 from depot.io.utils import FileIntent
3
+import transaction
3
 
4
 
4
 from tracim import models
5
 from tracim import models
5
 from tracim.fixtures import Fixture
6
 from tracim.fixtures import Fixture
9
 from tracim.lib.core.workspace import WorkspaceApi
10
 from tracim.lib.core.workspace import WorkspaceApi
10
 from tracim.models.data import ContentType
11
 from tracim.models.data import ContentType
11
 from tracim.models.data import UserRoleInWorkspace
12
 from tracim.models.data import UserRoleInWorkspace
13
+from tracim.models.revision_protection import new_revision
12
 
14
 
13
 
15
 
14
 class Content(Fixture):
16
 class Content(Fixture):
43
         )
45
         )
44
 
46
 
45
         # Workspaces
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
         # Workspaces roles
64
         # Workspaces roles
63
         role_api.create_one(
65
         role_api.create_one(
64
             user=bob,
66
             user=bob,
65
-            workspace=w1,
67
+            workspace=recipe_workspace,
66
             role_level=UserRoleInWorkspace.CONTENT_MANAGER,
68
             role_level=UserRoleInWorkspace.CONTENT_MANAGER,
67
             with_notif=False,
69
             with_notif=False,
68
         )
70
         )
69
-
70
         # Folders
71
         # Folders
71
-        w1f1 = content_api.create(
72
+
73
+        tool_workspace = content_api.create(
72
             content_type=ContentType.Folder,
74
             content_type=ContentType.Folder,
73
-            workspace=w1,
74
-            label='w1f1',
75
+            workspace=business_workspace,
76
+            label='Tools',
75
             do_save=True,
77
             do_save=True,
76
             do_notify=False,
78
             do_notify=False,
77
         )
79
         )
78
-        w1f2 = content_api.create(
80
+        menu_workspace = content_api.create(
79
             content_type=ContentType.Folder,
81
             content_type=ContentType.Folder,
80
-            workspace=w1,
81
-            label='w1f2',
82
+            workspace=business_workspace,
83
+            label='Menus',
82
             do_save=True,
84
             do_save=True,
83
             do_notify=False,
85
             do_notify=False,
84
         )
86
         )
85
 
87
 
86
-        w2f1 = content_api.create(
88
+        dessert_folder = content_api.create(
87
             content_type=ContentType.Folder,
89
             content_type=ContentType.Folder,
88
-            workspace=w2,
89
-            label='w2f1',
90
+            workspace=recipe_workspace,
91
+            label='Desserts',
90
             do_save=True,
92
             do_save=True,
91
             do_notify=False,
93
             do_notify=False,
92
         )
94
         )
93
-        w2f2 = content_api.create(
95
+        salads_folder = content_api.create(
94
             content_type=ContentType.Folder,
96
             content_type=ContentType.Folder,
95
-            workspace=w2,
96
-            label='w2f2',
97
+            workspace=recipe_workspace,
98
+            label='Salads',
97
             do_save=True,
99
             do_save=True,
98
             do_notify=False,
100
             do_notify=False,
99
         )
101
         )
100
-
101
-        w3f1 = content_api.create(
102
+        other_folder = content_api.create(
102
             content_type=ContentType.Folder,
103
             content_type=ContentType.Folder,
103
-            workspace=w3,
104
-            label='w3f3',
104
+            workspace=other_workspace,
105
+            label='Infos',
105
             do_save=True,
106
             do_save=True,
106
             do_notify=False,
107
             do_notify=False,
107
         )
108
         )
108
 
109
 
109
         # Pages, threads, ..
110
         # Pages, threads, ..
110
-        w1f1p1 = content_api.create(
111
+        tiramisu_page = content_api.create(
111
             content_type=ContentType.Page,
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
             do_save=True,
116
             do_save=True,
116
             do_notify=False,
117
             do_notify=False,
117
         )
118
         )
118
-        w1f1t1 = content_api.create(
119
+        best_cake_thread = content_api.create(
119
             content_type=ContentType.Thread,
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
             do_save=False,
124
             do_save=False,
124
             do_notify=False,
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
             content_type=ContentType.File,
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
             do_save=False,
134
             do_save=False,
134
             do_notify=False,
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
             'text/plain',
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
             content_type=ContentType.File,
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
             do_save=False,
149
             do_save=False,
149
             do_notify=False,
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
             'text/html',
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
             content_type=ContentType.Folder,
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
             do_save=True,
187
             do_save=True,
164
             do_notify=False,
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
             content_type=ContentType.Page,
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
             do_save=True,
203
             do_save=True,
173
             do_notify=False,
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
         self._session.flush()
250
         self._session.flush()

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from contextlib import contextmanager
2
 from contextlib import contextmanager
3
-
4
 import os
3
 import os
5
-
4
+import datetime
5
+import re
6
+import typing
6
 from operator import itemgetter
7
 from operator import itemgetter
8
+from operator import not_
7
 
9
 
8
 import transaction
10
 import transaction
9
 from sqlalchemy import func
11
 from sqlalchemy import func
10
 from sqlalchemy.orm import Query
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
 from depot.manager import DepotManager
13
 from depot.manager import DepotManager
21
 from depot.io.utils import FileIntent
14
 from depot.io.utils import FileIntent
22
-
23
 import sqlalchemy
15
 import sqlalchemy
24
 from sqlalchemy.orm import aliased
16
 from sqlalchemy.orm import aliased
25
 from sqlalchemy.orm import joinedload
17
 from sqlalchemy.orm import joinedload
29
 from sqlalchemy import distinct
21
 from sqlalchemy import distinct
30
 from sqlalchemy import or_
22
 from sqlalchemy import or_
31
 from sqlalchemy.sql.elements import and_
23
 from sqlalchemy.sql.elements import and_
24
+
32
 from tracim.lib.utils.utils import cmp_to_key
25
 from tracim.lib.utils.utils import cmp_to_key
33
 from tracim.lib.core.notifications import NotifierFactory
26
 from tracim.lib.core.notifications import NotifierFactory
34
 from tracim.exceptions import SameValueError
27
 from tracim.exceptions import SameValueError
44
 from tracim.models.data import RevisionReadStatus
37
 from tracim.models.data import RevisionReadStatus
45
 from tracim.models.data import UserRoleInWorkspace
38
 from tracim.models.data import UserRoleInWorkspace
46
 from tracim.models.data import Workspace
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
 def compare_content_for_sorting_by_type_and_name(
47
 def compare_content_for_sorting_by_type_and_name(
103
         ContentType.Comment,
101
         ContentType.Comment,
104
         ContentType.Thread,
102
         ContentType.Thread,
105
         ContentType.Page,
103
         ContentType.Page,
104
+        ContentType.MarkdownPage,
106
     )
105
     )
107
 
106
 
108
     def __init__(
107
     def __init__(
113
             show_archived: bool = False,
112
             show_archived: bool = False,
114
             show_deleted: bool = False,
113
             show_deleted: bool = False,
115
             show_temporary: bool = False,
114
             show_temporary: bool = False,
115
+            show_active: bool = True,
116
             all_content_in_treeview: bool = True,
116
             all_content_in_treeview: bool = True,
117
             force_show_all_types: bool = False,
117
             force_show_all_types: bool = False,
118
             disable_user_workspaces_filter: bool = False,
118
             disable_user_workspaces_filter: bool = False,
124
         self._show_archived = show_archived
124
         self._show_archived = show_archived
125
         self._show_deleted = show_deleted
125
         self._show_deleted = show_deleted
126
         self._show_temporary = show_temporary
126
         self._show_temporary = show_temporary
127
+        self._show_active = show_active
127
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
128
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
128
         self._force_show_all_types = force_show_all_types
129
         self._force_show_all_types = force_show_all_types
129
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
130
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
156
             self._show_deleted = previous_show_deleted
157
             self._show_deleted = previous_show_deleted
157
             self._show_temporary = previous_show_temporary
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
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
163
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
160
         """
164
         """
161
         Return the Content/ContentRevision query join condition
165
         Return the Content/ContentRevision query join condition
234
     def _base_query(self, workspace: Workspace=None) -> Query:
238
     def _base_query(self, workspace: Workspace=None) -> Query:
235
         result = self.__real_base_query(workspace)
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
         if not self._show_deleted:
246
         if not self._show_deleted:
238
             result = result.filter(Content.is_deleted==False)
247
             result = result.filter(Content.is_deleted==False)
239
 
248
 
686
 
695
 
687
         if parent_id:
696
         if parent_id:
688
             resultset = resultset.filter(Content.parent_id==parent_id)
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
             resultset = resultset.filter(Content.parent_id == None)
699
             resultset = resultset.filter(Content.parent_id == None)
700
+        # parent_id == None give all contents
691
 
701
 
692
         return resultset.all()
702
         return resultset.all()
693
 
703
 

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

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
 from tracim import CFG
7
 from tracim import CFG
8
 from tracim.models import User
8
 from tracim.models import User
9
 from tracim.models.auth import Profile
9
 from tracim.models.auth import Profile
10
+from tracim.models.data import Content
10
 from tracim.models.data import Workspace, UserRoleInWorkspace
11
 from tracim.models.data import Workspace, UserRoleInWorkspace
11
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
12
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
12
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
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
 class LoginCredentials(object):
24
 class LoginCredentials(object):
16
     """
25
     """
17
     Login credentials model for login
26
     Login credentials model for login
22
         self.password = password
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
 class UserInContext(object):
73
 class UserInContext(object):
26
     """
74
     """
27
     Interface to get User data and User data related to context.
75
     Interface to get User data and User data related to context.
211
             self.dbsession,
259
             self.dbsession,
212
             self.config
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
                 ]
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
 class ContentChecker(object):
544
 class ContentChecker(object):
541
 
545
 
1147
         return not self.is_archived and not self.is_deleted
1151
         return not self.is_archived and not self.is_deleted
1148
 
1152
 
1149
     @property
1153
     @property
1154
+    def is_active(self) -> bool:
1155
+        return self.is_editable
1156
+
1157
+    @property
1150
     def depot_file(self) -> UploadedFile:
1158
     def depot_file(self) -> UploadedFile:
1151
         return self.revision.depot_file
1159
         return self.revision.depot_file
1152
 
1160
 

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

129
             session=self.session,
129
             session=self.session,
130
             config=self.app_config,
130
             config=self.app_config,
131
         )
131
         )
132
-        workspace = wapi.get_one_by_label('w1')
132
+        workspace = wapi.get_one_by_label('Recipes')
133
         user = uapi.get_one_by_email('bob@fsf.local')
133
         user = uapi.get_one_by_email('bob@fsf.local')
134
         wapi.enable_notifications(user, workspace)
134
         wapi.enable_notifications(user, workspace)
135
 
135
 
161
         headers = response[0]['Content']['Headers']
161
         headers = response[0]['Content']['Headers']
162
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
162
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
163
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
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
 class TestNotificationsAsync(MailHogTest):
169
 class TestNotificationsAsync(MailHogTest):
221
             session=self.session,
221
             session=self.session,
222
             config=self.app_config,
222
             config=self.app_config,
223
         )
223
         )
224
-        workspace = wapi.get_one_by_label('w1')
224
+        workspace = wapi.get_one_by_label('Recipes')
225
         user = uapi.get_one_by_email('bob@fsf.local')
225
         user = uapi.get_one_by_email('bob@fsf.local')
226
         wapi.enable_notifications(user, workspace)
226
         wapi.enable_notifications(user, workspace)
227
 
227
 
262
         headers = response[0]['Content']['Headers']
262
         headers = response[0]['Content']['Headers']
263
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
263
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
264
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
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
         assert application['is_active'] is True
59
         assert application['is_active'] is True
60
         assert 'config' in application
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
         Get applications list with an unregistered user (bad auth)
64
         Get applications list with an unregistered user (bad auth)
65
         """
65
         """
75
         assert 'code' in res.json.keys()
75
         assert 'code' in res.json.keys()
76
         assert 'message' in res.json.keys()
76
         assert 'message' in res.json.keys()
77
         assert 'details' in res.json.keys()
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
         res = res.json_body
29
         res = res.json_body
30
         workspace = res[0]
30
         workspace = res[0]
31
         assert workspace['id'] == 1
31
         assert workspace['id'] == 1
32
-        assert workspace['label'] == 'w1'
32
+        assert workspace['label'] == 'Business'
33
         assert len(workspace['sidebar_entries']) == 7
33
         assert len(workspace['sidebar_entries']) == 7
34
 
34
 
35
         sidebar_entry = workspace['sidebar_entries'][0]
35
         sidebar_entry = workspace['sidebar_entries'][0]

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

28
         res = self.testapp.get('/api/v2/workspaces/1', status=200)
28
         res = self.testapp.get('/api/v2/workspaces/1', status=200)
29
         workspace = res.json_body
29
         workspace = res.json_body
30
         assert workspace['id'] == 1
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
         assert len(workspace['sidebar_entries']) == 7
34
         assert len(workspace['sidebar_entries']) == 7
35
 
35
 
36
         sidebar_entry = workspace['sidebar_entries'][0]
36
         sidebar_entry = workspace['sidebar_entries'][0]
153
             )
153
             )
154
         )
154
         )
155
         res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
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
         user_role = res[0]
157
         user_role = res[0]
158
         assert user_role['role_slug'] == 'workspace-manager'
158
         assert user_role['role_slug'] == 'workspace-manager'
159
         assert user_role['user_id'] == 1
159
         assert user_role['user_id'] == 1
215
         assert 'code' in res.json.keys()
215
         assert 'code' in res.json.keys()
216
         assert 'message' in res.json.keys()
216
         assert 'message' in res.json.keys()
217
         assert 'details' in res.json.keys()
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
         )
162
         )
163
 
163
 
164
         workspaces_names = [w.name for w in children]
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
                 workspaces_names,
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
             workspaces_names,
170
             workspaces_names,
171
         )
171
         )
172
 
172
 
186
         eq_(
186
         eq_(
187
             2,
187
             2,
188
             len(children),
188
             len(children),
189
-            msg='RootResource should return 2 workspaces instead {0}'.format(
189
+            msg='RootResource should return 3 workspaces instead {0}'.format(
190
                 len(children),
190
                 len(children),
191
             )
191
             )
192
         )
192
         )
193
 
193
 
194
         workspaces_names = [w.name for w in children]
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
             workspaces_names,
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
             workspaces_names,
199
             workspaces_names,
200
         )
200
         )
201
 
201
 
202
     def test_unit__list_workspace_folders__ok(self):
202
     def test_unit__list_workspace_folders__ok(self):
203
         provider = self._get_provider(self.app_config)
203
         provider = self._get_provider(self.app_config)
204
-        w1 = provider.getResourceInst(
205
-            '/w1/',
204
+        Recipes = provider.getResourceInst(
205
+            '/Recipes/',
206
             self._get_environ(
206
             self._get_environ(
207
                 provider,
207
                 provider,
208
                 'bob@fsf.local',
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
         eq_(
214
         eq_(
215
             2,
215
             2,
216
             len(children),
216
             len(children),
217
-            msg='w1 should list 2 folders instead {0}'.format(
217
+            msg='Recipes should list 2 folders instead {0}'.format(
218
                 len(children),
218
                 len(children),
219
             ),
219
             ),
220
         )
220
         )
221
 
221
 
222
         folders_names = [f.name for f in children]
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
                 folders_names,
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
                 folders_names,
227
                 folders_names,
228
         )
228
         )
229
 
229
 
230
     def test_unit__list_content__ok(self):
230
     def test_unit__list_content__ok(self):
231
         provider = self._get_provider(self.app_config)
231
         provider = self._get_provider(self.app_config)
232
-        w1f1 = provider.getResourceInst(
233
-            '/w1/w1f1',
232
+        Salads = provider.getResourceInst(
233
+            '/Recipes/Desserts',
234
             self._get_environ(
234
             self._get_environ(
235
                 provider,
235
                 provider,
236
                 'bob@fsf.local',
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
         eq_(
242
         eq_(
243
             5,
243
             5,
244
             len(children),
244
             len(children),
245
-            msg='w1f1 should list 5 folders instead {0}'.format(
245
+            msg='Salads should list 5 Files instead {0}'.format(
246
                 len(children),
246
                 len(children),
247
             ),
247
             ),
248
         )
248
         )
249
 
249
 
250
         content_names = [c.name for c in children]
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
                 content_names,
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
                 content_names,
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
                 content_names,
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
                 content_names,
270
                 content_names,
271
         )
271
         )
272
 
272
 
273
     def test_unit__get_content__ok(self):
273
     def test_unit__get_content__ok(self):
274
         provider = self._get_provider(self.app_config)
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
             self._get_environ(
277
             self._get_environ(
278
                 provider,
278
                 provider,
279
                 'bob@fsf.local',
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
     def test_unit__delete_content__ok(self):
286
     def test_unit__delete_content__ok(self):
287
         provider = self._get_provider(self.app_config)
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
             self._get_environ(
290
             self._get_environ(
291
                 provider,
291
                 provider,
292
                 'bob@fsf.local',
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
             .one()  # It must exist only one revision, cf fixtures
298
             .one()  # It must exist only one revision, cf fixtures
299
         eq_(
299
         eq_(
300
             False,
300
             False,
301
-            content_w1f1d1.is_deleted,
301
+            content_pie.is_deleted,
302
             msg='Content should not be deleted !'
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
         self.session.flush()
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
             .order_by(Content.revision_id.desc()) \
311
             .order_by(Content.revision_id.desc()) \
312
             .first()
312
             .first()
313
         eq_(
313
         eq_(
314
             True,
314
             True,
315
-            content_w1f1d1.is_deleted,
315
+            content_pie.is_deleted,
316
             msg='Content should be deleted !'
316
             msg='Content should be deleted !'
317
         )
317
         )
318
 
318
 
319
         result = provider.getResourceInst(
319
         result = provider.getResourceInst(
320
-            '/w1/w1f1/w1f1d1.txt',
320
+            '/Recipes/Desserts/Apple_Pie.txt',
321
             self._get_environ(
321
             self._get_environ(
322
                 provider,
322
                 provider,
323
                 'bob@fsf.local',
323
                 'bob@fsf.local',
334
             'bob@fsf.local',
334
             'bob@fsf.local',
335
         )
335
         )
336
         result = provider.getResourceInst(
336
         result = provider.getResourceInst(
337
-            '/w1/w1f1/new_file.txt',
337
+            '/Recipes/Salads/greek_salad.txt',
338
             environ,
338
             environ,
339
         )
339
         )
340
 
340
 
345
         result = self._put_new_text_file(
345
         result = self._put_new_text_file(
346
             provider,
346
             provider,
347
             environ,
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
         assert result, 'Result should not be None instead {0}'.format(
352
         assert result, 'Result should not be None instead {0}'.format(
353
             result
353
             result
354
         )
354
         )
355
         eq_(
355
         eq_(
356
-            b'hello\n',
356
+            b'Greek Salad\n',
357
             result.content.depot_file.file.read(),
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
                 result.content.depot_file.file.read()
359
                 result.content.depot_file.file.read()
360
             )
360
             )
361
         )
361
         )
367
             'bob@fsf.local',
367
             'bob@fsf.local',
368
         )
368
         )
369
         new_file = provider.getResourceInst(
369
         new_file = provider.getResourceInst(
370
-            '/w1/w1f1/new_file.txt',
370
+            '/Recipes/Salads/greek_salad.txt',
371
             environ,
371
             environ,
372
         )
372
         )
373
 
373
 
379
         new_file = self._put_new_text_file(
379
         new_file = self._put_new_text_file(
380
             provider,
380
             provider,
381
             environ,
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
         assert new_file, 'Result should not be None instead {0}'.format(
385
         assert new_file, 'Result should not be None instead {0}'.format(
386
             new_file
386
             new_file
387
         )
387
         )
388
 
388
 
389
         content_new_file = self.session.query(ContentRevisionRO) \
389
         content_new_file = self.session.query(ContentRevisionRO) \
390
-            .filter(Content.label == 'new_file') \
390
+            .filter(Content.label == 'greek_salad') \
391
             .one()  # It must exist only one revision
391
             .one()  # It must exist only one revision
392
         eq_(
392
         eq_(
393
             False,
393
             False,
400
         new_file.delete()
400
         new_file.delete()
401
 
401
 
402
         self.session.flush()
402
         self.session.flush()
403
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
403
+        content_pie = self.session.query(ContentRevisionRO) \
404
             .filter(Content.content_id == content_new_file_id) \
404
             .filter(Content.content_id == content_new_file_id) \
405
             .order_by(Content.revision_id.desc()) \
405
             .order_by(Content.revision_id.desc()) \
406
             .first()
406
             .first()
407
         eq_(
407
         eq_(
408
             True,
408
             True,
409
-            content_w1f1d1.is_deleted,
409
+            content_pie.is_deleted,
410
             msg='Content should be deleted !'
410
             msg='Content should be deleted !'
411
         )
411
         )
412
 
412
 
413
         result = provider.getResourceInst(
413
         result = provider.getResourceInst(
414
-            '/w1/w1f1/new_file.txt',
414
+            '/Recipes/Salads/greek_salad.txt',
415
             self._get_environ(
415
             self._get_environ(
416
                 provider,
416
                 provider,
417
                 'bob@fsf.local',
417
                 'bob@fsf.local',
425
         new_file = self._put_new_text_file(
425
         new_file = self._put_new_text_file(
426
             provider,
426
             provider,
427
             environ,
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
         assert new_file, 'Result should not be None instead {0}'.format(
431
         assert new_file, 'Result should not be None instead {0}'.format(
432
             new_file
432
             new_file
434
 
434
 
435
         # Previous file is still dleeted
435
         # Previous file is still dleeted
436
         self.session.flush()
436
         self.session.flush()
437
-        content_w1f1d1 = self.session.query(ContentRevisionRO) \
437
+        content_pie = self.session.query(ContentRevisionRO) \
438
             .filter(Content.content_id == content_new_file_id) \
438
             .filter(Content.content_id == content_new_file_id) \
439
             .order_by(Content.revision_id.desc()) \
439
             .order_by(Content.revision_id.desc()) \
440
             .first()
440
             .first()
441
         eq_(
441
         eq_(
442
             True,
442
             True,
443
-            content_w1f1d1.is_deleted,
443
+            content_pie.is_deleted,
444
             msg='Content should be deleted !'
444
             msg='Content should be deleted !'
445
         )
445
         )
446
 
446
 
447
         # And an other file exist for this name
447
         # And an other file exist for this name
448
         content_new_new_file = self.session.query(ContentRevisionRO) \
448
         content_new_new_file = self.session.query(ContentRevisionRO) \
449
-            .filter(Content.label == 'new_file') \
449
+            .filter(Content.label == 'greek_salad') \
450
             .order_by(Content.revision_id.desc()) \
450
             .order_by(Content.revision_id.desc()) \
451
             .first()
451
             .first()
452
         assert content_new_new_file.content_id != content_new_file_id,\
452
         assert content_new_new_file.content_id != content_new_file_id,\
464
             provider,
464
             provider,
465
             'bob@fsf.local',
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
             environ,
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
             .one()  # It must exist only one revision, cf fixtures
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
         # Database content is renamed
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
             .order_by(ContentRevisionRO.revision_id.desc()) \
483
             .order_by(ContentRevisionRO.revision_id.desc()) \
484
             .first()
484
             .first()
485
         eq_(
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
             provider,
496
             provider,
497
             'bob@fsf.local',
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
             environ,
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
             .one()  # It must exist only one revision, cf fixtures
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
         eq_(
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
         # Database content is moved
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
             .order_by(ContentRevisionRO.revision_id.desc()) \
521
             .order_by(ContentRevisionRO.revision_id.desc()) \
522
             .first()
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
     def test_unit__move_and_rename_content__ok(self):
529
     def test_unit__move_and_rename_content__ok(self):
532
             provider,
532
             provider,
533
             'bob@fsf.local',
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
             environ,
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
             .one()  # It must exist only one revision, cf fixtures
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
         eq_(
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
         # Database content is moved
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
             .order_by(ContentRevisionRO.revision_id.desc()) \
557
             .order_by(ContentRevisionRO.revision_id.desc()) \
558
             .first()
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
         eq_(
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
             'bob@fsf.local',
575
             'bob@fsf.local',
576
         )
576
         )
577
         content_to_move_res = provider.getResourceInst(
577
         content_to_move_res = provider.getResourceInst(
578
-            '/w1/w1f1/w1f1d1.txt',
578
+            '/Recipes/Desserts/Apple_Pie.txt',
579
             environ,
579
             environ,
580
         )
580
         )
581
 
581
 
582
         content_to_move = self.session.query(ContentRevisionRO) \
582
         content_to_move = self.session.query(ContentRevisionRO) \
583
-            .filter(Content.label == 'w1f1d1') \
583
+            .filter(Content.label == 'Apple_Pie') \
584
             .one()  # It must exist only one revision, cf fixtures
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
         content_to_move_id = content_to_move.content_id
586
         content_to_move_id = content_to_move.content_id
587
         content_to_move_parent = content_to_move.parent
587
         content_to_move_parent = content_to_move.parent
588
         eq_(
588
         eq_(
589
             content_to_move_parent.label,
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
         # Database content is moved
596
         # Database content is moved
597
         content_to_move = self.session.query(ContentRevisionRO) \
597
         content_to_move = self.session.query(ContentRevisionRO) \
601
 
601
 
602
         assert content_to_move.parent, 'Content should have a parent'
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
                 content_to_move.parent.label
606
                 content_to_move.parent.label
607
         )
607
         )
608
 
608
 
613
             'bob@fsf.local',
613
             'bob@fsf.local',
614
         )
614
         )
615
         result = provider.getResourceInst(
615
         result = provider.getResourceInst(
616
-            '/w1/w1f1/new_file.txt',
616
+            '/Recipes/Salads/greek_salad.txt',
617
             environ,
617
             environ,
618
         )
618
         )
619
 
619
 
624
         result = self._put_new_text_file(
624
         result = self._put_new_text_file(
625
             provider,
625
             provider,
626
             environ,
626
             environ,
627
-            '/w1/w1f1/new_file.txt',
627
+            '/Recipes/Salads/greek_salad.txt',
628
             b'hello\n',
628
             b'hello\n',
629
         )
629
         )
630
 
630
 

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

4
 from marshmallow.validate import OneOf
4
 from marshmallow.validate import OneOf
5
 
5
 
6
 from tracim.models.auth import Profile
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
 from tracim.models.context_models import LoginCredentials
15
 from tracim.models.context_models import LoginCredentials
8
 from tracim.models.data import UserRoleInWorkspace
16
 from tracim.models.data import UserRoleInWorkspace
9
 
17
 
65
         description = 'User account of Tracim'
73
         description = 'User account of Tracim'
66
 
74
 
67
 
75
 
76
+# Path Schemas
77
+
78
+
68
 class UserIdPathSchema(marshmallow.Schema):
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
 class WorkspaceIdPathSchema(marshmallow.Schema):
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
 class BasicAuthSchema(marshmallow.Schema):
135
 class BasicAuthSchema(marshmallow.Schema):
188
 
247
 
189
     class Meta:
248
     class Meta:
190
         description = 'Tracim Application informations'
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
 # coding=utf-8
1
 # coding=utf-8
2
 from pyramid.config import Configurator
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
 from tracim.lib.utils.authorization import require_profile
5
 from tracim.lib.utils.authorization import require_profile
6
 from tracim.models import Group
6
 from tracim.models import Group
7
 from tracim.models.applications import applications
7
 from tracim.models.applications import applications
8
+from tracim.models.contents import CONTENT_DEFAULT_TYPE
8
 
9
 
9
 try:  # Python 3.5+
10
 try:  # Python 3.5+
10
     from http import HTTPStatus
11
     from http import HTTPStatus
15
 from tracim.extensions import hapic
16
 from tracim.extensions import hapic
16
 from tracim.views.controllers import Controller
17
 from tracim.views.controllers import Controller
17
 from tracim.views.core_api.schemas import ApplicationSchema
18
 from tracim.views.core_api.schemas import ApplicationSchema
19
+from tracim.views.core_api.schemas import ContentTypeSchema
18
 
20
 
19
 
21
 
20
 class SystemController(Controller):
22
 class SystemController(Controller):
30
         """
32
         """
31
         return applications
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
     def bind(self, configurator: Configurator) -> None:
47
     def bind(self, configurator: Configurator) -> None:
34
         """
48
         """
35
         Create all routes and views using pyramid configurator
49
         Create all routes and views using pyramid configurator
40
         configurator.add_route('applications', '/system/applications', request_method='GET')  # nopep8
54
         configurator.add_route('applications', '/system/applications', request_method='GET')  # nopep8
41
         configurator.add_view(self.applications, route_name='applications')
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
 import typing
1
 import typing
2
 
2
 
3
+import transaction
3
 from pyramid.config import Configurator
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
 try:  # Python 3.5+
5
 try:  # Python 3.5+
13
     from http import HTTPStatus
6
     from http import HTTPStatus
14
 except ImportError:
7
 except ImportError:
15
     from http import client as HTTPStatus
8
     from http import client as HTTPStatus
16
 
9
 
17
 from tracim import hapic, TracimRequest
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
 from tracim.exceptions import NotAuthenticated
18
 from tracim.exceptions import NotAuthenticated
19
 from tracim.exceptions import InsufficientUserProfile
19
 from tracim.exceptions import InsufficientUserProfile
20
 from tracim.exceptions import WorkspaceNotFound
20
 from tracim.exceptions import WorkspaceNotFound
21
-from tracim.lib.core.user import UserApi
22
-from tracim.lib.core.workspace import WorkspaceApi
23
 from tracim.views.controllers import Controller
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
 from tracim.views.core_api.schemas import WorkspaceSchema
28
 from tracim.views.core_api.schemas import WorkspaceSchema
25
-from tracim.views.core_api.schemas import UserSchema
26
 from tracim.views.core_api.schemas import WorkspaceIdPathSchema
29
 from tracim.views.core_api.schemas import WorkspaceIdPathSchema
27
 from tracim.views.core_api.schemas import WorkspaceMemberSchema
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
 class WorkspaceController(Controller):
35
 class WorkspaceController(Controller):
30
 
36
 
77
             for user_role in roles
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
     def bind(self, configurator: Configurator) -> None:
334
     def bind(self, configurator: Configurator) -> None:
81
         """
335
         """
82
         Create all routes and views using
336
         Create all routes and views using
83
         pyramid configurator for this controller
337
         pyramid configurator for this controller
84
         """
338
         """
85
 
339
 
86
-        # Applications
340
+        # Workspace
87
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
341
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
88
         configurator.add_view(self.workspace, route_name='workspace')
342
         configurator.add_view(self.workspace, route_name='workspace')
343
+        # Workspace Members (Roles)
89
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
344
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
90
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
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