Browse Source

Merge pull request #270 from tracim/dev/232/preview_generator

Tracim 7 years ago
parent
commit
c4ee0e75db

+ 0 - 100
bin/tg2env-patch View File

1
-#!/usr/bin/python
2
-
3
-import os
4
-import sys
5
-
6
-# Go to parent folder
7
-
8
-def usage():
9
-    print('')
10
-    print('USAGE: '+__file__+' <step_1_or2> <site_packages_folder_path> ')
11
-    print('')
12
-    print('')
13
-
14
-
15
-def show_help_and_exit():
16
-    usage()
17
-    exit()
18
-
19
-def on_result_and_exit(error_code):
20
-    if error_code==0:
21
-        print('')
22
-        print('')
23
-        exit(0)
24
-    
25
-    print('ERRROR')
26
-    print('')
27
-    print('')
28
-    exit(error_code)
29
-
30
-#######
31
-
32
-
33
-if len(sys.argv)<=2:
34
-    show_help_and_exit()
35
-
36
-########################################
37
-#
38
-# BELOW ARE STANDARD ACTIONS
39
-#
40
-########################################
41
-
42
-patch_step = sys.argv[1]
43
-sitepackages_path = sys.argv[2]
44
-
45
-print('PATCHING PYTHON CODE')
46
-print('--------------------')
47
-print('site packages path:    %s'%(sitepackages_path))
48
-print('')
49
-
50
-if patch_step=='1':
51
-
52
-    patchable_paths = [
53
-        sitepackages_path+'/tgext/pluggable',
54
-        sitepackages_path+'/resetpassword',
55
-        sitepackages_path+'/babel'
56
-    ]
57
-
58
-    for patchable_path in patchable_paths:
59
-        print('2to3 conversion for %s...' % (patchable_path))
60
-        os.system('2to3 -w %s'%(patchable_path))
61
-        print('-> done')
62
-
63
-    babel_source_code_patch_content = """--- tg2env/lib/python3.2/site-packages/babel/messages/pofile.py 2014-11-07 15:35:14.039913184 +0100
64
-    +++ tg2env/lib/python3.2/site-packages/babel/messages/pofile.py 2014-10-30 17:37:36.295091573 +0100
65
-    @@ -384,8 +384,13 @@
66
-     
67
-         def _write(text):
68
-             if isinstance(text, text_type):
69
-    -            text = text.encode(catalog.charset, 'backslashreplace')
70
-    -        fileobj.write(text)
71
-    +            pass
72
-    +            # text = text.encode(catalog.charset, 'backslashreplace')
73
-    +        try:
74
-    +            fileobj.write(text.encode('UTF-8'))
75
-    +        except Exception as e:
76
-    +            fileobj.write(text)
77
-    +        
78
-     
79
-         def _write_comment(comment, prefix=''):
80
-             # xgettext always wraps comments even if --no-wrap is passed;
81
-    """
82
-
83
-    babel_patchable_file_path = sitepackages_path+'/babel/messages/pofile.py'
84
-    print('Patching code in file %s...'%(babel_patchable_file_path))
85
-    os.system('echo "%s"|patch -p1 %s'%(babel_source_code_patch_content, babel_patchable_file_path))
86
-    print('-> done')
87
-
88
-elif patch_step=='2':
89
-    resetpassword_patchable_file_path = sitepackages_path+'/resetpassword/lib/__init__.py'
90
-    print('Patching code in file %s...'%(resetpassword_patchable_file_path))
91
-    os.system("sed -i 's/body\.encode/body/g' %s" % (resetpassword_patchable_file_path))
92
-    print('-> done')
93
-
94
-else:
95
-    show_help_and_exit()
96
-
97
-print('')
98
-print('')
99
-
100
-

+ 1 - 0
tracim/development.ini.base View File

35
 # You can set french as default language by uncommenting next line
35
 # You can set french as default language by uncommenting next line
36
 # lang = fr
36
 # lang = fr
37
 cache_dir = %(here)s/data
37
 cache_dir = %(here)s/data
38
+preview_cache_dir = /tmp/tracim/cache/previews/
38
 beaker.session.key = tracim
39
 beaker.session.key = tracim
39
 beaker.session.secret = 3283411b-1904-4554-b0e1-883863b53080
40
 beaker.session.secret = 3283411b-1904-4554-b0e1-883863b53080
40
 
41
 

+ 25 - 0
tracim/migration/versions/69fb10c3d6f0_files_on_disk.py View File

1
+"""files on disk
2
+
3
+Revision ID: 69fb10c3d6f0
4
+Revises: c1cea4bbae16
5
+Create Date: 2017-06-07 17:25:47.306472
6
+
7
+"""
8
+
9
+from alembic import op
10
+from depot.fields.sqlalchemy import UploadedFileField
11
+import sqlalchemy as sa
12
+
13
+# revision identifiers, used by Alembic.
14
+revision = '69fb10c3d6f0'
15
+down_revision = 'c1cea4bbae16'
16
+
17
+
18
+def upgrade():
19
+    op.add_column('content_revisions',
20
+                  sa.Column('depot_file',
21
+                            UploadedFileField))
22
+
23
+
24
+def downgrade():
25
+    op.drop_column('content_revisions', 'depot_file')

+ 2 - 0
tracim/setup.py View File

52
     "unicode-slugify==0.1.3",
52
     "unicode-slugify==0.1.3",
53
     "pytz==2014.7",
53
     "pytz==2014.7",
54
     'rq==0.7.1',
54
     'rq==0.7.1',
55
+    'filedepot>=0.5.0',
56
+    'preview-generator'
55
     ]
57
     ]
56
 
58
 
57
 setup(
59
 setup(

+ 8 - 0
tracim/tracim/config/app_cfg.py View File

38
 from tracim.model.data import ActionDescription
38
 from tracim.model.data import ActionDescription
39
 from tracim.model.data import ContentType
39
 from tracim.model.data import ContentType
40
 
40
 
41
+from depot.manager import DepotManager
42
+DepotManager.configure(
43
+    'default',
44
+    {'depot.storage_path': '/tmp/depot_storage_path/'}
45
+)
46
+
41
 base_config = TracimAppConfig()
47
 base_config = TracimAppConfig()
42
 base_config.renderers = []
48
 base_config.renderers = []
43
 base_config.use_toscawidgets = False
49
 base_config.use_toscawidgets = False
188
 
194
 
189
     def __init__(self):
195
     def __init__(self):
190
 
196
 
197
+        self.PREVIEW_CACHE = str(tg.config.get('preview_cache_dir'))
198
+
191
         self.DATA_UPDATE_ALLOWED_DURATION = int(tg.config.get('content.update.allowed.duration', 0))
199
         self.DATA_UPDATE_ALLOWED_DURATION = int(tg.config.get('content.update.allowed.duration', 0))
192
 
200
 
193
         self.WEBSITE_TITLE = tg.config.get('website.title', 'TRACIM')
201
         self.WEBSITE_TITLE = tg.config.get('website.title', 'TRACIM')

+ 311 - 182
tracim/tracim/controllers/content.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+from tracim.config.app_cfg import CFG
3
+
2
 __author__ = 'damien'
4
 __author__ = 'damien'
3
 
5
 
4
 import sys
6
 import sys
5
 import traceback
7
 import traceback
6
 
8
 
7
 from cgi import FieldStorage
9
 from cgi import FieldStorage
10
+from depot.manager import DepotManager
11
+from preview_generator.manager import PreviewManager
12
+from sqlalchemy.orm.exc import NoResultFound
8
 import tg
13
 import tg
14
+from tg import abort
9
 from tg import tmpl_context
15
 from tg import tmpl_context
10
 from tg import require
16
 from tg import require
11
 from tg import predicates
17
 from tg import predicates
12
 from tg.i18n import ugettext as _
18
 from tg.i18n import ugettext as _
13
 from tg.predicates import not_anonymous
19
 from tg.predicates import not_anonymous
14
-from sqlalchemy.orm.exc import NoResultFound
15
-from tg import abort
16
 
20
 
17
 from tracim.controllers import TIMRestController
21
 from tracim.controllers import TIMRestController
18
 from tracim.controllers import StandardController
22
 from tracim.controllers import StandardController
22
 from tracim.lib import CST
26
 from tracim.lib import CST
23
 from tracim.lib.base import BaseController
27
 from tracim.lib.base import BaseController
24
 from tracim.lib.base import logger
28
 from tracim.lib.base import logger
29
+from tracim.lib.integrity import render_invalid_integrity_chosen_path
25
 from tracim.lib.utils import SameValueError
30
 from tracim.lib.utils import SameValueError
26
 from tracim.lib.utils import get_valid_header_file_name
31
 from tracim.lib.utils import get_valid_header_file_name
27
 from tracim.lib.utils import str_as_bool
32
 from tracim.lib.utils import str_as_bool
41
 from tracim.model.data import ContentType
46
 from tracim.model.data import ContentType
42
 from tracim.model.data import UserRoleInWorkspace
47
 from tracim.model.data import UserRoleInWorkspace
43
 from tracim.model.data import Workspace
48
 from tracim.model.data import Workspace
44
-from tracim.lib.integrity import render_invalid_integrity_chosen_path
45
 
49
 
46
 
50
 
47
 class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
51
 class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
62
 
66
 
63
     @tg.expose()
67
     @tg.expose()
64
     @tg.require(current_user_is_contributor())
68
     @tg.require(current_user_is_contributor())
65
-    def post(self, content=''):
69
+    def post(self, content: str = ''):
66
         # TODO - SECURE THIS
70
         # TODO - SECURE THIS
67
         workspace = tmpl_context.workspace
71
         workspace = tmpl_context.workspace
68
         thread = tmpl_context.thread
72
         thread = tmpl_context.thread
69
 
73
 
70
-
71
         api = ContentApi(tmpl_context.current_user)
74
         api = ContentApi(tmpl_context.current_user)
72
 
75
 
73
         comment = api.create_comment(workspace, thread, content, True)
76
         comment = api.create_comment(workspace, thread, content, True)
74
-
75
-        next_url = tg.url('/workspaces/{}/folders/{}/threads/{}').format(tmpl_context.workspace_id,
76
-                                                                              tmpl_context.folder_id,
77
-                                                                              tmpl_context.thread_id)
77
+        next_str = '/workspaces/{}/folders/{}/threads/{}'
78
+        next_url = tg.url(next_str).format(tmpl_context.workspace_id,
79
+                                           tmpl_context.folder_id,
80
+                                           tmpl_context.thread_id)
78
         tg.flash(_('Comment added'), CST.STATUS_OK)
81
         tg.flash(_('Comment added'), CST.STATUS_OK)
79
         tg.redirect(next_url)
82
         tg.redirect(next_url)
80
 
83
 
86
         # TODO - CHECK RIGHTS
89
         # TODO - CHECK RIGHTS
87
         item_id = int(item_id)
90
         item_id = int(item_id)
88
         content_api = ContentApi(tmpl_context.current_user)
91
         content_api = ContentApi(tmpl_context.current_user)
89
-        item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace)
90
-
92
+        item = content_api.get_one(item_id,
93
+                                   self._item_type,
94
+                                   tmpl_context.workspace)
95
+        next_or_back = '/workspaces/{}/folders/{}/threads/{}'
91
         try:
96
         try:
92
-
93
-            next_url = tg.url('/workspaces/{}/folders/{}/threads/{}').format(tmpl_context.workspace_id,
94
-                                                                             tmpl_context.folder_id,
95
-                                                                             tmpl_context.thread_id)
96
-            undo_url = tg.url('/workspaces/{}/folders/{}/threads/{}/comments/{}/put_delete_undo').format(tmpl_context.workspace_id,
97
-                                                                                                         tmpl_context.folder_id,
98
-                                                                                                         tmpl_context.thread_id,
99
-                                                                                                         item_id)
100
-
101
-            msg = _('{} deleted. <a class="alert-link" href="{}">Cancel action</a>').format(self._item_type_label, undo_url)
97
+            next_url = tg.url(next_or_back).format(tmpl_context.workspace_id,
98
+                                                   tmpl_context.folder_id,
99
+                                                   tmpl_context.thread_id)
100
+            undo_str = '{}/comments/{}/put_delete_undo'
101
+            undo_url = tg.url(undo_str).format(next_url,
102
+                                               item_id)
103
+            msg_str = ('{} deleted. '
104
+                       '<a class="alert-link" href="{}">Cancel action</a>')
105
+            msg = _(msg_str).format(self._item_type_label,
106
+                                    undo_url)
102
             with new_revision(item):
107
             with new_revision(item):
103
                 content_api.delete(item)
108
                 content_api.delete(item)
104
                 content_api.save(item, ActionDescription.DELETION)
109
                 content_api.save(item, ActionDescription.DELETION)
107
             tg.redirect(next_url)
112
             tg.redirect(next_url)
108
 
113
 
109
         except ValueError as e:
114
         except ValueError as e:
110
-            back_url = tg.url('/workspaces/{}/folders/{}/threads/{}').format(tmpl_context.workspace_id,
111
-                                                                             tmpl_context.folder_id,
112
-                                                                             tmpl_context.thread_id)
115
+            back_url = tg.url(next_or_back).format(tmpl_context.workspace_id,
116
+                                                   tmpl_context.folder_id,
117
+                                                   tmpl_context.thread_id)
113
             msg = _('{} not deleted: {}').format(self._item_type_label, str(e))
118
             msg = _('{} not deleted: {}').format(self._item_type_label, str(e))
114
             tg.flash(msg, CST.STATUS_ERROR)
119
             tg.flash(msg, CST.STATUS_ERROR)
115
             tg.redirect(back_url)
120
             tg.redirect(back_url)
116
 
121
 
117
-
118
     @tg.expose()
122
     @tg.expose()
119
     @tg.require(not_anonymous())
123
     @tg.require(not_anonymous())
120
     def put_delete_undo(self, item_id):
124
     def put_delete_undo(self, item_id):
121
         require_current_user_is_owner(int(item_id))
125
         require_current_user_is_owner(int(item_id))
122
 
126
 
123
         item_id = int(item_id)
127
         item_id = int(item_id)
124
-        content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items
125
-        item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace)
128
+        # Here we do not filter deleted items
129
+        content_api = ContentApi(tmpl_context.current_user, True, True)
130
+        item = content_api.get_one(item_id,
131
+                                   self._item_type,
132
+                                   tmpl_context.workspace)
133
+        next_or_back = '/workspaces/{}/folders/{}/threads/{}'
126
         try:
134
         try:
127
-            next_url = tg.url('/workspaces/{}/folders/{}/threads/{}').format(tmpl_context.workspace_id,
128
-                                                                             tmpl_context.folder_id,
129
-                                                                             tmpl_context.thread_id)
135
+            next_url = tg.url(next_or_back).format(tmpl_context.workspace_id,
136
+                                                   tmpl_context.folder_id,
137
+                                                   tmpl_context.thread_id)
130
             msg = _('{} undeleted.').format(self._item_type_label)
138
             msg = _('{} undeleted.').format(self._item_type_label)
131
             with new_revision(item):
139
             with new_revision(item):
132
                 content_api.undelete(item)
140
                 content_api.undelete(item)
137
 
145
 
138
         except ValueError as e:
146
         except ValueError as e:
139
             logger.debug(self, 'Exception: {}'.format(e.__str__))
147
             logger.debug(self, 'Exception: {}'.format(e.__str__))
140
-            back_url = tg.url('/workspaces/{}/folders/{}/threads/{}').format(tmpl_context.workspace_id,
141
-                                                                             tmpl_context.folder_id,
142
-                                                                             tmpl_context.thread_id)
143
-            msg = _('{} not un-deleted: {}').format(self._item_type_label, str(e))
148
+            back_url = tg.url(next_or_back).format(tmpl_context.workspace_id,
149
+                                                   tmpl_context.folder_id,
150
+                                                   tmpl_context.thread_id)
151
+            msg = _('{} not un-deleted: {}').format(self._item_type_label,
152
+                                                    str(e))
144
             tg.flash(msg, CST.STATUS_ERROR)
153
             tg.flash(msg, CST.STATUS_ERROR)
145
             tg.redirect(back_url)
154
             tg.redirect(back_url)
146
 
155
 
185
     @tg.expose('tracim.templates.file.getone')
194
     @tg.expose('tracim.templates.file.getone')
186
     def get_one(self, file_id, revision_id=None):
195
     def get_one(self, file_id, revision_id=None):
187
         file_id = int(file_id)
196
         file_id = int(file_id)
197
+        cache_path = CFG.get_instance().PREVIEW_CACHE
198
+        preview_manager = PreviewManager(cache_path, create_folder=True)
188
         user = tmpl_context.current_user
199
         user = tmpl_context.current_user
189
         workspace = tmpl_context.workspace
200
         workspace = tmpl_context.workspace
190
-
191
         current_user_content = Context(CTX.CURRENT_USER,
201
         current_user_content = Context(CTX.CURRENT_USER,
192
                                        current_user=user).toDict(user)
202
                                        current_user=user).toDict(user)
193
         current_user_content.roles.sort(key=lambda role: role.workspace.name)
203
         current_user_content.roles.sort(key=lambda role: role.workspace.name)
194
-
195
-        content_api = ContentApi(
196
-            user,
197
-            show_archived=True,
198
-            show_deleted=True,
199
-        )
204
+        content_api = ContentApi(user,
205
+                                 show_archived=True,
206
+                                 show_deleted=True)
200
         if revision_id:
207
         if revision_id:
201
-            file = content_api.get_one_from_revision(file_id,  self._item_type, workspace, revision_id)
208
+            file = content_api.get_one_from_revision(file_id,
209
+                                                     self._item_type,
210
+                                                     workspace,
211
+                                                     revision_id)
202
         else:
212
         else:
203
-            file = content_api.get_one(file_id, self._item_type, workspace)
213
+            file = content_api.get_one(file_id,
214
+                                       self._item_type,
215
+                                       workspace)
216
+            revision_id = file.revision_id
217
+
218
+        file_path = content_api.get_one_revision_filepath(revision_id)
219
+        nb_page = preview_manager.get_nb_page(file_path=file_path)
220
+        preview_urls = []
221
+        for page in range(int(nb_page)):
222
+            url_str = '/previews/{}/pages/{}?revision_id={}'
223
+            url = url_str.format(file_id,
224
+                                 page,
225
+                                 revision_id)
226
+            preview_urls.append(url)
204
 
227
 
205
         fake_api_breadcrumb = self.get_breadcrumb(file_id)
228
         fake_api_breadcrumb = self.get_breadcrumb(file_id)
206
-        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb, current_user=current_user_content)
229
+        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb,
230
+                                         current_user=current_user_content)
207
         fake_api = Context(CTX.FOLDER,
231
         fake_api = Context(CTX.FOLDER,
208
                            current_user=user).toDict(fake_api_content)
232
                            current_user=user).toDict(fake_api_content)
209
-
210
         dictified_file = Context(self._get_one_context,
233
         dictified_file = Context(self._get_one_context,
211
                                  current_user=user).toDict(file, 'file')
234
                                  current_user=user).toDict(file, 'file')
212
-        return DictLikeClass(result = dictified_file, fake_api=fake_api)
235
+        result = DictLikeClass(result=dictified_file,
236
+                               fake_api=fake_api,
237
+                               nb_page=nb_page,
238
+                               url=preview_urls)
239
+        return result
213
 
240
 
214
     @tg.require(current_user_is_reader())
241
     @tg.require(current_user_is_reader())
215
     @tg.expose()
242
     @tg.expose()
216
     def download(self, file_id, revision_id=None):
243
     def download(self, file_id, revision_id=None):
217
         file_id = int(file_id)
244
         file_id = int(file_id)
218
-        revision_id = int(revision_id) if revision_id!='latest' else None
245
+        revision_id = int(revision_id) if revision_id != 'latest' else None
219
         user = tmpl_context.current_user
246
         user = tmpl_context.current_user
220
         workspace = tmpl_context.workspace
247
         workspace = tmpl_context.workspace
221
 
248
 
222
         content_api = ContentApi(user)
249
         content_api = ContentApi(user)
223
         revision_to_send = None
250
         revision_to_send = None
224
         if revision_id:
251
         if revision_id:
225
-            item = content_api.get_one_from_revision(file_id,  self._item_type, workspace, revision_id)
252
+            item = content_api.get_one_from_revision(file_id,
253
+                                                     self._item_type,
254
+                                                     workspace,
255
+                                                     revision_id)
226
         else:
256
         else:
227
-            item = content_api.get_one(file_id, self._item_type, workspace)
257
+            item = content_api.get_one(file_id,
258
+                                       self._item_type,
259
+                                       workspace)
228
 
260
 
229
         revision_to_send = None
261
         revision_to_send = None
230
-        if item.revision_to_serialize<=0:
262
+        if item.revision_to_serialize <= 0:
231
             for revision in item.revisions:
263
             for revision in item.revisions:
232
                 if not revision_to_send:
264
                 if not revision_to_send:
233
                     revision_to_send = revision
265
                     revision_to_send = revision
234
 
266
 
235
-                if revision.revision_id>revision_to_send.revision_id:
267
+                if revision.revision_id > revision_to_send.revision_id:
236
                     revision_to_send = revision
268
                     revision_to_send = revision
237
         else:
269
         else:
238
             for revision in item.revisions:
270
             for revision in item.revisions:
239
-                if revision.revision_id==item.revision_to_serialize:
271
+                if revision.revision_id == item.revision_to_serialize:
240
                     revision_to_send = revision
272
                     revision_to_send = revision
241
                     break
273
                     break
242
 
274
 
243
         content_type = 'application/x-download'
275
         content_type = 'application/x-download'
244
         if revision_to_send.file_mimetype:
276
         if revision_to_send.file_mimetype:
245
             content_type = str(revision_to_send.file_mimetype)
277
             content_type = str(revision_to_send.file_mimetype)
246
-            tg.response.headers['Content-type'] = str(revision_to_send.file_mimetype)
278
+            tg.response.headers['Content-type'] = \
279
+                str(revision_to_send.file_mimetype)
247
 
280
 
248
         tg.response.headers['Content-Type'] = content_type
281
         tg.response.headers['Content-Type'] = content_type
249
         file_name = get_valid_header_file_name(revision_to_send.file_name)
282
         file_name = get_valid_header_file_name(revision_to_send.file_name)
250
         tg.response.headers['Content-Disposition'] = \
283
         tg.response.headers['Content-Disposition'] = \
251
             str('attachment; filename="{}"'.format(file_name))
284
             str('attachment; filename="{}"'.format(file_name))
252
-        return revision_to_send.file_content
285
+        return DepotManager.get().get(revision_to_send.depot_file)
253
 
