Browse Source

add endpoints for html_documents

Guénaël Muller 6 years ago
parent
commit
46bf33d984

+ 3 - 0
tracim/__init__.py View File

@@ -17,6 +17,7 @@ from tracim.lib.utils.authorization import AcceptAllAuthorizationPolicy
17 17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18 18
 from tracim.lib.webdav import WebdavAppFactory
19 19
 from tracim.views import BASE_API_V2
20
+from tracim.views.contents_api.html_document_controller import HTMLDocumentController  # nopep8
20 21
 from tracim.views.core_api.session_controller import SessionController
21 22
 from tracim.views.core_api.system_controller import SystemController
22 23
 from tracim.views.core_api.user_controller import UserController
@@ -73,11 +74,13 @@ def web(global_config, **local_settings):
73 74
     user_controller = UserController()
74 75
     workspace_controller = WorkspaceController()
75 76
     comment_controller = CommentController()
77
+    html_document_controller = HTMLDocumentController()
76 78
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
77 79
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
78 80
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
79 81
     configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
80 82
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
83
+    configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)
81 84
     hapic.add_documentation_view(
82 85
         '/api/v2/doc',
83 86
         'Tracim v2 API',

+ 12 - 0
tracim/exceptions.py View File

@@ -117,5 +117,17 @@ class UserNotFoundInTracimRequest(TracimException):
117 117
     pass
118 118
 
119 119
 
120
+class ContentNotFoundInTracimRequest(TracimException):
121
+    pass
122
+
123
+
124
+class ContentNotFound(TracimException):
125
+    pass
126
+
127
+
128
+class ContentTypeNotAllowed(TracimException):
129
+    pass
130
+
131
+
120 132
 class WorkspacesDoNotMatch(TracimException):
121 133
     pass

+ 12 - 0
tracim/fixtures/content.py View File

@@ -121,6 +121,18 @@ class Content(Fixture):
121 121
             do_save=True,
122 122
             do_notify=False,
123 123
         )
