Passed
Pull Request — develop (#123)
by inkhey
01:41
created

ContentInContext.jpeg_available()   A

Complexity

Conditions 2

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 22
rs 9.6
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_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 pdf_available(self) -> bool:
913
        """
914
        :return: bool about if pdf version of content is available
915
        """
916
        if self.content.depot_file:
917
            from tracim_backend.lib.core.content import ContentApi
918
            content_api = ContentApi(
919
                current_user=self._user,
920
                session=self.dbsession,
921
                config=self.config,
922
                show_deleted=True,
923
                show_archived=True,
924
                show_active=True,
925
                show_temporary=True,
926
            )
927
            return content_api.has_pdf_preview(
928
                self.content.revision_id,
929
                file_extension=self.content.file_extension
930
            )
931
        else:
932
            return False
933
934
    @property
935
    def jpeg_available(self) -> bool:
936
        """
937
        :return: bool about if jpeg version of content is available
938
        """
939
        if self.content.depot_file:
940
            from tracim_backend.lib.core.content import ContentApi
941
            content_api = ContentApi(
942
                current_user=self._user,
943
                session=self.dbsession,
944
                config=self.config,
945
                show_deleted=True,
946
                show_archived=True,
947
                show_active=True,
948
                show_temporary=True,
949
            )
950
            return content_api.has_jpeg_preview(
951
                self.content.revision_id,
952
                file_extension=self.content.file_extension
953
            )
954
        else:
955
            return False
956
957
    @property
958
    def file_extension(self) -> str:
959
        """
960
        :return: file extension with "." at the beginning, example : .txt
961
        """
962
        return self.content.file_extension
963
964
    @property
965
    def filename(self) -> str:
966
        """
967
        :return: complete filename with both label and file extension part
968
        """
969
        return self.content.file_name
970
971
972
class RevisionInContext(object):
973
    """
974
    Interface to get Content data and Content data related to context.
975
    """
976
977
    def __init__(self, content_revision: ContentRevisionRO, dbsession: Session, config: CFG,  user: User=None) -> None:  # nopep8
978
        assert content_revision is not None
979
        self.revision = content_revision
980
        self.dbsession = dbsession
981
        self.config = config
982
        self._user = user
983
984
    # Default
985
    @property
986
    def content_id(self) -> int:
987
        return self.revision.content_id
988
989
    @property
990
    def parent_id(self) -> int:
991
        """
992
        Return parent_id of the content
993
        """
994
        return self.revision.parent_id
995
996
    @property
997
    def workspace_id(self) -> int:
998
        return self.revision.workspace_id
999
1000
    @property
1001
    def label(self) -> str:
1002
        return self.revision.label
1003
1004
    @property
1005
    def revision_type(self) -> str:
1006
        return self.revision.revision_type
1007
1008
    @property
1009
    def content_type(self) -> str:
1010
        return content_type_list.get_one_by_slug(self.revision.type).slug
1011
1012
    @property
1013
    def sub_content_types(self) -> typing.List[str]:
1014
        return [_type.slug for _type
1015
                in self.revision.node.get_allowed_content_types()]
1016
1017
    @property
1018
    def status(self) -> str:
1019
        return self.revision.status
1020
1021
    @property
1022
    def is_archived(self) -> bool:
1023
        return self.revision.is_archived
1024
1025
    @property
1026
    def is_deleted(self) -> bool:
1027
        return self.revision.is_deleted
1028
1029
    @property
1030
    def is_editable(self) -> bool:
1031
        from tracim_backend.lib.core.content import ContentApi
1032
        content_api = ContentApi(
1033
            current_user=self._user,
1034
            session=self.dbsession,
1035
            config=self.config,
1036
            show_deleted=True,
1037
            show_archived=True,
1038
            show_active=True,
1039
            show_temporary=True,
1040
        )
1041
        # INFO - G.M - 2018-11-02 - check if revision is last one and if it is,
1042
        # return editability of content.
1043
        content = content_api.get_one(
1044
            content_id=self.revision.content_id,
1045
            content_type=content_type_list.Any_SLUG
1046
        )
1047
        if content.revision_id == self.revision_id:
1048
            return content_api.is_editable(content)
1049
        # INFO - G.M - 2018-11-02 - old revision are not editable
1050
        return False
1051
1052
    @property
1053
    def raw_content(self) -> str:
1054
        return self.revision.description
1055
1056
    @property
1057
    def author(self) -> UserInContext:
1058
        return UserInContext(
1059
            dbsession=self.dbsession,
1060
            config=self.config,
1061
            user=self.revision.owner
1062
        )
1063
1064
    @property
1065
    def revision_id(self) -> int:
1066
        return self.revision.revision_id
1067
1068
    @property
1069
    def created(self) -> datetime:
1070
        return self.updated
1071
1072
    @property
1073
    def modified(self) -> datetime:
1074
        return self.updated
1075
1076
    @property
1077
    def updated(self) -> datetime:
1078
        return self.revision.updated
1079
1080
    @property
1081
    def next_revision(self) -> typing.Optional[ContentRevisionRO]:
1082
        """
1083
        Get next revision (later revision)
1084
        :return: next_revision
1085
        """
1086
        next_revision = None
1087
        revisions = self.revision.node.revisions
1088
        # INFO - G.M - 2018-06-177 - Get revisions more recent that
1089
        # current one
1090
        next_revisions = [
1091
            revision for revision in revisions
1092
            if revision.revision_id > self.revision.revision_id
1093
        ]
1094
        if next_revisions:
1095
            # INFO - G.M - 2018-06-177 -sort revisions by date
1096
            sorted_next_revisions = sorted(
1097
                next_revisions,
1098
                key=lambda revision: revision.updated
1099
            )
1100
            # INFO - G.M - 2018-06-177 - return only next revision
1101
            return sorted_next_revisions[0]
1102
        else:
1103
            return None
1104
1105
    @property
1106
    def comment_ids(self) -> typing.List[int]:
1107
        """
1108
        Get list of ids of all current revision related comments
1109
        :return: list of comments ids
1110
        """
1111
        comments = self.revision.node.get_comments()
1112
        # INFO - G.M - 2018-06-177 - Get comments more recent than revision.
1113
        revision_comments = [
1114
            comment for comment in comments
1115
            if comment.created > self.revision.updated or
1116
               comment.revision_id > self.revision.revision_id
1117
        ]
1118
        if self.next_revision:
1119
            # INFO - G.M - 2018-06-177 - if there is a revision more recent
1120
            # than current remove comments from theses rev (comments older
1121
            # than next_revision.)
1122
            revision_comments = [
1123
                comment for comment in revision_comments
1124
                if comment.created < self.next_revision.updated or
1125
                   comment.revision_id < self.next_revision.revision_id
1126
            ]
1127
        sorted_revision_comments = sorted(
1128
            revision_comments,
1129
            key=lambda revision: revision.revision_id
1130
        )
1131
        comment_ids = []
1132
        for comment in sorted_revision_comments:
1133
            comment_ids.append(comment.content_id)
1134
        return comment_ids
1135
1136
    # Context-related
1137
    @property
1138
    def show_in_ui(self) -> bool:
1139
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
1140
        # if false, then do not show content in the treeview.
1141
        # This may his maybe used for specific contents or for sub-contents.
1142
        # Default is True.
1143
        # In first version of the API, this field is always True
1144
        return True
1145
1146
    @property
1147
    def slug(self) -> str:
1148
        return slugify(self.revision.label)
1149
1150
    # file specific
1151
    @property
1152
    def page_nb(self) -> typing.Optional[int]:
1153
        """
1154
        :return: page_nb of content if available, None if unavailable
1155
        """
1156
        if self.revision.depot_file:
1157
            # TODO - G.M - 2018-09-05 - Fix circular import better
1158
            from tracim_backend.lib.core.content import ContentApi
1159
            content_api = ContentApi(
1160
                current_user=self._user,
1161
                session=self.dbsession,
1162
                config=self.config,
1163
                show_deleted=True,
1164
                show_archived=True,
1165
                show_active=True,
1166
                show_temporary=True,
1167
            )
1168
            return content_api.get_preview_page_nb(
1169
                self.revision.revision_id,
1170
                file_extension=self.revision.file_extension
1171
            )
1172
        else:
1173
            return None
1174
1175
    @property
1176
    def mimetype(self) -> str:
1177
        """
1178
        :return: mimetype of content if available, None if unavailable
1179
        """
1180
        return self.revision.file_mimetype
1181
1182
    @property
1183
    def size(self) -> typing.Optional[int]:
1184
        """
1185
        :return: size of content if available, None if unavailable
1186
        """
1187
        if self.revision.depot_file:
1188
            return self.revision.depot_file.file.content_length
1189
        else:
1190
            return None
1191
1192
    @property
1193
    def pdf_available(self) -> bool:
1194
        """
1195
        :return: bool about if pdf version of content is available
1196
        """
1197
        if self.revision.depot_file:
1198
            from tracim_backend.lib.core.content import ContentApi
1199
            content_api = ContentApi(
1200
                current_user=self._user,
1201
                session=self.dbsession,
1202
                config=self.config,
1203
                show_deleted=True,
1204
                show_archived=True,
1205
                show_active=True,
1206
                show_temporary=True,
1207
            )
1208
            return content_api.has_pdf_preview(
1209
                self.revision.revision_id,
1210
                file_extension=self.revision.file_extension,
1211
            )
1212
        else:
1213
            return False
1214
1215
    @property
1216
    def jpeg_available(self) -> bool:
1217
        """
1218
        :return: bool about if jpeg version of content is available
1219
        """
1220
        if self.revision.depot_file:
1221
            from tracim_backend.lib.core.content import ContentApi
1222
            content_api = ContentApi(
1223
                current_user=self._user,
1224
                session=self.dbsession,
1225
                config=self.config,
1226
                show_deleted=True,
1227
                show_archived=True,
1228
                show_active=True,
1229
                show_temporary=True,
1230
            )
1231
            return content_api.has_jpeg_preview(
1232
                self.revision.revision_id,
1233
                file_extension=self.revision.file_extension
1234
            )
1235
        else:
1236
            return False
1237
1238
    @property
1239
    def file_extension(self) -> str:
1240
        """
1241
        :return: file extension with "." at the beginning, example : .txt
1242
        """
1243
        return self.revision.file_extension
1244
1245
    @property
1246
    def filename(self) -> str:
1247
        """
1248
        :return: complete filename with both label and file extension part
1249
        """
1250
        return self.revision.file_name