286
 
254
-
255
-    def get_all_fake(self, context_workspace: Workspace, context_folder: Content):
287
+    def get_all_fake(self,
288
+                     context_workspace: Workspace,
289
+                     context_folder: Content):
256
         """
290
         """
257
-        fake methods are used in other controllers in order to simulate a client/server api.
258
-        the "client" controller method will include the result into its own fake_api object
259
-        which will be available in the templates
260
-
261
-        :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior
291
+        fake methods are used in other controllers in order to simulate a
292
+        client/server api.  the "client" controller method will include the
293
+        result into its own fake_api object which will be available in the
294
+        templates
295
+
296
+        :param context_workspace: the workspace which would be taken from
297
+                                  tmpl_context if we were in the normal
298
+                                  behavior
262
         :return:
299
         :return:
263
         """
300
         """
264
         workspace = context_workspace
301
         workspace = context_workspace
265
         content_api = ContentApi(tmpl_context.current_user)
302
         content_api = ContentApi(tmpl_context.current_user)
266
-        files = content_api.get_all(context_folder.content_id, ContentType.File, workspace)
303
+        files = content_api.get_all(context_folder.content_id,
304
+                                    ContentType.File,
305
+                                    workspace)
267
 
306
 
268
         dictified_files = Context(CTX.FILES).toDict(files)
307
         dictified_files = Context(CTX.FILES).toDict(files)
269
-        return DictLikeClass(result = dictified_files)
270
-
308
+        return DictLikeClass(result=dictified_files)
271
 
309
 
272
     @tg.require(current_user_is_contributor())
310
     @tg.require(current_user_is_contributor())
273
     @tg.expose()
311
     @tg.expose()
279
         api = ContentApi(tmpl_context.current_user)
317
         api = ContentApi(tmpl_context.current_user)
280
         with DBSession.no_autoflush:
318
         with DBSession.no_autoflush:
281
             file = api.create(ContentType.File, workspace, folder, label)
319
             file = api.create(ContentType.File, workspace, folder, label)
282
-            api.update_file_data(file, file_data.filename, file_data.type, file_data.file.read())
283
-
320
+            api.update_file_data(file,
321
+                                 file_data.filename,
322
+                                 file_data.type,
323
+                                 file_data.file.read())
284
             # Display error page to user if chosen label is in conflict
324
             # Display error page to user if chosen label is in conflict
285
             if not self._path_validation.validate_new_content(file):
325
             if not self._path_validation.validate_new_content(file):
286
                 return render_invalid_integrity_chosen_path(
326
                 return render_invalid_integrity_chosen_path(
289
         api.save(file, ActionDescription.CREATION)
329
         api.save(file, ActionDescription.CREATION)
290
 
330
 
291
         tg.flash(_('File created'), CST.STATUS_OK)
331
         tg.flash(_('File created'), CST.STATUS_OK)
292
-        tg.redirect(tg.url('/workspaces/{}/folders/{}/files/{}').format(tmpl_context.workspace_id, tmpl_context.folder_id, file.content_id))
293
-
332
+        redirect = '/workspaces/{}/folders/{}/files/{}'
333
+        tg.redirect(tg.url(redirect).format(tmpl_context.workspace_id,
334
+                                            tmpl_context.folder_id,
335
+                                            file.content_id))
294
 
336
 
295
     @tg.require(current_user_is_contributor())
337
     @tg.require(current_user_is_contributor())
296
     @tg.expose()
338
     @tg.expose()
329
 
371
 
330
                     api.save(updated_item, ActionDescription.EDITION)
372
                     api.save(updated_item, ActionDescription.EDITION)
331
 
373
 
332
-                    # This case is the default "file title and description update"
333
-                    # In this case the file itself is not revisionned
374
+                    # This case is the default "file title and description
375
+                    # update" In this case the file itself is not revisionned
334
 
376
 
335
                 else:
377
                 else:
336
                     # So, now we may have a comment and/or a file revision
378
                     # So, now we may have a comment and/or a file revision
337
-                    if comment and ''==label:
379
+                    if comment and '' == label:
338
                         comment_item = api.create_comment(workspace,
380
                         comment_item = api.create_comment(workspace,
339
                                                           item, comment,
381
                                                           item, comment,
340
                                                           do_save=False)
382
                                                           do_save=False)
345
                             # The notification is only sent
387
                             # The notification is only sent
346
                             # if the file is NOT updated
388
                             # if the file is NOT updated
347
                             #
389
                             #
348
-                            #  If the file is also updated,
349
-                            #  then a 'file revision' notification will be sent.
390
+                            # If the file is also updated,
391
+                            # then a 'file revision' notification will be sent.
350
                             api.save(comment_item,
392
                             api.save(comment_item,
351
                                      ActionDescription.COMMENT,
393
                                      ActionDescription.COMMENT,
352
                                      do_notify=False)
394
                                      do_notify=False)
353
 
395
 
354
                     if isinstance(file_data, FieldStorage):
396
                     if isinstance(file_data, FieldStorage):
355
-                        api.update_file_data(item, file_data.filename, file_data.type, file_data.file.read())
397
+                        api.update_file_data(item,
398
+                                             file_data.filename,
399
+                                             file_data.type,
400
+                                             file_data.file.read())
356
 
401
 
357
                         # Display error page to user if chosen label is in
402
                         # Display error page to user if chosen label is in
358
                         # conflict
403
                         # conflict
367
 
412
 
368
             msg = _('{} updated').format(self._item_type_label)
413
             msg = _('{} updated').format(self._item_type_label)
369
             tg.flash(msg, CST.STATUS_OK)
414
             tg.flash(msg, CST.STATUS_OK)
370
-            tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id))
415
+            tg.redirect(self._std_url.format(tmpl_context.workspace_id,
416
+                                             tmpl_context.folder_id,
417
+                                             item.content_id))
371
 
418
 
372
         except ValueError as e:
419
         except ValueError as e:
373
-            msg = _('{} not updated - error: {}').format(self._item_type_label, str(e))
420
+            error = '{} not updated - error: {}'
421
+            msg = _(error).format(self._item_type_label,
422
+                                  str(e))
374
             tg.flash(msg, CST.STATUS_ERROR)
423
             tg.flash(msg, CST.STATUS_ERROR)
375
-            tg.redirect(self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
376
-
424
+            tg.redirect(self._err_url.format(tmpl_context.workspace_id,
425
+                                             tmpl_context.folder_id,
426
+                                             item_id))
377
 
427
 
378
 
428
 
379
 class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
429
 class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
418
         page_id = int(page_id)
468
         page_id = int(page_id)
419
         user = tmpl_context.current_user
469
         user = tmpl_context.current_user
420
         workspace = tmpl_context.workspace
470
         workspace = tmpl_context.workspace
421
-        workspace_id = tmpl_context.workspace_id
422
 
471
 
423
         current_user_content = Context(CTX.CURRENT_USER).toDict(user)
472
         current_user_content = Context(CTX.CURRENT_USER).toDict(user)
424
         current_user_content.roles.sort(key=lambda role: role.workspace.name)
473
         current_user_content.roles.sort(key=lambda role: role.workspace.name)
429
             show_archived=True,
478
             show_archived=True,
430
         )
479
         )
431
         if revision_id:
480
         if revision_id:
432
-            page = content_api.get_one_from_revision(page_id, ContentType.Page, workspace, revision_id)
481
+            page = content_api.get_one_from_revision(page_id,
482
+                                                     ContentType.Page,
483
+                                                     workspace,
484
+                                                     revision_id)
433
         else:
485
         else:
434
-            page = content_api.get_one(page_id, ContentType.Page, workspace)
486
+            page = content_api.get_one(page_id,
487
+                                       ContentType.Page,
488
+                                       workspace)
435
 
489
 
436
         fake_api_breadcrumb = self.get_breadcrumb(page_id)
490
         fake_api_breadcrumb = self.get_breadcrumb(page_id)
437
-        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb, current_user=current_user_content)
491
+        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb,
492
+                                         current_user=current_user_content)
438
         fake_api = Context(CTX.FOLDER).toDict(fake_api_content)
493
         fake_api = Context(CTX.FOLDER).toDict(fake_api_content)
439
 
494
 
440
         dictified_page = Context(CTX.PAGE).toDict(page, 'page')
495
         dictified_page = Context(CTX.PAGE).toDict(page, 'page')
441
-        return DictLikeClass(result = dictified_page, fake_api=fake_api)
442
-
496
+        return DictLikeClass(result=dictified_page,
497
+                             fake_api=fake_api)
443
 
498
 
444
-    def get_all_fake(self, context_workspace: Workspace, context_folder: Content):
499
+    def get_all_fake(self,
500
+                     context_workspace: Workspace,
501
+                     context_folder: Content):
445
         """
502
         """
446
-        fake methods are used in other controllers in order to simulate a client/server api.
447
-        the "client" controller method will include the result into its own fake_api object
448
-        which will be available in the templates
449
 
503
 
450
-        :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior
504
+        fake methods are used in other controllers in order to simulate a
505
+        client/server api.  the "client" controller method will include the
506
+        result into its own fake_api object which will be available in the
507
+        templates
508
+
509
+        :param context_workspace: the workspace which would be taken from
510
+                                  tmpl_context if we were in the normal
511
+                                  behavior
451
         :return:
512
         :return:
452
         """
513
         """
453
         workspace = context_workspace
514
         workspace = context_workspace
454
         content_api = ContentApi(tmpl_context.current_user)
515
         content_api = ContentApi(tmpl_context.current_user)
455
-        pages = content_api.get_all(context_folder.content_id, ContentType.Page, workspace)
516
+        pages = content_api.get_all(context_folder.content_id,
517
+                                    ContentType.Page,
518
+                                    workspace)
456
 
519
 
457
         dictified_pages = Context(CTX.PAGES).toDict(pages)
520
         dictified_pages = Context(CTX.PAGES).toDict(pages)
458
-        return DictLikeClass(result = dictified_pages)
459
-
521
+        return DictLikeClass(result=dictified_pages)
460
 
522
 
461
     @tg.require(current_user_is_contributor())
523
     @tg.require(current_user_is_contributor())
462
     @tg.expose()
524
     @tg.expose()
466
         api = ContentApi(tmpl_context.current_user)
528
         api = ContentApi(tmpl_context.current_user)
467
 
529
 
468
         with DBSession.no_autoflush:
530
         with DBSession.no_autoflush:
469
-            page = api.create(ContentType.Page, workspace, tmpl_context.folder, label)
531
+            page = api.create(ContentType.Page,
532
+                              workspace,
533
+                              tmpl_context.folder,
534
+                              label)
470
             page.description = content
535
             page.description = content
471
 
536
 
472
             if not self._path_validation.validate_new_content(page):
537
             if not self._path_validation.validate_new_content(page):
477
         api.save(page, ActionDescription.CREATION, do_notify=True)
542
         api.save(page, ActionDescription.CREATION, do_notify=True)
478
 
543
 
479
         tg.flash(_('Page created'), CST.STATUS_OK)
544
         tg.flash(_('Page created'), CST.STATUS_OK)
480
-        tg.redirect(tg.url('/workspaces/{}/folders/{}/pages/{}').format(tmpl_context.workspace_id, tmpl_context.folder_id, page.content_id))
481
-
545
+        redirect = '/workspaces/{}/folders/{}/pages/{}'
546
+        tg.redirect(tg.url(redirect).format(tmpl_context.workspace_id,
547
+                                            tmpl_context.folder_id,
548
+                                            page.content_id))
482
 