124
+        with new_revision(
125
+                session=self._session,
126
+                tm=transaction.manager,
127
+                content=tiramisu_page,
128
+        ):
129
+            content_api.update_content(
130
+                item=tiramisu_page,
131
+                new_content='<p>To cook a great Tiramisu, you need many ingredients.</p>',  # nopep8
132
+                new_label='Tiramisu Recipe',
133
+            )
134
+            content_api.save(tiramisu_page)
135
+
124 136
         best_cake_thread = content_api.create(
125 137
             content_type=ContentType.Thread,
126 138
             workspace=recipe_workspace,

+ 12 - 2
tracim/lib/core/content.py View File

@@ -16,6 +16,7 @@ import sqlalchemy
16 16
 from sqlalchemy.orm import aliased
17 17
 from sqlalchemy.orm import joinedload
18 18
 from sqlalchemy.orm.attributes import get_history
19
+from sqlalchemy.orm.exc import NoResultFound
19 20
 from sqlalchemy.orm.session import Session
20 21
 from sqlalchemy import desc
21 22
 from sqlalchemy import distinct
@@ -25,6 +26,7 @@ from sqlalchemy.sql.elements import and_
25 26
 from tracim.lib.utils.utils import cmp_to_key
26 27
 from tracim.lib.core.notifications import NotifierFactory
27 28
 from tracim.exceptions import SameValueError
29
+from tracim.exceptions import ContentNotFound
28 30
 from tracim.exceptions import WorkspacesDoNotMatch
29 31
 from tracim.lib.utils.utils import current_date_for_filename
30 32
 from tracim.models.revision_protection import new_revision
@@ -39,9 +41,9 @@ from tracim.models.data import RevisionReadStatus
39 41
 from tracim.models.data import UserRoleInWorkspace
40 42
 from tracim.models.data import Workspace
41 43
 from tracim.lib.utils.translation import fake_translator as _
44
+from tracim.models.context_models import RevisionInContext
42 45
 from tracim.models.context_models import ContentInContext
43 46
 
44
-
45 47
 __author__ = 'damien'
46 48
 
47 49
 
@@ -161,6 +163,10 @@ class ContentApi(object):
161 163
     def get_content_in_context(self, content: Content):
162 164
         return ContentInContext(content, self._session, self._config)
163 165
 
166
+    def get_revision_in_context(self, revision: ContentRevisionRO):
167
+        # TODO - G.M - 2018-06-173 - create revision in context object
168
+        return RevisionInContext(revision, self._session, self._config)
169
+    
164 170
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
165 171
         """
166 172
         Return the Content/ContentRevision query join condition
@@ -464,7 +470,11 @@ class ContentApi(object):
464 470
         if parent:
465 471
             base_request = base_request.filter(Content.parent_id==parent.content_id)  # nopep8
466 472
 
467
-        return base_request.one()
473
+        try:
474
+            content = base_request.one()
475
+        except NoResultFound as exc:
476
+            raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
477
+        return content
468 478
 
469 479
     def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
470 480
         """

+ 24 - 0
tracim/lib/utils/authorization.py View File

@@ -1,14 +1,20 @@
1 1
 # -*- coding: utf-8 -*-
2
+import typing
2 3
 from typing import TYPE_CHECKING
3 4
 
5
+import functools
4 6
 from pyramid.interfaces import IAuthorizationPolicy
5 7
 from zope.interface import implementer
8
+
9
+from tracim.models.contents import NewContentType
10
+
6 11
 try:
7 12
     from json.decoder import JSONDecodeError
8 13
 except ImportError:  # python3.4
9 14
     JSONDecodeError = ValueError
10 15
 
11 16
 from tracim.exceptions import InsufficientUserWorkspaceRole
17
+from tracim.exceptions import ContentTypeNotAllowed
12 18
 from tracim.exceptions import InsufficientUserProfile
13 19
 if TYPE_CHECKING:
14 20
     from tracim import TracimRequest
@@ -122,3 +128,21 @@ def require_candidate_workspace_role(minimal_required_role: int):
122 128
 
123 129
         return wrapper
124 130
     return decorator
131
+
132
+
133
+def require_content_types(content_types: typing.List['NewContentType']):
134
+    """
135
+    Restricts access to specific file type or raise an exception.
136
+    Check role for candidate_workspace.
137
+    :param content_types: list of NewContentType object
138
+    :return: decorator
139
+    """
140
+    def decorator(func):
141
+        @functools.wraps(func)
142
+        def wrapper(self, context, request: 'TracimRequest'):
143
+            content = request.current_content
144
+            if content.type in [content.slug for content in content_types]:
145
+                return func(self, context, request)
146
+            raise ContentTypeNotAllowed()
147
+        return wrapper
148
+    return decorator

+ 62 - 3
tracim/lib/utils/request.py View File

@@ -2,18 +2,21 @@
2 2
 from pyramid.request import Request
3 3
 from sqlalchemy.orm.exc import NoResultFound
4 4
 
5
-from tracim.exceptions import NotAuthenticated
5
+from tracim.exceptions import NotAuthenticated, ContentNotFound
6
+from tracim.exceptions import ContentNotFoundInTracimRequest
6 7
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
7 8
 from tracim.exceptions import UserNotFoundInTracimRequest
8 9
 from tracim.exceptions import UserDoesNotExist
9 10
 from tracim.exceptions import WorkspaceNotFound
10 11
 from tracim.exceptions import ImmutableAttribute
12
+from tracim.models.contents import ContentTypeLegacy as ContentType
13
+from tracim.lib.core.content import ContentApi
11 14
 from tracim.lib.core.user import UserApi
12 15
 from tracim.lib.core.workspace import WorkspaceApi
13 16
 from tracim.lib.utils.authorization import JSONDecodeError
14 17
 
15 18
 from tracim.models import User
16
-from tracim.models.data import Workspace
19
+from tracim.models.data import Workspace, Content
17 20
 
18 21
 
19 22
 class TracimRequest(Request):
@@ -35,6 +38,9 @@ class TracimRequest(Request):
35 38
             decode_param_names,
36 39
             **kw
37 40
         )
41
+        # Current content, found in request path
42
+        self._current_content = None  # type: Content
43
+
38 44
         # Current workspace, found in request path
39 45
         self._current_workspace = None  # type: Workspace
40 46
 
@@ -93,6 +99,27 @@ class TracimRequest(Request):
93 99
             )
94 100
         self._current_user = user
95 101
 
102
+    @property
103
+    def current_content(self) -> User:
104
+        """
105
+        Get current  content from path
106
+        """
107
+        if self._current_content is None:
108
+            self._current_content = self._get_current_content(
109
+                self.current_user,
110
+                self.current_workspace,
111
+                self
112
+                )
113
+        return self._current_content
114
+
115
+    @current_content.setter
116
+    def current_content(self, content: Content) -> None:
117
+        if self._current_content is not None:
118
+            raise ImmutableAttribute(
119
+                "Can't modify already setted current_content"
120
+            )
121
+        self._current_content = content
122
+
96 123
     # TODO - G.M - 24-05-2018 - Find a better naming for this ?
