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

@@ -0,0 +1,31 @@
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,4 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2
+import os
3
+
2 4
 from operator import itemgetter
3 5
 
4 6
 __author__ = 'damien'
@@ -402,8 +404,11 @@ class ContentApi(object):
402 404
 
403 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 413
         This method let us request the database to obtain a Content with its name and parent
409 414
         :param content_label: Either the content's label or the content's filename if the label is None
@@ -411,52 +416,137 @@ class ContentApi(object):
411 416
         :param workspace: The workspace's content
412 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 551
     def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
462 552
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE

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

@@ -303,9 +303,10 @@ class WsgiDavDaemon(Daemon):
303 303
 
304 304
         config['provider_mapping'] = {
305 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 310
                 manage_locks=config['manager_locks']
310 311
             )
311 312
         }

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

@@ -228,7 +228,7 @@ class EmailNotifier(object):
228 228
             subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
229 229
             subject = subject.replace(EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__())
230 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 232
             subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())
233 233
 
234 234
             message = MIMEMultipart('alternative')
@@ -306,11 +306,8 @@ class EmailNotifier(object):
306 306
                 content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
307 307
                 if content.description:
308 308
                     content_text = content.description
309
-                elif content.label:
310
-                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
311 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 312
             elif ContentType.Page == content.type:
316 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,6 +2,7 @@
2 2
 
3 3
 import re
4 4
 from os.path import basename, dirname, normpath
5
+from sqlalchemy.orm.exc import NoResultFound
5 6
 from tracim.lib.webdav.utils import transform_to_bdd
6 7
 
7 8
 from wsgidav.dav_provider import DAVProvider
@@ -75,8 +76,8 @@ class Provider(DAVProvider):
75 76
 
76 77
         content_api = ContentApi(
77 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 83
         content = self.get_content_from_path(
@@ -160,7 +161,9 @@ class Provider(DAVProvider):
160 161
         if parent_path == root_path or workspace is None:
161 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 168
         revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
166 169
 
@@ -246,18 +249,18 @@ class Provider(DAVProvider):
246 249
         parents = [transform_to_bdd(x) for x in parents]
247 250
 
248 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 258
             return None
256 259
 
257 260
     def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
258 261
         try:
259 262
             return api.get_one(revision.content_id, ContentType.Any)
260
-        except:
263
+        except NoResultFound:
261 264
             return None
262 265
 
263 266
     def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
@@ -266,5 +269,5 @@ class Provider(DAVProvider):
266 269
     def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
267 270
         try:
268 271
             return api.get_one_by_label(transform_to_bdd(path.split('/')[1]))
269
-        except:
272
+        except NoResultFound:
270 273
             return None

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

@@ -54,7 +54,7 @@ class ManageActions(object):
54 54
         try:
55 55
             # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
56 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 58
             raise DAVError(HTTP_FORBIDDEN)
59 59
         except NoResultFound:
60 60
             with new_revision(self.content):
@@ -69,7 +69,7 @@ class ManageActions(object):
69 69
         Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
70 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 73
         extension = ''
74 74
 
75 75
         # if the content has no label, the last .ext is important
@@ -219,7 +219,7 @@ class Workspace(DAVCollection):
219 219
             # the purpose is to display .history only if there's at least one content's type that has a history
220 220
             if content.type != ContentType.Folder:
221 221
                 self._file_count += 1
222
-            retlist.append(content.get_label())
222
+            retlist.append(content.get_label_as_file())
223 223
 
224 224
         return retlist
225 225
 
@@ -310,7 +310,7 @@ class Workspace(DAVCollection):
310 310
         children = self.content_api.get_all(False, ContentType.Any, self.workspace)
311 311
 
312 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 315
             if content.type == ContentType.Folder:
316 316
                 members.append(Folder(content_path, self.environ, self.workspace, content))
@@ -374,7 +374,7 @@ class Folder(Workspace):
374 374
         return mktime(self.content.created.timetuple())
375 375
 
376 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 379
     def getLastModified(self) -> float:
380 380
         return mktime(self.content.updated.timetuple())
@@ -472,7 +472,7 @@ class Folder(Workspace):
472 472
         )
473 473
 
474 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 477
             if content.type == ContentType.Folder:
478 478
                 members.append(Folder(content_path, self.environ, self.workspace, content))
@@ -555,7 +555,7 @@ class HistoryFolder(Folder):
555 555
         )
556 556
 
557 557
         return HistoryFileFolder(
558
-            path='%s/%s' % (self.path, content.get_label()),
558
+            path='%s/%s' % (self.path, content.get_label_as_file()),
559 559
             environ=self.environ,
560 560
             content=content)
561 561
 
@@ -568,7 +568,7 @@ class HistoryFolder(Folder):
568 568
                 self._is_deleted and content.is_deleted or
569 569
                 not (content.is_archived or self._is_archived or content.is_deleted or self._is_deleted))\
570 570
                     and content.type != ContentType.Folder:
571
-                ret.append(content.get_label())
571
+                ret.append(content.get_label_as_file())
572 572
 
573 573
         return ret
574 574
 
@@ -601,7 +601,7 @@ class HistoryFolder(Folder):
601 601
         for content in children:
602 602
             if content.is_archived == self._is_archived and content.is_deleted == self._is_deleted:
603 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 605
                     environ=self.environ,
606 606
                     content=content))
607 607
 
@@ -638,7 +638,7 @@ class DeletedFolder(HistoryFolder):
638 638
         )
639 639
 
640 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 642
             environ=self.environ
643 643
             )
644 644
 
@@ -652,7 +652,7 @@ class DeletedFolder(HistoryFolder):
652 652
 
653 653
         for content in children:
654 654
             if content.is_deleted:
655
-                retlist.append(content.get_label())
655
+                retlist.append(content.get_label_as_file())
656 656
 
657 657
                 if content.type != ContentType.Folder:
658 658
                     self._file_count += 1
@@ -669,7 +669,7 @@ class DeletedFolder(HistoryFolder):
669 669
 
670 670
         for content in children:
671 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 674
                 if content.type == ContentType.Folder:
675 675
                     members.append(Folder(content_path, self.environ, self.workspace, content))
@@ -723,7 +723,7 @@ class ArchivedFolder(HistoryFolder):
723 723
         )
724 724
 
725 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 727
             environ=self.environ
728 728
         )
729 729
 
@@ -732,7 +732,7 @@ class ArchivedFolder(HistoryFolder):
732 732
 
733 733
         for content in self.content_api.get_all_with_filter(
734 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 737
             if content.type != ContentType.Folder:
738 738
                 self._file_count += 1
@@ -749,7 +749,7 @@ class ArchivedFolder(HistoryFolder):
749 749
 
750 750
         for content in children:
751 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 754
                 if content.type == ContentType.Folder:
755 755
                     members.append(Folder(content_path, self.environ, self.workspace, content))
@@ -786,7 +786,7 @@ class HistoryFileFolder(HistoryFolder):
786 786
         return "<DAVCollection: HistoryFileFolder (%s)" % self.content.file_name
787 787
 
788 788
     def getDisplayName(self) -> str:
789
-        return self.content.get_label()
789
+        return self.content.get_label_as_file()
790 790
 
791 791
     def createCollection(self, name):
792 792
         raise DAVError(HTTP_FORBIDDEN)
@@ -817,7 +817,7 @@ class HistoryFileFolder(HistoryFolder):
817 817
                 content_revision=revision)
818 818
         else:
819 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 821
                 environ=self.environ,
822 822
                 content=self.content,
823 823
                 content_revision=revision)
@@ -862,15 +862,6 @@ class File(DAVNonCollection):
862 862
         # but i wasn't able to set this property so you'll have to look into it >.>
863 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 865
     def __repr__(self) -> str:
875 866
         return "<DAVNonCollection: File (%d)>" % self.content.revision_id
876 867
 
@@ -884,7 +875,7 @@ class File(DAVNonCollection):
884 875
         return mktime(self.content.created.timetuple())
885 876
 
886 877
     def getDisplayName(self) -> str:
887
-        return self.content.get_label()
878
+        return self.content.file_name
888 879
 
889 880
     def getLastModified(self) -> float:
890 881
         return mktime(self.content.updated.timetuple())
@@ -900,7 +891,7 @@ class File(DAVNonCollection):
900 891
         return FakeFileStream(
901 892
             content=self.content,
902 893
             content_api=self.content_api,
903
-            file_name=self.content.get_label(),
894
+            file_name=self.content.get_label_as_file(),
904 895
             workspace=self.content.workspace,
905 896
             path=self.path
906 897
         )
@@ -1048,7 +1039,7 @@ class OtherFile(File):
1048 1039
             self.path += '.html'
1049 1040
 
1050 1041
     def getDisplayName(self) -> str:
1051
-        return self.content.get_label()
1042
+        return self.content.get_label_as_file()
1052 1043
 
1053 1044
     def getPreferredPath(self):
1054 1045
         return self.path
@@ -1094,7 +1085,7 @@ class HistoryOtherFile(OtherFile):
1094 1085
 
1095 1086
     def getDisplayName(self) -> str:
1096 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 1090
     def getContent(self):
1100 1091
         filestream = compat.BytesIO()

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

@@ -31,7 +31,8 @@ class RevisionsIntegrity(object):
31 31
 
32 32
     @classmethod
33 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 37
     @classmethod
37 38
     def is_updatable(cls, revision: 'ContentRevisionRO') -> bool:

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

@@ -2,6 +2,7 @@
2 2
 
3 3
 import datetime as datetime_root
4 4
 import json
5
+import os
5 6
 from datetime import datetime
6 7
 
7 8
 import tg
@@ -522,7 +523,12 @@ class ContentRevisionRO(DeclarativeBase):
522 523
 
523 524
     label = Column(Unicode(1024), unique=False, nullable=False)
524 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 532
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
527 533
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=True))
528 534
     properties = Column('properties', Text(), unique=False, nullable=False, default='')
@@ -547,9 +553,28 @@ class ContentRevisionRO(DeclarativeBase):
547 553
 
548 554
     """ List of column copied when make a new revision from another """
549 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 580
     # Read by must be used like this:
@@ -562,6 +587,13 @@ class ContentRevisionRO(DeclarativeBase):
562 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 597
     @classmethod
566 598
     def new_from(cls, revision: 'ContentRevisionRO') -> 'ContentRevisionRO':
567 599
         """
@@ -607,7 +639,7 @@ class ContentRevisionRO(DeclarativeBase):
607 639
         return ContentStatus(self.status)
608 640
 
609 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 644
     def get_last_action(self) -> ActionDescription:
613 645
         return ActionDescription(self.revision_type)
@@ -626,6 +658,20 @@ class ContentRevisionRO(DeclarativeBase):
626 658
 
627 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 675
 Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
630 676
 Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
631 677
 
@@ -734,15 +780,32 @@ class Content(DeclarativeBase):
734 780
 
735 781
     @hybrid_property
736 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 788
     @file_name.setter
740 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 794
     @file_name.expression
744 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 810
     @hybrid_property
748 811
     def file_mimetype(self) -> str:
@@ -1042,7 +1105,13 @@ class Content(DeclarativeBase):
1042 1105
         return child_nb
1043 1106
 
1044 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 1116
     def get_status(self) -> ContentStatus:
1048 1117
         return ContentStatus(self.status, self.type.__str__())

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

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