Browse Source

WebDAV: Content.file_extension usage for files names

Bastien Sevajol (Algoo) 8 years ago
parent
commit
73f79ef811

+ 31 - 0
tracim/migration/versions/59fc98c3c965_delete_content_file_name_column.py View File

1
+"""delete content file name column
2
+
3
+Revision ID: 59fc98c3c965
4
+Revises: 15305f71bfda
5
+Create Date: 2016-11-25 14:55:22.176175
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '59fc98c3c965'
11
+down_revision = '15305f71bfda'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    op.drop_column('content_revisions', 'file_name')
19
+
20
+
21
+def downgrade():
22
+    op.add_column(
23
+        'content_revisions',
24
+        sa.Column(
25
+            'file_name',
26
+            sa.VARCHAR(length=255),
27
+            server_default=sa.text("''::character varying"),
28
+            autoincrement=False,
29
+            nullable=False
30
+        ),
31
+    )

+ 127 - 37
tracim/tracim/lib/content.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import os
3
+
2
 from operator import itemgetter
4
 from operator import itemgetter
3
 
5
 
4
 __author__ = 'damien'
6
 __author__ = 'damien'
402
 
404
 
403
         return revision
405
         return revision
404
 
406
 
405
-    def get_one_by_label_and_parent(self, content_label: str, content_parent: Content = None,
406
-                                    workspace: Workspace = None) -> Content:
407
+    def get_one_by_label_and_parent(
408
+            self,
409
+            content_label: str,
410
+            content_parent: Content=None,
411
+    ) -> Content:
407
         """
412
         """
408
         This method let us request the database to obtain a Content with its name and parent
413
         This method let us request the database to obtain a Content with its name and parent
409
         :param content_label: Either the content's label or the content's filename if the label is None
414
         :param content_label: Either the content's label or the content's filename if the label is None
411
         :param workspace: The workspace's content
416
         :param workspace: The workspace's content
412
         :return The corresponding Content
417
         :return The corresponding Content
413
         """