97 124
     @property
98 125
     def candidate_user(self) -> User:
@@ -132,7 +159,6 @@ class TracimRequest(Request):
132 159
         self._current_workspace = None
133 160
         self.dbsession.close()
134 161
 
135
-
136 162
     @candidate_user.setter
137 163
     def candidate_user(self, user: User) -> None:
138 164
         if self._candidate_user is not None:
@@ -145,6 +171,39 @@ class TracimRequest(Request):
145 171
     # Utils for TracimRequest
146 172
     ###
147 173
 
174
+    def _get_current_content(
175
+            self,
176
+            user: User,
177
+            workspace: Workspace,
178
+            request: 'TracimRequest'
179
+    ):
180
+        """
181
+        Get current content from request
182
+        :param user: User who want to check the workspace
183
+        :param request: pyramid request
184
+        :return: current content
185
+        """
186
+        content_id = ''
187
+        try:
188
+            if 'content_id' in request.matchdict:
189
+                content_id = int(request.matchdict['content_id'])
190
+            if not content_id:
191
+                raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
192
+            api = ContentApi(
193
+                current_user=user,
194
+                session=request.dbsession,
195
+                config=request.registry.settings['CFG']
196
+            )
197
+            content = api.get_one(content_id=content_id, workspace=workspace, content_type=ContentType.Any)  # nopep8
198
+        except JSONDecodeError:
199
+            raise ContentNotFound('Bad json body')
200
+        except NoResultFound:
201
+            raise ContentNotFound(
202
+                'Content {} does not exist '
203
+                'or is not visible for this user'.format(content_id)
204
+            )
205
+        return content
206
+
148 207
     def _get_candidate_user(
149 208
             self,
150 209
             request: 'TracimRequest',

+ 168 - 3
tracim/models/context_models.py View File

@@ -8,6 +8,7 @@ from tracim import CFG
8 8
 from tracim.models import User
9 9
 from tracim.models.auth import Profile
10 10
 from tracim.models.data import Content
11
+from tracim.models.data import ContentRevisionRO
11 12
 from tracim.models.data import Workspace, UserRoleInWorkspace
12 13
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13 14
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
@@ -89,7 +90,44 @@ class CommentCreation(object):
89 90
             self,
90 91
             raw_content: str,
91 92
     ):
92
-        self.raw_content=raw_content
93
+        self.raw_content = raw_content
94
+
95
+
96
+class SetContentStatus(object):
97
+    """
98
+    Set content status
99
+    """
100
+    def __init__(
101
+            self,
102
+            status: str,
103
+    ):
104
+        self.status = status
105
+
106
+
107
+class HTMLDocumentUpdate(object):
108
+    """
109
+    Comment creation model
110
+    """
111
+    def __init__(
112
+            self,
113
+            label: str,
114
+            raw_content: str,
115
+    ):
116
+        self.label = label
117
+        self.raw_content = raw_content
118
+
119
+
120
+class ThreadUpdate(object):
121
+    """
122
+    Comment creation model
123
+    """
124
+    def __init__(
125
+            self,
126
+            label: str,
127
+            raw_content: str,
128
+    ):
129
+        self.label = label
130
+        self.raw_content = raw_content
93 131
 
94 132
 
95 133
 class UserInContext(object):
@@ -331,7 +369,7 @@ class ContentInContext(object):
331 369
 
332 370
     @property
333 371
     def sub_content_types(self) -> typing.List[str]:
334
-        return [type.slug for type in self.content.get_allowed_content_types()]
372
+        return [_type.slug for _type in self.content.get_allowed_content_types()]
335 373
 
336 374
     @property
337 375
     def status(self) -> str:
@@ -357,9 +395,28 @@ class ContentInContext(object):
357 395
             user=self.content.owner
358 396
         )
359 397
 
360
-    # Context-related
398
+    @property
399
+    def current_revision_id(self):
400
+        return self.content.revision_id
401
+
402
+    @property
403
+    def created(self):
404
+        return self.content.created
405
+
406
+    @property
407
+    def modified(self):
408
+        return self.updated
409
+
410
+    @property
411
+    def updated(self):
412
+        return self.content.updated
361 413
 
