Completed
Push — develop ( f2955b...37a460 )
by
unknown
20s queued 12s
created

RevisionInContext.is_editable()   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 18
rs 9.75
c 0
b 0
f 0
cc 2
nop 1
1
# coding=utf-8
2
import cgi
3
import typing
4
from datetime import datetime
5
from enum import Enum
6
7
from slugify import slugify
8
from sqlalchemy.orm import Session
9
10
from tracim_backend.app_models.contents import content_type_list
11
from tracim_backend.app_models.workspace_menu_entries import WorkspaceMenuEntry
12
from tracim_backend.config import CFG
13
from tracim_backend.config import PreviewDim
14
from tracim_backend.extensions import app_list
15
from tracim_backend.lib.core.application import ApplicationApi
16
from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
17
from tracim_backend.lib.utils.utils import WORKSPACE_FRONTEND_URL_SCHEMA
18
from tracim_backend.lib.utils.utils import get_root_frontend_url
19
from tracim_backend.lib.utils.utils import password_generator
20
from tracim_backend.models import User
21
from tracim_backend.models.auth import Group
22
from tracim_backend.models.auth import Profile
23
from tracim_backend.models.data import Content
24
from tracim_backend.models.data import ContentRevisionRO
25
from tracim_backend.models.data import UserRoleInWorkspace
26
from tracim_backend.models.data import Workspace
27
from tracim_backend.models.roles import WorkspaceRoles
28
29
30
class AboutModel(object):
31
32
    def __init__(
33
        self,
34
        name: str,
35
        version: typing.Optional[str],
36
        datetime: datetime,
37
        website: str,
38
    ) -> None:
39
        self.name = name
40
        self.version = version
41
        self.datetime = datetime
42
        self.website = website
43
44
45
class ConfigModel(object):
46
47
    def __init__(
48
        self,
49
        email_notification_activated: bool
50
    ) -> None:
51
        self.email_notification_activated = email_notification_activated
52
53
54
class PreviewAllowedDim(object):
55
56
    def __init__(
57
            self,
58
            restricted: bool,
59
            dimensions: typing.List[PreviewDim]
60
    ) -> None:
61
        self.restricted = restricted
62
        self.dimensions = dimensions
63
64
65
class MoveParams(object):
66
    """
67
    Json body params for move action model
68
    """
69
    def __init__(self, new_parent_id: str, new_workspace_id: str = None) -> None:  # nopep8
70
        self.new_parent_id = new_parent_id
71
        self.new_workspace_id = new_workspace_id
72
73
74
class LoginCredentials(object):
75
    """
76
    Login credentials model for login model
77
    """
78
79
    def __init__(self, email: str, password: str) -> None:
80
        self.email = email
81
        self.password = password
82
83
84
class ResetPasswordRequest(object):
85
    """
86
    Reset password : request to reset password of user
87
    """
88
    def __init__(self, email: str) -> None:
89
        self.email = email
90
91
92
class ResetPasswordCheckToken(object):
93
    """
94
    Reset password : check reset password token
95
    """
96
    def __init__(
97
        self,
98
        reset_password_token: str,
99
        email: str,
100
    ) -> None:
101
        self.email = email
102
        self.reset_password_token = reset_password_token
103
104
105
class ResetPasswordModify(object):
106
    """
107
    Reset password : modification step
108
    """
109
    def __init__(
110
        self,
111
        reset_password_token: str,
112
        email: str,
113
        new_password: str,
114
        new_password2: str
115
    ) -> None:
116
        self.email = email
117
        self.reset_password_token = reset_password_token
118
        self.new_password = new_password
119
        self.new_password2 = new_password2
120
121
122
class SetEmail(object):
123
    """
124
    Just an email and password
125
    """
126
    def __init__(self, loggedin_user_password: str, email: str) -> None:
127
        self.loggedin_user_password = loggedin_user_password
128
        self.email = email
129
130
131
class SimpleFile(object):
132
    def __init__(self, files: cgi.FieldStorage) -> None:
133
        self.files = files
134
135
136
class FileCreation(object):
137
    """
138
    Simple parent_id object
139
    """
