Browse Source

Merge pull request #542 from inkhey/webdav_copy_fix

Damien Accorsi 6 years ago
parent
commit
47141cc4e6
No account linked to committer's email

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

861
 
861
 
862
         item.revision_type = ActionDescription.MOVE
862
         item.revision_type = ActionDescription.MOVE
863
 
863
 
864
+    def copy(
865
+        self,
866
+        item: Content,
867
+        new_parent: Content=None,
868
+        new_label: str=None,
869
+        do_save: bool=True,
870
+        do_notify: bool=True,
871
+    ) -> Content:
872
+        """
873
+        Copy nearly all content, revision included. Children not included, see
874
+        "copy_children" for this.
875
+        :param item: Item to copy
876
+        :param new_parent: new parent of the new copied item
877
+        :param new_label: new label of the new copied item
878
+        :param do_notify: notify copy or not
879
+        :return: Newly copied item
880
+        """
881
+        if (not new_parent and not new_label) or (new_parent == item.parent and new_label == item.label):  # nopep8
882
+            # TODO - G.M - 08-03-2018 - Use something else than value error
883
+            raise ValueError("You can't copy file into itself")
884
+        if new_parent:
885
+            workspace = new_parent.workspace
886
+            parent = new_parent
887
+        else:
888
+            workspace = item.workspace
889
+            parent = item.parent
890
+        label = new_label or item.label
891
+
892
+        content = item.copy(parent)
893
+        # INFO - GM - 15-03-2018 - add "copy" revision
894
+        with new_revision(content, force_create_new_revision=True) as rev:
895
+            rev.parent = parent
896
+            rev.workspace = workspace
897
+            rev.label = label
898
+            rev.revision_type = ActionDescription.COPY
899
+            rev.properties['origin'] = {
900
+                'content': item.id,
901
+                'revision': item.last_revision.revision_id,
902
+            }
903
+        if do_save:
904
+            self.save(content, ActionDescription.COPY, do_notify=do_notify)
905
+        return content
906
+
907
+    def copy_children(self, origin_content: Content, new_content: Content):
908
+        for child in origin_content.children:
909
+            self.copy(child, new_content)
910
+
864
     def move_recursively(self, item: Content,
911
     def move_recursively(self, item: Content,
865
                          new_parent: Content, new_workspace: Workspace):
912
                          new_parent: Content, new_workspace: Workspace):
866
         self.move(item, new_parent, False, new_workspace)
913
         self.move(item, new_parent, False, new_workspace)

+ 2 - 1
tracim/tracim/lib/webdav/design.py View File

120
     'unarchiving': 'Item unarchived',
120
     'unarchiving': 'Item unarchived',
121
     'undeletion': 'Item undeleted',
121
     'undeletion': 'Item undeleted',
122
     'move': 'Item moved',
122
     'move': 'Item moved',
123
-    'comment': 'Comment'
123
+    'comment': 'Comment',
124
+    'copy' : 'Item copied',
124
 }
125
 }
125
 
126
 
126
 
127
 

+ 42 - 0
tracim/tracim/lib/webdav/sql_resources.py View File

978
 
978
 
979
         transaction.commit()
979
         transaction.commit()
980
 
980
 
981
+    def copyMoveSingle(self, destpath, isMove):
982
+        if isMove:
983
+            # INFO - G.M - 12-03-2018 - This case should not happen
984
+            # As far as moveRecursive method exist, all move should not go
985
+            # through this method. If such case appear, try replace this to :
986
+            ####
987
+            # self.move_file(destpath)
988
+            # return
989
+            ####
990
+
991
+            raise NotImplemented
992
+
993
+        new_file_name = None
994
+        new_file_extension = None
995
+
996
+        # Inspect destpath
997
+        if basename(destpath) != self.getDisplayName():
998
+            new_given_file_name = transform_to_bdd(basename(destpath))
999
+            new_file_name, new_file_extension = \
1000
+                os.path.splitext(new_given_file_name)
1001
+
1002
+        workspace_api = WorkspaceApi(self.user)
1003
+        content_api = ContentApi(self.user)
1004
+        destination_workspace = self.provider.get_workspace_from_path(
1005
+            destpath,
1006
+            workspace_api,
1007
+        )
1008
+        destination_parent = self.provider.get_parent_from_path(
1009
+            destpath,
1010
+            content_api,
1011
+            destination_workspace,
1012
+        )
1013
+        workspace = self.content.workspace
1014
+        parent = self.content.parent
1015
+        new_content = self.content_api.copy(
1016
+            item=self.content,
1017
+            new_label=new_file_name,
1018
+            new_parent=destination_parent,
1019
+        )
1020
+        self.content_api.copy_children(self.content, new_content)
1021
+        transaction.commit()
1022
+
981
     def supportRecursiveMove(self, destPath):
