|
6 | 6 | from django.db.utils import IntegrityError |
7 | 7 | from drf_spectacular.utils import extend_schema_serializer |
8 | 8 | from packaging.requirements import Requirement |
| 9 | +from packaging.version import Version, InvalidVersion |
9 | 10 | from rest_framework import serializers |
| 11 | +from rest_framework_nested.serializers import NestedHyperlinkedModelSerializer |
10 | 12 | from pypi_attestations import AttestationError |
11 | 13 | from pydantic import TypeAdapter, ValidationError |
12 | 14 | from urllib.parse import urljoin |
13 | 15 |
|
| 16 | +# todo: cannot import from pulpcore.plugin! |
| 17 | +from pulpcore.app.serializers import NestedIdentityField |
| 18 | + |
14 | 19 | from pulpcore.plugin import models as core_models |
15 | 20 | from pulpcore.plugin import serializers as core_serializers |
16 | 21 | from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user |
@@ -780,6 +785,106 @@ class Meta: |
780 | 785 | model = python_models.PythonRemote |
781 | 786 |
|
782 | 787 |
|
| 788 | +class PythonBlocklistEntrySerializer( |
| 789 | + core_serializers.ModelSerializer, NestedHyperlinkedModelSerializer |
| 790 | +): |
| 791 | + """ |
| 792 | + Serializer for PythonBlocklistEntry. |
| 793 | +
|
| 794 | + The `repository` is supplied by the URL (not the request body) and is injected |
| 795 | + by the viewset before saving. |
| 796 | + """ |
| 797 | + |
| 798 | + pulp_href = NestedIdentityField( |
| 799 | + view_name="blocklist_entries-detail", |
| 800 | + parent_lookup_kwargs={"repository_pk": "repository__pk"}, |
| 801 | + ) |
| 802 | + repository = core_serializers.DetailRelatedField( |
| 803 | + read_only=True, |
| 804 | + view_name_pattern=r"repositories(-.*/.*)?-detail", |
| 805 | + help_text=_("Repository this blocklist entry belongs to."), |
| 806 | + ) |
| 807 | + name = serializers.CharField( |
| 808 | + required=False, |
| 809 | + allow_null=True, |
| 810 | + default=None, |
| 811 | + help_text=_( |
| 812 | + "Package name to block (for all versions). Compared after PEP 503 normalization. " |
| 813 | + "Required when 'filename' is not provided." |
| 814 | + ), |
| 815 | + ) |
| 816 | + version = serializers.CharField( |
| 817 | + required=False, |
| 818 | + allow_null=True, |
| 819 | + default=None, |
| 820 | + help_text=_("Exact version string to block (e.g. '1.0'). Only used when 'name' is set."), |
| 821 | + ) |
| 822 | + filename = serializers.CharField( |
| 823 | + required=False, |
| 824 | + allow_null=True, |
| 825 | + default=None, |
| 826 | + help_text=_("Exact filename to block. Required when 'name' is not provided."), |
| 827 | + ) |
| 828 | + added_by = serializers.CharField(read_only=True) |
| 829 | + |
| 830 | + def validate(self, data): |
| 831 | + """ |
| 832 | + Validate that the blocklist entry is well-formed and not a duplicate. |
| 833 | + """ |
| 834 | + name = data.get("name") |
| 835 | + filename = data.get("filename") |
| 836 | + version = data.get("version") |
| 837 | + |
| 838 | + if version and filename: |
| 839 | + raise serializers.ValidationError(_("'version' cannot be used with 'filename'.")) |
| 840 | + if version and not name: |
| 841 | + raise serializers.ValidationError(_("'version' requires 'name' to be provided.")) |
| 842 | + if name and filename: |
| 843 | + raise serializers.ValidationError(_("'name' and 'filename' are mutually exclusive.")) |
| 844 | + if not name and not filename: |
| 845 | + raise serializers.ValidationError(_("Either 'name' or 'filename' must be provided.")) |
| 846 | + |
| 847 | + if version: |
| 848 | + try: |
| 849 | + Version(version) |
| 850 | + except InvalidVersion: |
| 851 | + raise serializers.ValidationError( |
| 852 | + {"version": _("'{}' is not a valid version.").format(version)} |
| 853 | + ) |
| 854 | + |
| 855 | + repository = self.context.get("repository") |
| 856 | + if repository: |
| 857 | + qs = python_models.PythonBlocklistEntry.objects.filter(repository=repository) |
| 858 | + if name and qs.filter(name=name, version=version).exists(): |
| 859 | + raise serializers.ValidationError( |
| 860 | + _("A blocklist entry with this name and version already exists.") |
| 861 | + ) |
| 862 | + if filename and qs.filter(filename=filename).exists(): |
| 863 | + raise serializers.ValidationError( |
| 864 | + _("A blocklist entry with this filename already exists.") |
| 865 | + ) |
| 866 | + |
| 867 | + return data |
| 868 | + |
| 869 | + def create(self, validated_data): |
| 870 | + """ |
| 871 | + Create a new blocklist entry, recording the authenticated user in `added_by`. |
| 872 | + """ |
| 873 | + user = get_current_authenticated_user() # todo? |
| 874 | + validated_data["added_by"] = user.username if user else "" |
| 875 | + return super().create(validated_data) |
| 876 | + |
| 877 | + class Meta: |
| 878 | + fields = core_serializers.ModelSerializer.Meta.fields + ( |
| 879 | + "repository", |
| 880 | + "name", |
| 881 | + "version", |
| 882 | + "filename", |
| 883 | + "added_by", |
| 884 | + ) |
| 885 | + model = python_models.PythonBlocklistEntry |
| 886 | + |
| 887 | + |
783 | 888 | class PythonBanderRemoteSerializer(serializers.Serializer): |
784 | 889 | """ |
785 | 890 | A Serializer for the initial step of creating a Python Remote from a Bandersnatch config file |
|
0 commit comments