SimpleFile.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 2
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_frontend_ui_base_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_frontend_ui_base_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
            show_deleted=True,
799
            show_archived=True,
800
            show_active=True,
801
            show_temporary=True,
802
        )
803
        return content_api.is_editable(self.content)
804
805
    @property
806
    def raw_content(self) -> str:
807
        return self.content.description
808
809
    @property
810
    def author(self) -> UserInContext:
811
        return UserInContext(
812
            dbsession=self.dbsession,
813
            config=self.config,
814
            user=self.content.first_revision.owner
815
        )
816
817
    @property
818
    def current_revision_id(self) -> int:
819
        return self.content.revision_id
820
821
    @property
822
    def created(self) -> datetime:
823
        return self.content.created
824
825
    @property
826
    def modified(self) -> datetime:
827
        return self.updated
828
829
    @property
830
    def updated(self) -> datetime:
831
        return self.content.updated
832
833
    @property
834
    def last_modifier(self) -> UserInContext:
835
        return UserInContext(
836
            dbsession=self.dbsession,
837
            config=self.config,
838
            user=self.content.last_revision.owner
839
        )
840
841
    # Context-related
842
    @property
843
    def show_in_ui(self) -> bool:
844
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
845
        # if false, then do not show content in the treeview.
846
        # This may his maybe used for specific contents or for sub-contents.
847
        # Default is True.
848
        # In first version of the API, this field is always True
849
        return True
850
851
    @property
852
    def slug(self) -> str:
853
        return slugify(self.content.label)
854
855
    @property
856
    def read_by_user(self) -> bool:
857
        assert self._user
858
        return not self.content.has_new_information_for(self._user)
859
860
    @property
861
    def frontend_url(self) -> str:
862
        root_frontend_url = get_frontend_ui_base_url(self.config)
863
        content_frontend_url = CONTENT_FRONTEND_URL_SCHEMA.format(
864
            workspace_id=self.workspace_id,
865
            content_type=self.content_type,
866
            content_id=self.content_id,
867
        )
868
        return root_frontend_url + content_frontend_url
869
870
    # file specific
871
    @property
872
    def page_nb(self) -> typing.Optional[int]:
873
        """
874
        :return: page_nb of content if available, None if unavailable
875
        """
876
        if self.content.depot_file:
877
            from tracim_backend.lib.core.content import ContentApi
878
            content_api = ContentApi(
879
                current_user=self._user,
880
                session=self.dbsession,
881
                config=self.config,
882
                show_deleted=True,
883
                show_archived=True,
884
                show_active=True,
885
                show_temporary=True,
886
            )
887
            return content_api.get_preview_page_nb(
888
                self.content.revision_id,
889
                file_extension=self.content.file_extension
890
            )
891
        else:
892
            return None
893
894
    @property
895
    def mimetype(self) -> str:
896
        """
897
        :return: mimetype of content if available, None if unavailable
898
        """
899
        return self.content.file_mimetype
900
901
    @property
902
    def size(self) -> typing.Optional[int]:
903
        """
904
        :return: size of content if available, None if unavailable
905
        """
906
        if self.content.depot_file:
907
            return self.content.depot_file.file.content_length
908
        else:
909
            return None
910
911
    @property
912
    def has_pdf_preview(self) -> bool:
913
        """
914
        :return: bool about if pdf version of content is available
915
        """
916
        if not self.content.depot_file:
917
            return False
918
919
        from tracim_backend.lib.core.content import ContentApi
920
        content_api = ContentApi(
921
            current_user=self._user,
922
            session=self.dbsession,
923
            config=self.config,
924
            show_deleted=True,
925
            show_archived=True,
926
            show_active=True,
927
            show_temporary=True,
928
        )
929
        return content_api.has_pdf_preview(
930
            self.content.revision_id,
931
            file_extension=self.content.file_extension
932
            )
933
934
    @property
935
    def has_jpeg_preview(self) -> bool:
936
        """
937
        :return: bool about if jpeg version of content is available
938
        """
939
        if not self.content.depot_file:
940
            return False
941
942
        from tracim_backend.lib.core.content import ContentApi
943
        content_api = ContentApi(
944
            current_user=self._user,
945
            session=self.dbsession,
946
            config=self.config,
947
            show_deleted=True,
948
            show_archived=True,
949
            show_active=True,
950
            show_temporary=True,
951
        )
952
        return content_api.has_jpeg_preview(
953
            self.content.revision_id,
954
            file_extension=self.content.file_extension
955
        )
956
957
958
    @property
959
    def file_extension(self) -> str:
960
        """
961
        :return: file extension with "." at the beginning, example : .txt
962
        """
963
        return self.content.file_extension
964
965
    @property
966
    def filename(self) -> str:
967
        """
968
        :return: complete filename with both label and file extension part
969
        """
970
        return self.content.file_name
971
972
973
class RevisionInContext(object):
974
    """
975
    Interface to get Content data and Content data related to context.
976
    """
977
978
    def __init__(self, content_revision: ContentRevisionRO, dbsession: Session, config: CFG,  user: User=None) -> None:  # nopep8
979
        assert content_revision is not None
980
        self.revision = content_revision
981
        self.dbsession = dbsession
982
        self.config = config
983
        self._user = user
984
985
    # Default
986
    @property
987
    def content_id(self) -> int:
988
        return self.revision.content_id
989
990
    @property
991
    def parent_id(self) -> int:
992
        """
993
        Return parent_id of the content
994
        """
995
        return self.revision.parent_id
996
997
    @property
998
    def workspace_id(self) -> int:
999
        return self.revision.workspace_id
1000
1001
    @property
1002
    def label(self) -> str:
1003
        return self.revision.label
1004
1005
    @property
1006
    def revision_type(self) -> str:
1007
        return self.revision.revision_type
1008
1009
    @property
1010
    def content_type(self) -> str:
1011
        return content_type_list.get_one_by_slug(self.revision.type).slug
1012
1013
    @property
1014
    def sub_content_types(self) -> typing.List[str]:
1015
        return [_type.slug for _type
1016
                in self.revision.node.get_allowed_content_types()]
1017
1018
    @property
1019
    def status(self) -> str:
1020
        return self.revision.status
1021
1022
    @property
1023
    def is_archived(self) -> bool:
1024
        return self.revision.is_archived
1025
1026
    @property
1027
    def is_deleted(self) -> bool:
1028
        return self.revision.is_deleted
1029
1030
    @property
1031
    def is_editable(self) -> bool:
1032
        from tracim_backend.lib.core.content import ContentApi
1033
        content_api = ContentApi(
1034
            current_user=self._user,
1035
            session=self.dbsession,
1036
            config=self.config,
1037
            show_deleted=True,
1038
            show_archived=True,
1039
            show_active=True,
1040
            show_temporary=True,
1041
        )
1042
        # INFO - G.M - 2018-11-02 - check if revision is last one and if it is,
1043
        # return editability of content.
1044
        content = content_api.get_one(
1045
            content_id=self.revision.content_id,
1046
            content_type=content_type_list.Any_SLUG
1047
        )
1048
        if content.revision_id == self.revision_id:
1049
            return content_api.is_editable(content)
1050
        # INFO - G.M - 2018-11-02 - old revision are not editable
1051
        return False
1052
1053
    @property
1054
    def raw_content(self) -> str:
1055
        return self.revision.description
1056
1057
    @property
1058
    def author(self) -> UserInContext:
1059
        return UserInContext(
1060
            dbsession=self.dbsession,
1061
            config=self.config,
1062
            user=self.revision.owner
1063
        )
1064
1065
    @property
1066
    def revision_id(self) -> int:
1067
        return self.revision.revision_id
1068
1069
    @property
1070
    def created(self) -> datetime:
1071
        return self.updated
1072
1073
    @property
1074
    def modified(self) -> datetime:
1075
        return self.updated
1076
1077
    @property
1078
    def updated(self) -> datetime:
1079
        return self.revision.updated
1080
1081
    @property
1082
    def next_revision(self) -> typing.Optional[ContentRevisionRO]:
1083
        """
1084
        Get next revision (later revision)
1085
        :return: next_revision
1086
        """
1087
        next_revision = None
1088
        revisions = self.revision.node.revisions
1089
        # INFO - G.M - 2018-06-177 - Get revisions more recent that
1090
        # current one
1091
        next_revisions = [
1092
            revision for revision in revisions
1093
            if revision.revision_id > self.revision.revision_id
1094
        ]
1095
        if next_revisions:
1096
            # INFO - G.M - 2018-06-177 -sort revisions by date
1097
            sorted_next_revisions = sorted(
1098
                next_revisions,
1099
                key=lambda revision: revision.updated
1100
            )
1101
            # INFO - G.M - 2018-06-177 - return only next revision
1102
            return sorted_next_revisions[0]
1103
        else:
1104
            return None
1105
1106
    @property
1107
    def comment_ids(self) -> typing.List[int]:
1108
        """
1109
        Get list of ids of all current revision related comments
1110
        :return: list of comments ids
1111
        """