418
         """
414
-        assert content_label is not None# DYN_REMOVE
419
+        query = self._base_query(content_parent.workspace)
420
+        parent_id = content_parent.content_id if content_parent else None
421
+        query = query.filter(Content.parent_id == parent_id)
415
 
422
 
416
-        resultset = self._base_query(workspace)
423
+        file_name, file_extension = os.path.splitext(content_label)
417
 
424
 
418
-        parent_id = content_parent.content_id if content_parent else None
425
+        return query.filter(
426
+            or_(
427
+                and_(
428
+                    Content.type == ContentType.File,
429
+                    Content.label == file_name,
430
+                    Content.file_extension == file_extension,
431
+                ),
432
+                and_(
433
+                    Content.type == ContentType.Thread,
434
+                    Content.label == file_name,
435
+                ),
436
+                and_(
437
+                    Content.type == ContentType.Page,
438
+                    Content.label == file_name,
439
+                ),
440
+                and_(
441
+                    Content.type == ContentType.Folder,
442
+                    Content.label == content_label,
443
+                ),
444
+            )
445
+        ).one()
419
 
446
 
420
-        resultset = resultset.filter(Content.parent_id == parent_id)
447
+    def get_one_by_label_and_parent_labels(
448
+            self,
449
+            content_label: str,
450
+            workspace: Workspace,
451
+            content_parent_labels: [str]=None,
452
+    ):
453
+        """
454
+        TODO: Assurer l'unicité des path dans Tracim
455
+        TODO: Ecrire tous les cas de tests par rapports aux
456
+         cas d'erreurs possible (duplications de labels, etc) et TROUVES
457
+        Return content with it's label, workspace and parents labels (optional)
458
+        :param content_label: label of content (label or file_name)
459
+        :param workspace: workspace containing all of this
460
+        :param content_parent_labels: Ordered list of labels representing path
461
+            of folder (without workspace label).
462
+        E.g.: ['foo', 'bar'] for complete path /Workspace1/foo/bar folder
463
+        :return: Found Content
464
+        """
465
+        query = self._base_query(workspace)
466
+        file_name, file_extension = os.path.splitext(content_label)
467
+        parent_folder = None
421
 
468
 
422
-        return resultset.filter(or_(
423
-            Content.label == content_label,
424
-            Content.file_name == content_label,
425
-            Content.label == re.sub(r'\.[^.]+$', '', content_label)
426
-        )).one()
469
+        # Grab content parent folder if parent path given
470
+        if content_parent_labels:
471
+            parent_folder = self.get_folder_with_workspace_path_labels(
472
+                content_parent_labels,
473
+                workspace,
474
+            )
427
 
475
 
428
-    def get_one_by_label_and_parent_label(self, content_label: str, content_parent_label: [str]=None, workspace: Workspace=None):
429
-        assert content_label is not None  # DYN_REMOVE
430
-        resultset = self._base_query(workspace)
476
+        # Build query for found content by label
477
+        content_query = query.filter(or_(
478
+            and_(
479
+                Content.type == ContentType.File,
480
+                Content.label == file_name,
481
+                Content.file_extension == file_extension,
482
+            ),
483
+            and_(
484
+                Content.type == ContentType.Thread,
485
+                Content.label == file_name,
486
+            ),
487
+            and_(
488
+                Content.type == ContentType.Page,
489
+                Content.label == file_name,
490
+            ),
491
+            and_(
492
+                Content.type == ContentType.Folder,
493
+                Content.label == content_label,
494
+            ),
495
+        ))
431
 
496
 
432
-        res =  resultset.filter(or_(
433
-            Content.label == content_label,
434
-            Content.file_name == content_label,
435
-            Content.label == re.sub(r'\.[^.]+$', '', content_label)
436
-        )).all()
497
+        # Modify query to apply parent folder filter if any
498
+        if parent_folder:
499
+            content_query = content_query.filter(
500
+                Content.parent_id == parent_folder.content_id,
501
+                Content.workspace_id == workspace.workspace_id,
502
+            )
437
 
503
 
438
-        if content_parent_label:
439
-            tmp = dict()
440
-            for content in res:
441
-                tmp[content] = content.parent
504
+        # Return the content
505
+        return content_query\
506
+            .order_by(
507
+                Content.revision_id.desc(),
508
+            )\
509
+            .one()
442
 
510
 
443
-            for parent_label in reversed(content_parent_label):
444
-                a = []
445
-                tmp = {content: parent.parent for content, parent in tmp.items()
446
-                       if parent and parent.label == parent_label}
511
+    def get_folder_with_workspace_path_labels(
512
+            self,
513
+            path_labels: [str],
514
+            workspace: Workspace,
515
+    ) -> Content:
516
+        """
517
+        Return a Content folder for given relative path.
518
+        TODO BS 20161124: Not safe if web interface allow folder duplicate names
519
+        :param path_labels: List of labels representing path of folder
520
+        (without workspace label).
521
+        E.g.: ['foo', 'bar'] for complete path /Workspace1/foo/bar folder
522
+        :param workspace: workspace of folders
523
+        :return: Content folder
524
+        """
525
+        query = self._base_query(workspace)
526
+        folder = None
527
+
528
+        for label in path_labels:
529
+            # Filter query on label
530
+            folder_query = query \
531
+                .filter(
532
+                    Content.type == ContentType.Folder,
533
+                    Content.label == label,
534
+                    Content.workspace_id == workspace.workspace_id,
535
+                )
447
 
536
 
448
-                if len(tmp) == 1:
449
-                    content, last_parent = tmp.popitem()
450
-                    return content
451
-                elif len(tmp) == 0:
452
-                    return None
537
+            # Search into parent folder (if already deep)
538
+            if folder:
539
+                folder_query\
540
+                    .filter(
541
+                        Content.parent_id == folder.content_id,
542
+                    )
453
 
543
 
454
-            for content, parent_content in tmp.items():
455
-                if not parent_content:
456
-                    return content
544
+            # Get thirst corresponding folder
545
+            folder = folder_query \
546
+                .order_by(Content.revision_id.desc()) \
547
+                .one()
457
 
548
 
458
-            return None
459
-        return res[0]
549
+        return folder
460
 
550
 
461
     def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
551
     def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
462
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
552
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE

+ 4 - 3
tracim/tracim/lib/daemons.py View File

303
 
303
 
304
         config['provider_mapping'] = {
304
         config['provider_mapping'] = {
305
             config['root_path']: Provider(
305
             config['root_path']: Provider(
306
-                show_archived=config['show_archived'],
307
-                show_deleted=config['show_deleted'],
308
-                show_history=config['show_history'],
306
+                # TODO: Test to Re enabme archived and deleted
307
+                show_archived=False,  # config['show_archived'],
308
+                show_deleted=False,  # config['show_deleted'],
309
+                show_history=False,  # config['show_history'],
309
                 manage_locks=config['manager_locks']
310
                 manage_locks=config['manager_locks']
310
             )
311
             )
311
         }
312
         }

+ 2 - 5
tracim/tracim/lib/notifications.py View File

228
             subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
228
             subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
229
             subject = subject.replace(EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__())
229
             subject = subject.replace(EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__())
230
             subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__())
230
             subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__())
231
-            subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__() if main_content.label else main_content.file_name.__str__())
231
+            subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__())
232
             subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())
232
             subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())
233
 
233
 
234
             message = MIMEMultipart('alternative')
234
             message = MIMEMultipart('alternative')
306
                 content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
306
                 content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
307
                 if content.description:
307
                 if content.description:
308
                     content_text = content.description
308
                     content_text = content.description
309
-                elif content.label:
310
-                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
311
                 else:
309
                 else:
312
-                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.file_name)
313
-
310
+                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
314
 
311
 
315
             elif ContentType.Page == content.type:
312
             elif ContentType.Page == content.type:
316
                 content_intro = _('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)
313
                 content_intro = _('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)

+ 13 - 10
tracim/tracim/lib/webdav/sql_dav_provider.py View File

2
 
2
 
3
 import re
3
 import re
4
 from os.path import basename, dirname, normpath
4
 from os.path import basename, dirname, normpath
5
+from sqlalchemy.orm.exc import NoResultFound
5
 from tracim.lib.webdav.utils import transform_to_bdd
6
 from tracim.lib.webdav.utils import transform_to_bdd
6
 
7
 
7
 from wsgidav.dav_provider import DAVProvider
8
 from wsgidav.dav_provider import DAVProvider
75
 
76
 
76
         content_api = ContentApi(
77
         content_api = ContentApi(
77
             user,
78
             user,
78
-            show_archived=self._show_archive,
79
-            show_deleted=self._show_delete
79
+            show_archived=False,  # self._show_archive,
80
+            show_deleted=False,  # self._show_delete
80
         )
81
         )
81
 
82
 
82
         content = self.get_content_from_path(
83
         content = self.get_content_from_path(
160
         if parent_path == root_path or workspace is None:
161
         if parent_path == root_path or workspace is None:
161
             return workspace is not None
162
             return workspace is not None
162
 
163
 
163
-        content_api = ContentApi(user, show_archived=True, show_deleted=True)
164
+        # TODO bastien: Arnaud avait mis a True, verif le comportement
165
+        # lorsque l'on explore les dossiers archive et deleted
166
+        content_api = ContentApi(user, show_archived=False, show_deleted=False)
164
 
167
 
165
         revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
168
         revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
166
 
169
 
246
         parents = [transform_to_bdd(x) for x in parents]
249
         parents = [transform_to_bdd(x) for x in parents]
247
 
250
 
248
         try:
251
         try:
249
-            return content_api.get_one_by_label_and_parent_label(
250
-                transform_to_bdd(basename(path)),
251
-                parents,
252
-                workspace
252
+            return content_api.get_one_by_label_and_parent_labels(
253
+                content_label=transform_to_bdd(basename(path)),
254
+                content_parent_labels=parents,
255
+                workspace=workspace,
253
             )
256
             )
254
-        except:
257
+        except NoResultFound:
255
             return None
258
             return None
256
 
259
 
257
     def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
260
     def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
258
         try:
261
         try:
259
             return api.get_one(revision.content_id, ContentType.Any)
262
             return api.get_one(revision.content_id, ContentType.Any)
260
-        except:
263
+        except NoResultFound:
261
             return None
264
             return None
262
 
265
 
263
     def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
266
     def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
266
     def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
269
     def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
267
         try:
270
         try:
268
             return api.get_one_by_label(transform_to_bdd(path.split('/')[1]))
271
             return api.get_one_by_label(transform_to_bdd(path.split('/')[1]))
269
-        except:
272
+        except NoResultFound:
270
             return None
273
             return None

+ 21 - 30
tracim/tracim/lib/webdav/sql_resources.py View File

54
         try:
54
         try:
55
             # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
55
             # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
56
             # don't get an error and the database request send back a result, we stop the action
56
             # don't get an error and the database request send back a result, we stop the action
57
-            self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent, self.content.workspace)
57
+            self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent)
58
             raise DAVError(HTTP_FORBIDDEN)
58
             raise DAVError(HTTP_FORBIDDEN)
59
         except NoResultFound:
59
         except NoResultFound:
60
             with new_revision(self.content):
60
             with new_revision(self.content):
69
         Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
69
         Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
70
         removing this string when undeleting/unarchiving
70
         removing this string when undeleting/unarchiving
71
         """