362 414
     @property
415
+    def last_modifier(self):
416
+        # TODO - G.M - 2018-06-173 - Repair owner/last modifier
417
+        return self.author
418
+    # Context-related
419
+    @property
363 420
     def show_in_ui(self):
364 421
         # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
365 422
         # if false, then do not show content in the treeview.
@@ -371,3 +428,111 @@ class ContentInContext(object):
371 428
     @property
372 429
     def slug(self):
373 430
         return slugify(self.content.label)
431
+
432
+
433
+class RevisionInContext(object):
434
+    """
435
+    Interface to get Content data and Content data related to context.
436
+    """
437
+
438
+    def __init__(self, content: ContentRevisionRO, dbsession: Session, config: CFG):
439
+        self.revision = content
440
+        self.dbsession = dbsession
441
+        self.config = config
442
+
443
+    # Default
444
+    @property
445
+    def content_id(self) -> int:
446
+        return self.revision.content_id
447
+
448
+    @property
449
+    def id(self) -> int:
450
+        return self.content_id
451
+
452
+    @property
453
+    def parent_id(self) -> int:
454
+        """
455
+        Return parent_id of the content
456
+        """
457
+        return self.revision.parent_id
458
+
459
+    @property
460
+    def workspace_id(self) -> int:
461
+        return self.revision.workspace_id
462
+
463
+    @property
464
+    def label(self) -> str:
465
+        return self.revision.label
466
+
467
+    @property
468
+    def content_type(self) -> str:
469
+        return self.revision.type
470
+
471
+    @property
472
+    def sub_content_types(self) -> typing.List[str]:
473
+        return [_type.slug for _type
474
+                in self.revision.node.get_allowed_content_types()]
475
+
476
+    @property
477
+    def status(self) -> str:
478
+        return self.revision.status
479
+
480
+    @property
481
+    def is_archived(self):
482
+        return self.revision.is_archived
483
+
484
+    @property
485
+    def is_deleted(self):
486
+        return self.revision.is_deleted
487
+
488
+    @property
489
+    def raw_content(self):
490
+        return self.revision.description
491
+
492
+    @property
493
+    def author(self):
494
+        return UserInContext(
495
+            dbsession=self.dbsession,
496
+            config=self.config,
497
+            user=self.revision.owner
498
+        )
499
+
500
+    @property
501
+    def revision_id(self):
502
+        return self.revision.revision_id
503
+
504
+    @property
505
+    def created(self):
506
+        return self.revision.created
507
+
508
+    @property
509
+    def modified(self):
510
+        return self.updated
511
+
512
+    @property
513
+    def updated(self):
514
+        return self.revision.updated
515
+
516
+    @property
517
+    def last_modifier(self):
518
+        # TODO - G.M - 2018-06-173 - Repair owner/last modifier
519
+        return self.author
520
+    
521
+    @property
522
+    def comments_ids(self):
523
+        # TODO - G.M - 2018-06-173 - Return comments related to this revision
524
+        return []
525
+
526
+    # Context-related
527
+    @property
528
+    def show_in_ui(self):
529
+        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
530
+        # if false, then do not show content in the treeview.
531
+        # This may his maybe used for specific contents or for sub-contents.
532
+        # Default is True.
533
+        # In first version of the API, this field is always True
534
+        return True
535
+
536
+    @property
537
+    def slug(self):
538
+        return slugify(self.revision.label)

+ 0 - 3
tracim/tests/functional/test_comments.py View File

@@ -1,7 +1,4 @@
1 1
 # -*- coding: utf-8 -*-
2
-"""
3
-Tests for /api/v2/workspaces subpath endpoints.
4
-"""
5 2
 from tracim.tests import FunctionalTest
6 3
 from tracim.fixtures.content import Content as ContentFixtures
7 4
 from tracim.fixtures.users_and_groups import Base as BaseFixture

+ 218 - 0
tracim/tests/functional/test_contents.py View File