140
    def __init__(self, parent_id: int = 0) -> None:
141
        self.parent_id = parent_id
142
143
class SetPassword(object):
144
    """
145
    Just an password
146
    """
147
    def __init__(
148
        self,
149
        loggedin_user_password: str,
150
        new_password: str,
151
        new_password2: str
152
    ) -> None:
153
        self.loggedin_user_password = loggedin_user_password
154
        self.new_password = new_password
155
        self.new_password2 = new_password2
156
157
158
class UserInfos(object):
159
    """
160
    Just some user infos
161
    """
162
    def __init__(self, timezone: str, public_name: str, lang: str) -> None:
163
        self.timezone = timezone
164
        self.public_name = public_name
165
        self.lang = lang
166
167
168
class UserProfile(object):
169
    """
170
    Just some user infos
171
    """
172
    def __init__(self, profile: str) -> None:
173
        self.profile = profile
174
175
176
class UserCreation(object):
177
    """
178
    Just some user infos
179
    """
180
    def __init__(
181
            self,
182
            email: str,
183
            password: str = None,
184
            public_name: str = None,
185
            timezone: str = None,
186
            profile: str = None,
187
            lang: str = None,
188
            email_notification: bool = True,
189
    ) -> None:
190
        self.email = email
191
        # INFO - G.M - 2018-08-16 - cleartext password, default value
192
        # is auto-generated.
193
        self.password = password or password_generator()
194
        self.public_name = public_name or None
195
        self.timezone = timezone or ''
196
        self.lang = lang or None
197
        self.profile = profile or Group.TIM_USER_GROUPNAME
198
        self.email_notification = email_notification
199
200
201
class WorkspaceAndContentPath(object):
202
    """
203
    Paths params with workspace id and content_id model
204
    """
205
    def __init__(self, workspace_id: int, content_id: int) -> None:
206
        self.content_id = content_id
207
        self.workspace_id = workspace_id
208
209
210
class WorkspaceAndContentRevisionPath(object):
211
    """
212
    Paths params with workspace id and content_id model
213
    """
214
    def __init__(self, workspace_id: int, content_id: int, revision_id: int) -> None:
215
        self.content_id = content_id
216
        self.revision_id = revision_id
217
        self.workspace_id = workspace_id
218
219
220
class FilePath(object):
221
    def __init__(self, workspace_id: int, content_id: int, filename: str) -> None:
222
        self.content_id = content_id
223
        self.workspace_id = workspace_id
224
        self.filename = filename
225
226
227
class FileRevisionPath(object):
228
    def __init__(self, workspace_id: int, content_id: int, revision_id: int, filename: str) -> None:
229
        self.content_id = content_id
230
        self.workspace_id = workspace_id
231
        self.revision_id = revision_id
232
        self.filename = filename
233
234
235
class FilePreviewSizedPath(object):
236
    """
237
    Paths params with workspace id and content_id, width, heigth
238
    """
239
    def __init__(self, workspace_id: int, content_id: int, width: int, height: int, filename: str) -> None:  # nopep8
240
        self.content_id = content_id
241
        self.workspace_id = workspace_id
242
        self.width = width
243
        self.height = height
244
        self.filename = filename
245
246
247
class RevisionPreviewSizedPath(object):
248
    """
249
    Paths params with workspace id and content_id, revision_id width, heigth
250
    """
251
    def __init__(self, workspace_id: int, content_id: int, revision_id: int, width: int, height: int, filename: str) -> None:  # nopep8
252
        self.content_id = content_id
253
        self.revision_id = revision_id
254
        self.workspace_id = workspace_id
255
        self.width = width
256
        self.height = height
257
        self.filename = filename
258
259
260
class WorkspacePath(object):
261
    """
262
    Paths params with workspace id and user_id
263
    """
264
    def __init__(self, workspace_id: int) -> None:
265
        self.workspace_id = workspace_id
266
267
268
class WorkspaceAndUserPath(object):
269
    """
270
    Paths params with workspace id and user_id
271
    """
272
    def __init__(self, workspace_id: int, user_id: int) -> None:
273
        self.workspace_id = workspace_id
274
        self.user_id = user_id