1023
     def supportRecursiveMove(self, destPath):
982
         return True
1024
         return True
983
 
1025
 

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

120
 
120
 
121
 
121
 
122
 @contextmanager
122
 @contextmanager
123
-def new_revision(content: Content) -> Content:
123
+def new_revision(
124
+        content: Content,
125
+        force_create_new_revision: bool=False,
126
+) -> Content:
124
     """
127
     """
125
     Prepare context to update a Content. It will add a new updatable revision to the content.
128
     Prepare context to update a Content. It will add a new updatable revision to the content.
126
     :param content: Content instance to update
129
     :param content: Content instance to update
128
     """
131
     """
129
     with DBSession.no_autoflush:
132
     with DBSession.no_autoflush:
130
         try:
133
         try:
131
-            if inspect(content.revision).has_identity:
134
+            if force_create_new_revision \
135
+                    or inspect(content.revision).has_identity:
132
                 content.new_revision()
136
                 content.new_revision()
133
             RevisionsIntegrity.add_to_updatable(content.revision)
137
             RevisionsIntegrity.add_to_updatable(content.revision)
134
             yield content
138
             yield content

+ 51 - 5
tracim/tracim/model/data.py View File

204
     - closed-deprecated
204
     - closed-deprecated
205
     """
205
     """
206
 
206
 
207
+    COPY = 'copy'
207
     ARCHIVING = 'archiving'
208
     ARCHIVING = 'archiving'
208
     COMMENT = 'content-comment'
209
     COMMENT = 'content-comment'
209
     CREATION = 'creation'
210
     CREATION = 'creation'
225
         'status-update': 'fa-random',
226
         'status-update': 'fa-random',
226
         'unarchiving': 'fa-file-archive-o',
227
         'unarchiving': 'fa-file-archive-o',
227
         'undeletion': 'fa-trash-o',
228
         'undeletion': 'fa-trash-o',
228
-        'move': 'fa-arrows'
229
+        'move': 'fa-arrows',
230
+        'copy': 'fa-files-o',
229
     }
231
     }
230
 
232
 
231
     _LABELS = {
233
     _LABELS = {
238
         'status-update': l_('New status'),
240
         'status-update': l_('New status'),
239
         'unarchiving': l_('Item unarchived'),
241
         'unarchiving': l_('Item unarchived'),
240
         'undeletion': l_('Item undeleted'),
242
         'undeletion': l_('Item undeleted'),
241
-        'move': l_('Item moved')
243
+        'move': l_('Item moved'),
244
+        'copy': l_('Item copied'),
242
     }
245
     }
243
 
246
 
244
     def __init__(self, id):
247
     def __init__(self, id):
259
                 cls.STATUS_UPDATE,
262
                 cls.STATUS_UPDATE,
260
                 cls.UNARCHIVING,
263
                 cls.UNARCHIVING,
261
                 cls.UNDELETION,
264
                 cls.UNDELETION,
262
-                cls.MOVE]
265
+                cls.MOVE,
266
+                cls.COPY,
267
+                ]
263
 
268
 
264
 
269
 
265
 class ContentStatus(object):
270
 class ContentStatus(object):
514
                 return False
519
                 return False
515
             return True
520
             return True
516
 
521
 
522
+        # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
523
+        # Only content who can be copied need this
524
+        if item.type == ContentType.Any:
525
+            if 'origin' in properties.keys():
526
+                return True
517
         raise NotImplementedError
527
         raise NotImplementedError
518
 
528
 
519
     @classmethod
529
     @classmethod
580
         'is_archived',
590
         'is_archived',
581
         'is_deleted',
591
         'is_deleted',
582
         'label',
592
         'label',
583
-        'node',
584
         'owner',
593
         'owner',
585
         'owner_id',
594
         'owner_id',
586
         'parent',
595
         'parent',
640
 
649
 
641
         return new_rev
650
         return new_rev
642
 
651
 