549
 
483
     @tg.require(current_user_is_contributor())
550
     @tg.require(current_user_is_contributor())
484
     @tg.expose()
551
     @tg.expose()
485
-    def put(self, item_id, label='',content=''):
552
+    def put(self, item_id, label='', content=''):
486
         # INFO - D.A. This method is a raw copy of
553
         # INFO - D.A. This method is a raw copy of
487
         # TODO - SECURE THIS
554
         # TODO - SECURE THIS
488
         workspace = tmpl_context.workspace
555
         workspace = tmpl_context.workspace
502
 
569
 
503
             msg = _('{} updated').format(self._item_type_label)
570
             msg = _('{} updated').format(self._item_type_label)
504
             tg.flash(msg, CST.STATUS_OK)
571
             tg.flash(msg, CST.STATUS_OK)
505
-            tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id))
506
-
572
+            tg.redirect(self._std_url.format(tmpl_context.workspace_id,
573
+                                             tmpl_context.folder_id,
574
+                                             item.content_id))
507
         except SameValueError as e:
575
         except SameValueError as e:
508
-            msg = _('{} not updated: the content did not change').format(self._item_type_label)
576
+            not_updated = '{} not updated: the content did not change'
577
+            msg = _(not_updated).format(self._item_type_label)
509
             tg.flash(msg, CST.STATUS_WARNING)
578
             tg.flash(msg, CST.STATUS_WARNING)
510
-            tg.redirect(self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
511
-
579
+            tg.redirect(self._err_url.format(tmpl_context.workspace_id,
580
+                                             tmpl_context.folder_id,
581
+                                             item_id))
512
         except ValueError as e:
582
         except ValueError as e:
513
-            msg = _('{} not updated - error: {}').format(self._item_type_label, str(e))
583
+            not_updated = '{} not updated - error: {}'
584
+            msg = _(not_updated).format(self._item_type_label, str(e))
514
             tg.flash(msg, CST.STATUS_ERROR)
585
             tg.flash(msg, CST.STATUS_ERROR)
515
-            tg.redirect(self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
586
+            tg.redirect(self._err_url.format(tmpl_context.workspace_id,
587
+                                             tmpl_context.folder_id,
588
+                                             item_id))
516
 
589
 
517
 
590
 
518
 class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController):
591
 class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController):
546
     def _item_type(self):
619
     def _item_type(self):
547
         return ContentType.Thread
620
         return ContentType.Thread
548
 
621
 
549
-
550
     @property
622
     @property
551
     def _item_type_label(self):
623
     def _item_type_label(self):
552
         return _('Thread')
624
         return _('Thread')
553
 
625
 
554
-
555
     @property
626
     @property
556
     def _get_one_context(self) -> str:
627
     def _get_one_context(self) -> str:
557
         return CTX.THREAD
628
         return CTX.THREAD
564
     @tg.expose()
635
     @tg.expose()
565
     def post(self, label='', content='', parent_id=None):
636
     def post(self, label='', content='', parent_id=None):
566
         """
637
         """
567
-        Creates a new thread. Actually, on POST, the content will be included in a user comment instead of being the thread description
638
+        Creates a new thread. Actually, on POST, the content will be included
639
+        in a user comment instead of being the thread description
568
         :param label:
640
         :param label:
569
         :param content:
641
         :param content:
570
         :return:
642
         :return:
575
         api = ContentApi(tmpl_context.current_user)
647
         api = ContentApi(tmpl_context.current_user)
576
 
648
 
577
         with DBSession.no_autoflush:
649
         with DBSession.no_autoflush:
578
-            thread = api.create(ContentType.Thread, workspace, tmpl_context.folder, label)
579
-            # FIXME - DO NOT DUPLCIATE FIRST MESSAGE thread.description = content
650
+            thread = api.create(ContentType.Thread,
651
+                                workspace,
652
+                                tmpl_context.folder,
653
+                                label)
654
+            # FIXME - DO NOT DUPLCIATE FIRST MESSAGE
655
+            # thread.description = content
580
             api.save(thread, ActionDescription.CREATION, do_notify=False)
656
             api.save(thread, ActionDescription.CREATION, do_notify=False)
581
 
657
 
582
             comment = api.create(ContentType.Comment, workspace, thread, label)
658
             comment = api.create(ContentType.Comment, workspace, thread, label)
592
         api.do_notify(thread)
668
         api.do_notify(thread)
593
 
669
 
594
         tg.flash(_('Thread created'), CST.STATUS_OK)
670
         tg.flash(_('Thread created'), CST.STATUS_OK)
595
-        tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, thread.content_id))
596
-
671
+        tg.redirect(self._std_url.format(tmpl_context.workspace_id,
672
+                                         tmpl_context.folder_id,
673
+                                         thread.content_id))
597
 
674
 
598
     @tg.require(current_user_is_reader())
675
     @tg.require(current_user_is_reader())
599
     @tg.expose('tracim.templates.thread.getone')
676
     @tg.expose('tracim.templates.thread.getone')
620
         thread = content_api.get_one(thread_id, ContentType.Thread, workspace)
697
         thread = content_api.get_one(thread_id, ContentType.Thread, workspace)
621
 
698
 
622
         fake_api_breadcrumb = self.get_breadcrumb(thread_id)
699
         fake_api_breadcrumb = self.get_breadcrumb(thread_id)
623
-        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb, current_user=current_user_content)
700
+        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb,
701
+                                         current_user=current_user_content)
624
         fake_api = Context(CTX.FOLDER).toDict(fake_api_content)
702
         fake_api = Context(CTX.FOLDER).toDict(fake_api_content)
625
 
703
 
626
         dictified_thread = Context(CTX.THREAD).toDict(thread, 'thread')
704
         dictified_thread = Context(CTX.THREAD).toDict(thread, 'thread')
627
 
705
 
628
         if inverted:
706
         if inverted:
629
-          dictified_thread.thread.history = reversed(dictified_thread.thread.history)
707
+            dictified_thread.thread.history = \
708
+                reversed(dictified_thread.thread.history)
630
 
709
 
631
         return DictLikeClass(
710
         return DictLikeClass(
632
             result=dictified_thread,
711
             result=dictified_thread,
635
         )
714
         )
636
 
715
 
637
 
716
 
638
-
639
-class ItemLocationController(TIMWorkspaceContentRestController, BaseController):
717
+class ItemLocationController(TIMWorkspaceContentRestController,
718
+                             BaseController):
640
 
719
 
641
     @tg.require(current_user_is_content_manager())
720
     @tg.require(current_user_is_content_manager())
642
     @tg.expose()
721
     @tg.expose()
649
         raise NotImplementedError
728
         raise NotImplementedError
650
         return item
729
         return item
651
 
730
 
652
-
653
     @tg.require(current_user_is_content_manager())
731
     @tg.require(current_user_is_content_manager())
654
     @tg.expose('tracim.templates.folder.move')
732
     @tg.expose('tracim.templates.folder.move')
655
     def edit(self, item_id):
733
     def edit(self, item_id):
659
         :param item_id:
737
         :param item_id:
660
         :return:
738
         :return:
661
         """
739
         """
662
-        current_user_content = Context(CTX.CURRENT_USER).toDict(tmpl_context.current_user)
663
-        fake_api = Context(CTX.FOLDER).toDict(DictLikeClass(current_user=current_user_content))
740
+        current_user_content = \
741
+            Context(CTX.CURRENT_USER).toDict(tmpl_context.current_user)
742
+        fake_api = \
743
+            Context(CTX.FOLDER) \
744
+            .toDict(DictLikeClass(current_user=current_user_content))
664
 
745
 
665
         item_id = int(item_id)
746
         item_id = int(item_id)
666
         user = tmpl_context.current_user
747
         user = tmpl_context.current_user
670
         item = content_api.get_one(item_id, ContentType.Any, workspace)
751
         item = content_api.get_one(item_id, ContentType.Any, workspace)
671
 
752
 
672
         dictified_item = Context(CTX.DEFAULT).toDict(item, 'item')
753
         dictified_item = Context(CTX.DEFAULT).toDict(item, 'item')
673
-        return DictLikeClass(result = dictified_item, fake_api=fake_api)
754
+        return DictLikeClass(result=dictified_item, fake_api=fake_api)
674
 
755
 
675
     @tg.require(current_user_is_content_manager())
756
     @tg.require(current_user_is_content_manager())
676
     @tg.expose()
757
     @tg.expose()
677
     def put(self, item_id, folder_id='0'):
758
     def put(self, item_id, folder_id='0'):
678
         """
759
         """
679
         :param item_id:
760
         :param item_id:
680
-        :param folder_id: id of the folder, in a style like 'workspace_14__content_1586'
761
+        :param folder_id: id of the folder, in a style like
762
+                          'workspace_14__content_1586'
681
         :return:
763
         :return:
682
         """
764
         """
683
         # TODO - SECURE THIS
765
         # TODO - SECURE THIS
727
                 api.move(item, new_parent)
809
                 api.move(item, new_parent)
728
             next_url = self.parent_controller.url(item_id)
810
             next_url = self.parent_controller.url(item_id)
729
             if new_parent:
811
             if new_parent:
730
-                tg.flash(_('Item moved to {}').format(new_parent.label), CST.STATUS_OK)
812
+                tg.flash(_('Item moved to {}').format(new_parent.label),
813
+                         CST.STATUS_OK)
731
             else:
814
             else:
732
                 tg.flash(_('Item moved to workspace root'))
815
                 tg.flash(_('Item moved to workspace root'))
733
 
816
 
734
             tg.redirect(next_url)
817
             tg.redirect(next_url)
735
 
818
 
736
 
819
 
737
-
738
 class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
820
 class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
739
 
821
 
740
     TEMPLATE_NEW = 'mako:tracim.templates.folder.new'
822
     TEMPLATE_NEW = 'mako:tracim.templates.folder.new'
770
         folder = content_api.get_one(folder_id, ContentType.Folder, workspace)
852
         folder = content_api.get_one(folder_id, ContentType.Folder, workspace)
771
 
853
 
772
         dictified_folder = Context(CTX.FOLDER).toDict(folder, 'folder')
854
         dictified_folder = Context(CTX.FOLDER).toDict(folder, 'folder')
773
-        return DictLikeClass(result = dictified_folder)
774
-
855
+        return DictLikeClass(result=dictified_folder)
775
 
856
 
776
     @tg.require(current_user_is_reader())
857
     @tg.require(current_user_is_reader())
777
     @tg.expose('tracim.templates.folder.getone')
858
     @tg.expose('tracim.templates.folder.getone')
806
             )
887
             )
807
 
888
 
808
         fake_api_breadcrumb = self.get_breadcrumb(folder_id)
889
         fake_api_breadcrumb = self.get_breadcrumb(folder_id)
809
-        fake_api_subfolders = self.get_all_fake(workspace, folder.content_id).result
890
+        fake_api_subfolders = self.get_all_fake(workspace,
891
+                                                folder.content_id).result
810
         fake_api_pages = self.pages.get_all_fake(workspace, folder).result
892
         fake_api_pages = self.pages.get_all_fake(workspace, folder).result
811
         fake_api_files = self.files.get_all_fake(workspace, folder).result
893
         fake_api_files = self.files.get_all_fake(workspace, folder).result
812
         fake_api_threads = self.threads.get_all_fake(workspace, folder).result