275
276
277
class UserWorkspaceAndContentPath(object):
278
    """
279
    Paths params with user_id, workspace id and content_id model
280
    """
281
    def __init__(self, user_id: int, workspace_id: int, content_id: int) -> None:  # nopep8
282
        self.content_id = content_id
283
        self.workspace_id = workspace_id
284
        self.user_id = user_id
285
286
287
class CommentPath(object):
288
    """
289
    Paths params with workspace id and content_id and comment_id model
290
    """
291
    def __init__(
292
        self,
293
        workspace_id: int,
294
        content_id: int,
295
        comment_id: int
296
    ) -> None:
297
        self.content_id = content_id
298
        self.workspace_id = workspace_id
299
        self.comment_id = comment_id
300
301
302
class KnownMemberQuery(object):
303
    """
304
    Autocomplete query model
305
    """
306
    def __init__(
307
            self,
308
            acp: str,
309
            exclude_user_ids: typing.List[int] = None,
310
            exclude_workspace_ids: typing.List[int] = None
311
    ) -> None:
312
        self.acp = acp
313
        self.exclude_user_ids = exclude_user_ids or []  # DFV
314
        self.exclude_workspace_ids = exclude_workspace_ids or []  # DFV
315
316
317
318
class FileQuery(object):
319
    """
320
    File query model
321
    """
322
    def __init__(
323
        self,
324
        force_download: int = 0,
325
    ) -> None:
326
        self.force_download = force_download
327
328
329
class PageQuery(object):
330
    """
331
    Page query model
332
    """
333
    def __init__(
334
            self,
335
            force_download: int = 0,
336
            page: int = 1
337
    ) -> None:
338
        self.force_download = force_download
339
        self.page = page
340
341
342
class ContentFilter(object):
343
    """
344
    Content filter model
345
    """
346
    def __init__(
347
            self,
348
            workspace_id: int = None,
349
            parent_id: int = None,
350
            show_archived: int = 0,
351
            show_deleted: int = 0,
352
            show_active: int = 1,
353
            content_type: str = None,
354
            label: str = None,
355
            offset: int = None,
356
            limit: int = None,
357
    ) -> None:
358
        self.parent_id = parent_id
359
        self.workspace_id = workspace_id
360
        self.show_archived = bool(show_archived)
361
        self.show_deleted = bool(show_deleted)
362
        self.show_active = bool(show_active)
363
        self.limit = limit
364
        self.offset = offset
365
        self.label = label
366
        self.content_type = content_type
367
368
369
class ActiveContentFilter(object):
370
    def __init__(
371
            self,
372
            limit: int = None,
373
            before_content_id: datetime = None,
374
    ) -> None:
375
        self.limit = limit
376
        self.before_content_id = before_content_id
377
378
379
class ContentIdsQuery(object):
380
    def __init__(
381
            self,
382
            contents_ids: typing.List[int] = None,
383
    ) -> None:
384
        self.contents_ids = contents_ids
385
386
387
class RoleUpdate(object):
388
    """
389
    Update role
390
    """
391
    def __init__(
392
        self,
393
        role: str,
394
    ) -> None:
395
        self.role = role
396
397
398
class WorkspaceMemberInvitation(object):
399
    """
400
    Workspace Member Invitation
401
    """
402
    def __init__(
403
        self,
404
        user_id: int = None,
405
        user_email: str = None,
406
        user_public_name: str = None,
407
        role: str = None,
408
    ) -> None:
409
        self.role = role
410
        self.user_email = user_email
411
        self.user_public_name = user_public_name
412
        self.user_id = user_id
413
414
415
class WorkspaceUpdate(object):
416
    """
417
    Update workspace
418
    """
419
    def __init__(
420
        self,
421
        label: str,
422
        description: str,
423
    ) -> None:
424
        self.label = label
425
        self.description = description
426
427
428
class ContentCreation(object):
429
    """
430
    Content creation model
431
    """
432
    def __init__(
433
        self,
434
        label: str,
435
        content_type: str,
436
        parent_id: typing.Optional[int] = None,
437
    ) -> None:
438
        self.label = label
439
        self.content_type = content_type
440
        self.parent_id = parent_id or None
