Browse Source

Merge pull request #101 from tracim/feature/614_file_content_endpoints

inkhey 5 years ago
parent
commit
c273995464
No account linked to committer's email

+ 7 - 0
.travis.yml View File

@@ -5,6 +5,13 @@ python:
5 5
   - "3.5"
6 6
   - "3.6"
7 7
 
8
+addons:
9
+  apt:
10
+    packages:
11
+    - libreoffice
12
+    - imagemagick
13
+    - libmagickwand-dev
14
+    - ghostscript
8 15
 services:
9 16
   - docker
10 17
   - redis-server

+ 7 - 0
README.md View File

@@ -20,6 +20,13 @@ on Debian Stretch (9) with sudo:
20 20
     sudo apt install git
21 21
     sudo apt install python3 python3-venv python3-dev python3-pip
22 22
     sudo apt install redis-server
23
+    sudo apt install zlib1g-dev libjpeg-dev
24
+    sudo apt install imagemagick libmagickwand-dev ghostscript
25
+
26
+for better preview support:
27
+
28
+    sudo apt install libreoffice # most office documents file and text format
29
+    sudo apt install inkscape # for .svg files.
23 30
 
24 31
 ### Get the source ###
25 32
 

+ 11 - 0
development.ini.sample View File

@@ -175,6 +175,17 @@ wsgidav.config_path = %(here)s/wsgidav.conf
175 175
 ## Do not set http:// prefix.
176 176
 # wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>
177 177
 
178
+### Preview
179
+## You can parametrized allowed jpg preview dimension list, if not set, default
180
+## is 256x256. First {width}x{length} items is default preview dimensions.
181
+## all items should be separated by ',' and you should be really careful to do
182
+## set anything else than '{int}x{int}' item and ', ' separator
183
+# preview.jpg.allowed_dims = 256x256,1000x1000
184
+## Preview dimensions can be set as restricted, if set as restricted, access
185
+## endpoint to  to get any other preview dimensions than allowed_dims will
186
+## return error
187
+# preview.jpg.restricted_dims = True
188
+
178 189
 ###
179 190
 # wsgi server configuration
180 191
 ###

+ 3 - 1
setup.py View File