894
         fake_api_threads = self.threads.get_all_fake(workspace, folder).result
846
             show_archived=show_archived,
928
             show_archived=show_archived,
847
         )
929
         )
848
 
930
 
849
-
850
     def get_all_fake(self, context_workspace: Workspace, parent_id=None):
931
     def get_all_fake(self, context_workspace: Workspace, parent_id=None):
851
         """
932
         """
852
-        fake methods are used in other controllers in order to simulate a client/server api.
853
-        the "client" controller method will include the result into its own fake_api object
854
-        which will be available in the templates
855
-
856
-        :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior
933
+        fake methods are used in other controllers in order to simulate a
934
+        client/server api.  the "client" controller method will include the
935
+        result into its own fake_api object which will be available in the
936
+        templates
937
+
938
+        :param context_workspace: the workspace which would be taken from
939
+                                  tmpl_context if we were in the normal
940
+                                  behavior
857
         :return:
941
         :return:
858
         """
942
         """
859
         workspace = context_workspace
943
         workspace = context_workspace
863
         folders = content_api.get_child_folders(parent_folder, workspace)
947
         folders = content_api.get_child_folders(parent_folder, workspace)
864
 
948
 
865
         folders = Context(CTX.FOLDERS).toDict(folders)
949
         folders = Context(CTX.FOLDERS).toDict(folders)
866
-        return DictLikeClass(result = folders)
867
-
950
+        return DictLikeClass(result=folders)
868
 
951
 
869
     @tg.require(current_user_is_content_manager())
952
     @tg.require(current_user_is_content_manager())
870
     @tg.expose()
953
     @tg.expose()
871
-    def post(self, label, parent_id=None, can_contain_folders=False, can_contain_threads=False, can_contain_files=False, can_contain_pages=False):
954
+    def post(self,
955
+             label,
956
+             parent_id=None,
957
+             can_contain_folders=False,
958
+             can_contain_threads=False,
959
+             can_contain_files=False,
960
+             can_contain_pages=False):
872
         # TODO - SECURE THIS
961
         # TODO - SECURE THIS
873
         workspace = tmpl_context.workspace
962
         workspace = tmpl_context.workspace
874
 
963
 
877
         redirect_url_tmpl = '/workspaces/{}/folders/{}'
966
         redirect_url_tmpl = '/workspaces/{}/folders/{}'
878
         redirect_url = ''
967
         redirect_url = ''
879
 
968
 
880
-
881
         try:
969
         try:
882
             parent = None
970
             parent = None
883
             if parent_id:
971
             if parent_id:
884
-                parent = api.get_one(int(parent_id), ContentType.Folder, workspace)
972
+                parent = api.get_one(int(parent_id),
973
+                                     ContentType.Folder,
974
+                                     workspace)
885
 
975
 
886
             with DBSession.no_autoflush:
976
             with DBSession.no_autoflush:
887
-                folder = api.create(ContentType.Folder, workspace, parent, label)
977
+                folder = api.create(ContentType.Folder,
978
+                                    workspace,
979
+                                    parent,
980
+                                    label)
888
 
981
 
889
                 subcontent = dict(
982
                 subcontent = dict(
890
-                    folder = True if can_contain_folders=='on' else False,
891
-                    thread = True if can_contain_threads=='on' else False,
892
-                    file = True if can_contain_files=='on' else False,
893
-                    page = True if can_contain_pages=='on' else False
983
+                    folder=True if can_contain_folders == 'on' else False,
984
+                    thread=True if can_contain_threads == 'on' else False,
985
+                    file=True if can_contain_files == 'on' else False,
986
+                    page=True if can_contain_pages == 'on' else False
894
                 )
987
                 )
895
                 api.set_allowed_content(folder, subcontent)
988
                 api.set_allowed_content(folder, subcontent)
896
 
989
 
902
             api.save(folder)
995
             api.save(folder)
903
 
996
 
904
             tg.flash(_('Folder created'), CST.STATUS_OK)
997
             tg.flash(_('Folder created'), CST.STATUS_OK)
905
-            redirect_url = redirect_url_tmpl.format(tmpl_context.workspace_id, folder.content_id)
998
+            redirect_url = redirect_url_tmpl.format(tmpl_context.workspace_id,
999
+                                                    folder.content_id)
906
         except Exception as e:
1000
         except Exception as e:
907
-            logger.error(self, 'An unexpected exception has been catched. Look at the traceback below.')
1001
+            error_msg = 'An unexpected exception has been catched. ' \
1002
+                        'Look at the traceback below.'
1003
+            logger.error(self, error_msg)
908
             traceback.print_exc()
1004
             traceback.print_exc()
909
 
1005
 
910
             tb = sys.exc_info()[2]
1006
             tb = sys.exc_info()[2]
911
-            tg.flash(_('Folder not created: {}').format(e.with_traceback(tb)), CST.STATUS_ERROR)
1007
+            tg.flash(_('Folder not created: {}').format(e.with_traceback(tb)),
1008
+                     CST.STATUS_ERROR)
912
             if parent_id:
1009
             if parent_id:
913
-                redirect_url = redirect_url_tmpl.format(tmpl_context.workspace_id, parent_id)
1010
+                redirect_url = \
1011
+                    redirect_url_tmpl.format(tmpl_context.workspace_id,
1012
+                                             parent_id)
914
             else:
1013
             else:
915
-                redirect_url = '/workspaces/{}'.format(tmpl_context.workspace_id)
1014
+                redirect_url = \
1015
+                    '/workspaces/{}'.format(tmpl_context.workspace_id)
916
 
1016
 
917
         ####
1017
         ####
918
         #
1018
         #
921
         #
1021
         #
922
         tg.redirect(tg.url(redirect_url))
1022
         tg.redirect(tg.url(redirect_url))
923
 
1023
 
924
-
925
     @tg.require(current_user_is_content_manager())
1024
     @tg.require(current_user_is_content_manager())
926
     @tg.expose()
1025
     @tg.expose()
927
-    def put(self, folder_id, label, can_contain_folders=False, can_contain_threads=False, can_contain_files=False, can_contain_pages=False):
1026
+    def put(self,
1027
+            folder_id,
1028
+            label,
1029
+            can_contain_folders=False,
1030
+            can_contain_threads=False,
1031
+            can_contain_files=False,
1032
+            can_contain_pages=False):
928
         # TODO - SECURE THIS
1033
         # TODO - SECURE THIS
929
         workspace = tmpl_context.workspace
1034
         workspace = tmpl_context.workspace
930
 
1035
 
934
         try:
1039
         try:
935
             folder = api.get_one(int(folder_id), ContentType.Folder, workspace)
1040
             folder = api.get_one(int(folder_id), ContentType.Folder, workspace)
936
             subcontent = dict(
1041
             subcontent = dict(
937
-                folder = True if can_contain_folders=='on' else False,
938
-                thread = True if can_contain_threads=='on' else False,
939
-                file = True if can_contain_files=='on' else False,
940
-                page = True if can_contain_pages=='on' else False
1042
+                folder=True if can_contain_folders == 'on' else False,
1043
+                thread=True if can_contain_threads == 'on' else False,
1044
+                file=True if can_contain_files == 'on' else False,
1045
+                page=True if can_contain_pages == 'on' else False
941
             )
1046
             )
942
             with new_revision(folder):
1047
             with new_revision(folder):
943
                 if label != folder.label:
1048
                 if label != folder.label:
944
-                    # TODO - D.A. - 2015-05-25 - Allow to set folder description
1049
+                    # TODO - D.A. - 2015-05-25
1050
+                    # Allow to set folder description
945
                     api.update_content(folder, label, folder.description)
1051
                     api.update_content(folder, label, folder.description)
946
                 api.set_allowed_content(folder, subcontent)
1052
                 api.set_allowed_content(folder, subcontent)
947
 
1053
 
957
             next_url = self.url(folder.content_id)
1063
             next_url = self.url(folder.content_id)
958
 
1064
 
959
         except Exception as e:
1065
         except Exception as e:
960
-            tg.flash(_('Folder not updated: {}').format(str(e)), CST.STATUS_ERROR)
1066
+            tg.flash(_('Folder not updated: {}').format(str(e)),
1067
+                     CST.STATUS_ERROR)
961
             next_url = self.url(int(folder_id))
1068
             next_url = self.url(int(folder_id))
962
 
1069
 
963
         tg.redirect(next_url)
1070
         tg.redirect(next_url)
978
     def _item_type(self):
1085
     def _item_type(self):
979
         return ContentType.Folder
1086
         return ContentType.Folder
980
 
1087
 
981
-
982
     @tg.require(current_user_is_content_manager())
1088
     @tg.require(current_user_is_content_manager())
983
     @tg.expose()
1089
     @tg.expose()
984
     def put_archive(self, item_id):
1090
     def put_archive(self, item_id):
985
         # TODO - CHECK RIGHTS
1091
         # TODO - CHECK RIGHTS
986
         item_id = int(item_id)
1092
         item_id = int(item_id)
987
         content_api = ContentApi(tmpl_context.current_user)
1093
         content_api = ContentApi(tmpl_context.current_user)
988
-        item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace)
989
-
1094
+        item = content_api.get_one(item_id,
1095
+                                   self._item_type,
1096
+                                   tmpl_context.workspace)
990
         try:
1097
         try:
991
-            next_url = self._parent_url.format(item.workspace_id, item.parent_id)
992
-            undo_url = self._std_url.format(item.workspace_id, item.content_id)+'/put_archive_undo'
993
-            msg = _('{} archived. <a class="alert-link" href="{}">Cancel action</a>').format(self._item_type_label, undo_url)
994
-
1098
+            next_url = self._parent_url.format(item.workspace_id,
1099
+                                               item.parent_id)
1100
+            tmp_url = self._std_url.format(item.workspace_id,
1101
+                                           item.content_id)
1102
+            undo_url = tmp_url + '/put_archive_undo'
1103
+            archived_msg = '{} archived. ' \
1104
+                           '<a class="alert-link" href="{}">Cancel action</a>'
1105
+            msg = _(archived_msg).format(self._item_type_label,
1106
+                                         undo_url)
995
             with new_revision(item):
1107
             with new_revision(item):
996
                 content_api.archive(item)
1108
                 content_api.archive(item)
997
                 content_api.save(item, ActionDescription.ARCHIVING)
1109
                 content_api.save(item, ActionDescription.ARCHIVING)
998
-
999
-            tg.flash(msg, CST.STATUS_OK, no_escape=True) # TODO allow to come back
1110
+            # TODO allow to come back
1111
+            tg.flash(msg, CST.STATUS_OK, no_escape=True)
1000
             tg.redirect(next_url)
1112
             tg.redirect(next_url)
1001
         except ValueError as e:
1113
         except ValueError as e:
1002
-            next_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id)
1003
-            msg = _('{} not archived: {}').format(self._item_type_label, str(e))
1114
+            next_url = self._std_url.format(item.workspace_id,
1115
+                                            item.parent_id,
1116
+                                            item.content_id)
1117
+            msg = _('{} not archived: {}').format(self._item_type_label,
1118
+                                                  str(e))
1004
             tg.flash(msg, CST.STATUS_ERROR)
1119
             tg.flash(msg, CST.STATUS_ERROR)
1005
             tg.redirect(next_url)
1120
             tg.redirect(next_url)
1006
 
1121
 
1007
-
1008
     @tg.require(current_user_is_content_manager())
1122
     @tg.require(current_user_is_content_manager())
1009
     @tg.expose()