441
442
443
class CommentCreation(object):
444
    """
445
    Comment creation model
446
    """
447
    def __init__(
448
        self,
449
        raw_content: str,
450
    ) -> None:
451
        self.raw_content = raw_content
452
453
454
class SetContentStatus(object):
455
    """
456
    Set content status
457
    """
458
    def __init__(
459
        self,
460
        status: str,
461
    ) -> None:
462
        self.status = status
463
464
465
class TextBasedContentUpdate(object):
466
    """
467
    TextBasedContent update model
468
    """
469
    def __init__(
470
        self,
471
        label: str,
472
        raw_content: str,
473
    ) -> None:
474
        self.label = label
475
        self.raw_content = raw_content
476
477
478
class FolderContentUpdate(object):
479
    """
480
    Folder Content update model
481
    """
482
    def __init__(
483
        self,
484
        label: str,
485
        raw_content: str,
486
        sub_content_types: typing.List[str],
487
    ) -> None:
488
        self.label = label
489
        self.raw_content = raw_content
490
        self.sub_content_types = sub_content_types
491
492
493
class TypeUser(Enum):
494
    """Params used to find user"""
495
    USER_ID = 'found_id'
496
    EMAIL = 'found_email'
497
    PUBLIC_NAME = 'found_public_name'
498
499
500
class UserInContext(object):
501
    """
502
    Interface to get User data and User data related to context.
503
    """
504
505
    def __init__(self, user: User, dbsession: Session, config: CFG) -> None:
506
        self.user = user
507
        self.dbsession = dbsession
508
        self.config = config
509
510
    # Default
511
512
    @property
513
    def email(self) -> str:
514
        return self.user.email
515
516
    @property
517
    def user_id(self) -> int:
518
        return self.user.user_id
519
520
    @property
521
    def public_name(self) -> str:
522
        return self.display_name
523
524
    @property
525
    def display_name(self) -> str:
526
        return self.user.display_name
527
528
    @property
529
    def created(self) -> datetime:
530
        return self.user.created
531
532
    @property
533
    def is_active(self) -> bool:
534
        return self.user.is_active
535
536
    @property
537
    def timezone(self) -> str:
538
        return self.user.timezone
539
540
    @property
541
    def lang(self) -> str:
542
        return self.user.lang
543
544
    @property
545
    def profile(self) -> Profile:
546
        return self.user.profile.name
547
548
    @property
549
    def is_deleted(self) -> bool:
550
        return self.user.is_deleted
551
552
    # Context related
553
554
    @property
555
    def calendar_url(self) -> typing.Optional[str]:
556
        # TODO - G-M - 20-04-2018 - [Calendar] Replace calendar code to get
557
        # url calendar url.
558
        #
559
        # from tracim.lib.calendar import CalendarManager
560
        # calendar_manager = CalendarManager(None)
561
        # return calendar_manager.get_workspace_calendar_url(self.workspace_id)
562
        return None
563
564
    @property
565
    def avatar_url(self) -> typing.Optional[str]:
566
        # TODO - G-M - 20-04-2018 - [Avatar] Add user avatar feature
567
        return None
568
569
570
class WorkspaceInContext(object):
571
    """
572
    Interface to get Workspace data and Workspace data related to context.
573
    """
574
575
    def __init__(self, workspace: Workspace, dbsession: Session, config: CFG) -> None:  # nopep8
576
        self.workspace = workspace
577
        self.dbsession = dbsession
578
        self.config = config
579
580
    @property
581
    def workspace_id(self) -> int:
582
        """
583
        numeric id of the workspace.
584
        """
585
        return self.workspace.workspace_id
586
587
    @property
588
    def id(self) -> int:
589
        """
590
        alias of workspace_id
591
        """
592
        return self.workspace_id
593
594
    @property
595
    def label(self) -> str:
596
        """
597
        get workspace label
598
        """
599
        return self.workspace.label
600
601
    @property
602
    def description(self) -> str:
603
        """
604
        get workspace description
605
        """
606
        return self.workspace.description
607
608
    @property
609
    def slug(self) -> str:
610
        """
611
        get workspace slug
612
        """