652
+    @classmethod
653
+    def copy(
654
+            cls,
655
+            revision: 'ContentRevisionRO',
656
+            parent: 'Content'
657
+    ) -> 'ContentRevisionRO':
658
+
659
+        copy_rev = cls()
660
+        import copy
661
+        copy_columns = cls._cloned_columns
662
+        for column_name in copy_columns:
663
+            # INFO - G-M - 15-03-2018 - set correct parent
664
+            if column_name == 'parent_id':
665
+                column_value = copy.copy(parent.id)
666
+            elif column_name == 'parent':
667
+                column_value = copy.copy(parent)
668
+            else:
669
+                column_value = copy.copy(getattr(revision, column_name))
670
+            setattr(copy_rev, column_name, column_value)
671
+
672
+        # copy attached_file
673
+        if revision.depot_file:
674
+            copy_rev.depot_file = FileIntent(
675
+                revision.depot_file.file.read(),
676
+                revision.file_name,
677
+                revision.file_mimetype,
678
+            )
679
+        return copy_rev
680
+
681
+
643
     def __setattr__(self, key: str, value: 'mixed'):
682
     def __setattr__(self, key: str, value: 'mixed'):
644
         """
683
         """
645
         ContentRevisionUpdateError is raised if tried to update column and revision own identity
684
         ContentRevisionUpdateError is raised if tried to update column and revision own identity
1077
         revisions = sorted(self.revisions, key=lambda revision: revision.revision_id)
1116
         revisions = sorted(self.revisions, key=lambda revision: revision.revision_id)
1078
         return revisions[-1]
1117
         return revisions[-1]
1079
 
1118
 
1080
-    def new_revision(self) -> None:
1119
+    def new_revision(self) -> ContentRevisionRO:
1081
         """
1120
         """
1082
         Return and assign to this content a new revision.
1121
         Return and assign to this content a new revision.
1083
         If it's a new content, revision is totally new.
1122
         If it's a new content, revision is totally new.
1266
         cid = content.content_id
1305
         cid = content.content_id
1267
         return url_template.format(wid=wid, fid=fid, ctype=ctype, cid=cid)
1306
         return url_template.format(wid=wid, fid=fid, ctype=ctype, cid=cid)
1268
 
1307
 
1308
+    def copy(self, parent):
1309
+        cpy_content = Content()
1310
+        for rev in self.revisions:
1311
+            cpy_rev = ContentRevisionRO.copy(rev, parent)
1312
+            cpy_content.revisions.append(cpy_rev)
1313
+        return cpy_content
1314
+
1269
 
1315
 
1270
 class RevisionReadStatus(DeclarativeBase):
1316
 class RevisionReadStatus(DeclarativeBase):
1271
 
1317
 

+ 262 - 0
tracim/tracim/tests/library/test_content_api.py View File

4
 from nose.tools import eq_, ok_
4
 from nose.tools import eq_, ok_
5
 from nose.tools import raises
5
 from nose.tools import raises
6
 
6
 
7
+from depot.io.utils import FileIntent
7
 import transaction
8
 import transaction
8
 
9
 
9
 from tracim.lib.content import compare_content_for_sorting_by_type_and_name
10
 from tracim.lib.content import compare_content_for_sorting_by_type_and_name
347
         eq_('', c.label)
348
         eq_('', c.label)
348
         eq_(ActionDescription.COMMENT, c.revision_type)
349
         eq_(ActionDescription.COMMENT, c.revision_type)
349
 
350
 
