Completed
Push — develop ( 10efb0...b51ddf )
by Bastien
16s queued 14s
created

UserInContext.timezone()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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