613
        return slugify(self.workspace.label)
614
615
    @property
616
    def is_deleted(self) -> bool:
617
        """
618
        Is the workspace deleted ?
619
        """
620
        return self.workspace.is_deleted
621
622
    @property
623
    def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
624
        """
625
        get sidebar entries, those depends on activated apps.
626
        """
627
        # TODO - G.M - 22-05-2018 - Rework on this in
628
        # order to not use hardcoded list
629
        # list should be able to change (depending on activated/disabled
630
        # apps)
631
        app_api = ApplicationApi(
632
            app_list
633
        )
634
        return app_api.get_default_workspace_menu_entry(self.workspace)
635
636
    @property
637
    def frontend_url(self):
638
        root_frontend_url = get_root_frontend_url(self.config)
639
        workspace_frontend_url = WORKSPACE_FRONTEND_URL_SCHEMA.format(
640
            workspace_id=self.workspace_id,
641
        )
642
        return root_frontend_url + workspace_frontend_url
643
644
645
class UserRoleWorkspaceInContext(object):
646
    """
647
    Interface to get UserRoleInWorkspace data and related content
648
649
    """
650
    def __init__(
651
            self,
652
            user_role: UserRoleInWorkspace,
653
            dbsession: Session,
654
            config: CFG,
655
            # Extended params
656
            newly_created: bool = None,
657
            email_sent: bool = None
658
    )-> None:
659
        self.user_role = user_role
660
        self.dbsession = dbsession
661
        self.config = config
662
        # Extended params
663
        self.newly_created = newly_created
664
        self.email_sent = email_sent
665
666
    @property
667
    def user_id(self) -> int:
668
        """
669
        User who has the role has this id
670
        :return: user id as integer
671
        """
672
        return self.user_role.user_id
673
674
    @property
675
    def workspace_id(self) -> int:
676
        """
677
        This role apply only on the workspace with this workspace_id
678
        :return: workspace id as integer
679
        """
680
        return self.user_role.workspace_id
681
682
    # TODO - G.M - 23-05-2018 - Check the API spec for this this !
683
684
    @property
685
    def role_id(self) -> int:
686
        """
687
        role as int id, each value refer to a different role.
688
        """
689
        return self.user_role.role
690
691
    @property
692
    def role(self) -> str:
693
        return self.role_slug
694
695
    @property
696
    def role_slug(self) -> str:
697
        """
698
        simple name of the role of the user.
699
        can be anything from UserRoleInWorkspace SLUG, like
700
        'not_applicable', 'reader',
701
        'contributor', 'content-manager', 'workspace-manager'
702
        :return: user workspace role as slug.
703
        """
704
        return WorkspaceRoles.get_role_from_level(self.user_role.role).slug
705
706
    @property
707
    def is_active(self) -> bool:
708
        return self.user.is_active
709
710
    @property
711
    def do_notify(self) -> bool:
712
        return self.user_role.do_notify
713
714
    @property
715
    def user(self) -> UserInContext:
716
        """
717
        User who has this role, with context data
718
        :return: UserInContext object
719
        """
720
        return UserInContext(
721
            self.user_role.user,
722
            self.dbsession,
723
            self.config
724
        )
725
726
    @property
727
    def workspace(self) -> WorkspaceInContext:
728
        """
729
        Workspace related to this role, with his context data
730
        :return: WorkspaceInContext object
731
        """
732
        return WorkspaceInContext(
733
            self.user_role.workspace,
734
            self.dbsession,
735
            self.config
736
        )
737
738
739
class ContentInContext(object):
740
    """
741
    Interface to get Content data and Content data related to context.
742
    """
743
744
    def __init__(self, content: Content, dbsession: Session, config: CFG, user: User=None) -> None:  # nopep8
745
        self.content = content
746
        self.dbsession = dbsession
747
        self.config = config
748
        self._user = user
749
750
    # Default
751
    @property
752
    def content_id(self) -> int:
753
        return self.content.content_id
754
755
    @property
756
    def parent_id(self) -> int:
757
        """
758
        Return parent_id of the content
759
        """
760
        return self.content.parent_id
761
762
    @property
763
    def workspace_id(self) -> int:
764
        return self.content.workspace_id
765
766
    @property
767
    def label(self) -> str:
768
        return self.content.label
769
770
    @property
771
    def content_type(self) -> str:
772
        content_type = content_type_list.get_one_by_slug(self.content.type)
773
        return content_type.slug
774
775
    @property
776
    def sub_content_types(self) -> typing.List[str]:
777
        return [_type.slug for _type in self.content.get_allowed_content_types()]  # nopep8
778
779
    @property
780
    def status(self) -> str:
781
        return self.content.status
782
783
    @property
784
    def is_archived(self) -> bool:
785
        return self.content.is_archived
786
787
    @property
788
    def is_deleted(self) -> bool:
789
        return self.content.is_deleted
790
791
    @property
792
    def is_editable(self) -> bool:
793
        from tracim_backend.lib.core.content import ContentApi
794
        content_api = ContentApi(
795
            current_user=self._user,
796
            session=self.dbsession,
797
            config=self.config
798
        )
799
        return content_api.is_editable(self.content)
800
801
    @property
802
    def raw_content(self) -> str:
803
        return self.content.description
804
805
    @property
806
    def author(self) -> UserInContext:
807
        return UserInContext(
808
            dbsession=self.dbsession,
809
            config=self.config,
810
            user=self.content.first_revision.owner
811
        )
812
813
    @property
814
    def current_revision_id(self) -> int:
815
        return self.content.revision_id
816
817
    @property
818
    def created(self) -> datetime:
819
        return self.content.created
820
821
    @property
822
    def modified(self) -> datetime:
823
        return self.updated
824
825
    @property
826
    def updated(self) -> datetime:
827
        return self.content.updated
828
829
    @property
830
    def last_modifier(self) -> UserInContext:
831
        return UserInContext(
832
            dbsession=self.dbsession,
833
            config=self.config,
834
            user=self.content.last_revision.owner
835
        )
836
837
    # Context-related
838
    @property
839
    def show_in_ui(self) -> bool:
840
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
841
        # if false, then do not show content in the treeview.
842
        # This may his maybe used for specific contents or for sub-contents.
843
        # Default is True.
844
        # In first version of the API, this field is always True
845
        return True
846
847
    @property
848
    def slug(self) -> str:
849
        return slugify(self.content.label)
850
851
    @property
852
    def read_by_user(self) -> bool:
853
        assert self._user
854
        return not self.content.has_new_information_for(self._user)
855
856
    @property
857
    def frontend_url(self) -> str:
858
        root_frontend_url = get_root_frontend_url(self.config)
859
        content_frontend_url = CONTENT_FRONTEND_URL_SCHEMA.format(
860
            workspace_id=self.workspace_id,
861
            content_type=self.content_type,
862
            content_id=self.content_id,
863
        )
864
        return root_frontend_url + content_frontend_url
865
866
    # file specific
867
    @property
868
    def page_nb(self) -> typing.Optional[int]:
869
        """
870
        :return: page_nb of content if available, None if unavailable
871
        """
872
        if self.content.depot_file:
873
            from tracim_backend.lib.core.content import ContentApi
874
            content_api = ContentApi(
875
                current_user=self._user,
876
                session=self.dbsession,
877
                config=self.config
878
            )
879
            return content_api.get_preview_page_nb(
880
                self.content.revision_id,
881
                file_extension=self.content.file_extension
882
            )
883
        else:
884
            return None
885
886
    @property
887
    def mimetype(self) -> str:
888
        """
889
        :return: mimetype of content if available, None if unavailable
890
        """
891
        return self.content.file_mimetype
892
893
    @property
894
    def size(self) -> typing.Optional[int]:
895
        """
896
        :return: size of content if available, None if unavailable
897
        """
898
        if self.content.depot_file:
899
            return self.content.depot_file.file.content_length
900
        else:
901
            return None
902
903
    @property
904
    def pdf_available(self) -> bool:
905
        """
906
        :return: bool about if pdf version of content is available
907
        """
908
        if self.content.depot_file:
909
            from tracim_backend.lib.core.content import ContentApi
