# -*- encoding: utf-8 -*- import logging from urllib.parse import unquote from rest_framework import serializers from apps.home.models import SubChannel, Filter, Subscription, SavedGuilds, TrackedContent, ArticleMutator, GuildSettings log = logging.getLogger(__name__) # This DynamicModelSerializer is from a StackOverflow user in an obscure thread. # I wish that I could remember which thread, because god bless that man. class DynamicModelSerializer(serializers.ModelSerializer): """ For use with GET requests, to specify which fields to include or exclude Mimics some graphql functionality. Usage: Inherit your ModelSerializer with this class. Add "only_fields" or "exclude_fields" to the query parameters of your GET request. This also works with nested foreign keys, for example: ?only_fields=name,age&company__only_fields=id,name Some more examples: ?only_fields=company,name&company__exclude_fields=name ?exclude_fields=name&company__only_fields=id ?company__exclude_fields=name Note: the Foreign Key serializer must also inherit from this class """ def only_keep_fields(self, fields_to_keep): fields_to_keep = set(fields_to_keep.split(",")) all_fields = set(self.fields.keys()) for field in all_fields - fields_to_keep: self.fields.pop(field, None) def exclude_fields(self, fields_to_exclude): fields_to_exclude = fields_to_exclude.split(",") for field in fields_to_exclude: self.fields.pop(field, None) def remove_unwanted_fields(self, dynamic_params): if fields_to_keep := dynamic_params.pop("only_fields", None): self.only_keep_fields(fields_to_keep) if fields_to_exclude := dynamic_params.pop("exclude_fields", None): self.exclude_fields(fields_to_exclude) def get_or_create_dynamic_params(self, child): if "dynamic_params" not in self.fields[child]._context: self.fields[child]._context.update({"dynamic_params": {}}) return self.fields[child]._context["dynamic_params"] @staticmethod def split_param(dynamic_param): crumbs = dynamic_param.split("__") return crumbs[0], "__".join(crumbs[1:]) if len(crumbs) > 1 else None def set_dynamic_params_for_children(self, dynamic_params): for param, fields in dynamic_params.items(): child, child_dynamic_param = self.split_param(param) if child in set(self.fields.keys()): dynamic_params = self.get_or_create_dynamic_params(child) dynamic_params.update({child_dynamic_param: fields}) @staticmethod def is_param_dynamic(p): return p.endswith("only_fields") or p.endswith("exclude_fields") def get_dynamic_params_for_root(self, request): query_params = request.query_params.items() return {k: v for k, v in query_params if self.is_param_dynamic(k)} def get_dynamic_params(self): """ When dynamic params get passed down in set_context_for_children If the child is a subclass of ListSerializer (has many=True) The context must be fetched from ListSerializer Class """ if isinstance(self.parent, serializers.ListSerializer): return self.parent._context.get("dynamic_params", {}) return self._context.get("dynamic_params", {}) def __init__(self, *args, **kwargs): request = kwargs.get("context", {}).get("request") super().__init__(*args, **kwargs) is_root = bool(request) if is_root: if request.method != "GET": return dynamic_params = self.get_dynamic_params_for_root(request) self._context.update({"dynamic_params": dynamic_params}) def to_representation(self, *args, **kwargs): if dynamic_params := self.get_dynamic_params().copy(): self.remove_unwanted_fields(dynamic_params) self.set_dynamic_params_for_children(dynamic_params) return super().to_representation(*args, **kwargs) class Meta: abstract = True class SubChannelSerializer(DynamicModelSerializer): """ Serializer for SubChannel Model. """ class Meta: model = SubChannel fields = ("id", "channel_id", "channel_name", "subscription") class FilterSerializer(DynamicModelSerializer): """ Serializer for the Filter Model. """ class Meta: model = Filter fields = ("id", "name", "matching_algorithm", "match", "is_insensitive", "is_whitelist", "guild_id") class ArticleMutatorSerializer(DynamicModelSerializer): class Meta: model = ArticleMutator fields = ("id", "name", "value") class SubscriptionSerializer_GET(DynamicModelSerializer): """ Serializer for the Subscription Model. """ article_title_mutators = ArticleMutatorSerializer(many=True) article_desc_mutators = ArticleMutatorSerializer(many=True) active = serializers.BooleanField(initial=True) class Meta: model = Subscription fields = ( "id", "name", "url", "guild_id", "channels_count", "creation_datetime", "extra_notes", "filters", "article_title_mutators", "article_desc_mutators", "article_fetch_image", "published_threshold", "embed_colour", "active" ) class SubscriptionSerializer_POST(DynamicModelSerializer): """ Serializer for the Subscription Model. """ class Meta: model = Subscription fields = ( "id", "name", "url", "guild_id", "channels_count", "creation_datetime", "extra_notes", "filters", "article_title_mutators", "article_desc_mutators", "article_fetch_image", "published_threshold", "embed_colour", "active" ) class SavedGuildSerializer(DynamicModelSerializer): """ Serializer for the SavedGuild model. """ class Meta: model = SavedGuilds fields = ("id", "guild_id", "name", "icon", "added_by", "permissions", "owner") class GuildSettingsSerializer(DynamicModelSerializer): """ Serializer for the GuildSettings model. """ class Meta: model = GuildSettings fields = ("id", "guild_id", "default_embed_colour", "active") class TrackedContentSerializer_GET(DynamicModelSerializer): """ Serializer for the TrackedContent model. """ subscription = SubscriptionSerializer_GET() class Meta: model = TrackedContent fields = ("id", "guid", "title", "url", "subscription", "channel_id", "message_id", "blocked", "creation_datetime") # def to_representation(self, instance): # representation = super().to_representation(instance) # log.info(representation.get("guid", "nothing")) # if 'guid' in representation: # representation['guid'] = unquote(representation['guid']) # log.info(representation.get("guid", "nothing")) # return representation class TrackedContentSerializer_POST(DynamicModelSerializer): """ Serializer for the TrackedContent model. """ class Meta: model = TrackedContent fields = ("id", "guid", "title", "url", "subscription", "channel_id", "message_id", "blocked", "creation_datetime")