소스 검색

Merge pull request #542 from inkhey/webdav_copy_fix

Damien Accorsi 6 년 전
부모
커밋
47141cc4e6
No account linked to committer's email

+ 47 - 0
tracim/tracim/lib/content.py 파일 보기

@@ -861,6 +861,53 @@ class ContentApi(object):
861 861
 
862 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 911
     def move_recursively(self, item: Content,
865 912
                          new_parent: Content, new_workspace: Workspace):
866 913
         self.move(item, new_parent, False, new_workspace)

+ 2 - 1
tracim/tracim/lib/webdav/design.py 파일 보기

@@ -120,7 +120,8 @@ _LABELS = {
120 120
     'unarchiving': 'Item unarchived',
121 121
     'undeletion': 'Item undeleted',
122 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 파일 보기

@@ -978,6 +978,48 @@ class File(DAVNonCollection):
978 978
 
979 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 1023
     def supportRecursiveMove(self, destPath):
982 1024
         return True
983 1025
 

+ 6 - 2
tracim/tracim/model/__init__.py 파일 보기

@@ -120,7 +120,10 @@ def prevent_content_revision_delete(session: Session, flush_context: UOWTransact
120 120
 
121 121
 
122 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 128
     Prepare context to update a Content. It will add a new updatable revision to the content.
126 129
     :param content: Content instance to update
@@ -128,7 +131,8 @@ def new_revision(content: Content) -> Content:
128 131
     """
129 132
     with DBSession.no_autoflush:
130 133
         try:
131
-            if inspect(content.revision).has_identity:
134
+            if force_create_new_revision \
135
+                    or inspect(content.revision).has_identity:
132 136
                 content.new_revision()
133 137
             RevisionsIntegrity.add_to_updatable(content.revision)
134 138
             yield content

+ 51 - 5
tracim/tracim/model/data.py 파일 보기

@@ -204,6 +204,7 @@ class ActionDescription(object):
204 204
     - closed-deprecated
205 205
     """
206 206
 
207
+    COPY = 'copy'
207 208
     ARCHIVING = 'archiving'
208 209
     COMMENT = 'content-comment'
209 210
     CREATION = 'creation'
@@ -225,7 +226,8 @@ class ActionDescription(object):
225 226
         'status-update': 'fa-random',
226 227
         'unarchiving': 'fa-file-archive-o',
227 228
         'undeletion': 'fa-trash-o',
228
-        'move': 'fa-arrows'
229
+        'move': 'fa-arrows',
230
+        'copy': 'fa-files-o',
229 231
     }
230 232
 
231 233
     _LABELS = {
@@ -238,7 +240,8 @@ class ActionDescription(object):
238 240
         'status-update': l_('New status'),
239 241
         'unarchiving': l_('Item unarchived'),
240 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 247
     def __init__(self, id):
@@ -259,7 +262,9 @@ class ActionDescription(object):
259 262
                 cls.STATUS_UPDATE,
260 263
                 cls.UNARCHIVING,
261 264
                 cls.UNDELETION,
262
-                cls.MOVE]
265
+                cls.MOVE,
266
+                cls.COPY,
267
+                ]
263 268
 
264 269
 
265 270
 class ContentStatus(object):
@@ -514,6 +519,11 @@ class ContentChecker(object):
514 519
                 return False
515 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 527
         raise NotImplementedError
518 528
 
519 529
     @classmethod
@@ -580,7 +590,6 @@ class ContentRevisionRO(DeclarativeBase):
580 590
         'is_archived',
581 591
         'is_deleted',
582 592
         'label',
583
-        'node',
584 593
         'owner',
585 594
         'owner_id',
586 595
         'parent',
@@ -640,6 +649,36 @@ class ContentRevisionRO(DeclarativeBase):
640 649
 
641 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 682
     def __setattr__(self, key: str, value: 'mixed'):
644 683
         """
645 684
         ContentRevisionUpdateError is raised if tried to update column and revision own identity
@@ -1077,7 +1116,7 @@ class Content(DeclarativeBase):
1077 1116
         revisions = sorted(self.revisions, key=lambda revision: revision.revision_id)
1078 1117
         return revisions[-1]
1079 1118
 
1080
-    def new_revision(self) -> None:
1119
+    def new_revision(self) -> ContentRevisionRO:
1081 1120
         """
1082 1121
         Return and assign to this content a new revision.
1083 1122
         If it's a new content, revision is totally new.
@@ -1266,6 +1305,13 @@ class Content(DeclarativeBase):
1266 1305
         cid = content.content_id
1267 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 1316
 class RevisionReadStatus(DeclarativeBase):
1271 1317
 

+ 262 - 0
tracim/tracim/tests/library/test_content_api.py 파일 보기

@@ -4,6 +4,7 @@ import datetime
4 4
 from nose.tools import eq_, ok_
5 5
 from nose.tools import raises
6 6
 
7
+from depot.io.utils import FileIntent
7 8
 import transaction
8 9
 
9 10
 from tracim.lib.content import compare_content_for_sorting_by_type_and_name
@@ -347,6 +348,267 @@ class TestContentApi(BaseTest, TestStandard):
347 348
         eq_('', c.label)
348 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 613
     def test_mark_read__workspace(self):
352 614
         uapi = UserApi(None)

+ 7 - 1
tracim/tracim/tests/models/test_content_revision.py 파일 보기

@@ -13,7 +13,13 @@ from tracim.tests import TestStandard, BaseTest
13 13
 class TestContentRevision(BaseTest, TestStandard):
14 14
 
15 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 23
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
18 24
         new_revision = ContentRevisionRO()
19 25