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,100 +0,0 @@
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,6 +35,7 @@ full_stack = true
35 35
 # You can set french as default language by uncommenting next line
36 36
 # lang = fr
37 37
 cache_dir = %(here)s/data
38
+preview_cache_dir = /tmp/tracim/cache/previews/
38 39
 beaker.session.key = tracim
39 40
 beaker.session.secret = 3283411b-1904-4554-b0e1-883863b53080
40 41
 

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

@@ -0,0 +1,25 @@
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,6 +52,8 @@ install_requires=[
52 52
     "unicode-slugify==0.1.3",
53 53
     "pytz==2014.7",
54 54
     'rq==0.7.1',
55
+    'filedepot>=0.5.0',
56
+    'preview-generator'
55 57
     ]
56 58
 
57 59
 setup(

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

@@ -38,6 +38,12 @@ from tracim.lib.daemons import WsgiDavDaemon
38 38
 from tracim.model.data import ActionDescription
39 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 47
 base_config = TracimAppConfig()
42 48
 base_config.renderers = []
43 49
 base_config.use_toscawidgets = False
@@ -188,6 +194,8 @@ class CFG(object):
188 194
 
189 195
     def __init__(self):
190 196
 
197
+        self.PREVIEW_CACHE = str(tg.config.get('preview_cache_dir'))
198
+
191 199
         self.DATA_UPDATE_ALLOWED_DURATION = int(tg.config.get('content.update.allowed.duration', 0))
192 200
 
193 201
         self.WEBSITE_TITLE = tg.config.get('website.title', 'TRACIM')

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

@@ -1,18 +1,22 @@
1 1
 # -*- coding: utf-8 -*-
2
+from tracim.config.app_cfg import CFG
3
+
2 4
 __author__ = 'damien'
3 5
 
4 6
 import sys
5 7
 import traceback
6 8
 
7 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 13
 import tg
14
+from tg import abort
9 15
 from tg import tmpl_context
10 16
 from tg import require
11 17
 from tg import predicates
12 18
 from tg.i18n import ugettext as _
13 19
 from tg.predicates import not_anonymous
14
-from sqlalchemy.orm.exc import NoResultFound
15
-from tg import abort
16 20
 
17 21
 from tracim.controllers import TIMRestController
18 22
 from tracim.controllers import StandardController
@@ -22,6 +26,7 @@ from tracim.controllers import TIMWorkspaceContentRestController
22 26
 from tracim.lib import CST
23 27
 from tracim.lib.base import BaseController
24 28
 from tracim.lib.base import logger
29
+from tracim.lib.integrity import render_invalid_integrity_chosen_path
25 30
 from tracim.lib.utils import SameValueError
26 31
 from tracim.lib.utils import get_valid_header_file_name
27 32
 from tracim.lib.utils import str_as_bool
@@ -41,7 +46,6 @@ from tracim.model.data import Content
41 46
 from tracim.model.data import ContentType
42 47
 from tracim.model.data import UserRoleInWorkspace
43 48
 from tracim.model.data import Workspace
44
-from tracim.lib.integrity import render_invalid_integrity_chosen_path
45 49
 
46 50
 
47 51
 class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
@@ -62,19 +66,18 @@ class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
62 66
 
63 67
     @tg.expose()
64 68
     @tg.require(current_user_is_contributor())
65
-    def post(self, content=''):
69
+    def post(self, content: str = ''):
66 70
         # TODO - SECURE THIS
67 71
         workspace = tmpl_context.workspace
68 72
         thread = tmpl_context.thread
69 73
 
70
-
71 74
         api = ContentApi(tmpl_context.current_user)
72 75
 
73 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 81
         tg.flash(_('Comment added'), CST.STATUS_OK)
79 82
         tg.redirect(next_url)
80 83
 
@@ -86,19 +89,21 @@ class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
86 89
         # TODO - CHECK RIGHTS
87 90
         item_id = int(item_id)
88 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 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 107
             with new_revision(item):
103 108
                 content_api.delete(item)
104 109
                 content_api.save(item, ActionDescription.DELETION)
@@ -107,26 +112,29 @@ class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
107 112
             tg.redirect(next_url)
108 113
 
109 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 118
             msg = _('{} not deleted: {}').format(self._item_type_label, str(e))
114 119
             tg.flash(msg, CST.STATUS_ERROR)
115 120
             tg.redirect(back_url)
116 121
 
117
-
118 122
     @tg.expose()
119 123
     @tg.require(not_anonymous())
120 124
     def put_delete_undo(self, item_id):
121 125
         require_current_user_is_owner(int(item_id))
122 126
 
123 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 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 138
             msg = _('{} undeleted.').format(self._item_type_label)
131 139
             with new_revision(item):
132 140
                 content_api.undelete(item)
@@ -137,10 +145,11 @@ class UserWorkspaceFolderThreadCommentRestController(TIMRestController):
137 145
 
138 146
         except ValueError as e:
139 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 153
             tg.flash(msg, CST.STATUS_ERROR)
145 154
             tg.redirect(back_url)
146 155
 
@@ -185,89 +194,118 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
185 194
     @tg.expose('tracim.templates.file.getone')
186 195
     def get_one(self, file_id, revision_id=None):
187 196
         file_id = int(file_id)
197
+        cache_path = CFG.get_instance().PREVIEW_CACHE
198
+        preview_manager = PreviewManager(cache_path, create_folder=True)
188 199
         user = tmpl_context.current_user
189 200
         workspace = tmpl_context.workspace
190
-
191 201
         current_user_content = Context(CTX.CURRENT_USER,
192 202
                                        current_user=user).toDict(user)
193 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 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 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 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 231
         fake_api = Context(CTX.FOLDER,
208 232
                            current_user=user).toDict(fake_api_content)
209
-
210 233
         dictified_file = Context(self._get_one_context,
211 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 241
     @tg.require(current_user_is_reader())
215 242
     @tg.expose()
216 243
     def download(self, file_id, revision_id=None):
217 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 246
         user = tmpl_context.current_user
220 247
         workspace = tmpl_context.workspace
221 248
 
222 249
         content_api = ContentApi(user)
223 250
         revision_to_send = None
224 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 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 261
         revision_to_send = None
230
-        if item.revision_to_serialize<=0:
262
+        if item.revision_to_serialize <= 0:
231 263
             for revision in item.revisions:
232 264
                 if not revision_to_send:
233 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 268
                     revision_to_send = revision
237 269
         else:
238 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 272
                     revision_to_send = revision
241 273
                     break
242 274
 
243 275
         content_type = 'application/x-download'
244 276
         if revision_to_send.file_mimetype:
245 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 281
         tg.response.headers['Content-Type'] = content_type
249 282
         file_name = get_valid_header_file_name(revision_to_send.file_name)
250 283
         tg.response.headers['Content-Disposition'] = \
251 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 299
         :return:
263 300
         """
264 301
         workspace = context_workspace
265 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 307
         dictified_files = Context(CTX.FILES).toDict(files)
269
-        return DictLikeClass(result = dictified_files)
270
-
308
+        return DictLikeClass(result=dictified_files)
271 309
 
272 310
     @tg.require(current_user_is_contributor())
273 311
     @tg.expose()
@@ -279,8 +317,10 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
279 317
         api = ContentApi(tmpl_context.current_user)
280 318
         with DBSession.no_autoflush:
281 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 324
             # Display error page to user if chosen label is in conflict
285 325
             if not self._path_validation.validate_new_content(file):
286 326
                 return render_invalid_integrity_chosen_path(
@@ -289,8 +329,10 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
289 329
         api.save(file, ActionDescription.CREATION)
290 330
 
291 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 337
     @tg.require(current_user_is_contributor())
296 338
     @tg.expose()
@@ -329,12 +371,12 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
329 371
 
330 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 377
                 else:
336 378
                     # So, now we may have a comment and/or a file revision
337
-                    if comment and ''==label:
379
+                    if comment and '' == label:
338 380
                         comment_item = api.create_comment(workspace,
339 381
                                                           item, comment,
340 382
                                                           do_save=False)
@@ -345,14 +387,17 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
345 387
                             # The notification is only sent
346 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 392
                             api.save(comment_item,
351 393
                                      ActionDescription.COMMENT,
352 394
                                      do_notify=False)
353 395
 
354 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 402
                         # Display error page to user if chosen label is in
358 403
                         # conflict
@@ -367,13 +412,18 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
367 412
 
368 413
             msg = _('{} updated').format(self._item_type_label)
369 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 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 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 429
 class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
@@ -418,7 +468,6 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
418 468
         page_id = int(page_id)
419 469
         user = tmpl_context.current_user
420 470
         workspace = tmpl_context.workspace
421
-        workspace_id = tmpl_context.workspace_id
422 471
 
423 472
         current_user_content = Context(CTX.CURRENT_USER).toDict(user)
424 473
         current_user_content.roles.sort(key=lambda role: role.workspace.name)
@@ -429,34 +478,47 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
429 478
             show_archived=True,
430 479
         )
431 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 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 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 493
         fake_api = Context(CTX.FOLDER).toDict(fake_api_content)
439 494
 
440 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 512
         :return:
452 513
         """
453 514
         workspace = context_workspace
454 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 520
         dictified_pages = Context(CTX.PAGES).toDict(pages)
458
-        return DictLikeClass(result = dictified_pages)
459
-
521
+        return DictLikeClass(result=dictified_pages)
460 522
 
461 523
     @tg.require(current_user_is_contributor())
462 524
     @tg.expose()
@@ -466,7 +528,10 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
466 528
         api = ContentApi(tmpl_context.current_user)
467 529
 
468 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 535
             page.description = content
471 536
 
472 537
             if not self._path_validation.validate_new_content(page):
@@ -477,12 +542,14 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
477 542
         api.save(page, ActionDescription.CREATION, do_notify=True)
478 543
 
479 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 550
     @tg.require(current_user_is_contributor())
484 551
     @tg.expose()
485
-    def put(self, item_id, label='',content=''):
552
+    def put(self, item_id, label='', content=''):
486 553
         # INFO - D.A. This method is a raw copy of
487 554
         # TODO - SECURE THIS
488 555
         workspace = tmpl_context.workspace
@@ -502,17 +569,23 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
502 569
 
503 570
             msg = _('{} updated').format(self._item_type_label)
504 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 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 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 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 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 591
 class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController):
@@ -546,12 +619,10 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
546 619
     def _item_type(self):
547 620
         return ContentType.Thread
548 621
 
549
-
550 622
     @property
551 623
     def _item_type_label(self):
552 624
         return _('Thread')
553 625
 
554
-
555 626
     @property
556 627
     def _get_one_context(self) -> str:
557 628
         return CTX.THREAD
@@ -564,7 +635,8 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
564 635
     @tg.expose()
565 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 640
         :param label:
569 641
         :param content:
570 642
         :return:
@@ -575,8 +647,12 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
575 647
         api = ContentApi(tmpl_context.current_user)
576 648
 
577 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 656
             api.save(thread, ActionDescription.CREATION, do_notify=False)
581 657
 
582 658
             comment = api.create(ContentType.Comment, workspace, thread, label)
@@ -592,8 +668,9 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
592 668
         api.do_notify(thread)
593 669
 
594 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 675
     @tg.require(current_user_is_reader())
599 676
     @tg.expose('tracim.templates.thread.getone')
@@ -620,13 +697,15 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
620 697
         thread = content_api.get_one(thread_id, ContentType.Thread, workspace)
621 698
 
622 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 702
         fake_api = Context(CTX.FOLDER).toDict(fake_api_content)
625 703
 
626 704
         dictified_thread = Context(CTX.THREAD).toDict(thread, 'thread')
627 705
 
628 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 710
         return DictLikeClass(
632 711
             result=dictified_thread,
@@ -635,8 +714,8 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
635 714
         )
636 715
 
637 716
 
638
-
639
-class ItemLocationController(TIMWorkspaceContentRestController, BaseController):
717
+class ItemLocationController(TIMWorkspaceContentRestController,
718
+                             BaseController):
640 719
 
641 720
     @tg.require(current_user_is_content_manager())
642 721
     @tg.expose()
@@ -649,7 +728,6 @@ class ItemLocationController(TIMWorkspaceContentRestController, BaseController):
649 728
         raise NotImplementedError
650 729
         return item
651 730
 
652
-
653 731
     @tg.require(current_user_is_content_manager())
654 732
     @tg.expose('tracim.templates.folder.move')
655 733
     def edit(self, item_id):
@@ -659,8 +737,11 @@ class ItemLocationController(TIMWorkspaceContentRestController, BaseController):
659 737
         :param item_id:
660 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 746
         item_id = int(item_id)
666 747
         user = tmpl_context.current_user
@@ -670,14 +751,15 @@ class ItemLocationController(TIMWorkspaceContentRestController, BaseController):
670 751
         item = content_api.get_one(item_id, ContentType.Any, workspace)
671 752
 
672 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 756
     @tg.require(current_user_is_content_manager())
676 757
     @tg.expose()
677 758
     def put(self, item_id, folder_id='0'):
678 759
         """
679 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 763
         :return:
682 764
         """
683 765
         # TODO - SECURE THIS
@@ -727,14 +809,14 @@ class ItemLocationController(TIMWorkspaceContentRestController, BaseController):
727 809
                 api.move(item, new_parent)
728 810
             next_url = self.parent_controller.url(item_id)
729 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 814
             else:
732 815
                 tg.flash(_('Item moved to workspace root'))
733 816
 
734 817
             tg.redirect(next_url)
735 818
 
736 819
 
737
-
738 820
 class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
739 821
 
740 822
     TEMPLATE_NEW = 'mako:tracim.templates.folder.new'
@@ -770,8 +852,7 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
770 852
         folder = content_api.get_one(folder_id, ContentType.Folder, workspace)
771 853
 
772 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 857
     @tg.require(current_user_is_reader())
777 858
     @tg.expose('tracim.templates.folder.getone')
@@ -806,7 +887,8 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
806 887
             )