@@ -0,0 +1,218 @@
1
+# -*- coding: utf-8 -*-
2
+from tracim.tests import FunctionalTest
3
+from tracim.fixtures.content import Content as ContentFixtures
4
+from tracim.fixtures.users_and_groups import Base as BaseFixture
5
+
6
+
7
+class TestHtmlDocuments(FunctionalTest):
8
+    """
9
+    Tests for /api/v2/workspaces/{workspace_id}/html-documents/{content_id}
10
+    endpoint
11
+    """
12
+
13
+    fixtures = [BaseFixture, ContentFixtures]
14
+
15
+    def test_api__get_html_document__err_400__wrong_content_type(self) -> None:
16
+        """
17
+        Get one html document of a content
18
+        """
19
+        self.testapp.authorization = (
20
+            'Basic',
21
+            (
22
+                'admin@admin.admin',
23
+                'admin@admin.admin'
24
+            )
25
+        )
26
+        res = self.testapp.get(
27
+            '/api/v2/workspaces/2/html-documents/7',
28
+            status=400
29
+        )   # nopep8
30
+
31
+    def test_api__get_html_document__ok_200__nominal_case(self) -> None:
32
+        """
33
+        Get one html document of a content
34
+        """
35
+        self.testapp.authorization = (
36
+            'Basic',
37
+            (
38
+                'admin@admin.admin',
39
+                'admin@admin.admin'
40
+            )
41
+        )
42
+        res = self.testapp.get(
43
+            '/api/v2/workspaces/2/html-documents/6',
44
+            status=200
45
+        )   # nopep8
46
+        content = res.json_body
47
+        assert content['content_type'] == 'page'
48
+        assert content['content_id'] == 6
49
+        assert content['is_archived'] is False
50
+        assert content['is_deleted'] is False
51
+        assert content['label'] == 'Tiramisu Recipe'
52
+        assert content['parent_id'] == 3
53
+        assert content['show_in_ui'] is True
54
+        assert content['slug'] == 'tiramisu-recipe'
55
+        assert content['status'] == 'open'
56
+        assert content['workspace_id'] == 2
57
+        assert content['current_revision_id'] == 7
58
+        # TODO - G.M - 2018-06-173 - check date format
59
+        assert content['created']
60
+        assert content['author']
61
+        assert content['author']['user_id'] == 1
62
+        assert content['author']['avatar_url'] is None
63
+        assert content['author']['public_name'] == 'Global manager'
64
+        # TODO - G.M - 2018-06-173 - check date format
65
+        assert content['modified']
66
+        assert content['last_modifier'] == content['author']
67
+        assert content['raw_content'] == '<p>To cook a great Tiramisu, you need many ingredients.</p>'  # nopep8
68
+
69
+    def test_api__update_html_document__ok_200__nominal_case(self) -> None:
70
+        """
71
+        Update(put) one html document of a content
72
+        """
73
+        self.testapp.authorization = (
74
+            'Basic',
75
+            (
76
+                'admin@admin.admin',
77
+                'admin@admin.admin'
78
+            )
79
+        )
80
+        params = {
81
+            'label' : 'My New label',
82
+            'raw_content': '<p> Le nouveau contenu </p>',
83
+        }
84
+        res = self.testapp.put_json(
85
+            '/api/v2/workspaces/2/html-documents/6',
86
+            params=params,
87
+            status=200
88
+        )
89
+        content = res.json_body
90
+        assert content['content_type'] == 'page'
91
+        assert content['content_id'] == 6
92
+        assert content['is_archived'] is False
93
+        assert content['is_deleted'] is False
94
+        assert content['label'] == 'My New label'
95
+        assert content['parent_id'] == 3
96
+        assert content['show_in_ui'] is True
97
+        assert content['slug'] == 'my-new-label'
98
+        assert content['status'] == 'open'
99
+        assert content['workspace_id'] == 2
100
+        assert content['current_revision_id'] == 26
101
+        # TODO - G.M - 2018-06-173 - check date format
102
+        assert content['created']
103
+        assert content['author']
104
+        assert content['author']['user_id'] == 1
105
+        assert content['author']['avatar_url'] is None
106
+        assert content['author']['public_name'] == 'Global manager'
107
+        # TODO - G.M - 2018-06-173 - check date format
108
+        assert content['modified']
109
+        assert content['last_modifier'] == content['author']
110
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
111
+
112
+        res = self.testapp.get(
113
+            '/api/v2/workspaces/2/html-documents/6',
114
+            status=200
115
+        )   # nopep8
116
+        content = res.json_body
117
+        assert content['content_type'] == 'page'
118
+        assert content['content_id'] == 6
119
+        assert content['is_archived'] is False
120
+        assert content['is_deleted'] is False
121
+        assert content['label'] == 'My New label'
122
+        assert content['parent_id'] == 3
123
+        assert content['show_in_ui'] is True
124
+        assert content['slug'] == 'my-new-label'
125
+        assert content['status'] == 'open'
126
+        assert content['workspace_id'] == 2
127
+        assert content['current_revision_id'] == 26
128
+        # TODO - G.M - 2018-06-173 - check date format
129
+        assert content['created']
130
+        assert content['author']
131
+        assert content['author']['user_id'] == 1
132
+        assert content['author']['avatar_url'] is None
133
+        assert content['author']['public_name'] == 'Global manager'
134
+        # TODO - G.M - 2018-06-173 - check date format
135
+        assert content['modified']
136
+        assert content['last_modifier'] == content['author']
137
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
138
+
139
+    def test_api__get_html_document_revisions__ok_200__nominal_case(self) -> None:
140
+        """
141
+        Get one html document of a content
142
+        """
143
+        self.testapp.authorization = (
144
+            'Basic',
145
+            (
146
+                'admin@admin.admin',
147
+                'admin@admin.admin'
148
+            )
149
+        )
150
+        res = self.testapp.get(
151
+            '/api/v2/workspaces/2/html-documents/6/revisions',
152
+            status=200
153
+        )
154
+        revisions = res.json_body
155
+        assert len(revisions) == 2
156
+        revision = revisions[0]
157
+        assert revision['content_type'] == 'page'
158
+        assert revision['content_id'] == 6
159
+        assert revision['is_archived'] is False
160
+        assert revision['is_deleted'] is False
161
+        assert revision['label'] == 'Tiramisu Recipe'
162
+        assert revision['parent_id'] == 3
163
+        assert revision['show_in_ui'] is True
164
+        assert revision['slug'] == 'tiramisu-recipe'
165
+        assert revision['status'] == 'open'
166
+        assert revision['workspace_id'] == 2
167
+        assert revision['revision_id'] == 6
168
+        assert revision['sub_content_types']
169
+        # TODO - G.M - 2018-06-173 - Test with real comments
170
+        assert revision['comments_ids'] == []
171
+        # TODO - G.M - 2018-06-173 - check date format
172
+        assert revision['created']
173
+        assert revision['author']
174
+        assert revision['author']['user_id'] == 1
175
+        assert revision['author']['avatar_url'] is None
176
+        assert revision['author']['public_name'] == 'Global manager'
177
+
178
+    def test_api__set_html_document_status__ok_200__nominal_case(self) -> None:
179
+        """
180
+        Get one html document of a content
181
+        """
182
+        self.testapp.authorization = (
183
+            'Basic',
184
+            (
185
+                'admin@admin.admin',
186
+                'admin@admin.admin'
187
+            )
188
+        )
189
+        params = {
190
+            'status': 'closed-deprecated',
191
+        }
192
+
193
+        # before
194
+        res = self.testapp.get(
195
+            '/api/v2/workspaces/2/html-documents/6',
196
+            status=200
197
+        )   # nopep8
198
+        content = res.json_body
199
+        assert content['content_type'] == 'page'
200
+        assert content['content_id'] == 6
201
+        assert content['status'] == 'open'
202
+
203
+        # set status
204
+        res = self.testapp.put_json(
205
+            '/api/v2/workspaces/2/html-documents/6/status',
206
+            params=params,
207
+            status=204
208
+        )
209
+
210
+        # after
211
+        res = self.testapp.get(
212
+            '/api/v2/workspaces/2/html-documents/6',
213
+            status=200
214
+        )   # nopep8
215
+        content = res.json_body
216
+        assert content['content_type'] == 'page'
217
+        assert content['content_id'] == 6
218
+        assert content['status'] == 'closed-deprecated'

