Example #1
0
class User(AbstractUser):

    # First Name and Last Name do not cover name patterns
    # around the globe.
    name = models.CharField(_("Name of User"), blank=True, max_length=255)

    # updated on logout or password change, to invalidate JWT
    secret_key = models.UUIDField(default=uuid.uuid4, null=True)
    privacy_level = fields.get_privacy_field()

    # Unfortunately, Subsonic API assumes a MD5/password authentication
    # scheme, which is weak in terms of security, and not achievable
    # anyway since django use stronger schemes for storing passwords.
    # Users that want to use the subsonic API from external client
    # should set this token and use it as their password in such clients
    subsonic_api_token = models.CharField(blank=True,
                                          null=True,
                                          max_length=255)

    # permissions
    permission_moderation = models.BooleanField(
        PERMISSIONS_CONFIGURATION["moderation"]["label"],
        help_text=PERMISSIONS_CONFIGURATION["moderation"]["help_text"],
        default=False,
    )
    permission_library = models.BooleanField(
        PERMISSIONS_CONFIGURATION["library"]["label"],
        help_text=PERMISSIONS_CONFIGURATION["library"]["help_text"],
        default=False,
    )
    permission_settings = models.BooleanField(
        PERMISSIONS_CONFIGURATION["settings"]["label"],
        help_text=PERMISSIONS_CONFIGURATION["settings"]["help_text"],
        default=False,
    )

    last_activity = models.DateTimeField(default=None, null=True, blank=True)

    invitation = models.ForeignKey(
        "Invitation",
        related_name="users",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    avatar = VersatileImageField(
        upload_to=get_file_path,
        null=True,
        blank=True,
        max_length=150,
        validators=[
            common_validators.ImageDimensionsValidator(min_width=50,
                                                       min_height=50),
            common_validators.FileValidator(
                allowed_extensions=["png", "jpg", "jpeg", "gif"],
                max_size=1024 * 1024 * 2,
            ),
        ],
    )
    actor = models.OneToOneField(
        "federation.Actor",
        related_name="user",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

    upload_quota = models.PositiveIntegerField(null=True, blank=True)

    instance_support_message_display_date = models.DateTimeField(
        default=get_default_instance_support_message_display_date,
        null=True,
        blank=True)
    funkwhale_support_message_display_date = models.DateTimeField(
        default=get_default_funkwhale_support_message_display_date,
        null=True,
        blank=True,
    )
    settings = JSONField(default=None, null=True, blank=True, max_length=50000)

    objects = UserManager()

    def __str__(self):
        return self.username

    def get_permissions(self, defaults=None):
        defaults = defaults or preferences.get("users__default_permissions")
        perms = {}
        for p in PERMISSIONS:
            v = (self.is_superuser or getattr(self, "permission_{}".format(p))
                 or p in defaults)
            perms[p] = v
        return perms

    @property
    def all_permissions(self):
        return self.get_permissions()

    @transaction.atomic
    def set_settings(self, **settings):
        u = self.__class__.objects.select_for_update().get(pk=self.pk)
        if not u.settings:
            u.settings = {}
        for key, value in settings.items():
            u.settings[key] = value
        u.save(update_fields=["settings"])
        self.settings = u.settings

    def has_permissions(self, *perms, **kwargs):
        operator = kwargs.pop("operator", "and")
        if operator not in ["and", "or"]:
            raise ValueError("Invalid operator {}".format(operator))
        permissions = self.get_permissions()
        checker = all if operator == "and" else any
        return checker([permissions[p] for p in perms])

    def get_absolute_url(self):
        return reverse("users:detail", kwargs={"username": self.username})

    def update_secret_key(self):
        self.secret_key = uuid.uuid4()
        return self.secret_key

    def update_subsonic_api_token(self):
        self.subsonic_api_token = get_token()
        return self.subsonic_api_token

    def set_password(self, raw_password):
        super().set_password(raw_password)
        self.update_secret_key()
        if self.subsonic_api_token:
            self.update_subsonic_api_token()

    def get_activity_url(self):
        return settings.FUNKWHALE_URL + "/@{}".format(self.username)

    def record_activity(self):
        """
        Simply update the last_activity field if current value is too old
        than a threshold. This is useful to keep a track of inactive accounts.
        """
        current = self.last_activity
        delay = 60 * 15  # fifteen minutes
        now = timezone.now()

        if current is None or current < now - datetime.timedelta(
                seconds=delay):
            self.last_activity = now
            self.save(update_fields=["last_activity"])

    def create_actor(self, **kwargs):
        self.actor = create_actor(self, **kwargs)
        self.save(update_fields=["actor"])
        return self.actor

    def get_upload_quota(self):
        return (self.upload_quota if self.upload_quota is not None else
                preferences.get("users__upload_quota"))

    def get_quota_status(self):
        data = self.actor.get_current_usage()
        max_ = self.get_upload_quota()
        return {
            "max": max_,
            "remaining": max(max_ - (data["total"] / 1000 / 1000), 0),
            "current": data["total"] / 1000 / 1000,
            "draft": data["draft"] / 1000 / 1000,
            "skipped": data["skipped"] / 1000 / 1000,
            "pending": data["pending"] / 1000 / 1000,
            "finished": data["finished"] / 1000 / 1000,
            "errored": data["errored"] / 1000 / 1000,
        }

    def get_channels_groups(self):
        groups = ["imports", "inbox"]
        groups = ["user.{}.{}".format(self.pk, g) for g in groups]

        for permission, value in self.all_permissions.items():
            if value:
                groups.append("admin.{}".format(permission))

        return groups

    def full_username(self):
        return "{}@{}".format(self.username, settings.FEDERATION_HOSTNAME)

    def get_avatar(self):
        if not self.actor:
            return
        return self.actor.attachment_icon
Example #2
0
class Playlist(models.Model):
    name = models.CharField(max_length=50)
    user = models.ForeignKey("users.User",
                             related_name="playlists",
                             on_delete=models.CASCADE)
    creation_date = models.DateTimeField(default=timezone.now)
    modification_date = models.DateTimeField(auto_now=True)
    privacy_level = fields.get_privacy_field()

    objects = PlaylistQuerySet.as_manager()

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return "/library/playlists/{}".format(self.pk)

    @transaction.atomic
    def insert(self, plt, index=None, allow_duplicates=True):
        """
        Given a PlaylistTrack, insert it at the correct index in the playlist,
        and update other tracks index if necessary.
        """
        old_index = plt.index
        move = old_index is not None
        if index is not None and index == old_index:
            # moving at same position, just skip
            return index

        existing = self.playlist_tracks.select_for_update()
        if move:
            existing = existing.exclude(pk=plt.pk)
        total = existing.filter(index__isnull=False).count()

        if index is None:
            # we simply increment the last track index by 1
            index = total

        if index > total:
            raise exceptions.ValidationError("Index is not continuous")

        if index < 0:
            raise exceptions.ValidationError("Index must be zero or positive")

        if not allow_duplicates:
            existing_without_current_plt = existing.exclude(pk=plt.pk)
            self._check_duplicate_add(existing_without_current_plt,
                                      [plt.track])

        if move:
            # we remove the index temporarily, to avoid integrity errors
            plt.index = None
            plt.save(update_fields=["index"])
            if index > old_index:
                # new index is higher than current, we decrement previous tracks
                to_update = existing.filter(index__gt=old_index,
                                            index__lte=index)
                to_update.update(index=models.F("index") - 1)
            if index < old_index:
                # new index is lower than current, we increment next tracks
                to_update = existing.filter(index__lt=old_index,
                                            index__gte=index)
                to_update.update(index=models.F("index") + 1)
        else:
            to_update = existing.filter(index__gte=index)
            to_update.update(index=models.F("index") + 1)

        plt.index = index
        plt.save(update_fields=["index"])
        self.save(update_fields=["modification_date"])
        return index

    @transaction.atomic
    def remove(self, index):
        existing = self.playlist_tracks.select_for_update()
        self.save(update_fields=["modification_date"])
        to_update = existing.filter(index__gt=index)
        return to_update.update(index=models.F("index") - 1)

    @transaction.atomic
    def insert_many(self, tracks, allow_duplicates=True):
        existing = self.playlist_tracks.select_for_update()
        now = timezone.now()
        total = existing.filter(index__isnull=False).count()
        max_tracks = preferences.get("playlists__max_tracks")
        if existing.count() + len(tracks) > max_tracks:
            raise exceptions.ValidationError(
                "Playlist would reach the maximum of {} tracks".format(
                    max_tracks))

        if not allow_duplicates:
            self._check_duplicate_add(existing, tracks)

        self.save(update_fields=["modification_date"])
        start = total
        plts = [
            PlaylistTrack(creation_date=now,
                          playlist=self,
                          track=track,
                          index=start + i) for i, track in enumerate(tracks)
        ]
        return PlaylistTrack.objects.bulk_create(plts)

    def _check_duplicate_add(self, existing_playlist_tracks, tracks_to_add):
        track_ids = [t.pk for t in tracks_to_add]

        duplicates = existing_playlist_tracks.filter(
            track__pk__in=track_ids).values_list("track__pk", flat=True)
        if duplicates:
            duplicate_tracks = [t for t in tracks_to_add if t.pk in duplicates]
            raise exceptions.ValidationError({
                "non_field_errors": [{
                    "tracks":
                    duplicate_tracks,
                    "playlist_name":
                    self.name,
                    "code":
                    "tracks_already_exist_in_playlist",
                }]
            })