Source code for marsha.core.models

"""This module holds the models for the marsha project."""

from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _

from safedelete import HARD_DELETE

from marsha.core.base_models import BaseModel, NonDeletedUniqueIndex
from marsha.core.managers import UserManager
from marsha.stubs import M2MType, ReverseFKType


[docs]class User(BaseModel, AbstractUser): """Model representing a user that can be authenticated to act on the Marsha instance.""" # `related_name` of fields pointing to this model, for typing administrated_sites: M2MType["ConsumerSite"] sites_admins: ReverseFKType["SiteAdmin"] managed_organizations: M2MType["Organization"] author_organizations: M2MType["Organization"] authoring: ReverseFKType["Authoring"] authored_videos: ReverseFKType["Video"] created_playlists: ReverseFKType["Playlist"] managed_playlists: M2MType["Playlist"] playlists_accesses: ReverseFKType["PlaylistAccess"] managed_organizations_links: ReverseFKType["OrganizationManager"] objects = UserManager() class Meta: """Options for the ``User`` model.""" db_table: str = "user" verbose_name: str = _("user") verbose_name_plural: str = _("users") def __str__(self) -> str: """Get the string representation of an instance.""" result: str = f"{self.username}" if self.email: result += f" ({self.email})" if self.deleted: result += _(" [deleted]") return result
[docs]class ConsumerSite(BaseModel): """Model representing an external site with access to the Marsha instance.""" name: str = models.CharField( max_length=255, verbose_name=_("name"), help_text=_("Name of the site") ) admins: M2MType["User"] = models.ManyToManyField( to=User, through="SiteAdmin", related_name="administrated_sites", verbose_name=_("administrators"), help_text=_("users able to manage this site"), ) # `related_name` of fields pointing to this model, for typing sites_admins: ReverseFKType["SiteAdmin"] organizations: M2MType["Organization"] organizations_links: ReverseFKType["SiteOrganization"] class Meta: """Options for the ``ConsumerSite`` model.""" db_table: str = "consumer_site" verbose_name: str = _("site") verbose_name_plural: str = _("sites") def __str__(self) -> str: """Get the string representation of an instance.""" result: str = f"{self.name}" if self.deleted: result += _(" [deleted]") return result
[docs]class SiteAdmin(BaseModel): """Model representing users with access to manage a site. ``through`` model between ``ConsumerSite.admins`` and ``User.administrated_sites``. """ # we allow deleting entries in this through table _safedelete_policy = HARD_DELETE user: User = models.ForeignKey( to=User, related_name="sites_admins", verbose_name=_("user"), help_text=_("user with access to the site"), # link is (soft-)deleted if user is (soft-)deleted on_delete=models.CASCADE, ) site: ConsumerSite = models.ForeignKey( to=ConsumerSite, related_name="sites_admins", verbose_name=_("site"), help_text=_("site to which the user has access"), # link is (soft-)deleted if site is (soft-)deleted on_delete=models.CASCADE, ) class Meta: """Options for the ``SiteAdmin`` model.""" db_table: str = "site_admin" verbose_name: str = _("site admin") verbose_name_plural: str = _("site admins") indexes = [NonDeletedUniqueIndex(["user", "site"])] def __str__(self) -> str: """Get the string representation of an instance.""" args: dict = {"user": str(self.user), "site": str(self.site)} if self.deleted: return _("{user} was admin of {site}").format(**args) return _("{user} is admin of {site}").format(**args)
[docs]class Organization(BaseModel): """Model representing an organization to manage its playlists on one or many sites.""" name: str = models.CharField( max_length=255, verbose_name=_("name"), help_text=_("name of the organization") ) sites: M2MType[ConsumerSite] = models.ManyToManyField( to=ConsumerSite, through="SiteOrganization", related_name="organizations", verbose_name="sites", help_text=_("sites where this organization is present"), ) managers: M2MType[User] = models.ManyToManyField( to=User, through="OrganizationManager", related_name="managed_organizations", verbose_name=_("managers"), help_text=_("users able to manage this organization"), ) authors: M2MType[User] = models.ManyToManyField( to=User, through="Authoring", related_name="author_organizations", verbose_name=_("authors"), help_text=_("users able to manage playlists in this organization"), ) # `related_name` of fields pointing to this model, for typing authoring: ReverseFKType["Authoring"] playlists: ReverseFKType["Playlist"] sites_links: ReverseFKType["SiteOrganization"] managers_links: ReverseFKType["OrganizationManager"] class Meta: """Options for the ``Organization`` model.""" db_table: str = "organization" verbose_name: str = _("organization") verbose_name_plural: str = _("organizations") def __str__(self) -> str: """Get the string representation of an instance.""" result: str = f"{self.name}" if self.deleted: result += _(" [deleted]") return result
[docs]class SiteOrganization(BaseModel): """Model representing organizations in sites. ``through`` model between ``Organization.sites`` and ``ConsumerSite.organizations``. """ # we allow deleting entries in this through table _safedelete_policy = HARD_DELETE site: ConsumerSite = models.ForeignKey( to=ConsumerSite, related_name="organizations_links", verbose_name=_("site"), help_text=_("site having this organization"), # link is (soft-)deleted if site is (soft-)deleted on_delete=models.CASCADE, ) organization: Organization = models.ForeignKey( to=Organization, related_name="sites_links", verbose_name=_("organization"), help_text=_("organization in this site"), # link is (soft-)deleted if organization is (soft-)deleted on_delete=models.CASCADE, ) class Meta: """Options for the ``SiteOrganization`` model.""" db_table: str = "site_organization" verbose_name: str = _("organization in site") verbose_name_plural: str = _("organizations in sites") indexes = [NonDeletedUniqueIndex(["site", "organization"])] def __str__(self) -> str: """Get the string representation of an instance.""" args: dict = {"organization": str(self.organization), "site": str(self.site)} if self.deleted: return _("{organization} was in {site}").format(**args) return _("{organization} is in {site}").format(**args)
[docs]class OrganizationManager(BaseModel): """Model representing managers of organizations. ``through`` model between ``Organization.managers`` and ``User.managed_organizations``. """ # we allow deleting entries in this through table _safedelete_policy = HARD_DELETE user: User = models.ForeignKey( to=User, related_name="managed_organizations_links", verbose_name=_("manager"), help_text=_("user managing this organization"), # link is (soft-)deleted if user is (soft-)deleted on_delete=models.CASCADE, ) organization: Organization = models.ForeignKey( to=Organization, related_name="managers_links", verbose_name=_("organization"), help_text=_("organization managed by this user"), # link is (soft-)deleted if organization is (soft-)deleted on_delete=models.CASCADE, ) class Meta: """Options for the ``OrganizationManager`` model.""" db_table: str = "organization_manager" verbose_name: str = _("organization manager") verbose_name_plural: str = _("organizations managers") indexes = [NonDeletedUniqueIndex(["user", "organization"])] def __str__(self) -> str: """Get the string representation of an instance.""" args: dict = {"user": str(self.user), "organization": str(self.organization)} if self.deleted: return _("{user} was manager of {organization}").format(**args) return _("{user} is manager of {organization}").format(**args)
[docs]class Authoring(BaseModel): """Model representing authors in an organization. ``through`` model between ``Organization.authors`` and ``User.authoring``. """ # we allow deleting entries in this through table _safedelete_policy = HARD_DELETE user: User = models.ForeignKey( to=User, related_name="authoring", verbose_name=_("author"), help_text=_("user having authoring access in this organization"), # link is (soft-)deleted if user is (soft-)deleted on_delete=models.CASCADE, ) organization: Organization = models.ForeignKey( to=Organization, related_name="authoring", verbose_name=_("organization"), help_text=_("organization on which the user is an author"), # link is (soft-)deleted if organization is (soft-)deleted on_delete=models.CASCADE, ) class Meta: """Options for the ``Authoring`` model.""" db_table: str = "authoring" verbose_name: str = _("author in organization") verbose_name_plural: str = _("authors in organizations") indexes = [NonDeletedUniqueIndex(["user", "organization"])] def __str__(self) -> str: """Get the string representation of an instance.""" args: dict = {"user": str(self.user), "organization": str(self.organization)} if self.deleted: return _("{user} was author in {organization}").format(**args) return _("{user} is author in {organization}").format(**args)
[docs]class Video(BaseModel): """Model representing a video, by an author.""" name: str = models.CharField( max_length=255, verbose_name=_("name"), help_text=_("name of the video") ) description: str = models.TextField( verbose_name=_("description"), help_text=_("description of the video"), blank=True, null=True, ) author: User = models.ForeignKey( to=User, related_name="authored_videos", verbose_name=_("author"), help_text=_("author of the video"), # video is (soft-)deleted if author is (soft-)deleted on_delete=models.CASCADE, null=True, ) language: str = models.CharField( max_length=5, choices=settings.LANGUAGES, verbose_name=_("language"), help_text=_("language of the video"), ) duplicated_from: "Video" = models.ForeignKey( to="self", related_name="duplicates", verbose_name=_("duplicate from"), help_text=_("original video this one was duplicated from"), # don't delete a video if the one it was duplicated from is hard deleted on_delete=models.SET_NULL, null=True, blank=True, ) # `related_name` of fields pointing to this model, for typing audiotracks: ReverseFKType["AudioTrack"] subtitletracks: ReverseFKType["SubtitleTrack"] signtracks: ReverseFKType["SignTrack"] duplicates: ReverseFKType["Video"] playlists: M2MType["Playlist"] playlists_links: ReverseFKType["PlaylistVideo"] class Meta: """Options for the ``Video`` model.""" db_table: str = "video" verbose_name: str = _("video") verbose_name_plural: str = _("videos") def __str__(self) -> str: """Get the string representation of an instance.""" result: str = f"{self.name} by f{self.author.username}" if self.deleted: result += _(" [deleted]") return result
[docs]class BaseTrack(BaseModel): """Base model for different kinds of tracks tied to a video.""" video: Video = models.ForeignKey( to=Video, related_name="%(class)ss", # will be `audiotracks` for `AudioTrack` model, verbose_name=_("video"), help_text=_("video for which this track is"), # track is (soft-)deleted if video is (soft-)deleted on_delete=models.CASCADE, ) language: str = models.CharField( max_length=5, choices=settings.LANGUAGES, verbose_name=_("track language"), help_text=_("language of this track"), ) class Meta: """Options for the ``BaseTrack`` model.""" abstract: bool = True
[docs]class AudioTrack(BaseTrack): """Model representing an additional audio track for a video.""" # annotate inherited fields for mypy video: Video language: str class Meta: """Options for the ``AudioTrack`` model.""" db_table: str = "audio_track" verbose_name: str = _("audio track") verbose_name_plural: str = _("audio tracks") indexes = [NonDeletedUniqueIndex(["video", "language"])]
[docs]class SubtitleTrack(BaseTrack): """Model representing a subtitle track for a video.""" has_closed_captioning: bool = models.BooleanField( default=False, verbose_name=_("closed captioning"), help_text=_( "if closed captioning (for death or hard of hearing viewers) " "is on for this subtitle track" ), ) # annotate inherited fields for mypy video: Video language: str class Meta: """Options for the ``SubtitleTrack`` model.""" db_table: str = "subtitle_track" verbose_name: str = _("subtitles track") verbose_name_plural: str = _("subtitles tracks") indexes = [ NonDeletedUniqueIndex(["video", "language", "has_closed_captioning"]) ]
[docs]class SignTrack(BaseTrack): """Model representing a signs language track for a video.""" # annotate inherited fields for mypy video: Video language: str class Meta: """Options for the ``SignTrack`` model.""" db_table: str = "sign_track" verbose_name: str = _("signs language track") verbose_name_plural: str = _("signs language tracks") indexes = [NonDeletedUniqueIndex(["video", "language"])]
[docs]class Playlist(BaseModel): """Model representing a playlist which is a list of videos.""" name: str = models.CharField( max_length=255, verbose_name=_("name"), help_text=_("name of the playlist") ) organization: Organization = models.ForeignKey( to=Organization, related_name="playlists", # playlist is (soft-)deleted if organization is (soft-)deleted on_delete=models.CASCADE, null=True, ) author: User = models.ForeignKey( to=User, related_name="created_playlists", # playlist is (soft-)deleted if author is (soft-)deleted on_delete=models.CASCADE, null=True, ) editors: M2MType[User] = models.ManyToManyField( to=User, through="PlaylistAccess", related_name="managed_playlists", verbose_name=_("editors"), help_text=_("users allowed to manage this playlist"), ) is_public: bool = models.BooleanField( default=False, verbose_name=_("is public"), help_text=_("if this playlist can be viewed without any access control"), ) duplicated_from: "Playlist" = models.ForeignKey( to="self", related_name="duplicates", verbose_name=_("duplicate from"), help_text=_("original playlist this one was duplicated from"), # don't delete a playlist if the one it was duplicated from is hard deleted on_delete=models.SET_NULL, null=True, blank=True, ) videos: M2MType[Video] = models.ManyToManyField( to=Video, through="PlaylistVideo", related_name="playlists", verbose_name=_("videos"), help_text=_("videos in this playlist"), ) # `related_name` of fields pointing to this model, for typing duplicates: ReverseFKType["Playlist"] videos_links: ReverseFKType["PlaylistVideo"] users_accesses: ReverseFKType["PlaylistAccess"] class Meta: """Options for the ``Playlist`` model.""" db_table: str = "playlist" verbose_name: str = _("playlist") verbose_name_plural: str = _("playlists")
[docs]class PlaylistVideo(BaseModel): """Model representing a video in a playlist. ``through`` model between ``Playlist.videos`` and ``Video.playlists``. """ # we allow deleting entries in this through table _safedelete_policy = HARD_DELETE video: Video = models.ForeignKey( to=Video, related_name="playlists_links", verbose_name=_("video"), help_text=_("video contained in this playlist"), # link is (soft-)deleted if video is (soft-)deleted on_delete=models.CASCADE, ) playlist: Playlist = models.ForeignKey( to=Playlist, related_name="videos_links", verbose_name=_("playlist"), help_text=_("playlist containing this video"), # link is (soft-)deleted if playlist is (soft-)deleted on_delete=models.CASCADE, ) order: int = models.PositiveIntegerField( verbose_name=_("order"), help_text=_("video order in the playlist") ) class Meta: """Options for the ``PlaylistVideo`` model.""" db_table: str = "playlist_video" verbose_name: str = _("playlist video link") verbose_name_plural: str = _("playlist video links") indexes = [NonDeletedUniqueIndex(["video", "playlist"])]
[docs]class PlaylistAccess(BaseModel): """Model representing a user having right to manage a playlist. ``through`` model between ``Playlist.editors`` and ``User.managed_playlists``. """ # we allow deleting entries in this through table _safedelete_policy = HARD_DELETE user: User = models.ForeignKey( to=User, related_name="playlists_accesses", verbose_name=_("user"), help_text=_("user having rights to manage this playlist"), # link is (soft-)deleted if user is (soft-)deleted on_delete=models.CASCADE, ) playlist: Playlist = models.ForeignKey( to=Playlist, related_name="users_accesses", verbose_name=_("playlist"), help_text=_("playlist the user has rights to manage"), # link is (soft-)deleted if playlist is (soft-)deleted on_delete=models.CASCADE, ) class Meta: """Options for the ``PlaylistAccess`` model.""" db_table: str = "playlist_access" verbose_name: str = _("playlist access") verbose_name_plural: str = _("playlists accesses") indexes = [NonDeletedUniqueIndex(["user", "playlist"])]