71
         """
72
-        new_name = self.content.get_label()
72
+        new_name = self.content.get_label_as_file()
73
         extension = ''
73
         extension = ''
74
 
74
 
75
         # if the content has no label, the last .ext is important
75
         # if the content has no label, the last .ext is important
219
             # the purpose is to display .history only if there's at least one content's type that has a history
219
             # the purpose is to display .history only if there's at least one content's type that has a history
220
             if content.type != ContentType.Folder:
220
             if content.type != ContentType.Folder:
221
                 self._file_count += 1
221
                 self._file_count += 1
222
-            retlist.append(content.get_label())
222
+            retlist.append(content.get_label_as_file())
223
 
223
 
224
         return retlist
224
         return retlist
225
 
225
 
310
         children = self.content_api.get_all(False, ContentType.Any, self.workspace)
310
         children = self.content_api.get_all(False, ContentType.Any, self.workspace)
311
 
311
 
312
         for content in children:
312
         for content in children:
313
-            content_path = '%s/%s' % (self.path, transform_to_display(content.get_label()))
313
+            content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
314
 
314
 
315
             if content.type == ContentType.Folder:
315
             if content.type == ContentType.Folder:
316
                 members.append(Folder(content_path, self.environ, self.workspace, content))
316
                 members.append(Folder(content_path, self.environ, self.workspace, content))
374
         return mktime(self.content.created.timetuple())
374
         return mktime(self.content.created.timetuple())
375
 
375
 
376
     def getDisplayName(self) -> str:
376
     def getDisplayName(self) -> str:
377
-        return transform_to_display(self.content.get_label())
377
+        return transform_to_display(self.content.get_label_as_file())
378
 
378
 
379
     def getLastModified(self) -> float:
379
     def getLastModified(self) -> float:
380
         return mktime(self.content.updated.timetuple())
380
         return mktime(self.content.updated.timetuple())
472
         )
472
         )
473
 
473
 
474
         for content in visible_children:
474
         for content in visible_children:
475
-            content_path = '%s/%s' % (self.path, transform_to_display(content.get_label()))
475
+            content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
476
 
476
 
477
             if content.type == ContentType.Folder:
477
             if content.type == ContentType.Folder:
478
                 members.append(Folder(content_path, self.environ, self.workspace, content))
478
                 members.append(Folder(content_path, self.environ, self.workspace, content))
555
         )
555
         )
556
 
556
 
557
         return HistoryFileFolder(
557
         return HistoryFileFolder(
558
-            path='%s/%s' % (self.path, content.get_label()),
558
+            path='%s/%s' % (self.path, content.get_label_as_file()),
559
             environ=self.environ,
559
             environ=self.environ,
560
             content=content)
560
             content=content)
561
 
561
 
568
                 self._is_deleted and content.is_deleted or
568
                 self._is_deleted and content.is_deleted or
569
                 not (content.is_archived or self._is_archived or content.is_deleted or self._is_deleted))\
569
                 not (content.is_archived or self._is_archived or content.is_deleted or self._is_deleted))\
570
                     and content.type != ContentType.Folder:
570
                     and content.type != ContentType.Folder:
571
-                ret.append(content.get_label())
571
+                ret.append(content.get_label_as_file())
572
 
572
 
573
         return ret
573
         return ret
574
 
574
 
601
         for content in children:
601
         for content in children:
602
             if content.is_archived == self._is_archived and content.is_deleted == self._is_deleted:
602
             if content.is_archived == self._is_archived and content.is_deleted == self._is_deleted:
603
                 members.append(HistoryFileFolder(
603
                 members.append(HistoryFileFolder(
604
-                    path='%s/%s' % (self.path, content.get_label()),
604
+                    path='%s/%s' % (self.path, content.get_label_as_file()),
605
                     environ=self.environ,
605
                     environ=self.environ,
606
                     content=content))
606
                     content=content))
607
 
607
 
638
         )
638
         )
639
 
639
 
640
         return self.provider.getResourceInst(
640
         return self.provider.getResourceInst(
641
-            path='%s/%s' % (self.path, transform_to_display(content.get_label())),
641
+            path='%s/%s' % (self.path, transform_to_display(content.get_label_as_file())),
642
             environ=self.environ
642
             environ=self.environ
643
             )
643
             )
644
 
644
 
652
 
652
 
653
         for content in children:
653
         for content in children:
654
             if content.is_deleted:
654
             if content.is_deleted:
655
-                retlist.append(content.get_label())
655
+                retlist.append(content.get_label_as_file())
656
 
656
 
657
                 if content.type != ContentType.Folder:
657
                 if content.type != ContentType.Folder:
658
                     self._file_count += 1
658
                     self._file_count += 1
669
 
669
 
670
         for content in children:
670
         for content in children:
671
             if content.is_deleted:
671
             if content.is_deleted:
672
-                content_path = '%s/%s' % (self.path, transform_to_display(content.get_label()))
672
+                content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
673
 
673
 
674
                 if content.type == ContentType.Folder:
674
                 if content.type == ContentType.Folder:
675
                     members.append(Folder(content_path, self.environ, self.workspace, content))
675
                     members.append(Folder(content_path, self.environ, self.workspace, content))
723
         )
723
         )
724
 
724
 
725
         return self.provider.getResourceInst(
725
         return self.provider.getResourceInst(
726
-            path=self.path + '/' + transform_to_display(content.get_label()),
726
+            path=self.path + '/' + transform_to_display(content.get_label_as_file()),
727
             environ=self.environ
727
             environ=self.environ
728
         )
728
         )
729
 
729
 
732
 
732
 
733
         for content in self.content_api.get_all_with_filter(
733
         for content in self.content_api.get_all_with_filter(
734
                 self.content if self.content is None else self.content.id, ContentType.Any):
734
                 self.content if self.content is None else self.content.id, ContentType.Any):
735
-            retlist.append(content.get_label())
735
+            retlist.append(content.get_label_as_file())
736
 
736
 
737
             if content.type != ContentType.Folder:
737
             if content.type != ContentType.Folder:
738
                 self._file_count += 1
738
                 self._file_count += 1
749
 
749
 
750
         for content in children:
750
         for content in children:
751
             if content.is_archived:
751
             if content.is_archived:
752
-                content_path = '%s/%s' % (self.path, transform_to_display(content.get_label()))
752
+                content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
753
 
753
 
754
                 if content.type == ContentType.Folder:
754
                 if content.type == ContentType.Folder:
755
                     members.append(Folder(content_path, self.environ, self.workspace, content))
755
                     members.append(Folder(content_path, self.environ, self.workspace, content))
786
         return "<DAVCollection: HistoryFileFolder (%s)" % self.content.file_name
786
         return "<DAVCollection: HistoryFileFolder (%s)" % self.content.file_name
787
 
787
 
788
     def getDisplayName(self) -> str:
788
     def getDisplayName(self) -> str:
789
-        return self.content.get_label()
789
+        return self.content.get_label_as_file()
790
 
790
 
791
     def createCollection(self, name):
791
     def createCollection(self, name):
792
         raise DAVError(HTTP_FORBIDDEN)
792
         raise DAVError(HTTP_FORBIDDEN)
817
                 content_revision=revision)
817
                 content_revision=revision)
818
         else:
818
         else:
819
             return HistoryOtherFile(
819
             return HistoryOtherFile(
820
-                path='%s%s' % (left_side, transform_to_display(revision.get_label())),
820
+                path='%s%s' % (left_side, transform_to_display(revision.get_label_as_file())),
821
                 environ=self.environ,
821
                 environ=self.environ,
822
                 content=self.content,
822
                 content=self.content,
823
                 content_revision=revision)
823
                 content_revision=revision)
862
         # but i wasn't able to set this property so you'll have to look into it >.>
862
         # but i wasn't able to set this property so you'll have to look into it >.>
863
         # self.setPropertyValue('Win32FileAttributes', '00000021')
863
         # self.setPropertyValue('Win32FileAttributes', '00000021')
864
 
864
 
865
-    def getPreferredPath(self):
866
-        fix_txt = '.txt' if self.getContentType() == 'text/plain' else mimetypes.guess_extension(self.getContentType())
867
-        if not fix_txt:
868
-            fix_txt = ''
869
-        if self.content and self.path and (self.content.label == '' or self.path.endswith(fix_txt)):
870
-            return self.path
871
-        else:
872
-            return self.path + fix_txt
873
-
874
     def __repr__(self) -> str:
865
     def __repr__(self) -> str:
875
         return "<DAVNonCollection: File (%d)>" % self.content.revision_id
866
         return "<DAVNonCollection: File (%d)>" % self.content.revision_id
876
 
867
 
884
         return mktime(self.content.created.timetuple())
875
         return mktime(self.content.created.timetuple())
885
 
876
 
886
     def getDisplayName(self) -> str:
877
     def getDisplayName(self) -> str:
887
-        return self.content.get_label()
878
+        return self.content.file_name
888
 
879
 
889
     def getLastModified(self) -> float:
880
     def getLastModified(self) -> float:
890
         return mktime(self.content.updated.timetuple())
881
         return mktime(self.content.updated.timetuple())
900
         return FakeFileStream(
891
         return FakeFileStream(
901
             content=self.content,
892
             content=self.content,
902
             content_api=self.content_api,
893
             content_api=self.content_api,
903
-            file_name=self.content.get_label(),
894
+            file_name=self.content.get_label_as_file(),
904
             workspace=self.content.workspace,
895
             workspace=self.content.workspace,
905
             path=self.path
896
             path=self.path
906
         )
897
         )
1048
             self.path += '.html'
1039
             self.path += '.html'
1049
 
1040
 
1050
     def getDisplayName(self) -> str:
1041
     def getDisplayName(self) -> str:
1051
-        return self.content.get_label()
1042
+        return self.content.get_label_as_file()
1052
 
1043
 
1053
     def getPreferredPath(self):
1044
     def getPreferredPath(self):
1054
         return self.path
1045
         return self.path
1094
 
1085
 
1095
     def getDisplayName(self) -> str:
1086
     def getDisplayName(self) -> str:
1096
         left_side = '(%d - %s) ' % (self.content_revision.revision_id, self.content_revision.revision_type)
1087
         left_side = '(%d - %s) ' % (self.content_revision.revision_id, self.content_revision.revision_type)
1097
-        return '%s%s' % (left_side, transform_to_display(self.content_revision.get_label()))
1088
+        return '%s%s' % (left_side, transform_to_display(self.content_revision.get_label_as_file()))
1098
 
1089
 
1099
     def getContent(self):
1090
     def getContent(self):
1100
         filestream = compat.BytesIO()
1091
         filestream = compat.BytesIO()

+ 2 - 1
tracim/tracim/model/__init__.py View File

31
 
31
 
32
     @classmethod
32
     @classmethod
33
     def remove_from_updatable(cls, revision: 'ContentRevisionRO') -> None:
33
     def remove_from_updatable(cls, revision: 'ContentRevisionRO') -> None:
34
-        cls._updatable_revisions.remove(revision)
34
+        if revision in cls._updatable_revisions:
35
+            cls._updatable_revisions.remove(revision)
35
 
36
 
36
     @classmethod
37
     @classmethod
37
     def is_updatable(cls, revision: 'ContentRevisionRO') -> bool:
38
     def is_updatable(cls, revision: 'ContentRevisionRO') -> bool:

+ 78 - 9
tracim/tracim/model/data.py View File

2
 
2
 
3
 import datetime as datetime_root
3
 import datetime as datetime_root
4
 import json
4
 import json
5
+import os
5
 from datetime import datetime
6
 from datetime import datetime
6
 
7
 
7
 import tg
8
 import tg
522
 
523
 
523
     label = Column(Unicode(1024), unique=False, nullable=False)
524
     label = Column(Unicode(1024), unique=False, nullable=False)
524
     description = Column(Text(), unique=False, nullable=False, default='')
525
     description = Column(Text(), unique=False, nullable=False, default='')
525
-    file_name = Column(Unicode(255),  unique=False, nullable=False, default='')
526
+    file_extension = Column(
527
+        Unicode(255),
528
+        unique=False,
529
+        nullable=False,
530
+        server_default='',
531
+    )
526
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
532
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
527
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=True))
533
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=True))
528
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
534
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
547
 
553
 
548
     """ List of column copied when make a new revision from another """
554
     """ List of column copied when make a new revision from another """
549
     _cloned_columns = (
555
     _cloned_columns = (
550
-        'content_id', 'created', 'description', 'file_content', 'file_mimetype', 'file_name', 'is_archived',
551
-        'is_deleted', 'label', 'node', 'owner', 'owner_id', 'parent', 'parent_id', 'properties', 'revision_type',
552
-        'status', 'type', 'updated', 'workspace', 'workspace_id', 'is_temporary',
556
+        'content_id',
557
+        'created',
558
+        'description',
559
+        'file_content',
560
+        'file_mimetype',
561
+        'file_extension',
562
+        'is_archived',
563
+        'is_deleted',
564
+        'label',
565
+        'node',
566
+        'owner',
567
+        'owner_id',
568
+        'parent',
569
+        'parent_id',
570
+        'properties',
571
+        'revision_type',
572
+        'status',
573
+        'type',
574
+        'updated',
575
+        'workspace',
576
+        'workspace_id',
577
+        'is_temporary',
553
     )
578
     )
554
 
579
 
555
     # Read by must be used like this:
580
     # Read by must be used like this:
562
             RevisionReadStatus(user=k, view_datetime=v)
587
             RevisionReadStatus(user=k, view_datetime=v)
563
     )
588
     )
564
 
589
 
590
+    @property
591
+    def file_name(self):
592
+        return '{0}{1}'.format(
593
+            self.label,
594
+            self.file_extension,
595
+        )
596
+
565
     @classmethod
597
     @classmethod
566
     def new_from(cls, revision: 'ContentRevisionRO') -> 'ContentRevisionRO':
598
     def new_from(cls, revision: 'ContentRevisionRO') -> 'ContentRevisionRO':
567
         """
