Bläddra i källkod

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

inkhey 5 år sedan
förälder
incheckning
c273995464
No account linked to committer's email

+ 7 - 0
.travis.yml Visa fil

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

+ 7 - 0
README.md Visa fil

20
     sudo apt install git
20
     sudo apt install git
21
     sudo apt install python3 python3-venv python3-dev python3-pip
21
     sudo apt install python3 python3-venv python3-dev python3-pip
22
     sudo apt install redis-server
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
 ### Get the source ###
31
 ### Get the source ###
25
 
32
 

+ 11 - 0
development.ini.sample Visa fil

175
 ## Do not set http:// prefix.
175
 ## Do not set http:// prefix.
176
 # wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>
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
 # wsgi server configuration
190
 # wsgi server configuration
180
 ###
191
 ###

+ 3 - 1
setup.py Visa fil

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

+ 5 - 0
tracim/__init__.py Visa fil

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

+ 33 - 0
tracim/config.py Visa fil

411
         #     self.RADICALE_CLIENT_BASE_URL_HOST,
411
         #     self.RADICALE_CLIENT_BASE_URL_HOST,
412
         #     self.RADICALE_CLIENT_BASE_URL_PREFIX,
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
     def configure_filedepot(self):
435
     def configure_filedepot(self):
416
         depot_storage_name = self.DEPOT_STORAGE_NAME
436
         depot_storage_name = self.DEPOT_STORAGE_NAME
427
 
447
 
428
         TREEVIEW_FOLDERS = 'folders'
448
         TREEVIEW_FOLDERS = 'folders'
429
         TREEVIEW_ALL = 'all'
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 Visa fil

178
 
178
 
179
 class ParentNotFound(NotFound):
179
 class ParentNotFound(NotFound):
180
     pass
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 Visa fil

7
 from operator import itemgetter
7
 from operator import itemgetter
8
 
8
 
9
 import transaction
9
 import transaction
10
+from preview_generator.manager import PreviewManager
10
 from sqlalchemy import func
11
 from sqlalchemy import func
11
 from sqlalchemy.orm import Query
12
 from sqlalchemy.orm import Query
12
 from depot.manager import DepotManager
13
 from depot.manager import DepotManager
25
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.exceptions import SameValueError
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
 from tracim.exceptions import EmptyCommentContentNotAllowed
32
 from tracim.exceptions import EmptyCommentContentNotAllowed
29
 from tracim.exceptions import EmptyLabelNotAllowed
33
 from tracim.exceptions import EmptyLabelNotAllowed
30
 from tracim.exceptions import ContentNotFound
34
 from tracim.exceptions import ContentNotFound
43
 from tracim.models.data import Workspace
47
 from tracim.models.data import Workspace
44
 from tracim.lib.utils.translation import fake_translator as _
48
 from tracim.lib.utils.translation import fake_translator as _
45
 from tracim.models.context_models import RevisionInContext
49
 from tracim.models.context_models import RevisionInContext
50
+from tracim.models.context_models import PreviewAllowedDim
46
 from tracim.models.context_models import ContentInContext
51
 from tracim.models.context_models import ContentInContext
47
 
52
 
48
 __author__ = 'damien'
53
 __author__ = 'damien'
49
 
54
 
50
 
55
 