@@ -35,6 +35,7 @@ requires = [
35 35
     'filedepot',
36 36
     'babel',
37 37
     'python-slugify',
38
+    'preview-generator',
38 39
     # mail-notifier
39 40
     'mako',
40 41
     'lxml',
@@ -48,7 +49,8 @@ tests_require = [
48 49
     'pytest-cov',
49 50
     'pep8',
50 51
     'mypy',
51
-    'requests'
52
+    'requests',
53
+    'Pillow'
52 54
 ]
53 55
 
54 56
 mysql_require = [

+ 5 - 0
tracim/__init__.py View File

@@ -1,4 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2
+
3
+
2 4
 try:  # Python 3.5+
3 5
     from http import HTTPStatus
4 6
 except ImportError:
@@ -27,6 +29,7 @@ from tracim.views.core_api.system_controller import SystemController
27 29
 from tracim.views.core_api.user_controller import UserController
28 30
 from tracim.views.core_api.workspace_controller import WorkspaceController
29 31
 from tracim.views.contents_api.comment_controller import CommentController
32
+from tracim.views.contents_api.file_controller import FileController
30 33
 from tracim.views.errors import ErrorSchema
31 34
 from tracim.exceptions import NotAuthenticated
32 35
 from tracim.exceptions import InvalidId
@@ -109,6 +112,7 @@ def web(global_config, **local_settings):
109 112
     comment_controller = CommentController()
110 113
     html_document_controller = HTMLDocumentController()
111 114
     thread_controller = ThreadController()
115
+    file_controller = FileController()
112 116
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
113 117
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
114 118
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
@@ -116,6 +120,7 @@ def web(global_config, **local_settings):
116 120
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
117 121
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
118 122
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
123
+    configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
119 124
 
120 125
     hapic.add_documentation_view(
121 126
         '/api/v2/doc',

+ 33 - 0
tracim/config.py View File

@@ -411,6 +411,26 @@ class CFG(object):
411 411
         #     self.RADICALE_CLIENT_BASE_URL_HOST,
412 412
         #     self.RADICALE_CLIENT_BASE_URL_PREFIX,
413 413
         # )
414
+        self.PREVIEW_JPG_RESTRICTED_DIMS = asbool(settings.get(
415
+            'preview.jpg.restricted_dims', False
416
+        ))
417
+        preview_jpg_allowed_dims_str = settings.get('preview.jpg.allowed_dims', '')  # nopep8
418
+        allowed_dims = []
419
+        if preview_jpg_allowed_dims_str:
420
+            for sizes in preview_jpg_allowed_dims_str.split(','):
421
+                parts = sizes.split('x')
422
+                assert len(parts) == 2
423
+                width, height = parts
424
+                assert width.isdecimal()
425
+                assert height.isdecimal()
426
+                size = PreviewDim(int(width), int(height))
427
+                allowed_dims.append(size)
428
+
429
+        if not allowed_dims:
430
+            size = PreviewDim(256, 256)
431
+            allowed_dims.append(size)
432
+
433
+        self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims
414 434
 
415 435
     def configure_filedepot(self):
416 436
         depot_storage_name = self.DEPOT_STORAGE_NAME
@@ -427,3 +447,16 @@ class CFG(object):
427 447
 
428 448
         TREEVIEW_FOLDERS = 'folders'
429 449
         TREEVIEW_ALL = 'all'
450
+
451
+
452
+class PreviewDim(object):
453
+
454
+    def __init__(self, width: int, height: int) -> None:
455
+        self.width = width
456
+        self.height = height
457
+
458
+    def __repr__(self):
459
+        return "<PreviewDim width:{width} height:{height}>".format(
460
+            width=self.width,
461
+            height=self.height,
462
+        )

+ 12 - 0
tracim/exceptions.py View File

@@ -178,3 +178,15 @@ class UserCreationFailed(TracimException):
178 178
 
179 179
 class ParentNotFound(NotFound):
180 180
     pass
181
+
182
+
183
+class RevisionDoesNotMatchThisContent(TracimException):
184
+    pass
185
+
186
+
187
+class PageOfPreviewNotFound(NotFound):
188
+    pass
189
+
190
+
191
+class PreviewDimNotAllowed(TracimException):
192
+    pass

+ 185 - 3
tracim/lib/core/content.py View File

@@ -7,6 +7,7 @@ import typing
7 7
 from operator import itemgetter
8 8
 
9 9
 import transaction
10
+from preview_generator.manager import PreviewManager
10 11
 from sqlalchemy import func
11 12
 from sqlalchemy.orm import Query
12 13
 from depot.manager import DepotManager
@@ -25,6 +26,9 @@ 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 PageOfPreviewNotFound
30
+from tracim.exceptions import PreviewDimNotAllowed
31
+from tracim.exceptions import RevisionDoesNotMatchThisContent
28 32
 from tracim.exceptions import EmptyCommentContentNotAllowed
29 33
 from tracim.exceptions import EmptyLabelNotAllowed
30 34
 from tracim.exceptions import ContentNotFound
@@ -43,11 +47,13 @@ from tracim.models.data import UserRoleInWorkspace
43 47
 from tracim.models.data import Workspace
44 48
 from tracim.lib.utils.translation import fake_translator as _
45 49
 from tracim.models.context_models import RevisionInContext
50
+from tracim.models.context_models import PreviewAllowedDim
46 51
 from tracim.models.context_models import ContentInContext
47 52
 
48 53
 __author__ = 'damien'
49 54
 
50 55
 
56
+# TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
51 57
 def compare_content_for_sorting_by_type_and_name(
52 58
         content1: Content,
53 59
         content2: Content
@@ -86,7 +92,7 @@ def compare_content_for_sorting_by_type_and_name(
86 92
         else:
87 93
             return 0
88 94
 
89
-
95
+# TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
90 96
 def compare_tree_items_for_sorting_by_type_and_name(
91 97
         item1: NodeTreeItem,
92 98
         item2: NodeTreeItem
@@ -133,6 +139,7 @@ class ContentApi(object):
133 139
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
134 140
         self._force_show_all_types = force_show_all_types
135 141
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
142
+        self.preview_manager = PreviewManager(self._config.PREVIEW_CACHE_DIR, create_folder=True)  # nopep8
136 143
 
137 144
     @contextmanager
138 145
     def show(
@@ -190,6 +197,7 @@ class ContentApi(object):
190 197
         return self._session.query(Content)\
191 198
             .join(ContentRevisionRO, self._get_revision_join())
192 199
 
200
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
193 201
     @classmethod
194 202
     def sort_tree_items(
195 203
         cls,
@@ -205,6 +213,7 @@ class ContentApi(object):
205 213
 
206 214
         return content_list
207 215
 
216
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
208 217
     @classmethod
209 218
     def sort_content(
210 219
         cls,
@@ -500,16 +509,24 @@ class ContentApi(object):
500 509
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
501 510
         return content
502 511
 
503
-    def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
512
+    def get_one_revision(self, revision_id: int = None, content: Content= None) -> ContentRevisionRO:  # nopep8
504 513
         """
505 514
         This method allow us to get directly any revision with its id
506 515
         :param revision_id: The content's revision's id that we want to return
516
+        :param content: The content related to the revision, if None do not
517
+        check if revision is related to this content.
507 518
         :return: An item Content linked with the correct revision
508 519
         """
509 520
         assert revision_id is not None# DYN_REMOVE
510 521
 
511 522
         revision = self._session.query(ContentRevisionRO).filter(ContentRevisionRO.revision_id == revision_id).one()
512
-
523
+        if content and revision.content_id != content.content_id:
524
+            raise RevisionDoesNotMatchThisContent(
525
+                'revision {revision_id} is not a revision of content {content_id}'.format(  # nopep8
526
+                    revision_id=revision.revision_id,
527
+                    content_id=content.content_id,
528
+                    )
529
+            )
513 530
         return revision
514 531
 
515 532
     # INFO - A.P - 2017-07-03 - python file object getter
@@ -632,6 +649,7 @@ class ContentApi(object):
632 649
             )\
633 650
             .one()
634 651
 
652
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
635 653
     def get_folder_with_workspace_path_labels(
636 654
             self,
637 655
             path_labels: [str],
@@ -725,6 +743,104 @@ class ContentApi(object):
725 743
             ),
726 744
         ))
727 745
 
746
+
747
+    def get_pdf_preview_path(
748
+            self,
749
+            content_id: int,
750
+            revision_id: int,
751
+            page: int
752
+    ) -> str:
753
+        """
754
+        Get pdf preview of revision of content
755
+        :param content_id: id of content
756
+        :param revision_id: id of content revision
757
+        :param page: page number of the preview, useful for multipage content
758
+        :return: preview_path as string
759
+        """
760
+        file_path = self.get_one_revision_filepath(revision_id)
761
+        if page >= self.preview_manager.get_page_nb(file_path):
762
+            raise PageOfPreviewNotFound(
763
+                'page {page} of content {content_id} does not exist'.format(
764
+                    page=page,
765
+                    content_id=content_id
766
+                ),
767
+            )
768
+        jpg_preview_path = self.preview_manager.get_pdf_preview(
769
+            file_path,
770
+            page=page
771
+        )
772
+        return jpg_preview_path
773
+
774
+    def get_full_pdf_preview_path(self, revision_id: int) -> str:
775
+        """
776
+        Get full(multiple page) pdf preview of revision of content
777
+        :param revision_id: id of revision
778
+        :return: path of the full pdf preview of this revision
779
+        """
780
+        file_path = self.get_one_revision_filepath(revision_id)
781
+        pdf_preview_path = self.preview_manager.get_pdf_preview(file_path)
782
+        return pdf_preview_path
783
+
784
+    def get_jpg_preview_allowed_dim(self) -> PreviewAllowedDim:
785
+        """
786
+        Get jpg preview allowed dimensions and strict bool param.
787
+        """
788
+        return PreviewAllowedDim(
789
+            self._config.PREVIEW_JPG_RESTRICTED_DIMS,
790
+            self._config.PREVIEW_JPG_ALLOWED_DIMS,
791
+        )
792
+
793
+    def get_jpg_preview_path(
794
+        self,
795
+        content_id: int,
796
+        revision_id: int,
797
+        page: int,
798
+        width: int = None,
799
+        height: int = None,
800
+    ) -> str:
801
+        """
802
+        Get jpg preview of revision of content
803
+        :param content_id: id of content
804
+        :param revision_id: id of content revision
805
+        :param page: page number of the preview, useful for multipage content
806
+        :param width: width in pixel
807
+        :param height: height in pixel
808
+        :return: preview_path as string
809
+        """
810
+        file_path = self.get_one_revision_filepath(revision_id)
811
+        if page >= self.preview_manager.get_page_nb(file_path):
812
+            raise Exception(
813
+                'page {page} of revision {revision_id} of content {content_id} does not exist'.format(  # nopep8
814
+                    page=page,
815
+                    revision_id=revision_id,
816
+                    content_id=content_id,
817
+                ),
818
+            )
819
+        if not width and not height:
820
+            width = self._config.PREVIEW_JPG_ALLOWED_DIMS[0].width
821
+            height = self._config.PREVIEW_JPG_ALLOWED_DIMS[0].height
822
+
823
+        allowed_dim = False
824
+        for preview_dim in self._config.PREVIEW_JPG_ALLOWED_DIMS:
825
+            if width == preview_dim.width and height == preview_dim.height:
826
+                allowed_dim = True
827
+                break
828
+
829
+        if not allowed_dim and self._config.PREVIEW_JPG_RESTRICTED_DIMS:
830
+            raise PreviewDimNotAllowed(
831
+                'Size {width}x{height} is not allowed for jpeg preview'.format(
832
+                    width=width,
833
+                    height=height,
834
+                )
835
+            )
836
+        jpg_preview_path = self.preview_manager.get_jpeg_preview(
837
+            file_path,
838
+            page=page,
839
+            width=width,
840
+            height=height,
841
+        )
842
+        return jpg_preview_path
843
+
728 844
     def _get_all_query(
729 845
         self,
730 846
         parent_id: int = None,
@@ -797,6 +913,67 @@ class ContentApi(object):
797 913
 
798 914
         return resultset.all()
799 915
 
916
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
917
+    def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
918
+        assert content_type is not None# DYN_REMOVE
919
+
920
+        resultset = self._base_query(workspace)
921
+
922
+        if content_type != ContentType.Any:
923
+            resultset = resultset.filter(Content.type==content_type)
924
+
925
+        return resultset.all()
926
+
927
+    def get_last_active(self, parent_id: int, content_type: str, workspace: Workspace=None, limit=10) -> typing.List[Content]:
928
+        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
929
+        assert content_type is not None# DYN_REMOVE
930
+        assert isinstance(content_type, str) # DYN_REMOVE
931
+
932
+        resultset = self._base_query(workspace) \
933
+            .filter(Content.workspace_id == Workspace.workspace_id) \
934
+            .filter(Workspace.is_deleted.is_(False)) \
935
+            .order_by(desc(Content.updated))
936
+
937
+        if content_type!=ContentType.Any:
938
+            resultset = resultset.filter(Content.type==content_type)
939
+
940
+        if parent_id:
941
+            resultset = resultset.filter(Content.parent_id==parent_id)
942
+
943
+        result = []
944
+        for item in resultset:
945
+            new_item = None
946
+            if ContentType.Comment == item.type:
947
+                new_item = item.parent
948
+            else:
949
+                new_item = item
950
+
951
+            # INFO - D.A. - 2015-05-20
952
+            # We do not want to show only one item if the last 10 items are
953
+            # comments about one thread for example
954
+            if new_item not in result:
955
+                result.append(new_item)
956
+
957
+            if len(result) >= limit:
958
+                break
959
+
960
+        return result
961
+
962
+    def get_last_unread(self, parent_id: int, content_type: str,
963
+                        workspace: Workspace=None, limit=10) -> typing.List[Content]:
964
+        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
965
+        assert content_type is not None# DYN_REMOVE
966
+        assert isinstance(content_type, str) # DYN_REMOVE
967
+
968
+        read_revision_ids = self._session.query(RevisionReadStatus.revision_id) \
969
+            .filter(RevisionReadStatus.user_id==self._user_id)
970
+
971
+        not_read_revisions = self._revisions_base_query(workspace) \
972
+            .filter(~ContentRevisionRO.revision_id.in_(read_revision_ids)) \
973
+            .filter(ContentRevisionRO.workspace_id == Workspace.workspace_id) \
974
+            .filter(Workspace.is_deleted.is_(False)) \
975
+            .subquery()
976
+
800 977
     # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
801 978
     # def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
802 979
     #     assert content_type is not None# DYN_REMOVE
@@ -1281,6 +1458,7 @@ class ContentApi(object):
1281 1458
 
1282 1459
         return ContentType.sorted(content_types)
1283 1460
 
1461
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1284 1462
     def exclude_unavailable(
1285 1463
         self,
1286 1464
         contents: typing.List[Content],
@@ -1294,6 +1472,7 @@ class ContentApi(object):
1294 1472
                 contents.remove(content)
1295 1473
         return contents
1296 1474
 
1475
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1297 1476
     def content_under_deleted(self, content: Content) -> bool:
1298 1477
         if content.parent:
1299 1478
             if content.parent.is_deleted:
@@ -1302,6 +1481,7 @@ class ContentApi(object):
1302 1481
                 return self.content_under_deleted(content.parent)
1303 1482
         return False
1304 1483
 
1484
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1305 1485
     def content_under_archived(self, content: Content) -> bool:
1306 1486
         if content.parent:
1307 1487
             if content.parent.is_archived:
@@ -1310,6 +1490,7 @@ class ContentApi(object):
1310 1490
                 return self.content_under_archived(content.parent)
1311 1491
         return False
1312 1492
 
1493
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1313 1494
     def find_one_by_unique_property(
1314 1495
             self,
1315 1496
             property_name: str,
@@ -1338,6 +1519,7 @@ class ContentApi(object):
1338 1519
         )
1339 1520
         return query.one()
1340 1521
 
1522
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1341 1523
     def generate_folder_label(
1342 1524
             self,
1343 1525
             workspace: Workspace,

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

@@ -6,6 +6,7 @@ from enum import Enum
6 6
 from slugify import slugify
7 7
 from sqlalchemy.orm import Session
8 8
 from tracim import CFG
9
+from tracim.config import PreviewDim
9 10
 from tracim.models import User
10 11
 from tracim.models.auth import Profile
11 12
 from tracim.models.data import Content
@@ -18,6 +19,17 @@ from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
18 19
 from tracim.models.contents import ContentTypeLegacy as ContentType
19 20
 
20 21
 
22
+class PreviewAllowedDim(object):
23
+
24
+    def __init__(
25
+            self,
26
+            restricted:bool,
27
+            dimensions: typing.List[PreviewDim]
28
+    ) -> None:
29
+        self.restricted = restricted
30
+        self.dimensions = dimensions
31
+
32
+
21 33
 class MoveParams(object):
22 34
     """
23 35
     Json body params for move action model
@@ -46,6 +58,39 @@ class WorkspaceAndContentPath(object):
46 58
         self.workspace_id = workspace_id
47 59
 
48 60
 
61
+class WorkspaceAndContentRevisionPath(object):
62
+    """
63
+    Paths params with workspace id and content_id model
64
+    """
65
+    def __init__(self, workspace_id: int, content_id: int, revision_id) -> None:
66
+        self.content_id = content_id
67
+        self.revision_id = revision_id
68
+        self.workspace_id = workspace_id
69
+
70
+
71
+class ContentPreviewSizedPath(object):
72
+    """
73
+    Paths params with workspace id and content_id, width, heigth
74
+    """
75
+    def __init__(self, workspace_id: int, content_id: int, width: int, height: int) -> None:  # nopep8
76
+        self.content_id = content_id
77
+        self.workspace_id = workspace_id
78
+        self.width = width
79
+        self.height = height
80
+
81
+
82
+class RevisionPreviewSizedPath(object):
83
+    """
84
+    Paths params with workspace id and content_id, revision_id width, heigth
85
+    """
86
+    def __init__(self, workspace_id: int, content_id: int, revision_id: int, width: int, height: int) -> None:  # nopep8
87
+        self.content_id = content_id
88
+        self.revision_id = revision_id
89
+        self.workspace_id = workspace_id
90
+        self.width = width
91
+        self.height = height
92
+
93
+
49 94
 class WorkspaceAndUserPath(object):
50 95
     """
51 96
     Paths params with workspace id and user_id
@@ -80,6 +125,17 @@ class CommentPath(object):
80 125
         self.comment_id = comment_id
81 126
 
82 127
 
128
+class PageQuery(object):
129
+    """
130
+    Page query model
131
+    """
132
+    def __init__(
133
+            self,
134
+            page: int = 0
135
+    ):
136
+        self.page = page
137
+
138
+
83 139
 class ContentFilter(object):
84 140
     """
85 141
     Content filter model

+ 12 - 1
tracim/tests/__init__.py View File

@@ -25,6 +25,8 @@ from tracim.config import CFG
25 25
 from tracim.extensions import hapic
26 26
 from tracim import web
27 27
 from webtest import TestApp
28
+from io import BytesIO
29
+from PIL import Image
28 30
 
29 31
 
30 32
 def eq_(a, b, msg=None):
@@ -54,6 +56,15 @@ def set_html_document_slug_to_legacy(session_factory) -> None:
54 56
     assert content_query.count() > 0
55 57
 
56 58
 
59
+def create_1000px_png_test_image():
60
+    file = BytesIO()
61
+    image = Image.new('RGBA', size=(1000, 1000), color=(0, 0, 0))
62
+    image.save(file, 'png')
63
+    file.name = 'test_image.png'
64
+    file.seek(0)
65
+    return file
66
+
67
+
57 68
 class FunctionalTest(unittest.TestCase):
58 69
 
59 70
     fixtures = [BaseFixture]
@@ -68,8 +79,8 @@ class FunctionalTest(unittest.TestCase):
68 79
             'depot_storage_dir': '/tmp/test/depot',
69 80
             'depot_storage_name': 'test',
70 81
             'preview_cache_dir': '/tmp/test/preview_cache',
82
+            'preview.jpg.restricted_dims': True,
71 83
             'email.notification.activated': 'false',
72
-
73 84
         }
74 85
         hapic.reset_context()
75 86
         self.engine = get_engine(self.settings)

File diff suppressed because it is too large
+ 1356 - 124
tracim/tests/functional/test_contents.py


+ 572 - 0
tracim/views/contents_api/file_controller.py View File

@@ -0,0 +1,572 @@
1
+# coding=utf-8
2
+import typing
3
+
4
+import transaction
5
+from depot.manager import DepotManager
6
+from preview_generator.exception import UnavailablePreviewType
7
+from pyramid.config import Configurator
8
+from pyramid.response import FileResponse, FileIter
9
+
10
+try:  # Python 3.5+
11
+    from http import HTTPStatus
12
+except ImportError:
13
+    from http import client as HTTPStatus
14
+
15
+from tracim import TracimRequest
16
+from tracim.extensions import hapic
17
+from tracim.lib.core.content import ContentApi
18
+from tracim.views.controllers import Controller
19
+from tracim.views.core_api.schemas import FileContentSchema
20
+from tracim.views.core_api.schemas import AllowedJpgPreviewDimSchema
21
+from tracim.views.core_api.schemas import ContentPreviewSizedPathSchema
22
+from tracim.views.core_api.schemas import RevisionPreviewSizedPathSchema
23
+from tracim.views.core_api.schemas import PageQuerySchema
24
+from tracim.views.core_api.schemas import WorkspaceAndContentRevisionIdPathSchema  # nopep8
25
+from tracim.views.core_api.schemas import FileRevisionSchema
26
+from tracim.views.core_api.schemas import SetContentStatusSchema
27
+from tracim.views.core_api.schemas import FileContentModifySchema
28
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
29
+from tracim.views.core_api.schemas import NoContentSchema
30
+from tracim.lib.utils.authorization import require_content_types
31
+from tracim.lib.utils.authorization import require_workspace_role
32
+from tracim.models.data import UserRoleInWorkspace
33
+from tracim.models.context_models import ContentInContext
34
+from tracim.models.context_models import RevisionInContext
35
+from tracim.models.contents import ContentTypeLegacy as ContentType
36
+from tracim.models.contents import file_type
37
+from tracim.models.revision_protection import new_revision
38
+from tracim.exceptions import EmptyLabelNotAllowed
39
+from tracim.exceptions import PageOfPreviewNotFound
40
+from tracim.exceptions import PreviewDimNotAllowed
41
+
42
+FILE_ENDPOINTS_TAG = 'Files'
43
+
44
+
45
+class FileController(Controller):
46
+    """
47
+    Endpoints for File Content
48
+    """
49
+
50
+    # File data
51
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
52
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
53
+    @require_content_types([file_type])
54
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
55
+    # TODO - G.M - 2018-07-24 - Use hapic for input file
56
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
57
+    def upload_file(self, context, request: TracimRequest, hapic_data=None):
58
+        """
59
+        Upload a new version of raw file of content. This will create a new
60
+        revision.
61
+        """
62
+        app_config = request.registry.settings['CFG']
63
+        api = ContentApi(
64
+            current_user=request.current_user,
65
+            session=request.dbsession,
66
+            config=app_config,
67
+        )
68
+        content = api.get_one(
69
+            hapic_data.path.content_id,
70
+            content_type=ContentType.Any
71
+        )
72
+        file = request.POST['files']
73
+        with new_revision(
74
+                session=request.dbsession,
75
+                tm=transaction.manager,
76
+                content=content
77
+        ):
78
+            api.update_file_data(
79
+                content,
80
+                new_filename=file.filename,
81
+                new_mimetype=file.type,
82
+                new_content=file.file,
83
+            )
84
+
85
+        return
86
+
87
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
88
+    @require_workspace_role(UserRoleInWorkspace.READER)
89
+    @require_content_types([file_type])
90
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
91
+    @hapic.output_file([])
92
+    def download_file(self, context, request: TracimRequest, hapic_data=None):
93
+        """
94
+        Download raw file of last revision of content.
95
+        """
96
+        app_config = request.registry.settings['CFG']
97
+        api = ContentApi(
98
+            current_user=request.current_user,
99
+            session=request.dbsession,
100
+            config=app_config,
101
+        )
102
+        content = api.get_one(
103
+            hapic_data.path.content_id,
104
+            content_type=ContentType.Any
105
+        )
106
+        file = DepotManager.get().get(content.depot_file)
107
+        response = request.response
108
+        response.content_type = file.content_type
109
+        response.app_iter = FileIter(file)
110
+        return response
111
+
112
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
113
+    @require_workspace_role(UserRoleInWorkspace.READER)
114
+    @require_content_types([file_type])
115
+    @hapic.input_path(WorkspaceAndContentRevisionIdPathSchema())
116
+    @hapic.output_file([])
117
+    def download_revisions_file(self, context, request: TracimRequest, hapic_data=None):  # nopep8
118
+        """
119
+        Download raw file for specific revision of content.
120
+        """
121
+        app_config = request.registry.settings['CFG']
122
+        api = ContentApi(
123
+            current_user=request.current_user,
124
+            session=request.dbsession,
125
+            config=app_config,
126
+        )
127
+        content = api.get_one(
128
+            hapic_data.path.content_id,
129
+            content_type=ContentType.Any
130
+        )
131
+        revision = api.get_one_revision(
132
+            revision_id=hapic_data.path.revision_id,
133
+            content=content
134
+        )
135
+        file = DepotManager.get().get(revision.depot_file)
136
+        response = request.response
137
+        response.content_type = file.content_type
138
+        response.app_iter = FileIter(file)
139
+        return response
140
+
141
+    # preview
142
+    # pdf
143
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
144
+    @require_workspace_role(UserRoleInWorkspace.READER)
145
+    @require_content_types([file_type])
146
+    @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
147
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
148
+    @hapic.input_query(PageQuerySchema())
149
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
150
+    @hapic.output_file([])
151
+    def preview_pdf(self, context, request: TracimRequest, hapic_data=None):
152
+        """
153
+        Obtain a specific page pdf preview of last revision of content.
154
+        """
155
+        app_config = request.registry.settings['CFG']
156
+        api = ContentApi(
157
+            current_user=request.current_user,
158
+            session=request.dbsession,
159
+            config=app_config,
160
+        )
161
+        content = api.get_one(
162
+            hapic_data.path.content_id,
163
+            content_type=ContentType.Any
164
+        )
165
+        pdf_preview_path = api.get_pdf_preview_path(
166
+            content.content_id,
167
+            content.revision_id,
168
+            page=hapic_data.query.page
169
+        )
170
+        return FileResponse(pdf_preview_path)
171
+
172
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
173
+    @require_workspace_role(UserRoleInWorkspace.READER)
174
+    @require_content_types([file_type])
175
+    @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
176
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
177
+    @hapic.output_file([])
178
+    def preview_pdf_full(self, context, request: TracimRequest, hapic_data=None):  # nopep8
179
+        """
180
+        Obtain a full pdf preview (all page) of last revision of content.
181
+        """
182
+        app_config = request.registry.settings['CFG']
183
+        api = ContentApi(
184
+            current_user=request.current_user,
185
+            session=request.dbsession,
186
+            config=app_config,
187
+        )
188
+        content = api.get_one(
189
+            hapic_data.path.content_id,
190
+            content_type=ContentType.Any
191
+        )
192
+        pdf_preview_path = api.get_full_pdf_preview_path(content.revision_id)
193
+        return FileResponse(pdf_preview_path)
194
+
195
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
196
+    @require_workspace_role(UserRoleInWorkspace.READER)
197
+    @require_content_types([file_type])
198
+    @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
199
+    @hapic.input_path(WorkspaceAndContentRevisionIdPathSchema())
200
+    @hapic.input_query(PageQuerySchema())
201
+    @hapic.output_file([])
202
+    def preview_pdf_revision(self, context, request: TracimRequest, hapic_data=None):  # nopep8
203
+        """
204
+        Obtain a specific page pdf preview of a specific revision of content.
205
+        """
206
+        app_config = request.registry.settings['CFG']
207
+        api = ContentApi(
208
+            current_user=request.current_user,
209
+            session=request.dbsession,
210
+            config=app_config,
211
+        )
212
+        content = api.get_one(
213
+            hapic_data.path.content_id,
214
+            content_type=ContentType.Any
215
+        )
216
+        revision = api.get_one_revision(
217
+            revision_id=hapic_data.path.revision_id,
218
+            content=content
219
+        )
220
+        pdf_preview_path = api.get_pdf_preview_path(
221
+            revision.content_id,
222
+            revision.revision_id,
223
+            page=hapic_data.query.page
224
+        )
225
+        return FileResponse(pdf_preview_path)
226
+
227
+    # jpg
228
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
229
+    @require_workspace_role(UserRoleInWorkspace.READER)
230
+    @require_content_types([file_type])
231
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
232
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
233
+    @hapic.input_query(PageQuerySchema())
234
+    @hapic.output_file([])
235
+    def preview_jpg(self, context, request: TracimRequest, hapic_data=None):
236
+        """
237
+        Obtain normally sied jpg preview of last revision of content.
238
+        """
239
+        app_config = request.registry.settings['CFG']
240
+        api = ContentApi(
241
+            current_user=request.current_user,
242
+            session=request.dbsession,
243
+            config=app_config,
244
+        )
245
+        content = api.get_one(
246
+            hapic_data.path.content_id,
247
+            content_type=ContentType.Any
248
+        )
249
+        allowed_dim = api.get_jpg_preview_allowed_dim()
250
+        jpg_preview_path = api.get_jpg_preview_path(
251
+            content_id=content.content_id,
252
+            revision_id=content.revision_id,
253
+            page=hapic_data.query.page,
254
+            width=allowed_dim.dimensions[0].width,
255
+            height=allowed_dim.dimensions[0].height,
256
+        )
257
+        return FileResponse(jpg_preview_path)
258
+
259
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
260
+    @require_workspace_role(UserRoleInWorkspace.READER)
261
+    @require_content_types([file_type])
262
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
263
+    @hapic.handle_exception(PreviewDimNotAllowed, HTTPStatus.BAD_REQUEST)
264
+    @hapic.input_query(PageQuerySchema())
265
+    @hapic.input_path(ContentPreviewSizedPathSchema())
266
+    @hapic.output_file([])
267
+    def sized_preview_jpg(self, context, request: TracimRequest, hapic_data=None):  # nopep8
268
+        """
269
+        Obtain resized jpg preview of last revision of content.
270
+        """
271
+        app_config = request.registry.settings['CFG']
272
+        api = ContentApi(
273
+            current_user=request.current_user,
274
+            session=request.dbsession,
275
+            config=app_config,
276
+        )
277
+        content = api.get_one(
278
+            hapic_data.path.content_id,
279
+            content_type=ContentType.Any
280
+        )
281
+        jpg_preview_path = api.get_jpg_preview_path(
282
+            content_id=content.content_id,
283
+            revision_id=content.revision_id,
284
+            page=hapic_data.query.page,
285
+            height=hapic_data.path.height,
286
+            width=hapic_data.path.width,
287
+        )
288
+        return FileResponse(jpg_preview_path)
289
+
290
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
291
+    @require_workspace_role(UserRoleInWorkspace.READER)
292
+    @require_content_types([file_type])
293
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
294
+    @hapic.handle_exception(PreviewDimNotAllowed, HTTPStatus.BAD_REQUEST)
295
+    @hapic.input_path(RevisionPreviewSizedPathSchema())
296
+    @hapic.input_query(PageQuerySchema())
297
+    @hapic.output_file([])
298
+    def sized_preview_jpg_revision(self, context, request: TracimRequest, hapic_data=None):  # nopep8
299
+        """
300
+        Obtain resized jpg preview of a specific revision of content.
301
+        """
302
+        app_config = request.registry.settings['CFG']
303
+        api = ContentApi(
304
+            current_user=request.current_user,
305
+            session=request.dbsession,
306
+            config=app_config,
307
+        )
308
+        content = api.get_one(
309
+            hapic_data.path.content_id,
310
+            content_type=ContentType.Any
311
+        )
312
+        revision = api.get_one_revision(
313
+            revision_id=hapic_data.path.revision_id,
314
+            content=content
315
+        )
316
+        jpg_preview_path = api.get_jpg_preview_path(
317
+            content_id=content.content_id,
318
+            revision_id=revision.revision_id,
319
+            page=hapic_data.query.page,
320
+            height=hapic_data.path.height,
321
+            width=hapic_data.path.width,
322
+        )
323
+        return FileResponse(jpg_preview_path)
324
+
325
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
326
+    @require_workspace_role(UserRoleInWorkspace.READER)
327
+    @require_content_types([file_type])
328
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
329
+    @hapic.output_body(AllowedJpgPreviewDimSchema())
330
+    def allowed_dim_preview_jpg(self, context, request: TracimRequest, hapic_data=None):  # nopep8
331
+        """
332
+        Get allowed dimensions of jpg preview. If restricted is true,
333
+        only those dimensions are strictly accepted.
334
+        """
335
+        app_config = request.registry.settings['CFG']
336
+        api = ContentApi(
337
+            current_user=request.current_user,
338
+            session=request.dbsession,
339
+            config=app_config,
340
+        )
341
+        return api.get_jpg_preview_allowed_dim()
342
+
343
+    # File infos
344
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
345
+    @require_workspace_role(UserRoleInWorkspace.READER)
346
+    @require_content_types([file_type])
347
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
348
+    @hapic.output_body(FileContentSchema())
349
+    def get_file_infos(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
350
+        """
351
+        Get thread content
352
+        """
353
+        app_config = request.registry.settings['CFG']
354
+        api = ContentApi(
355
+            current_user=request.current_user,
356
+            session=request.dbsession,
357
+            config=app_config,
358
+        )
359
+        content = api.get_one(
360
+            hapic_data.path.content_id,
361
+            content_type=ContentType.Any
362
+        )
363
+        return api.get_content_in_context(content)
364
+
365
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
366
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
367
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
368
+    @require_content_types([file_type])
369
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
370
+    @hapic.input_body(FileContentModifySchema())
371
+    @hapic.output_body(FileContentSchema())
372
+    def update_file_info(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
373
+        """
374
+        update thread
375
+        """
376
+        app_config = request.registry.settings['CFG']
377
+        api = ContentApi(
378
+            current_user=request.current_user,
379
+            session=request.dbsession,
380
+            config=app_config,
381
+        )
382
+        content = api.get_one(
383
+            hapic_data.path.content_id,
384
+            content_type=ContentType.Any
385
+        )
386
+        with new_revision(
387
+                session=request.dbsession,
388
+                tm=transaction.manager,
389
+                content=content
390
+        ):
391
+            api.update_content(
392
+                item=content,
393
+                new_label=hapic_data.body.label,
394
+                new_content=hapic_data.body.raw_content,
395
+
396
+            )
397
+            api.save(content)
398
+        return api.get_content_in_context(content)
399
+
400
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
401
+    @require_workspace_role(UserRoleInWorkspace.READER)
402
+    @require_content_types([file_type])
403
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
404
+    @hapic.output_body(FileRevisionSchema(many=True))
405
+    def get_file_revisions(
406
+            self,
407
+            context,
408
+            request: TracimRequest,
409
+            hapic_data=None
410
+    ) -> typing.List[RevisionInContext]:
411
+        """
412
+        get file revisions
413
+        """
414
+        app_config = request.registry.settings['CFG']
415
+        api = ContentApi(
416
+            current_user=request.current_user,
417
+            session=request.dbsession,
418
+            config=app_config,
419
+        )
420
+        content = api.get_one(
421
+            hapic_data.path.content_id,
422
+            content_type=ContentType.Any
423
+        )
424
+        revisions = content.revisions
425
+        return [
426
+            api.get_revision_in_context(revision)
427
+            for revision in revisions
428
+        ]
429
+
430
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
431
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
432
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
433
+    @require_content_types([file_type])
434
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
435
+    @hapic.input_body(SetContentStatusSchema())
436
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
437
+    def set_file_status(self, context, request: TracimRequest, hapic_data=None) -> None:  # nopep8
438
+        """
439
+        set file status
440
+        """
441
+        app_config = request.registry.settings['CFG']
442
+        api = ContentApi(
443
+            current_user=request.current_user,
444
+            session=request.dbsession,
445
+            config=app_config,
446
+        )
447
+        content = api.get_one(
448
+            hapic_data.path.content_id,
449
+            content_type=ContentType.Any
450
+        )
451
+        with new_revision(
452
+                session=request.dbsession,
453
+                tm=transaction.manager,
454
+                content=content
455
+        ):
456
+            api.set_status(
457
+                content,
458
+                hapic_data.body.status,
459
+            )
460
+            api.save(content)
461
+        return
462
+
463
+    def bind(self, configurator: Configurator) -> None:
464
+        """
465
+        Add route to configurator.
466
+        """
467
+
468
+        # file info #
469
+        # Get file info
470
+        configurator.add_route(
471
+            'file_info',
472
+            '/workspaces/{workspace_id}/files/{content_id}',
473
+            request_method='GET'
474
+        )
475
+        configurator.add_view(self.get_file_infos, route_name='file_info')  # nopep8
476
+        # update file
477
+        configurator.add_route(
478
+            'update_file_info',
479
+            '/workspaces/{workspace_id}/files/{content_id}',
480
+            request_method='PUT'
481
+        )  # nopep8
482
+        configurator.add_view(self.update_file_info, route_name='update_file_info')  # nopep8
483
+
484
+        # raw file #
485
+        # upload raw file
486
+        configurator.add_route(
487
+            'upload_file',
488
+            '/workspaces/{workspace_id}/files/{content_id}/raw',  # nopep8
489
+            request_method='PUT'
490
+        )
491
+        configurator.add_view(self.upload_file, route_name='upload_file')  # nopep8
492
+        # download raw file
493
+        configurator.add_route(
494
+            'download_file',
495
+            '/workspaces/{workspace_id}/files/{content_id}/raw',  # nopep8
496
+            request_method='GET'
497
+        )
498
+        configurator.add_view(self.download_file, route_name='download_file')  # nopep8
499
+        # download raw file of revision
500
+        configurator.add_route(
501
+            'download_revision',
502
+            '/workspaces/{workspace_id}/files/{content_id}/revisions/{revision_id}/raw',  # nopep8
503
+            request_method='GET'
504
+        )
505
+        configurator.add_view(self.download_revisions_file, route_name='download_revision')  # nopep8
506
+
507
+        # previews #
508
+        # get preview pdf full
509
+        configurator.add_route(
510
+            'preview_pdf_full',
511
+            '/workspaces/{workspace_id}/files/{content_id}/preview/pdf/full',  # nopep8
512
+            request_method='GET'
513
+        )
514
+        configurator.add_view(self.preview_pdf_full, route_name='preview_pdf_full')  # nopep8
515
+        # get preview pdf
516
+        configurator.add_route(
517
+            'preview_pdf',
518
+            '/workspaces/{workspace_id}/files/{content_id}/preview/pdf',  # nopep8
519
+            request_method='GET'
520
+        )
521
+        configurator.add_view(self.preview_pdf, route_name='preview_pdf')  # nopep8
522
+        # get preview jpg allowed dims
523
+        configurator.add_route(
524
+            'allowed_dim_preview_jpg',
525
+            '/workspaces/{workspace_id}/files/{content_id}/preview/jpg/allowed_dims',  # nopep8
526
+            request_method='GET'
527
+        )
528
+        configurator.add_view(self.allowed_dim_preview_jpg, route_name='allowed_dim_preview_jpg')  # nopep8
529
+        # get preview jpg
530
+        configurator.add_route(
531
+            'preview_jpg',
532
+            '/workspaces/{workspace_id}/files/{content_id}/preview/jpg',  # nopep8
533
+            request_method='GET'
534
+        )
535
+        configurator.add_view(self.preview_jpg, route_name='preview_jpg')  # nopep8
536
+        # get preview jpg with size
537
+        configurator.add_route(
538
+            'sized_preview_jpg',
539
+            '/workspaces/{workspace_id}/files/{content_id}/preview/jpg/{width}x{height}',  # nopep8
540
+            request_method='GET'
541
+        )
542
+        configurator.add_view(self.sized_preview_jpg, route_name='sized_preview_jpg')  # nopep8
543
+        # get jpg preview for revision
544
+        configurator.add_route(
545
+            'sized_preview_jpg_revision',
546
+            '/workspaces/{workspace_id}/files/{content_id}/revisions/{revision_id}/preview/jpg/{width}x{height}',  # nopep8
547
+            request_method='GET'
548
+        )
549
+        configurator.add_view(self.sized_preview_jpg_revision, route_name='sized_preview_jpg_revision')  # nopep8
550
+        # get jpg preview for revision
551
+        configurator.add_route(
552
+            'preview_pdf_revision',
553
+            '/workspaces/{workspace_id}/files/{content_id}/revisions/{revision_id}/preview/pdf',  # nopep8
554
+            request_method='GET'
555
+        )
556
+        configurator.add_view(self.preview_pdf_revision, route_name='preview_pdf_revision')  # nopep8
557
+        # others #
558
+        # get file revisions
559
+        configurator.add_route(
560
+            'file_revisions',
561
+            '/workspaces/{workspace_id}/files/{content_id}/revisions',  # nopep8
562
+            request_method='GET'
563
+        )
564
+        configurator.add_view(self.get_file_revisions, route_name='file_revisions')  # nopep8
565
+
566
+        # get file status
567
+        configurator.add_route(
568
+            'set_file_status',
569
+            '/workspaces/{workspace_id}/files/{content_id}/status',  # nopep8
570
+            request_method='PUT'
571
+        )
572
+        configurator.add_view(self.set_file_status, route_name='set_file_status')  # nopep8

+ 84 - 0
tracim/views/core_api/schemas.py View File

@@ -14,6 +14,10 @@ from tracim.models.context_models import ActiveContentFilter
14 14
 from tracim.models.context_models import ContentIdsQuery
15 15
 from tracim.models.context_models import UserWorkspaceAndContentPath
16 16
 from tracim.models.context_models import ContentCreation
17
+from tracim.models.context_models import ContentPreviewSizedPath
18
+from tracim.models.context_models import RevisionPreviewSizedPath
19
+from tracim.models.context_models import PageQuery
20
+from tracim.models.context_models import WorkspaceAndContentRevisionPath
17 21
 from tracim.models.context_models import WorkspaceMemberInvitation
18 22
 from tracim.models.context_models import WorkspaceUpdate
19 23
 from tracim.models.context_models import RoleUpdate
@@ -114,6 +118,10 @@ class ContentIdPathSchema(marshmallow.Schema):
114 118
     )
115 119
 
116 120
 
121
+class RevisionIdPathSchema(marshmallow.Schema):
122
+    revision_id = marshmallow.fields.Int(example=6, required=True)
123
+
124
+
117 125
 class WorkspaceAndUserIdPathSchema(
118 126
     UserIdPathSchema,
119 127
     WorkspaceIdPathSchema
@@ -132,6 +140,52 @@ class WorkspaceAndContentIdPathSchema(
132 140
         return WorkspaceAndContentPath(**data)
133 141
 
134 142
 
143
+class WidthAndHeightPathSchema(marshmallow.Schema):
144
+    width = marshmallow.fields.Int(example=256)
145
+    height = marshmallow.fields.Int(example=256)
146
+
147
+
148
+class AllowedJpgPreviewSizesSchema(marshmallow.Schema):
149
+    width = marshmallow.fields.Int(example=256)
150
+    height = marshmallow.fields.Int(example=256)
151
+
152
+
153
+class AllowedJpgPreviewDimSchema(marshmallow.Schema):
154
+    restricted = marshmallow.fields.Bool()
155
+    dimensions = marshmallow.fields.Nested(
156
+        AllowedJpgPreviewSizesSchema,
157
+        many=True
158
+    )
159
+
160
+
161
+class WorkspaceAndContentRevisionIdPathSchema(
162
+    WorkspaceIdPathSchema,
163
+    ContentIdPathSchema,
164
+    RevisionIdPathSchema,
165
+):
166
+    @post_load
167
+    def make_path_object(self, data):
168
+        return WorkspaceAndContentRevisionPath(**data)
169
+
170
+
171
+class ContentPreviewSizedPathSchema(
172
+    WorkspaceAndContentIdPathSchema,
173
+    WidthAndHeightPathSchema
174
+):
175
+    @post_load
176
+    def make_path_object(self, data):
177
+        return ContentPreviewSizedPath(**data)
178
+
179
+
180
+class RevisionPreviewSizedPathSchema(
181
+    WorkspaceAndContentRevisionIdPathSchema,
182
+    WidthAndHeightPathSchema
183
+):
184
+    @post_load
185
+    def make_path_object(self, data):
186
+        return RevisionPreviewSizedPath(**data)
187
+
188
+
135 189
 class UserWorkspaceAndContentIdPathSchema(
136 190
     UserIdPathSchema,
137 191
     WorkspaceIdPathSchema,
@@ -164,6 +218,19 @@ class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
164 218
         return CommentPath(**data)
165 219
 
166 220
 
221
+class PageQuerySchema(marshmallow.Schema):
222
+    page = marshmallow.fields.Int(
223
+        example=2,
224
+        default=0,
225
+        description='allow to show a specific page of a pdf file',
226
+        validate=Range(min=0, error="Value must be positive or 0"),
227
+    )
228
+
229
+    @post_load
230
+    def make_page_query(self, data):
231
+        return PageQuery(**data)
232
+
233
+
167 234
 class FilterContentQuerySchema(marshmallow.Schema):
168 235
     parent_id = marshmallow.fields.Int(
169 236
         example=2,
@@ -594,10 +661,19 @@ class TextBasedDataAbstractSchema(marshmallow.Schema):
594 661
     )
595 662
 
596 663
 
664
+class FileInfoAbstractSchema(marshmallow.Schema):
665
+    raw_content = marshmallow.fields.String(
666
+        description='raw text or html description of the file'
667
+    )
668
+
669
+
597 670
 class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
598 671
     pass
599 672
 
600 673
 
674
+class FileContentSchema(ContentSchema, FileInfoAbstractSchema):
675
+    pass
676
+
601 677
 #####
602 678
 # Revision
603 679
 #####
@@ -629,6 +705,10 @@ class TextBasedRevisionSchema(RevisionSchema, TextBasedDataAbstractSchema):
629 705
     pass
630 706
 
631 707
 
708
+class FileRevisionSchema(RevisionSchema, FileInfoAbstractSchema):
709
+    pass
710
+
711
+
632 712
 class CommentSchema(marshmallow.Schema):
633 713
     content_id = marshmallow.fields.Int(
634 714
         example=6,
@@ -673,6 +753,10 @@ class TextBasedContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbs
673 753
         return TextBasedContentUpdate(**data)
674 754
 
675 755
 
756
+class FileContentModifySchema(TextBasedContentModifySchema):
757
+    pass
758
+
759
+
676 760
 class SetContentStatusSchema(marshmallow.Schema):
677 761
     status = marshmallow.fields.Str(
678 762
         example='closed-deprecated',