599
         """
607
         return ContentStatus(self.status)
639
         return ContentStatus(self.status)
608
 
640
 
609
     def get_label(self) -> str:
641
     def get_label(self) -> str:
610
-        return self.label if self.label else self.file_name if self.file_name else ''
642
+        return self.label or self.file_name or ''
611
 
643
 
612
     def get_last_action(self) -> ActionDescription:
644
     def get_last_action(self) -> ActionDescription:
613
         return ActionDescription(self.revision_type)
645
         return ActionDescription(self.revision_type)
626
 
658
 
627
         return False
659
         return False
628
 
660
 
661
+    def get_label_as_file(self):
662
+        file_extension = self.file_extension
663
+
664
+        if self.type == ContentType.Thread:
665
+            file_extension = '.html'
666
+        elif self.type == ContentType.Page:
667
+            file_extension = '.html'
668
+
669
+        return '{0}{1}'.format(
670
+            self.label,
671
+            file_extension,
672
+        )
673
+
674
+
629
 Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
675
 Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
630
 Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
676
 Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
631
 
677
 
734
 
780
 
735
     @hybrid_property
781
     @hybrid_property
736
     def file_name(self) -> str:
782
     def file_name(self) -> str:
737
-        return self.revision.file_name
783
+        return '{0}{1}'.format(
784
+            self.revision.label,
785
+            self.revision.file_extension,
786
+        )
738
 
787
 
739
     @file_name.setter
788
     @file_name.setter
740
     def file_name(self, value: str) -> None:
789
     def file_name(self, value: str) -> None:
741
-        self.revision.file_name = value
790
+        file_name, file_extension = os.path.splitext(value)
791
+        self.revision.label = file_name
792
+        self.revision.file_extension = file_extension
742
 
793
 
743
     @file_name.expression
794
     @file_name.expression
744
     def file_name(cls) -> InstrumentedAttribute:
795
     def file_name(cls) -> InstrumentedAttribute:
745
-        return ContentRevisionRO.file_name
796
+        return ContentRevisionRO.file_name + ContentRevisionRO.file_extension
797
+
798
+    @hybrid_property
799
+    def file_extension(self) -> str:
800
+        return self.revision.file_extension
801
+
802
+    @file_extension.setter
803
+    def file_extension(self, value: str) -> None:
804
+        self.revision.file_extension = value
805
+
806
+    @file_extension.expression
807
+    def file_extension(cls) -> InstrumentedAttribute:
808
+        return ContentRevisionRO.file_extension
746
 
809
 
747
     @hybrid_property
810
     @hybrid_property
748
     def file_mimetype(self) -> str:
811
     def file_mimetype(self) -> str:
1042
         return child_nb
1105
         return child_nb
1043
 
1106
 
1044
     def get_label(self):
1107
     def get_label(self):
1045
-        return self.label if self.label else self.file_name if self.file_name else ''
1108
+        return self.label or self.file_name or ''
1109
+
1110
+    def get_label_as_file(self) -> str:
1111
+        """
1112
+        :return: Return content label in file representation context
1113
+        """
1114
+        return self.revision.get_label_as_file()
1046
 
1115
 
1047
     def get_status(self) -> ContentStatus:
1116
     def get_status(self) -> ContentStatus:
1048
         return ContentStatus(self.status, self.type.__str__())
1117
         return ContentStatus(self.status, self.type.__str__())

+ 5 - 5
tracim/tracim/model/serializers.py View File

262
 def serialize_version_for_page_or_file(version: ContentRevisionRO, context: Context):
262
 def serialize_version_for_page_or_file(version: ContentRevisionRO, context: Context):
263
     return DictLikeClass(
263
     return DictLikeClass(
264
         id = version.revision_id,
264
         id = version.revision_id,
265
-        label = version.label if version.label else version.file_name,
265
+        label = version.label,
266
         owner = context.toDict(version.owner),
266
         owner = context.toDict(version.owner),
267
         created = version.created,
267
         created = version.created,
268
         action = context.toDict(version.get_last_action())
268
         action = context.toDict(version.get_last_action())
285
 
285
 
286
     result = DictLikeClass(
286
     result = DictLikeClass(
287
         id = content.content_id,
287
         id = content.content_id,
288
-        label = content.label if content.label else content.file_name,
288
+        label = content.label,
289
         icon = ContentType.get_icon(content.type),
289
         icon = ContentType.get_icon(content.type),
290
         status = context.toDict(content.get_status()),
290
         status = context.toDict(content.get_status()),
291
         folder = context.toDict(DictLikeClass(id = content.parent.content_id if content.parent else None)),
291
         folder = context.toDict(DictLikeClass(id = content.parent.content_id if content.parent else None)),
341
     if content.type==ContentType.File:
341
     if content.type==ContentType.File:
342
         result = DictLikeClass(
342
         result = DictLikeClass(
343
             id = content.content_id,
343
             id = content.content_id,
344
-            label = content.label if content.label else content.file_name,
344
+            label = content.label,
345
             status = context.toDict(content.get_status()),
345
             status = context.toDict(content.get_status()),
346
             folder = Context(CTX.DEFAULT).toDict(content.parent)
346
             folder = Context(CTX.DEFAULT).toDict(content.parent)
347
         )
347
         )
396
         )
396
         )
397
 
397
 
398
         if content.type==ContentType.File:
398
         if content.type==ContentType.File:
399
-            result.label = content.label if content.label else content.file_name
399
+            result.label = content.label
400
             result['file'] = DictLikeClass(
400
             result['file'] = DictLikeClass(
401
                 name = data_container.file_name,
401
                 name = data_container.file_name,
402
                 size = len(data_container.file_content),
402
                 size = len(data_container.file_content),
763
         )
763
         )
764
 
764
 
765
         if content.type==ContentType.File:
765
         if content.type==ContentType.File:
766
-            result.label = content.label.__str__() if content.label else content.file_name.__str__()
766
+            result.label = content.label.__str__()
767
 
767
 
768
         if not result.label or ''==result.label:
768
         if not result.label or ''==result.label:
769
             result.label = 'No title'
769
             result.label = 'No title'