1112
        comments = self.revision.node.get_comments()
1113
        # INFO - G.M - 2018-06-177 - Get comments more recent than revision.
1114
        revision_comments = [
1115
            comment for comment in comments
1116
            if comment.created > self.revision.updated or
1117
               comment.revision_id > self.revision.revision_id
1118
        ]
1119
        if self.next_revision:
1120
            # INFO - G.M - 2018-06-177 - if there is a revision more recent
1121
            # than current remove comments from theses rev (comments older
1122
            # than next_revision.)
1123
            revision_comments = [
1124
                comment for comment in revision_comments
1125
                if comment.created < self.next_revision.updated or
1126
                   comment.revision_id < self.next_revision.revision_id
1127
            ]
1128
        sorted_revision_comments = sorted(
1129
            revision_comments,
1130
            key=lambda revision: revision.revision_id
1131
        )
1132
        comment_ids = []
1133
        for comment in sorted_revision_comments:
1134
            comment_ids.append(comment.content_id)
1135
        return comment_ids
1136
1137
    # Context-related
1138
    @property
1139
    def show_in_ui(self) -> bool:
1140
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
1141
        # if false, then do not show content in the treeview.
1142
        # This may his maybe used for specific contents or for sub-contents.
1143
        # Default is True.
1144
        # In first version of the API, this field is always True
1145
        return True
1146
1147
    @property
1148
    def slug(self) -> str:
1149
        return slugify(self.revision.label)
1150
1151
    # file specific
1152
    @property
1153
    def page_nb(self) -> typing.Optional[int]:
1154
        """
1155
        :return: page_nb of content if available, None if unavailable
1156
        """
1157
        if self.revision.depot_file:
1158
            # TODO - G.M - 2018-09-05 - Fix circular import better
1159
            from tracim_backend.lib.core.content import ContentApi
1160
            content_api = ContentApi(
1161
                current_user=self._user,
1162
                session=self.dbsession,
1163
                config=self.config,
1164
                show_deleted=True,
1165
                show_archived=True,
1166
                show_active=True,
1167
                show_temporary=True,
1168
            )
1169
            return content_api.get_preview_page_nb(
1170
                self.revision.revision_id,
1171
                file_extension=self.revision.file_extension
1172
            )
1173
        else:
1174
            return None
1175
1176
    @property
1177
    def mimetype(self) -> str:
1178
        """
1179
        :return: mimetype of content if available, None if unavailable
1180
        """
1181
        return self.revision.file_mimetype
1182
1183
    @property
1184
    def size(self) -> typing.Optional[int]:
1185
        """
1186
        :return: size of content if available, None if unavailable
1187
        """
1188
        if self.revision.depot_file:
1189
            return self.revision.depot_file.file.content_length
1190
        else:
1191
            return None
1192
1193
    @property
1194
    def has_pdf_preview(self) -> bool:
1195
        """
1196
        :return: bool about if pdf version of content is available
1197
        """
1198
        if not self.revision.depot_file:
1199
            return False
1200
1201
        from tracim_backend.lib.core.content import ContentApi
1202
        content_api = ContentApi(
1203
            current_user=self._user,
1204
            session=self.dbsession,
1205
            config=self.config,
1206
            show_deleted=True,
1207
            show_archived=True,
1208
            show_active=True,
1209
            show_temporary=True,
1210
        )
1211
        return content_api.has_pdf_preview(
1212
            self.revision.revision_id,
1213
            file_extension=self.revision.file_extension,
1214
        )
1215
1216
    @property
1217
    def has_jpeg_preview(self) -> bool:
1218
        """
1219
        :return: bool about if jpeg version of content is available
1220
        """
1221
        if not self.revision.depot_file:
1222
            return False
1223
1224
        from tracim_backend.lib.core.content import ContentApi
1225
        content_api = ContentApi(
1226
            current_user=self._user,
1227
            session=self.dbsession,
1228
            config=self.config,
1229
            show_deleted=True,
1230
            show_archived=True,
1231
            show_active=True,
1232
            show_temporary=True,
1233
        )
1234
        return content_api.has_jpeg_preview(
1235
            self.revision.revision_id,
1236
            file_extension=self.revision.file_extension
1237
        )
1238
1239
    @property
1240
    def file_extension(self) -> str:
1241
        """
1242
        :return: file extension with "." at the beginning, example : .txt
1243
        """
1244
        return self.revision.file_extension
1245
1246
    @property
1247
    def filename(self) -> str:
1248
        """
1249
        :return: complete filename with both label and file extension part
1250
        """
1251
        return self.revision.file_name