+ 196 - 0
tracim/views/contents_api/html_document_controller.py View File

@@ -0,0 +1,196 @@
1
+# coding=utf-8
2
+import transaction
3
+from pyramid.config import Configurator
4
+
5
+from tracim.models.data import UserRoleInWorkspace
6
+
7
+try:  # Python 3.5+
8
+    from http import HTTPStatus
9
+except ImportError:
10
+    from http import client as HTTPStatus
11
+
12
+from tracim import TracimRequest
13
+from tracim.extensions import hapic
14
+from tracim.lib.core.content import ContentApi
15
+from tracim.views.controllers import Controller
16
+from tracim.views.core_api.schemas import HtmlDocumentContentSchema
17
+from tracim.views.core_api.schemas import HtmlDocumentRevisionSchema
18
+from tracim.views.core_api.schemas import SetContentStatusSchema
19
+from tracim.views.core_api.schemas import HtmlDocumentModifySchema
20
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21
+from tracim.views.core_api.schemas import NoContentSchema
22
+from tracim.lib.utils.authorization import require_content_types
23
+from tracim.lib.utils.authorization import require_workspace_role
24
+from tracim.exceptions import WorkspaceNotFound, ContentTypeNotAllowed
25
+from tracim.exceptions import InsufficientUserProfile
26
+from tracim.exceptions import NotAuthenticated
27
+from tracim.exceptions import AuthenticationFailed
28
+from tracim.models.contents import ContentTypeLegacy as ContentType
29
+from tracim.models.contents import htmlpage_type
30
+from tracim.models.revision_protection import new_revision
31
+
32
+HTML_DOCUMENT_ENDPOINTS_TAG = 'HTML documents'
33
+
34
+
35
+class HTMLDocumentController(Controller):
36
+
37
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
38
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
39
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
40
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
41
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
42
+    @hapic.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
43
+    @require_workspace_role(UserRoleInWorkspace.READER)
44
+    @require_content_types([htmlpage_type])
45
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
46
+    @hapic.output_body(HtmlDocumentContentSchema())
47
+    def get_html_document(self, context, request: TracimRequest, hapic_data=None):  # nopep8
48
+        """
49
+        Get html document content
50
+        """
51
+        app_config = request.registry.settings['CFG']
52
+        api = ContentApi(
53
+            current_user=request.current_user,
54
+            session=request.dbsession,
55
+            config=app_config,
56
+        )
57
+        content = api.get_one(
58
+            hapic_data.path.content_id,
59
+            content_type=ContentType.Any
60
+        )
61
+        return api.get_content_in_context(content)
62
+
63
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
64
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
65
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
66
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
67
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
68
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
69
+    @require_content_types([htmlpage_type])
70
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
71
+    @hapic.input_body(HtmlDocumentModifySchema())
72
+    @hapic.output_body(HtmlDocumentContentSchema())
73
+    def update_html_document(self, context, request: TracimRequest, hapic_data=None):
74
+        """
75
+        update_html_document
76
+        """
77
+        app_config = request.registry.settings['CFG']
78
+        api = ContentApi(
79
+            current_user=request.current_user,
80
+            session=request.dbsession,
81
+            config=app_config,
82
+        )
83
+        content = api.get_one(
84
+            hapic_data.path.content_id,
85
+            content_type=ContentType.Any
86
+        )
87
+        with new_revision(
88
+                session=request.dbsession,
89
+                tm=transaction.manager,
90
+                content=content
91
+        ):
92
+            api.update_content(
93
+                item=content,
94
+                new_label=hapic_data.body.label,
95
+                new_content=hapic_data.body.raw_content,
96
+
97
+            )
98
+            api.save(content)
99
+        return api.get_content_in_context(content)
100
+
101
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
102
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
103
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
104
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
105
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
106
+    @require_workspace_role(UserRoleInWorkspace.READER)
107
+    @require_content_types([htmlpage_type])
108
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
109
+    @hapic.output_body(HtmlDocumentRevisionSchema(many=True))
110
+    def get_html_document_revisions(self, context, request: TracimRequest, hapic_data=None):  # nopep8
111
+        """
112
+        get html_document revisions
113
+        """
114
+        app_config = request.registry.settings['CFG']
115
+        api = ContentApi(
116
+            current_user=request.current_user,
117
+            session=request.dbsession,
118
+            config=app_config,
119
+        )
120
+        content = api.get_one(
121
+            hapic_data.path.content_id,
122
+            content_type=ContentType.Any
123
+        )
124
+        revisions = content.revisions
125
+        return [
126
+            api.get_revision_in_context(revision)
127
+            for revision in revisions
128
+        ]
129
+
130
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
131
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
132
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
133
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
134
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
135
+    @require_content_types([htmlpage_type])
136
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
137
+    @hapic.input_body(SetContentStatusSchema())
138
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
139
+    def set_html_document_status(self, context, request: TracimRequest, hapic_data=None):  # nopep8
140
+        """
141
+        set html_document status
142
+        """
143
+        app_config = request.registry.settings['CFG']
144
+        api = ContentApi(
145
+            current_user=request.current_user,
146
+            session=request.dbsession,
147
+            config=app_config,
148
+        )
149
+        content = api.get_one(
150
+            hapic_data.path.content_id,
151
+            content_type=ContentType.Any
152
+        )
153
+        with new_revision(
154
+                session=request.dbsession,
155
+                tm=transaction.manager,
156
+                content=content
157
+        ):
158
+            api.set_status(
159
+                content,
160
+                hapic_data.body.status,
161
+            )
162
+            api.save(content)
163
+        return
164
+
165
+    def bind(self, configurator: Configurator):
166
+        # Get html-document
167
+        configurator.add_route(
168
+            'html_document',
169
+            '/workspaces/{workspace_id}/html-documents/{content_id}',
170
+            request_method='GET'
171
+        )
172
+        configurator.add_view(self.get_html_document, route_name='html_document')  # nopep8
173
+
174
+        # update html-document
175
+        configurator.add_route(
176
+            'update_html_document',
177
+            '/workspaces/{workspace_id}/html-documents/{content_id}',
178
+            request_method='PUT'
179
+        )  # nopep8
180
+        configurator.add_view(self.update_html_document, route_name='update_html_document')  # nopep8
181
+
182
+        # get html document revisions
183
+        configurator.add_route(
184
+            'html_document_revisions',
185
+            '/workspaces/{workspace_id}/html-documents/{content_id}/revisions',  # nopep8
186
+            request_method='GET'
187
+        )
188
+        configurator.add_view(self.get_html_document_revisions, route_name='html_document_revisions')  # nopep8
189
+
190
+        # get html document revisions
191
+        configurator.add_route(
192
+            'set_html_document_status',
193
+            '/workspaces/{workspace_id}/html-documents/{content_id}/status',  # nopep8
194
+            request_method='PUT'
195
+        )
196
+        configurator.add_view(self.set_html_document_status, route_name='set_html_document_status')  # nopep8