56
+# TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
51
 def compare_content_for_sorting_by_type_and_name(
57
 def compare_content_for_sorting_by_type_and_name(
52
         content1: Content,
58
         content1: Content,
53
         content2: Content
59
         content2: Content
86
         else:
92
         else:
87
             return 0
93
             return 0
88
 
94
 
89
-
95
+# TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
90
 def compare_tree_items_for_sorting_by_type_and_name(
96
 def compare_tree_items_for_sorting_by_type_and_name(
91
         item1: NodeTreeItem,
97
         item1: NodeTreeItem,
92
         item2: NodeTreeItem
98
         item2: NodeTreeItem
133
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
139
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
134
         self._force_show_all_types = force_show_all_types
140
         self._force_show_all_types = force_show_all_types
135
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
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
     @contextmanager
144
     @contextmanager
138
     def show(
145
     def show(
190
         return self._session.query(Content)\
197
         return self._session.query(Content)\
191
             .join(ContentRevisionRO, self._get_revision_join())
198
             .join(ContentRevisionRO, self._get_revision_join())
192
 
199
 
200
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
193
     @classmethod
201
     @classmethod
194
     def sort_tree_items(
202
     def sort_tree_items(
195
         cls,
203
         cls,
205
 
213
 
206
         return content_list
214
         return content_list
207
 
215
 
216
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
208
     @classmethod
217
     @classmethod
209
     def sort_content(
218
     def sort_content(
210
         cls,
219
         cls,
500
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
509
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
501
         return content
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
         This method allow us to get directly any revision with its id
514
         This method allow us to get directly any revision with its id
506
         :param revision_id: The content's revision's id that we want to return
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
         :return: An item Content linked with the correct revision
518
         :return: An item Content linked with the correct revision
508
         """
519
         """
509
         assert revision_id is not None# DYN_REMOVE
520
         assert revision_id is not None# DYN_REMOVE
510
 
521
 
511
         revision = self._session.query(ContentRevisionRO).filter(ContentRevisionRO.revision_id == revision_id).one()
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
         return revision
530
         return revision
514
 
531
 
515
     # INFO - A.P - 2017-07-03 - python file object getter
532
     # INFO - A.P - 2017-07-03 - python file object getter
632
             )\
649
             )\
633
             .one()
650
             .one()
634
 
651
 
652
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
635
     def get_folder_with_workspace_path_labels(
653
     def get_folder_with_workspace_path_labels(
636
             self,
654
             self,
637
             path_labels: [str],
655
             path_labels: [str],
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
     def _get_all_query(
844
     def _get_all_query(
729
         self,
845
         self,
730
         parent_id: int = None,
846
         parent_id: int = None,
797
 
913
 
798
         return resultset.all()
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
     # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
977
     # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
801
     # def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
978
     # def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
802
     #     assert content_type is not None# DYN_REMOVE
979
     #     assert content_type is not None# DYN_REMOVE
1281
 
1458
 
1282
         return ContentType.sorted(content_types)
1459
         return ContentType.sorted(content_types)
1283
 
1460
 
1461
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1284
     def exclude_unavailable(
1462
     def exclude_unavailable(
1285
         self,
1463
         self,
1286
         contents: typing.List[Content],
1464
         contents: typing.List[Content],
1294
                 contents.remove(content)
1472
                 contents.remove(content)
1295
         return contents
1473
         return contents
1296
 
1474
 
1475
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1297
     def content_under_deleted(self, content: Content) -> bool:
1476
     def content_under_deleted(self, content: Content) -> bool:
1298
         if content.parent:
1477
         if content.parent:
1299
             if content.parent.is_deleted:
1478
             if content.parent.is_deleted:
1302
                 return self.content_under_deleted(content.parent)
1481
                 return self.content_under_deleted(content.parent)
1303
         return False
1482
         return False
1304
 
1483
 
1484
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1305
     def content_under_archived(self, content: Content) -> bool:
1485
     def content_under_archived(self, content: Content) -> bool:
1306
         if content.parent:
1486
         if content.parent:
1307
             if content.parent.is_archived:
1487
             if content.parent.is_archived:
1310
                 return self.content_under_archived(content.parent)
1490
                 return self.content_under_archived(content.parent)
1311
         return False
1491
         return False
1312
 
1492
 
1493
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1313
     def find_one_by_unique_property(
1494
     def find_one_by_unique_property(
1314
             self,
1495
             self,
1315
             property_name: str,
1496
             property_name: str,
1338
         )
1519
         )
1339
         return query.one()
1520
         return query.one()
1340
 
1521
 
1522
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1341
     def generate_folder_label(
1523
     def generate_folder_label(
1342
             self,
1524
             self,
1343
             workspace: Workspace,
1525
             workspace: Workspace,

+ 56 - 0
tracim/models/context_models.py Visa fil

6
 from slugify import slugify
6
 from slugify import slugify
7
 from sqlalchemy.orm import Session
7
 from sqlalchemy.orm import Session
8
 from tracim import CFG
8
 from tracim import CFG
9
+from tracim.config import PreviewDim
9
 from tracim.models import User
10
 from tracim.models import User
10
 from tracim.models.auth import Profile
11
 from tracim.models.auth import Profile
11
 from tracim.models.data import Content
12
 from tracim.models.data import Content
18
 from tracim.models.contents import ContentTypeLegacy as ContentType
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
 class MoveParams(object):
33
 class MoveParams(object):
22
     """
34
     """
23
     Json body params for move action model
35
     Json body params for move action model
46
         self.workspace_id = workspace_id
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
 class WorkspaceAndUserPath(object):
94
 class WorkspaceAndUserPath(object):
50
     """
95
     """
51
     Paths params with workspace id and user_id
96
     Paths params with workspace id and user_id
80
         self.comment_id = comment_id
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
 class ContentFilter(object):
139
 class ContentFilter(object):
84
     """
140
     """
85
     Content filter model
141
     Content filter model

+ 12 - 1
tracim/tests/__init__.py Visa fil

25
 from tracim.extensions import hapic
25
 from tracim.extensions import hapic
26
 from tracim import web
26
 from tracim import web
27
 from webtest import TestApp
27
 from webtest import TestApp
28
+from io import BytesIO
29
+from PIL import Image
28
 
30
 
29
 
31
 
30
 def eq_(a, b, msg=None):
32
 def eq_(a, b, msg=None):
54
     assert content_query.count() > 0
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
 class FunctionalTest(unittest.TestCase):
68
 class FunctionalTest(unittest.TestCase):
58
 
69
 
59
     fixtures = [BaseFixture]
70
     fixtures = [BaseFixture]
68
             'depot_storage_dir': '/tmp/test/depot',
79
             'depot_storage_dir': '/tmp/test/depot',
69
             'depot_storage_name': 'test',
80
             'depot_storage_name': 'test',
70
             'preview_cache_dir': '/tmp/test/preview_cache',
81
             'preview_cache_dir': '/tmp/test/preview_cache',
82
+            'preview.jpg.restricted_dims': True,
71
             'email.notification.activated': 'false',
83
             'email.notification.activated': 'false',
72
-
73
         }
84
         }
74
         hapic.reset_context()
85
         hapic.reset_context()
75
         self.engine = get_engine(self.settings)
86
         self.engine = get_engine(self.settings)

Filskillnaden har hållits tillbaka eftersom den är för stor
+ 1356 - 124
tracim/tests/functional/test_contents.py


+ 572 - 0
tracim/views/contents_api/file_controller.py Visa fil

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 Visa fil

14
 from tracim.models.context_models import ContentIdsQuery
14
 from tracim.models.context_models import ContentIdsQuery
15
 from tracim.models.context_models import UserWorkspaceAndContentPath
15
 from tracim.models.context_models import UserWorkspaceAndContentPath
16
 from tracim.models.context_models import ContentCreation
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
 from tracim.models.context_models import WorkspaceMemberInvitation
21
 from tracim.models.context_models import WorkspaceMemberInvitation
18
 from tracim.models.context_models import WorkspaceUpdate
22
 from tracim.models.context_models import WorkspaceUpdate
19
 from tracim.models.context_models import RoleUpdate
23
 from tracim.models.context_models import RoleUpdate
114
     )
118
     )
115
 
119
 
116
 
120
 
121
+class RevisionIdPathSchema(marshmallow.Schema):
122
+    revision_id = marshmallow.fields.Int(example=6, required=True)
123
+
124
+
117
 class WorkspaceAndUserIdPathSchema(
125
 class WorkspaceAndUserIdPathSchema(
118
     UserIdPathSchema,
126
     UserIdPathSchema,
119
     WorkspaceIdPathSchema
127
     WorkspaceIdPathSchema
132
         return WorkspaceAndContentPath(**data)
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
 class UserWorkspaceAndContentIdPathSchema(
189
 class UserWorkspaceAndContentIdPathSchema(
136
     UserIdPathSchema,
190
     UserIdPathSchema,
137
     WorkspaceIdPathSchema,
191
     WorkspaceIdPathSchema,
164
         return CommentPath(**data)
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
 class FilterContentQuerySchema(marshmallow.Schema):
234
 class FilterContentQuerySchema(marshmallow.Schema):
168
     parent_id = marshmallow.fields.Int(
235
     parent_id = marshmallow.fields.Int(
169
         example=2,
236
         example=2,
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
 class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
670
 class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
598
     pass
671
     pass
599
 
672
 
600
 
673
 
674
+class FileContentSchema(ContentSchema, FileInfoAbstractSchema):
675
+    pass
676
+
601
 #####
677
 #####
602
 # Revision
678
 # Revision
603
 #####
679
 #####
629
     pass
705
     pass
630
 
706
 
631
 
707
 
708
+class FileRevisionSchema(RevisionSchema, FileInfoAbstractSchema):
709
+    pass
710
+
711
+
632
 class CommentSchema(marshmallow.Schema):
712
 class CommentSchema(marshmallow.Schema):
633
     content_id = marshmallow.fields.Int(
713
     content_id = marshmallow.fields.Int(
634
         example=6,
714
         example=6,
673
         return TextBasedContentUpdate(**data)
753
         return TextBasedContentUpdate(**data)
674
 
754
 
675
 
755
 
756
+class FileContentModifySchema(TextBasedContentModifySchema):
757
+    pass
758
+
759
+
676
 class SetContentStatusSchema(marshmallow.Schema):
760
 class SetContentStatusSchema(marshmallow.Schema):
677
     status = marshmallow.fields.Str(
761
     status = marshmallow.fields.Str(
678
         example='closed-deprecated',
762
         example='closed-deprecated',