1123
     @tg.expose()
1010
     def put_archive_undo(self, item_id):
1124
     def put_archive_undo(self, item_id):
1011
         # TODO - CHECK RIGHTS
1125
         # TODO - CHECK RIGHTS
1012
         item_id = int(item_id)
1126
         item_id = int(item_id)
1013
-        content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items
1014
-        item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace)
1127
+        # Here we do not filter deleted items
1128
+        content_api = ContentApi(tmpl_context.current_user, True, True)
1129
+        item = content_api.get_one(item_id,
1130
+                                   self._item_type,
1131
+                                   tmpl_context.workspace)
1015
         try:
1132
         try:
1016
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1133
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1017
             msg = _('{} unarchived.').format(self._item_type_label)
1134
             msg = _('{} unarchived.').format(self._item_type_label)
1020
                 content_api.save(item, ActionDescription.UNARCHIVING)
1137
                 content_api.save(item, ActionDescription.UNARCHIVING)
1021
 
1138
 
1022
             tg.flash(msg, CST.STATUS_OK)
1139
             tg.flash(msg, CST.STATUS_OK)
1023
-            tg.redirect(next_url )
1140
+            tg.redirect(next_url)
1024
 
1141
 
1025
         except ValueError as e:
1142
         except ValueError as e:
1026
-            msg = _('{} not un-archived: {}').format(self._item_type_label, str(e))
1143
+            msg = _('{} not un-archived: {}').format(self._item_type_label,
1144
+                                                     str(e))
1027
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1145
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1028
             # We still use std url because the item has not been archived
1146
             # We still use std url because the item has not been archived
1029
             tg.flash(msg, CST.STATUS_ERROR)
1147
             tg.flash(msg, CST.STATUS_ERROR)
1035
         # TODO - CHECK RIGHTS
1153
         # TODO - CHECK RIGHTS
1036
         item_id = int(item_id)
1154
         item_id = int(item_id)
1037
         content_api = ContentApi(tmpl_context.current_user)
1155
         content_api = ContentApi(tmpl_context.current_user)
1038
-        item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace)
1156
+        item = content_api.get_one(item_id,
1157
+                                   self._item_type,
1158
+                                   tmpl_context.workspace)
1039
         try:
1159
         try:
1040
 
1160
 
1041
-            next_url = self._parent_url.format(item.workspace_id, item.parent_id)
1042
-            undo_url = self._std_url.format(item.workspace_id, item.content_id)+'/put_delete_undo'
1043
-            msg = _('{} deleted. <a class="alert-link" href="{}">Cancel action</a>').format(self._item_type_label, undo_url)
1161
+            next_url = self._parent_url.format(item.workspace_id,
1162
+                                               item.parent_id)
1163
+            tmp_url = self._std_url.format(item.workspace_id,
1164
+                                           item.content_id)
1165
+            undo_url = tmp_url + '/put_delete_undo'
1166
+            deleted_msg = '{} deleted. ' \
1167
+                          '<a class="alert-link" href="{}">Cancel action</a>'
1168
+            msg = _(deleted_msg).format(self._item_type_label,
1169
+                                        undo_url)
1044
             with new_revision(item):
1170
             with new_revision(item):
1045
                 content_api.delete(item)
1171
                 content_api.delete(item)
1046
                 content_api.save(item, ActionDescription.DELETION)
1172
                 content_api.save(item, ActionDescription.DELETION)
1054
             tg.flash(msg, CST.STATUS_ERROR)
1180
             tg.flash(msg, CST.STATUS_ERROR)
1055
             tg.redirect(back_url)
1181
             tg.redirect(back_url)
1056
 
1182
 
1057
-
1058
     @tg.require(current_user_is_content_manager())
1183
     @tg.require(current_user_is_content_manager())
1059
     @tg.expose()
1184
     @tg.expose()
1060
     def put_delete_undo(self, item_id):
1185
     def put_delete_undo(self, item_id):
1061
         # TODO - CHECK RIGHTS
1186
         # TODO - CHECK RIGHTS
1062
 
1187
 
1063
         item_id = int(item_id)
1188
         item_id = int(item_id)
1064
-        content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items
1065
-        item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace)
1189
+        # Here we do not filter deleted items
1190
+        content_api = ContentApi(tmpl_context.current_user, True, True)
1191
+        item = content_api.get_one(item_id,
1192
+                                   self._item_type,
1193
+                                   tmpl_context.workspace)
1066
         try:
1194
         try:
1067
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1195
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1068
             msg = _('{} undeleted.').format(self._item_type_label)
1196
             msg = _('{} undeleted.').format(self._item_type_label)
1075
 
1203
 
1076
         except ValueError as e:
1204
         except ValueError as e:
1077
             logger.debug(self, 'Exception: {}'.format(e.__str__))
1205
             logger.debug(self, 'Exception: {}'.format(e.__str__))
1078
-            back_url = self._parent_url.format(item.workspace_id, item.parent_id)
1079
-            msg = _('{} not un-deleted: {}').format(self._item_type_label, str(e))
1206
+            back_url = self._parent_url.format(item.workspace_id,
1207
+                                               item.parent_id)
1208
+            msg = _('{} not un-deleted: {}').format(self._item_type_label,
1209
+                                                    str(e))
1080
             tg.flash(msg, CST.STATUS_ERROR)
1210
             tg.flash(msg, CST.STATUS_ERROR)
1081
             tg.redirect(back_url)
1211
             tg.redirect(back_url)
1082
 
1212
 
1083
 
1213
 
1084
 class ContentController(StandardController):
1214
 class ContentController(StandardController):
1085
-
1086
-    '''
1087
-    Class of controllers used for example in home to mark read the unread 
1215
+    """
1216
+    Class of controllers used for example in home to mark read the unread
1088
     contents via mark_all_read()
1217
     contents via mark_all_read()
1089
-    '''
1218
+    """
1090
 
1219
 
1091
     @classmethod
1220
     @classmethod
1092
     def current_item_id_key_in_context(cls) -> str:
1221
     def current_item_id_key_in_context(cls) -> str:

+ 105 - 0
tracim/tracim/controllers/page.py View File

1
+import tg
2
+from tg import expose
3
+from tg import tmpl_context
4
+from preview_generator.manager import PreviewManager
5
+
6
+from tracim.config.app_cfg import CFG
7
+from tracim.controllers import TIMRestController
8
+from tracim.lib.content import ContentApi
9
+from tracim.model.data import ContentType
10
+
11
+__all__ = ['PagesController']
12
+
13
+
14
+class PagesController(TIMRestController):
15
+
16
+    @expose()
17
+    def _default(self):
18
+        return '<h2> Error Loading Page</h2>'
19
+
20
+    @expose()
21
+    def get_all(self, *args, **kwargs):
22
+        file_id = int(tg.request.controller_state.routing_args.get('file_id'))
23
+        return 'all the pages of document {}'.format(file_id)
24
+
25
+    # FIXME https://github.com/tracim/tracim/issues/271
26
+    @expose(content_type='image/jpeg')
27
+    def get_one(self,
28
+                page_id: str='-1',
29
+                revision_id: str=None,
30
+                size: int=300,
31
+                *args, **kwargs):
32
+        file_id = int(tg.request.controller_state.routing_args.get('file_id'))
33
+        page = int(page_id)
34
+        revision_id = int(revision_id) if revision_id != 'latest' else None
35
+        cache_path = CFG.get_instance().PREVIEW_CACHE
36
+        preview_manager = PreviewManager(cache_path, create_folder=True)
37
+        user = tmpl_context.current_user
38
+        content_api = ContentApi(user,
39
+                                 show_archived=True,
40
+                                 show_deleted=True)
41
+        if revision_id:
42
+            file_path = content_api.get_one_revision_filepath(revision_id)
43
+        else:
44
+            file = content_api.get_one(file_id, self._item_type)
45
+            file_path = content_api.get_one_revision_filepath(file.revision_id)
46
+        path = preview_manager.get_jpeg_preview(file_path=file_path,
47
+                                                page=page,
48
+                                                height=size,
49
+                                                width=size)
50
+        with open(path, 'rb') as large:
51
+            return large.read()
52
+
53
+    @expose(content_type='image/jpeg')
54
+    def high_quality(self,
55
+                     page_id: str='-1',
56
+                     revision_id: int=None,
57
+                     size: int=1000,
58
+                     *args, **kwargs):
59
+        result = self.get_one(page_id=page_id,
60
+                              revision_id=revision_id,
61
+                              size=size,
62
+                              args=args,
63
+                              kwargs=kwargs)
64
+        return result
65
+
66
+    @expose(content_type='application/pdf')
67
+    def download_pdf_full(self,
68
+                          page_id: str,
69
+                          revision_id: str='-1',
70
+                          *args, **kwargs):
71
+        return self.download_pdf_one(page_id='-1',
72
+                                     revision_id=revision_id,
73
+                                     args=args, kwargs=kwargs)
74
+
75
+    # FIXME https://github.com/tracim/tracim/issues/271
76
+    @expose(content_type='application/pdf')
77
+    def download_pdf_one(self,
78
+                         page_id: str,
79
+                         revision_id: str=None,
80
+                         *args, **kwargs):
81
+        file_id = int(tg.request.controller_state.routing_args.get('file_id'))
82
+        revision_id = int(revision_id) if revision_id != 'latest' else None
83
+        page = int(page_id)
84
+        cache_path = CFG.get_instance().PREVIEW_CACHE
85
+        preview_manager = PreviewManager(cache_path, create_folder=True)
86
+        user = tmpl_context.current_user
87
+        content_api = ContentApi(user,
88
+                                 show_archived=True,
89
+                                 show_deleted=True)
90
+        file = content_api.get_one(file_id, self._item_type)
91
+        if revision_id:
92
+            file_path = content_api.get_one_revision_filepath(revision_id)
93
+        else:
94
+            file = content_api.get_one(file_id, self._item_type)
95
+            file_path = content_api.get_one_revision_filepath(file.revision_id)
96
+        path = preview_manager.get_pdf_preview(file_path=file_path,
97
+                                               page=page)
98
+        tg.response.headers['Content-Disposition'] = \
99
+            'attachment; filename="{}"'.format(file.file_name)
100
+        with open(path, 'rb') as pdf:
101
+            return pdf.read()
102
+
103
+    @property
104
+    def _item_type(self):
105
+        return ContentType.File

+ 29 - 0
tracim/tracim/controllers/previews.py View File

1
+from tg import expose
2
+from tg import tmpl_context
3
+
4
+from tracim.controllers import TIMRestController
5
+from tracim.controllers.page import PagesController
6
+
7
+__all__ = ['PreviewsController']
8
+
9
+
10
+# FIXME https://github.com/tracim/tracim/issues/272
11
+# unused, future removal planned
12
+class PreviewsController(TIMRestController):
13
+
14
+    pages = PagesController()
15
+
16
+    @expose()
17
+    def _default(self, *args, **kwargs) -> str:
18
+        return '<h2> Error Loading Page</h2>'
19
+
20
+    @expose()
21
+    def get_all(self, *args, **kwargs) -> str:
22
+        print('getall _ document')
23
+        return 'all the files'
24
+
25
+    @expose()
26
+    def get_one(self, file_id: int, *args, **kwargs) -> str:
27
+        print('getone _ document')
28
+        tmpl_context.file = file_id
29
+        return 'File n°{}'.format(file_id)

+ 16 - 25
tracim/tracim/controllers/root.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-import tg
3
 from tg import expose
2
 from tg import expose
4
 from tg import flash
3
 from tg import flash
5
 from tg import lurl
4
 from tg import lurl
9
 from tg import require
8
 from tg import require
10
 from tg import tmpl_context
9
 from tg import tmpl_context
11
 from tg import url
10
 from tg import url
12
-
13
 from tg.i18n import ugettext as _
11
 from tg.i18n import ugettext as _
14
-from tracim.controllers.api import APIController
15
-from tracim.controllers.content import ContentController
16
-
17
-from tracim.lib import CST
18
-from tracim.lib.base import logger
19
-from tracim.lib.user import CurrentUserGetterApi
20
-from tracim.lib.content import ContentApi
21
 
12
 
22
 from tracim.controllers import StandardController
13
 from tracim.controllers import StandardController
23
 from tracim.controllers.admin import AdminController
14
 from tracim.controllers.admin import AdminController
15
+from tracim.controllers.api import APIController
16
+from tracim.controllers.calendar import CalendarConfigController
17
+from tracim.controllers.calendar import CalendarController
18
+from tracim.controllers.content import ContentController
24
 from tracim.controllers.debug import DebugController
19
 from tracim.controllers.debug import DebugController
25
 from tracim.controllers.error import ErrorController
20
 from tracim.controllers.error import ErrorController
26
 from tracim.controllers.help import HelpController
21
 from tracim.controllers.help import HelpController
27
-from tracim.controllers.calendar import CalendarController
28
-from tracim.controllers.calendar import CalendarConfigController
22
+from tracim.controllers.previews import PreviewsController
29
 from tracim.controllers.user import UserRestController
23
 from tracim.controllers.user import UserRestController
30
 from tracim.controllers.workspace import UserWorkspaceRestController
24
 from tracim.controllers.workspace import UserWorkspaceRestController
25
+from tracim.lib import CST
26
+from tracim.lib.base import logger
27
+from tracim.lib.content import ContentApi
28
+from tracim.lib.user import CurrentUserGetterApi
31
 from tracim.lib.utils import replace_reset_password_templates
29
 from tracim.lib.utils import replace_reset_password_templates
32
-
33
 from tracim.model.data import ContentType
30
 from tracim.model.data import ContentType
34
-from tracim.model.serializers import DictLikeClass
35
-from tracim.model.serializers import CTX
36
 from tracim.model.serializers import Context
31
 from tracim.model.serializers import Context
32
+from tracim.model.serializers import CTX
33
+from tracim.model.serializers import DictLikeClass
37
 
34
 
38
 
35
 
39
 class RootController(StandardController):
36
 class RootController(StandardController):
58
     debug = DebugController()
55
     debug = DebugController()
59
     error = ErrorController()
56
     error = ErrorController()
60
 
57
 
61
-
62
     # Rest controllers
58
     # Rest controllers
63
     workspaces = UserWorkspaceRestController()
59
     workspaces = UserWorkspaceRestController()
64
     user = UserRestController()
60
     user = UserRestController()
61
+    previews = PreviewsController()
65
 
62
 
66
     content = ContentController()
63
     content = ContentController()
67
 
64
 
76
         super(RootController, self)._before(args, kw)
73
         super(RootController, self)._before(args, kw)
77
         tmpl_context.project_name = "tracim"
74
         tmpl_context.project_name = "tracim"
78
 
75
 
79
-
80
     @expose('tracim.templates.index')
76
     @expose('tracim.templates.index')
81
     def index(self, came_from='', *args, **kwargs):
77
     def index(self, came_from='', *args, **kwargs):
82
         if request.identity:
78
         if request.identity:
107
         logger.info(self, 'came_from: {}'.format(kwargs))
103
         logger.info(self, 'came_from: {}'.format(kwargs))
108
         return self.index(came_from, args, *kwargs)
104
         return self.index(came_from, args, *kwargs)
109
 
105
 
110
-
111
     @expose()
106
     @expose()
112
     def post_login(self, came_from=lurl('/home')):
107
     def post_login(self, came_from=lurl('/home')):
113
         """