910
            content_api = ContentApi(
911
                current_user=self._user,
912
                session=self.dbsession,
913
                config=self.config
914
            )
915
            return content_api.has_pdf_preview(
916
                self.content.revision_id,
917
                file_extension=self.content.file_extension
918
            )
919
        else:
920
            return False
921
922
    @property
923
    def file_extension(self) -> str:
924
        """
925
        :return: file extension with "." at the beginning, example : .txt
926
        """
927
        return self.content.file_extension
928
929
    @property
930
    def filename(self) -> str:
931
        """
932
        :return: complete filename with both label and file extension part
933
        """
934
        return self.content.file_name
935
936
937
class RevisionInContext(object):
938
    """
939
    Interface to get Content data and Content data related to context.
940
    """
941
942
    def __init__(self, content_revision: ContentRevisionRO, dbsession: Session, config: CFG,  user: User=None) -> None:  # nopep8
943
        assert content_revision is not None
944
        self.revision = content_revision
945
        self.dbsession = dbsession
946
        self.config = config
947
        self._user = user
948
949
    # Default
950
    @property
951
    def content_id(self) -> int:
952
        return self.revision.content_id
953
954
    @property
955
    def parent_id(self) -> int:
956
        """
957
        Return parent_id of the content
958
        """
959
        return self.revision.parent_id
960
961
    @property
962
    def workspace_id(self) -> int:
963
        return self.revision.workspace_id
964
965
    @property
966
    def label(self) -> str:
967
        return self.revision.label
968
969
    @property
970
    def revision_type(self) -> str:
971
        return self.revision.revision_type
972
973
    @property
974
    def content_type(self) -> str:
975
        return content_type_list.get_one_by_slug(self.revision.type).slug
976
977
    @property
978
    def sub_content_types(self) -> typing.List[str]:
979
        return [_type.slug for _type
980
                in self.revision.node.get_allowed_content_types()]
981
982
    @property
983
    def status(self) -> str:
984
        return self.revision.status
985
986
    @property
987
    def is_archived(self) -> bool:
988
        return self.revision.is_archived
989
990
    @property
991
    def is_deleted(self) -> bool:
992
        return self.revision.is_deleted
993
994
    @property
995
    def is_editable(self) -> bool:
996
        from tracim_backend.lib.core.content import ContentApi
997
        content_api = ContentApi(
998
            current_user=self._user,
999
            session=self.dbsession,
1000
            config=self.config
1001
        )
1002
        # INFO - G.M - 2018-11-02 - check if revision is last one and if it is,
1003
        # return editability of content.
1004
        content = content_api.get_one(
1005
            content_id=self.revision.content_id,
1006
            content_type=content_type_list.Any_SLUG
1007
        )
1008
        if content.revision_id == self.revision_id:
1009
            return content_api.is_editable(content)
1010
        # INFO - G.M - 2018-11-02 - old revision are not editable
1011
        return False
1012
1013
    @property
1014
    def raw_content(self) -> str:
1015
        return self.revision.description
1016
1017
    @property
1018
    def author(self) -> UserInContext:
1019
        return UserInContext(
1020
            dbsession=self.dbsession,
1021
            config=self.config,
1022
            user=self.revision.owner
1023
        )
1024
1025
    @property
1026
    def revision_id(self) -> int:
1027
        return self.revision.revision_id
1028
1029
    @property
1030
    def created(self) -> datetime:
1031
        return self.updated
1032
1033
    @property
1034
    def modified(self) -> datetime:
1035
        return self.updated
1036
1037
    @property
1038
    def updated(self) -> datetime:
1039
        return self.revision.updated
1040
1041
    @property
1042
    def next_revision(self) -> typing.Optional[ContentRevisionRO]:
1043
        """
1044
        Get next revision (later revision)
1045
        :return: next_revision
1046
        """
1047
        next_revision = None
1048
        revisions = self.revision.node.revisions
1049
        # INFO - G.M - 2018-06-177 - Get revisions more recent that
1050
        # current one
1051
        next_revisions = [
1052
            revision for revision in revisions
1053
            if revision.revision_id > self.revision.revision_id
1054
        ]
1055
        if next_revisions:
1056
            # INFO - G.M - 2018-06-177 -sort revisions by date
1057
            sorted_next_revisions = sorted(
1058
                next_revisions,
1059
                key=lambda revision: revision.updated
1060
            )
1061
            # INFO - G.M - 2018-06-177 - return only next revision
1062
            return sorted_next_revisions[0]
1063
        else:
1064
            return None
1065
1066
    @property
1067
    def comment_ids(self) -> typing.List[int]:
1068
        """
1069
        Get list of ids of all current revision related comments
1070
        :return: list of comments ids
1071
        """
1072
        comments = self.revision.node.get_comments()
1073
        # INFO - G.M - 2018-06-177 - Get comments more recent than revision.
1074
        revision_comments = [
1075
            comment for comment in comments
1076
            if comment.created > self.revision.updated or
1077
               comment.revision_id > self.revision.revision_id
1078
        ]
1079
        if self.next_revision:
1080
            # INFO - G.M - 2018-06-177 - if there is a revision more recent
1081
            # than current remove comments from theses rev (comments older
1082
            # than next_revision.)
1083
            revision_comments = [
1084
                comment for comment in revision_comments
1085
                if comment.created < self.next_revision.updated or
1086
                   comment.revision_id < self.next_revision.revision_id
1087
            ]
1088
        sorted_revision_comments = sorted(
1089
            revision_comments,
1090
            key=lambda revision: revision.revision_id
1091
        )
1092
        comment_ids = []
1093
        for comment in sorted_revision_comments:
1094
            comment_ids.append(comment.content_id)
1095
        return comment_ids
1096
1097
    # Context-related
1098
    @property
1099
    def show_in_ui(self) -> bool:
1100
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
1101
        # if false, then do not show content in the treeview.
1102
        # This may his maybe used for specific contents or for sub-contents.
1103
        # Default is True.
1104
        # In first version of the API, this field is always True
1105
        return True
1106
1107
    @property
1108
    def slug(self) -> str:
1109
        return slugify(self.revision.label)
1110
1111
    # file specific
1112
    @property
1113
    def page_nb(self) -> typing.Optional[int]:
1114
        """
1115
        :return: page_nb of content if available, None if unavailable
1116
        """
1117
        if self.revision.depot_file:
1118
            # TODO - G.M - 2018-09-05 - Fix circular import better
1119
            from tracim_backend.lib.core.content import ContentApi
1120
            content_api = ContentApi(
1121
                current_user=self._user,
1122
                session=self.dbsession,
1123
                config=self.config
1124
            )
1125
            return content_api.get_preview_page_nb(
1126
                self.revision.revision_id,
1127
                file_extension=self.revision.file_extension
1128
            )
1129
        else:
1130
            return None
1131
1132
    @property
1133
    def mimetype(self) -> str:
1134
        """
1135
        :return: mimetype of content if available, None if unavailable
1136
        """
1137
        return self.revision.file_mimetype
1138
1139
    @property
1140
    def size(self) -> typing.Optional[int]:
1141
        """
1142
        :return: size of content if available, None if unavailable
1143
        """
1144
        if self.revision.depot_file:
1145
            return self.revision.depot_file.file.content_length
1146
        else:
1147
            return None
1148
1149
    @property
1150
    def pdf_available(self) -> bool:
1151
        """
1152
        :return: bool about if pdf version of content is available
1153
        """
1154
        if self.revision.depot_file:
1155
            from tracim_backend.lib.core.content import ContentApi
1156
            content_api = ContentApi(
1157
                current_user=self._user,
1158
                session=self.dbsession,
1159
                config=self.config
1160
            )
1161
            return content_api.has_pdf_preview(
1162
                self.revision.revision_id,
1163
                file_extension=self.revision.file_extension,
1164
            )
1165
        else:
1166
            return False
1167
1168
    @property
1169
    def file_extension(self) -> str:
1170
        """
1171
        :return: file extension with "." at the beginning, example : .txt
1172
        """
1173
        return self.revision.file_extension
1174
1175
    @property
1176
    def filename(self) -> str:
1177
        """
1178
        :return: complete filename with both label and file extension part
1179
        """
1180
        return self.revision.file_name