807 888
 
808 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 892
         fake_api_pages = self.pages.get_all_fake(workspace, folder).result
811 893
         fake_api_files = self.files.get_all_fake(workspace, folder).result
812 894
         fake_api_threads = self.threads.get_all_fake(workspace, folder).result
@@ -846,14 +928,16 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
846 928
             show_archived=show_archived,
847 929
         )
848 930
 
849
-
850 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 941
         :return:
858 942
         """
859 943
         workspace = context_workspace
@@ -863,12 +947,17 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
863 947
         folders = content_api.get_child_folders(parent_folder, workspace)
864 948
 
865 949
         folders = Context(CTX.FOLDERS).toDict(folders)
866
-        return DictLikeClass(result = folders)
867
-
950
+        return DictLikeClass(result=folders)
868 951
 
869 952
     @tg.require(current_user_is_content_manager())
870 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 961
         # TODO - SECURE THIS
873 962
         workspace = tmpl_context.workspace
874 963
 
@@ -877,20 +966,24 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
877 966
         redirect_url_tmpl = '/workspaces/{}/folders/{}'
878 967
         redirect_url = ''
879 968
 
880
-
881 969
         try:
882 970
             parent = None
883 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 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 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 988
                 api.set_allowed_content(folder, subcontent)
896 989
 
@@ -902,17 +995,24 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
902 995
             api.save(folder)
903 996
 
904 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 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 1004
             traceback.print_exc()
909 1005
 
910 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 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 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,10 +1021,15 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
921 1021
         #
922 1022
         tg.redirect(tg.url(redirect_url))
923 1023
 
924
-
925 1024
     @tg.require(current_user_is_content_manager())
926 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 1033
         # TODO - SECURE THIS
929 1034
         workspace = tmpl_context.workspace
930 1035
 
@@ -934,14 +1039,15 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
934 1039
         try:
935 1040
             folder = api.get_one(int(folder_id), ContentType.Folder, workspace)
936 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 1047
             with new_revision(folder):
943 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 1051
                     api.update_content(folder, label, folder.description)
946 1052
                 api.set_allowed_content(folder, subcontent)
947 1053
 
@@ -957,7 +1063,8 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
957 1063
             next_url = self.url(folder.content_id)
958 1064
 
959 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 1068
             next_url = self.url(int(folder_id))
962 1069
 
963 1070
         tg.redirect(next_url)
@@ -978,40 +1085,50 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
978 1085
     def _item_type(self):
979 1086
         return ContentType.Folder
980 1087
 
981
-
982 1088
     @tg.require(current_user_is_content_manager())
983 1089
     @tg.expose()
984 1090
     def put_archive(self, item_id):
985 1091
         # TODO - CHECK RIGHTS
986 1092
         item_id = int(item_id)
987 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 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 1107
             with new_revision(item):
996 1108
                 content_api.archive(item)
997 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 1112
             tg.redirect(next_url)
1001 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 1119
             tg.flash(msg, CST.STATUS_ERROR)
1005 1120
             tg.redirect(next_url)
1006 1121
 
1007
-
1008 1122
     @tg.require(current_user_is_content_manager())
1009 1123
     @tg.expose()
1010 1124
     def put_archive_undo(self, item_id):
1011 1125
         # TODO - CHECK RIGHTS
1012 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 1132
         try:
1016 1133
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1017 1134
             msg = _('{} unarchived.').format(self._item_type_label)
@@ -1020,10 +1137,11 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
1020 1137
                 content_api.save(item, ActionDescription.UNARCHIVING)
1021 1138
 
1022 1139
             tg.flash(msg, CST.STATUS_OK)
1023
-            tg.redirect(next_url )
1140
+            tg.redirect(next_url)
1024 1141
 
1025 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 1145
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1028 1146
             # We still use std url because the item has not been archived
1029 1147
             tg.flash(msg, CST.STATUS_ERROR)
@@ -1035,12 +1153,20 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
1035 1153
         # TODO - CHECK RIGHTS
1036 1154
         item_id = int(item_id)
1037 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 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 1170
             with new_revision(item):
1045 1171
                 content_api.delete(item)
1046 1172
                 content_api.save(item, ActionDescription.DELETION)
@@ -1054,15 +1180,17 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
1054 1180
             tg.flash(msg, CST.STATUS_ERROR)
1055 1181
             tg.redirect(back_url)
1056 1182
 
1057
-
1058 1183
     @tg.require(current_user_is_content_manager())
1059 1184
     @tg.expose()
1060 1185
     def put_delete_undo(self, item_id):
1061 1186
         # TODO - CHECK RIGHTS
1062 1187
 
1063 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 1194
         try:
1067 1195
             next_url = self._std_url.format(item.workspace_id, item.content_id)
1068 1196
             msg = _('{} undeleted.').format(self._item_type_label)
@@ -1075,18 +1203,19 @@ class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb):
1075 1203
 
1076 1204
         except ValueError as e:
1077 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 1210
             tg.flash(msg, CST.STATUS_ERROR)
1081 1211
             tg.redirect(back_url)
1082 1212
 
1083 1213
 
1084 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 1217
     contents via mark_all_read()
1089
-    '''
1218
+    """
1090 1219
 