108
         """
117
         if not request.identity:
112
         if not request.identity:
118
             login_counter = request.environ.get('repoze.who.logins', 0) + 1
113
             login_counter = request.environ.get('repoze.who.logins', 0) + 1
119
             redirect(url('/login'),
114
             redirect(url('/login'),
120
-                params=dict(came_from=came_from, __logins=login_counter))
115
+                     params=dict(came_from=came_from, __logins=login_counter))
121
 
116
 
122
         user = CurrentUserGetterApi.get_current_user()
117
         user = CurrentUserGetterApi.get_current_user()
123
 
118
 
127
     @expose()
122
     @expose()
128
     def post_logout(self, came_from=lurl('/')):
123
     def post_logout(self, came_from=lurl('/')):
129
         """
124
         """
130
-        Redirect the user to the initially requested page on logout and say  goodbye as well.
125
+        Redirect the user to the initially requested page on logout and say
126
+        goodbye as well.
131
         """
127
         """
132
         flash(_('Successfully logged out. We hope to see you soon!'))
128
         flash(_('Successfully logged out. We hope to see you soon!'))
133
         redirect(came_from)
129
         redirect(came_from)
134
-        
135
 
130
 
136
     @require(predicates.not_anonymous())
131
     @require(predicates.not_anonymous())
137
     @expose('tracim.templates.home')
132
     @expose('tracim.templates.home')
142
         fake_api = Context(CTX.CURRENT_USER).toDict({
137
         fake_api = Context(CTX.CURRENT_USER).toDict({
143
             'current_user': current_user_content})
138
             'current_user': current_user_content})
144
 
139
 
145
-
146
         last_active_contents = ContentApi(user).get_last_active(None, ContentType.Any, None)
140
         last_active_contents = ContentApi(user).get_last_active(None, ContentType.Any, None)
147
         fake_api.last_actives = Context(CTX.CONTENT_LIST).toDict(last_active_contents, 'contents', 'nb')
141
         fake_api.last_actives = Context(CTX.CONTENT_LIST).toDict(last_active_contents, 'contents', 'nb')
148
 
142
 
173
         #
167
         #
174
         # return DictLikeClass(result = dictified_user, fake_api=fake_api)
168
         # return DictLikeClass(result = dictified_user, fake_api=fake_api)
175
 
169
 
176
-
177
     @require(predicates.not_anonymous())
170
     @require(predicates.not_anonymous())
178
     @expose('tracim.templates.search.display')
171
     @expose('tracim.templates.search.display')
179
-    def search(self, keywords = ''):
172
+    def search(self, keywords=''):
180
         from tracim.lib.content import ContentApi
173
         from tracim.lib.content import ContentApi
181
 
174
 
182
         user = tmpl_context.current_user
175
         user = tmpl_context.current_user
197
         search_results.keywords = keyword_list
190
         search_results.keywords = keyword_list
198
 
191
 
199
         return DictLikeClass(fake_api=fake_api, search=search_results)
192
         return DictLikeClass(fake_api=fake_api, search=search_results)
200
-
201
-

+ 29 - 0
tracim/tracim/lib/content.py View File

15
 import tg
15
 import tg
16
 from tg.i18n import ugettext as _
16
 from tg.i18n import ugettext as _
17
 
17
 
18
+from depot.manager import DepotManager
19
+
18
 import sqlalchemy
20
 import sqlalchemy
19
 from sqlalchemy.orm import aliased
21
 from sqlalchemy.orm import aliased
20
 from sqlalchemy.orm import joinedload
22
 from sqlalchemy.orm import joinedload
450
 
452
 
451
         return revision
453
         return revision
452
 
454
 
455
+    # INFO - A.P - 2017-07-03 - python file object getter
456
+    # in case of we cook a version of preview manager that allows a pythonic
457
+    # access to files
458
+    # def get_one_revision_file(self, revision_id: int = None):
459
+    #     """
460
+    #     This function allows us to directly get a Python file object from its
461
+    #     revision identifier.
462
+    #     :param revision_id: The revision id of the file we want to return
463
+    #     :return: The corresponding Python file object
464
+    #     """
465
+    #     revision = self.get_one_revision(revision_id)
466
+    #     return DepotManager.get().get(revision.depot_file)
467
+
468
+    def get_one_revision_filepath(self, revision_id: int = None) -> str:
469
+        """
470
+        This method allows us to directly get a file path from its revision
471
+        identifier.
472
+        :param revision_id: The revision id of the filepath we want to return
473
+        :return: The corresponding filepath
474
+        """
475
+        revision = self.get_one_revision(revision_id)
476
+        depot = DepotManager.get()
477
+        depot_stored_file = depot.get(revision.depot_file)  # type: StoredFile
478
+        depot_file_path = depot_stored_file._file_path  # type: str
479
+        return depot_file_path
480
+
453
     def get_one_by_label_and_parent(
481
     def get_one_by_label_and_parent(
454
             self,
482
             self,
455
             content_label: str,
483
             content_label: str,
850
         item.file_name = new_filename
878
         item.file_name = new_filename
851
         item.file_mimetype = new_mimetype
879
         item.file_mimetype = new_mimetype
852
         item.file_content = new_file_content
880
         item.file_content = new_file_content
881
+        item.depot_file = new_file_content
853
         item.revision_type = ActionDescription.REVISION
882
         item.revision_type = ActionDescription.REVISION
854
         return item
883
         return item
855
 
884
 

+ 36 - 1
tracim/tracim/model/data.py View File

24
 from sqlalchemy.types import LargeBinary
24
 from sqlalchemy.types import LargeBinary
25
 from sqlalchemy.types import Text
25
 from sqlalchemy.types import Text
26
 from sqlalchemy.types import Unicode
26
 from sqlalchemy.types import Unicode
27
+from depot.fields.sqlalchemy import UploadedFileField
28
+from depot.fields.upload import UploadedFile
27
 
29
 
28
 from tracim.lib.utils import lazy_ugettext as l_
30
 from tracim.lib.utils import lazy_ugettext as l_
29
 from tracim.lib.exception import ContentRevisionUpdateError
31
 from tracim.lib.exception import ContentRevisionUpdateError
543
         server_default='',
545
         server_default='',
544
     )
546
     )
545
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
547
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
548
+    # TODO - A.P - 2017-07-03 - future removal planned
549
+    # file_content is to be replaced by depot_file, for now both coexist as
550
+    # this:
551
+    # - file_content data is still setted
552
+    # - newly created revision also gets depot_file data setted
553
+    # - access to the file of a revision from depot_file exclusively
554
+    # Here is the tasks workflow of the DB to OnDisk Switch :
555
+    # - Add depot_file "prototype style"
556
+    #   https://github.com/tracim/tracim/issues/233 - DONE
557
+    # - Integrate preview generator feature "prototype style"
558
+    #   https://github.com/tracim/tracim/issues/232 - DONE
559
+    # - Write migrations
560
+    #   https://github.com/tracim/tracim/issues/245
561
+    #   https://github.com/tracim/tracim/issues/246
562
+    # - Stabilize preview generator integration
563
+    #   includes dropping DB file content
564
+    #   https://github.com/tracim/tracim/issues/249
546
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=True))
565
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=True))
566
+    # INFO - A.P - 2017-07-03 - Depot Doc
567
+    # http://depot.readthedocs.io/en/latest/#attaching-files-to-models
568
+    # http://depot.readthedocs.io/en/latest/api.html#module-depot.fields
569
+    depot_file = Column(UploadedFileField, unique=False, nullable=True)
547
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
570
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
548
 