+ 19 - 3
tracim/views/core_api/schemas.py View File

@@ -9,12 +9,15 @@ from tracim.models.contents import CONTENT_DEFAULT_STATUS
9 9
 from tracim.models.contents import GlobalStatus
10 10
 from tracim.models.contents import open_status
11 11
 from tracim.models.context_models import ContentCreation
12
+from tracim.models.context_models import SetContentStatus
12 13
 from tracim.models.context_models import CommentCreation
13 14
 from tracim.models.context_models import CommentPath
14 15
 from tracim.models.context_models import MoveParams
15 16
 from tracim.models.context_models import WorkspaceAndContentPath
16 17
 from tracim.models.context_models import ContentFilter
17 18
 from tracim.models.context_models import LoginCredentials
19
+from tracim.models.context_models import HTMLDocumentUpdate
20
+from tracim.models.context_models import ThreadUpdate
18 21
 from tracim.models.data import UserRoleInWorkspace
19 22
 
20 23
 
@@ -412,7 +415,7 @@ class HtmlDocumentContentSchema(ContentSchema):
412 415
 
413 416
 
414 417
 class RevisionSchema(ContentDigestSchema):
415
-    comments_id = marshmallow.fields.List(marshmallow.fields.Int(example=4))
418
+    comments_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
416 419
     revision_id = marshmallow.fields.Int(example=12)
