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
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", }] })