1091 1220
     @classmethod
1092 1221
     def current_item_id_key_in_context(cls) -> str:

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

@@ -0,0 +1,105 @@
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

@@ -0,0 +1,29 @@
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,5 +1,4 @@
1 1
 # -*- coding: utf-8 -*-
2
-import tg
3 2
 from tg import expose
4 3
 from tg import flash
5 4
 from tg import lurl
@@ -9,31 +8,29 @@ from tg import request
9 8
 from tg import require
10 9
 from tg import tmpl_context
11 10
 from tg import url
12
-
13 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 13
 from tracim.controllers import StandardController
23 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 19
 from tracim.controllers.debug import DebugController
25 20
 from tracim.controllers.error import ErrorController
26 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 23
 from tracim.controllers.user import UserRestController
30 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 29
 from tracim.lib.utils import replace_reset_password_templates
32
-
33 30
 from tracim.model.data import ContentType
34
-from tracim.model.serializers import DictLikeClass
35
-from tracim.model.serializers import CTX
36 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 36
 class RootController(StandardController):
@@ -58,10 +55,10 @@ class RootController(StandardController):
58 55
     debug = DebugController()
59 56
     error = ErrorController()
60 57
 
61
-
62 58
     # Rest controllers
63 59
     workspaces = UserWorkspaceRestController()