571
 
549
     type = Column(Unicode(32), unique=False, nullable=False)
572
     type = Column(Unicode(32), unique=False, nullable=False)
626
             setattr(new_rev, column_name, column_value)
649
             setattr(new_rev, column_name, column_value)
627
 
650
 
628
         new_rev.updated = datetime.utcnow()
651
         new_rev.updated = datetime.utcnow()
652
+        # TODO APY tweaks here depot_file
653
+        # import pudb; pu.db
654
+        # new_rev.depot_file = DepotManager.get().get(revision.depot_file)
655
+        new_rev.depot_file = revision.file_content
629
 
656
 
630
         return new_rev
657
         return new_rev
631
 
658
 
1050
     def is_editable(self) -> bool:
1077
     def is_editable(self) -> bool:
1051
         return not self.is_archived and not self.is_deleted
1078
         return not self.is_archived and not self.is_deleted
1052
 
1079
 
1080
+    @property
1081
+    def depot_file(self) -> UploadedFile:
1082
+        return self.revision.depot_file
1083
+
1084
+    @depot_file.setter
1085
+    def depot_file(self, value):
1086
+        self.revision.depot_file = value
1087
+
1053
     def get_current_revision(self) -> ContentRevisionRO:
1088
     def get_current_revision(self) -> ContentRevisionRO:
1054
         if not self.revisions:
1089
         if not self.revisions:
1055
             return self.new_revision()
1090
             return self.new_revision()
1364
             else:
1399
             else:
1365
                 aff = '%d hour%s ago' % (delta.seconds/3600, 's' if delta.seconds/3600>=2 else '')
1400
                 aff = '%d hour%s ago' % (delta.seconds/3600, 's' if delta.seconds/3600>=2 else '')
1366
 
1401
 
1367
-        return aff
1402
+        return aff

+ 9 - 6
tracim/tracim/model/serializers.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import cherrypy
2
 import cherrypy
3
-import os
4
 
3
 
5
 import types
4
 import types
6
 
5
 
7
-from bs4 import BeautifulSoup
8
 from babel.dates import format_timedelta
6
 from babel.dates import format_timedelta
9
 from babel.dates import format_datetime
7
 from babel.dates import format_datetime
10
 
8
 
12
 import tg
10
 import tg
13
 from tg.i18n import ugettext as _
11
 from tg.i18n import ugettext as _
14
 from tg.util import LazyString
12
 from tg.util import LazyString
13
+
14
+from depot.manager import DepotManager
15
+
15
 from tracim.lib.base import logger
16
 from tracim.lib.base import logger
16
 from tracim.lib.user import CurrentUserGetterApi
17
 from tracim.lib.user import CurrentUserGetterApi
17
 from tracim.model.auth import Profile
18
 from tracim.model.auth import Profile
407
             })
408
             })
408
         )
409
         )
409
 
410
 
410
-        if content.type==ContentType.File:
411
+        if content.type == ContentType.File:
412
+            depot = DepotManager.get()
413
+            depot_stored_file = depot.get(data_container.depot_file)
411
             result.label = content.label
414
             result.label = content.label
412
             result['file'] = DictLikeClass(
415
             result['file'] = DictLikeClass(
413
-                name = data_container.file_name,
414
-                size = len(data_container.file_content),
415
-                mimetype = data_container.file_mimetype)
416
+                name=data_container.file_name,
417
+                size=depot_stored_file.content_length,
418
+                mimetype=data_container.file_mimetype)
416
         return result
419
         return result
417
 
420
 
418
     if content.type==ContentType.Folder:
421
     if content.type==ContentType.Folder:

+ 84 - 0
tracim/tracim/templates/file/getone.mak View File

111
         <div class="t-half-spacer-above">
111
         <div class="t-half-spacer-above">
112
             <table class="table table-hover table-condensed table-striped table-bordered">
112
             <table class="table table-hover table-condensed table-striped table-bordered">
113
                 <tr>
113
                 <tr>
114
+                    <td class="tracim-title">${_('Preview')}</td>
115
+                    <td>
116
+                        <table>
117
+                            <tr>
118
+                                <td>
119
+                                    <button type="button" id="prev" onclick="previous_page()">
120
+                                        <span class="pull-left">
121
+                                            ${ICON.FA_FW('fa fa-chevron-left')}
122
+                                        </span>
123
+                                    </button>
124
+                                </td>
125
+                                <td>
126
+                                    <a id="preview_link"><img id='preview' alt="Preview"></a>
127
+                                </td>
128
+                                <td>
129
+                                    <button type="button" id="next" onclick="next_page()">
130
+                                        <span>
131
+                                            ${ICON.FA_FW('fa fa-chevron-right')}
132
+                                        </span>
133
+                                    </button>
134
+                                </td>
135
+                                <td>
136
+                                    <a type="button" id="dl_one_pdf">${_('this page')}
137
+                                        <span class="pull-left">
138
+                                            ${ICON.FA_FW('fa fa-download')}
139
+                                        </span>
140
+                                    </a>
141
+                                </td>
142
+                                <td>
143
+                                    <a type="button" id="dl_full_pdf">${_('all pages')}
144
+                                        <span class="pull-left">
145
+                                            ${ICON.FA_FW('fa fa-download')}
146
+                                        </span>
147
+                                    </a>
148
+                                </td>
149
+                            </tr>
150
+                        </table>
151
+
152
+                        <script type="text/javascript">
153
+                            var nb_page = parseInt(${nb_page});
154
+                            console.log(nb_page);
155
+                            var page = 0;
156
+                            var urls = [];
157
+                            % for one_url in url:
158
+                            urls.push('${one_url}');
159
+                            % endfor
160
+                            console.log(urls);
161
+                            document.getElementById('preview').src = urls[page];
162
+                            refresh_button();
163
+
164
+                            function next_page(){
165
+                                page = page+1;
166
+                                console.log('page next');
167
+                                console.log(urls[page]);
168
+                                document.getElementById('preview').src = urls[page];
169
+                                refresh_button();
170
+                            }
171
+
172
+                            function previous_page(){
173
+                                page = page-1;
174
+                                console.log('page previous');
175
+                                console.log(urls[page]);
176
+                                document.getElementById('preview').src = urls[page];
177
+                                refresh_button();
178
+                            }
179
+
180
+                            function refresh_button(){
181
+                                console.log(page);
182
+                                document.getElementById('prev').disabled = false;
183
+                                document.getElementById('next').disabled = false;
184
+                                document.getElementById('dl_one_pdf').href = "/previews/${result.file.id}/pages/" + page + "/download_pdf_one?revision_id=${result.file.selected_revision}";
185
+                                document.getElementById('dl_full_pdf').href = "/previews/${result.file.id}/pages/" + page + "/download_pdf_full?revision_id=${result.file.selected_revision}";
186
+                                document.getElementById('preview_link').href = "/previews/${result.file.id}/pages/" + page + "/high_quality?revision_id=${result.file.selected_revision}";
187
+                                if(page >= nb_page-1){
188
+                                    document.getElementById('next').disabled = true;
189
+                                }
190
+                                if(page <= 0){
191
+                                    document.getElementById('prev').disabled = true;
192
+                                }
193
+                            }
194
+                        </script>
195
+                    </td>
196
+                </tr>
197
+                <tr>
114
                     <td class="tracim-title">${_('File')}</td>
198
                     <td class="tracim-title">${_('File')}</td>
115
                     <td>
199
                     <td>
116
                         <a href="${download_url}" tittle="${_('Download the file (last revision)')}">
200
                         <a href="${download_url}" tittle="${_('Download the file (last revision)')}">

+ 48 - 4
tracim/tracim/tests/models/test_content.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import time
2
 import time
3
-from nose.tools import raises, ok_
3
+
4
+from depot.fields.upload import UploadedFile
5
+from nose.tools import ok_
6
+from nose.tools import raises
4
 from sqlalchemy.sql.elements import and_
7
 from sqlalchemy.sql.elements import and_
5
 from sqlalchemy.testing import eq_
8
 from sqlalchemy.testing import eq_
6
 
9
 
7
 from tracim.lib.content import ContentApi
10
 from tracim.lib.content import ContentApi
8
 from tracim.lib.exception import ContentRevisionUpdateError
11
 from tracim.lib.exception import ContentRevisionUpdateError
9
-from tracim.model import DBSession, User, Content, new_revision
10
-from tracim.model.data import ContentRevisionRO, Workspace, ActionDescription, ContentType
12
+from tracim.model import Content
13
+from tracim.model import DBSession
14
+from tracim.model import new_revision
15
+from tracim.model import User
16
+from tracim.model.data import ActionDescription
17
+from tracim.model.data import ContentRevisionRO
18
+from tracim.model.data import ContentType
19
+from tracim.model.data import Workspace
11
 from tracim.tests import TestStandard
20
 from tracim.tests import TestStandard
12
 
21
 
13
 
22
 
111
             revision_type=ActionDescription.CREATION,
120
             revision_type=ActionDescription.CREATION,
112
             is_deleted=False,  # TODO: pk ?
121
             is_deleted=False,  # TODO: pk ?
113
             is_archived=False,  # TODO: pk ?
122
             is_archived=False,  # TODO: pk ?
114
-            #file_content=None,  # TODO: pk ? (J'ai du mettre nullable=True)
123
+            # file_content=None,  # TODO: pk ? (J'ai du mettre nullable=True)
115
         )
124
         )
116
 
125
 
117
         eq_(1, DBSession.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'TEST_CONTENT_1').count())
126
         eq_(1, DBSession.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'TEST_CONTENT_1').count())
163
 
172
 
164
         return created_content
173
         return created_content
165
 
174
 
175
+    def _get_user(self):
176
+        email = 'admin@admin.admin'
177
+        user_query = DBSession.query(User)
178
+        user_filter = user_query.filter(User.email == email)
179
+        user = user_filter.one()
180
+        return user
181
+
166
     def _create_content(self, *args, **kwargs):
182
     def _create_content(self, *args, **kwargs):
167
         content = Content(*args, **kwargs)
183
         content = Content(*args, **kwargs)
168
         DBSession.add(content)
184
         DBSession.add(content)
169
         DBSession.flush()
185
         DBSession.flush()
186
+        return content
170
 
187
 
188
+    def _create_content_from_nothing(self):
189
+        user_admin = self._get_user()
190
+        workspace = Workspace(label="TEST_WORKSPACE_1")
191
+        content = self._create_content(
192
+            owner=user_admin,
193
+            workspace=workspace,
194
+            type=ContentType.File,
195
+            label='TEST_CONTENT_1',
196
+            description='TEST_CONTENT_DESCRIPTION_1',
197
+            revision_type=ActionDescription.CREATION,
198
+        )
171
         return content
199
         return content
200
+
201
+    def test_unit__content_depot_file(self):
202
+        """ Depot file access thought content property methods. """
203
+        content = self._create_content_from_nothing()
204
+        # tests uninitialized depot file
205
+        eq_(content.depot_file, None)
206
+        # initializes depot file
207
+        # which is able to behave like a python file object
208
+        content.depot_file = b'test'
209
+        # tests initialized depot file
210
+        ok_(content.depot_file)
211
+        # tests type of initialized depot file
212
+        eq_(type(content.depot_file), UploadedFile)
213
+        # tests content of initialized depot file
214
+        # using depot_file.file of type StoredFile to fetch content back
215
+        eq_(content.depot_file.file.read(), b'test')