351
+    def test_unit_copy_file_different_label_different_parent_ok(self):
352
+        uapi = UserApi(None)
353
+        groups = [
354
+            GroupApi(None).get_one(Group.TIM_USER),
355
+            GroupApi(None).get_one(Group.TIM_MANAGER),
356
+            GroupApi(None).get_one(Group.TIM_ADMIN)
357
+        ]
358
+
359
+        user = uapi.create_user(
360
+            email='user1@user',
361
+            groups=groups,
362
+            save_now=True
363
+        )
364
+        user2 = uapi.create_user(
365
+            email='user2@user',
366
+            groups=groups,
367
+            save_now=True
368
+        )
369
+        workspace = WorkspaceApi(user).create_workspace(
370
+            'test workspace',
371
+            save_now=True
372
+        )
373
+        RoleApi(user).create_one(
374
+            user2,
375
+            workspace,
376
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
377
+            with_notif=False
378
+        )
379
+        api = ContentApi(user)
380
+        foldera = api.create(
381
+            ContentType.Folder,
382
+            workspace,
383
+            None,
384
+            'folder a',
385
+            True
386
+        )
387
+        with DBSession.no_autoflush:
388
+            text_file = api.create(
389
+                content_type=ContentType.File,
390
+                workspace=workspace,
391
+                parent=foldera,
392
+                label='test_file',
393
+                do_save=False,
394
+            )
395
+            api.update_file_data(
396
+                text_file,
397
+                'test_file',
398
+                'text/plain',
399
+                b'test_content'
400
+            )
401
+
402
+        api.save(text_file, ActionDescription.CREATION)
403
+        api2 = ContentApi(user2)
404
+        workspace2 = WorkspaceApi(user2).create_workspace(
405
+            'test workspace2',
406
+            save_now=True
407
+        )
408
+        folderb = api2.create(
409
+            ContentType.Folder,
410
+            workspace2,
411
+            None,
412
+            'folder b',
413
+            True
414
+        )
415
+
416
+        api2.copy(
417
+            item=text_file,
418
+            new_parent=folderb,
419
+            new_label='test_file_copy'
420
+        )
421
+
422
+        transaction.commit()
423
+        text_file_copy = api2.get_one_by_label_and_parent(
424
+            'test_file_copy',
425
+            folderb,
426
+        )
427
+
428
+        assert text_file != text_file_copy
429
+        assert text_file_copy.content_id != text_file.content_id
430
+        assert text_file_copy.workspace_id == workspace2.workspace_id
431
+        assert text_file_copy.depot_file.file.read() == text_file.depot_file.file.read()   # nopep8
432
+        assert text_file_copy.depot_file.path != text_file.depot_file.path
433
+        assert text_file_copy.label == 'test_file_copy'
434
+        assert text_file_copy.type == text_file.type
435
+        assert text_file_copy.parent.content_id == folderb.content_id
436
+        assert text_file_copy.owner.user_id == user.user_id
437
+        assert text_file_copy.description == text_file.description
438
+        assert text_file_copy.file_extension == text_file.file_extension
439
+        assert text_file_copy.file_mimetype == text_file.file_mimetype
440
+        assert text_file_copy.revision_type == ActionDescription.COPY
441
+        assert len(text_file_copy.revisions) == len(text_file.revisions) + 1
442
+
443
+    def test_unit_copy_file__same_label_different_parent_ok(self):
444
+        uapi = UserApi(None)
445
+        groups = [GroupApi(None).get_one(Group.TIM_USER),
446
+                  GroupApi(None).get_one(Group.TIM_MANAGER),
447
+                  GroupApi(None).get_one(Group.TIM_ADMIN)]
448
+
449
+        user = uapi.create_user(
450
+            email='user1@user',
451
+            groups=groups,
452
+            save_now=True
453
+        )
454
+        user2 = uapi.create_user(
455
+            email='user2@user',
456
+            groups=groups,
457
+            save_now=True
458
+        )
459
+        workspace = WorkspaceApi(user).create_workspace(
460
+            'test workspace',
461
+            save_now=True
462
+        )
463
+        RoleApi(user).create_one(
464
+            user2,
465
+            workspace,
466
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
467
+            with_notif=False
468
+        )
469
+        api = ContentApi(user)
470
+        foldera = api.create(
471
+            ContentType.Folder,
472
+            workspace,
473
+            None,
474
+            'folder a',
475
+            True
476
+        )
477
+        with DBSession.no_autoflush:
478
+            text_file = api.create(
479
+                content_type=ContentType.File,
480
+                workspace=workspace,
481
+                parent=foldera,
482
+                label='test_file',
483
+                do_save=False,
484
+            )
485
+            api.update_file_data(
486
+                text_file,
487
+                'test_file',
488
+                'text/plain',
489
+                b'test_content'
490
+            )
491
+
492
+        api.save(text_file, ActionDescription.CREATION)
493
+        api2 = ContentApi(user2)
494
+        workspace2 = WorkspaceApi(user2).create_workspace(
495
+            'test workspace2',
496
+            save_now=True
497
+        )
498
+        folderb = api2.create(
499
+            ContentType.Folder,
500
+            workspace2,
501
+            None,
502
+            'folder b',
503
+            True
504
+        )
505
+        api2.copy(
506
+            item=text_file,
507
+            new_parent=folderb,
508
+        )
509
+
510
+        transaction.commit()
511
+        text_file_copy = api2.get_one_by_label_and_parent(
512
+            'test_file',
513
+            folderb,
514
+        )
515
+
516
+        assert text_file != text_file_copy
517
+        assert text_file_copy.content_id != text_file.content_id
518
+        assert text_file_copy.workspace_id == workspace2.workspace_id
519
+        assert text_file_copy.depot_file.file.read() == text_file.depot_file.file.read()  # nopep8
520
+        assert text_file_copy.depot_file.path != text_file.depot_file.path
521
+        assert text_file_copy.label == text_file.label
522
+        assert text_file_copy.type == text_file.type
523
+        assert text_file_copy.parent.content_id == folderb.content_id
524
+        assert text_file_copy.owner.user_id == user.user_id
525
+        assert text_file_copy.description == text_file.description
526
+        assert text_file_copy.file_extension == text_file.file_extension
527
+        assert text_file_copy.file_mimetype == text_file.file_mimetype
528
+        assert text_file_copy.revision_type == ActionDescription.COPY
529
+        assert len(text_file_copy.revisions) == len(text_file.revisions) + 1
530
+
531
+    def test_unit_copy_file_different_label_same_parent_ok(self):
532
+        uapi = UserApi(None)
533
+        groups = [
534
+            GroupApi(None).get_one(Group.TIM_USER),
535
+            GroupApi(None).get_one(Group.TIM_MANAGER),
536
+            GroupApi(None).get_one(Group.TIM_ADMIN),
537
+        ]
538
+
539
+        user = uapi.create_user(
540
+            email='user1@user',
541
+            groups=groups,
542
+            save_now=True,
543
+        )
544
+        user2 = uapi.create_user(
545
+            email='user2@user',
546
+            groups=groups,
547
+            save_now=True
548
+        )
549
+        workspace = WorkspaceApi(user).create_workspace(
550
+            'test workspace',
551
+            save_now=True
552
+        )
553
+        RoleApi(user).create_one(
554
+            user2, workspace,
555
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
556
+            with_notif=False
557
+        )
558
+        api = ContentApi(user)
559
+        foldera = api.create(
560
+            ContentType.Folder,
561
+            workspace,
562
+            None,
563
+            'folder a',
564
+            True
565
+        )
566
+        with DBSession.no_autoflush:
567
+            text_file = api.create(
568
+                content_type=ContentType.File,
569
+                workspace=workspace,
570
+                parent=foldera,
571
+                label='test_file',
572
+                do_save=False,
573
+            )
574
+            api.update_file_data(
575
+                text_file,
576
+                'test_file',
577
+                'text/plain',
578
+                b'test_content'
579
+            )
580
+
581
+        api.save(
582
+            text_file,
583
+            ActionDescription.CREATION
584
+        )
585
+        api2 = ContentApi(user2)
586
+
587
+        api2.copy(
588
+            item=text_file,
589
+            new_label='test_file_copy'
590
+        )
591
+
592
+        transaction.commit()
593
+        text_file_copy = api2.get_one_by_label_and_parent(
594
+            'test_file_copy',
595
+            foldera,
596
+        )
597
+
598
+        assert text_file != text_file_copy
599
+        assert text_file_copy.content_id != text_file.content_id
600
+        assert text_file_copy.workspace_id == workspace.workspace_id
601
+        assert text_file_copy.depot_file.file.read() == text_file.depot_file.file.read()  # nopep8
602
+        assert text_file_copy.depot_file.path != text_file.depot_file.path
603
+        assert text_file_copy.label == 'test_file_copy'
604
+        assert text_file_copy.type == text_file.type
605
+        assert text_file_copy.parent.content_id == foldera.content_id
606
+        assert text_file_copy.owner.user_id == user.user_id
607
+        assert text_file_copy.description == text_file.description
608
+        assert text_file_copy.file_extension == text_file.file_extension
609
+        assert text_file_copy.file_mimetype == text_file.file_mimetype
610
+        assert text_file_copy.revision_type == ActionDescription.COPY
611
+        assert len(text_file_copy.revisions) == len(text_file.revisions) + 1
350
 
612
 
351
     def test_mark_read__workspace(self):
613
     def test_mark_read__workspace(self):
352
         uapi = UserApi(None)
614
         uapi = UserApi(None)

+ 7 - 1
tracim/tracim/tests/models/test_content_revision.py View File

13
 class TestContentRevision(BaseTest, TestStandard):
13
 class TestContentRevision(BaseTest, TestStandard):
14
 
14
 
15
     def _new_from(self, revision):
15
     def _new_from(self, revision):
16
-        excluded_columns = ('revision_id', '_sa_instance_state', 'depot_file')
16
+        excluded_columns = (
17
+            'revision_id',
18
+            '_sa_instance_state',
19
+            'depot_file',
20
+            'node',
21
+            'revision_read_statuses',
22
+        )
17
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
23
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
18
         new_revision = ContentRevisionRO()
24
         new_revision = ContentRevisionRO()
19
 
25