64 60
     user = UserRestController()
61
+    previews = PreviewsController()
65 62
 
66 63
     content = ContentController()
67 64
 
@@ -76,7 +73,6 @@ class RootController(StandardController):
76 73
         super(RootController, self)._before(args, kw)
77 74
         tmpl_context.project_name = "tracim"
78 75
 
79
-
80 76
     @expose('tracim.templates.index')
81 77
     def index(self, came_from='', *args, **kwargs):
82 78
         if request.identity:
@@ -107,7 +103,6 @@ class RootController(StandardController):
107 103
         logger.info(self, 'came_from: {}'.format(kwargs))
108 104
         return self.index(came_from, args, *kwargs)
109 105
 
110
-
111 106
     @expose()
112 107
     def post_login(self, came_from=lurl('/home')):
113 108
         """
@@ -117,7 +112,7 @@ class RootController(StandardController):
117 112
         if not request.identity:
118 113
             login_counter = request.environ.get('repoze.who.logins', 0) + 1
119 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 117
         user = CurrentUserGetterApi.get_current_user()
123 118
 
@@ -127,11 +122,11 @@ class RootController(StandardController):
127 122
     @expose()
128 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 128
         flash(_('Successfully logged out. We hope to see you soon!'))
133 129
         redirect(came_from)
134
-        
135 130
 
136 131
     @require(predicates.not_anonymous())
137 132
     @expose('tracim.templates.home')
@@ -142,7 +137,6 @@ class RootController(StandardController):
142 137
         fake_api = Context(CTX.CURRENT_USER).toDict({
143 138
             'current_user': current_user_content})
144 139
 
145
-
146 140
         last_active_contents = ContentApi(user).get_last_active(None, ContentType.Any, None)
147 141
         fake_api.last_actives = Context(CTX.CONTENT_LIST).toDict(last_active_contents, 'contents', 'nb')
148 142
 
@@ -173,10 +167,9 @@ class RootController(StandardController):
173 167
         #
174 168
         # return DictLikeClass(result = dictified_user, fake_api=fake_api)
175 169
 
176
-
177 170
     @require(predicates.not_anonymous())
178 171
     @expose('tracim.templates.search.display')
179
-    def search(self, keywords = ''):
172
+    def search(self, keywords=''):
180 173
         from tracim.lib.content import ContentApi
181 174
 
182 175
         user = tmpl_context.current_user
@@ -197,5 +190,3 @@ class RootController(StandardController):
197 190
         search_results.keywords = keyword_list
198 191
 
199 192
         return DictLikeClass(fake_api=fake_api, search=search_results)
200
-
201
-

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

@@ -15,6 +15,8 @@ import re
15 15
 import tg
16 16
 from tg.i18n import ugettext as _
17 17
 
18
+from depot.manager import DepotManager
19
+
18 20
 import sqlalchemy
19 21
 from sqlalchemy.orm import aliased
20 22
 from sqlalchemy.orm import joinedload
@@ -450,6 +452,32 @@ class ContentApi(object):
450 452
 
451 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 481
     def get_one_by_label_and_parent(
454 482
             self,
455 483
             content_label: str,
@@ -850,6 +878,7 @@ class ContentApi(object):
850 878
         item.file_name = new_filename
851 879
         item.file_mimetype = new_mimetype
852 880
         item.file_content = new_file_content
881
+        item.depot_file = new_file_content
853 882
         item.revision_type = ActionDescription.REVISION
854 883
         return item
855 884
 

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

@@ -24,6 +24,8 @@ from sqlalchemy.types import Integer
24 24
 from sqlalchemy.types import LargeBinary
25 25
 from sqlalchemy.types import Text
26 26
 from sqlalchemy.types import Unicode
27
+from depot.fields.sqlalchemy import UploadedFileField
28
+from depot.fields.upload import UploadedFile
27 29
 
28 30
 from tracim.lib.utils import lazy_ugettext as l_
29 31
 from tracim.lib.exception import ContentRevisionUpdateError
@@ -543,7 +545,28 @@ class ContentRevisionRO(DeclarativeBase):
543 545
         server_default='',
544 546
     )
545 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 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 570
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
548 571
 
549 572
     type = Column(Unicode(32), unique=False, nullable=False)
@@ -626,6 +649,10 @@ class ContentRevisionRO(DeclarativeBase):
626 649
             setattr(new_rev, column_name, column_value)
627 650
 
628 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 657
         return new_rev
631 658
 
@@ -1050,6 +1077,14 @@ class Content(DeclarativeBase):
1050 1077
     def is_editable(self) -> bool:
1051 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 1088
     def get_current_revision(self) -> ContentRevisionRO:
1054 1089
         if not self.revisions:
1055 1090
             return self.new_revision()
@@ -1364,4 +1399,4 @@ class VirtualEvent(object):
1364 1399
             else:
1365 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,10 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import cherrypy
3
-import os
4 3
 
5 4
 import types
6 5
 
7
-from bs4 import BeautifulSoup
8 6
 from babel.dates import format_timedelta
9 7
 from babel.dates import format_datetime
10 8
 
@@ -12,6 +10,9 @@ from datetime import datetime
12 10
 import tg
13 11
 from tg.i18n import ugettext as _
14 12
 from tg.util import LazyString
13
+
14
+from depot.manager import DepotManager
15
+
15 16
 from tracim.lib.base import logger
16 17
 from tracim.lib.user import CurrentUserGetterApi
17 18
 from tracim.model.auth import Profile
@@ -407,12 +408,14 @@ def serialize_node_for_page(content: Content, context: Context):
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 414
             result.label = content.label
412 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 419
         return result
417 420
 
418 421
     if content.type==ContentType.Folder:

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

@@ -111,6 +111,90 @@
111 111
         <div class="t-half-spacer-above">
112 112
             <table class="table table-hover table-condensed table-striped table-bordered">
113 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 198
                     <td class="tracim-title">${_('File')}</td>
115 199
                     <td>
116 200
                         <a href="${download_url}" tittle="${_('Download the file (last revision)')}">

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

@@ -1,13 +1,22 @@
1 1
 # -*- coding: utf-8 -*-
2 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 7
 from sqlalchemy.sql.elements import and_
5 8
 from sqlalchemy.testing import eq_
6 9
 
7 10
 from tracim.lib.content import ContentApi
8 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 20
 from tracim.tests import TestStandard
12 21
 
13 22
 
@@ -111,7 +120,7 @@ class TestContent(TestStandard):
111 120
             revision_type=ActionDescription.CREATION,
112 121
             is_deleted=False,  # TODO: pk ?
113 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 126
         eq_(1, DBSession.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'TEST_CONTENT_1').count())
@@ -163,9 +172,44 @@ class TestContent(TestStandard):
163 172
 
164 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 182
     def _create_content(self, *args, **kwargs):
167 183
         content = Content(*args, **kwargs)
168 184
         DBSession.add(content)
169 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 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')