417 420
     created = marshmallow.fields.DateTime(
418 421
         format='%Y-%m-%dT%H:%M:%SZ',
@@ -450,10 +453,18 @@ class ContentModifySchema(marshmallow.Schema):
450 453
 class HtmlDocumentModifySchema(ContentModifySchema):
451 454
     raw_content = marshmallow.fields.String('<p>Html page Content !</p>')
452 455
 
456
+    @post_load
457
+    def html_document_update(self, data):
458
+        return HTMLDocumentUpdate(**data)
459
+
453 460
 
454 461
 class ThreadModifySchema(ContentModifySchema):
455 462
     raw_content = marshmallow.fields.String('Description of Thread')
456 463
 
464
+    @post_load
465
+    def thread_update(self, data):
466
+        return ThreadUpdate(**data)
467
+
457 468
 
458 469
 class SetCommentSchema(marshmallow.Schema):
459 470
     raw_content = marshmallow.fields.String(
@@ -470,5 +481,10 @@ class SetContentStatusSchema(marshmallow.Schema):
470 481
         example='closed-deprecated',
471 482
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
472 483
         description='this slug is found in content_type available statuses',
473
-        default=open_status
474
-    )
484
+        default=open_status,
485
+        required=True,
486
+    )
487
+
488
+    @post_load
489
+    def set_status(self, data):
490
+        return SetContentStatus(**data)