I am quite new to Django. I am working on a django app which needs to store films, and when I try to load film instances with the following program:
import os
import json
import requests
# Assuming the JSON content is stored in a variable named 'films_json'
json_file_path: str = os.path.join(os.getcwd(), "films.json")
# Load the JSON content from the file
with open(json_file_path, "r") as file:
films_json: dict = json.load(file)
# URL of the endpoint
endpoint_url = "http://127.0.0.1:8000/site-admin/add-film/"
for film_data in films_json["films"]:
print()
print(json.dumps(film_data, indent=4))
response: requests.Response = requests.post(endpoint_url, json=film_data)
if response.status_code == 201:
print(f"Film '{film_data['name']}' added successfully")
else:
print(f"Failed to add film '{film_data['name']}':")
print(json.dumps(dict(response.headers),indent=4))
print(f"{json.dumps(response.json(), indent=4)}")
I get the following response (for each film):
{
"name": "Pulp Fiction",
"genre": "Crime",
"duration": 154,
"director": "Quentin Tarantino",
"cast": [
"John Travolta",
"Uma Thurman",
"Samuel L. Jackson",
"Bruce Willis"
],
"description": "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.",
"image_url": "https://m.media-amazon.com/images/M/MV5BNGNhMDIzZTUtNTBlZi00MTRlLWFjM2ItYzViMjE3YzI5MjljXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg",
"release": "1994-00-00"
}
Failed to add film 'Pulp Fiction':
Bad Request
{
"Date": "Fri, 03 May 2024 22:35:32 GMT",
"Server": "WSGIServer/0.2 CPython/3.10.10",
"Content-Type": "application/json",
"Vary": "Accept, Cookie",
"Allow": "POST, OPTIONS",
"djdt-store-id": "bf9dc157db354419a70d69f3be5e8dd7",
"Server-Timing": "TimerPanel_utime;dur=0;desc=\"User CPU time\", TimerPanel_stime;dur=0;desc=\"System CPU time\", TimerPanel_total;dur=0;desc=\"Total CPU time\", TimerPanel_total_time;dur=50.99039999186061;desc=\"Elapsed time\", SQLPanel_sql_time;dur=2.606799971545115;desc=\"SQL 14 queries\", CachePanel_total_time;dur=0;desc=\"Cache 0 Calls\"",
"X-Frame-Options": "DENY",
"Content-Length": "197",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "same-origin",
"Cross-Origin-Opener-Policy": "same-origin"
}
{
"release": [
"Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
],
"director_id": [
"Invalid pk \"1\" - object does not exist."
],
"cast": [
"Invalid pk \"2\" - object does not exist."
]
}
This is my view:
class AggregateFilmView(generics.CreateAPIView):
serializer_class: type = FilmSerializer
def post(self, request) -> Response:
print(f"### {type(request.data)}", file=sys.stderr)
with transaction.atomic(): # Ensure all or nothing persistence
try:
film_data: dict = request.data
# Handle Director
if "director" not in film_data.keys():
raise ValidationError("director field must be provided")
director_name: str = film_data.pop("director", {})
if not type(director_name) is str:
raise ValidationError("diretor name must be a string")
director_data: dict[str, str] = {"name": director_name}
director_serializer = DirectorSerializer(data=director_data)
director_serializer.is_valid(raise_exception=True)
director: Director = director_serializer.save()
# Handle Cast (Actors)
if "cast" not in film_data.keys():
raise ValidationError("cast field must be provided")
cast_names: list[str] = film_data.pop("cast", [])
if type(cast_names) is not list:
raise ValidationError("cast must be a list of names (list[string])")
cast_data: list[dict[str, str]] = []
if not len(cast_names) == 0:
for name in cast_names:
if not type(name) is str:
raise ValidationError(
"cast must be a list of names (list[string])"
)
actor_data: dict[str, str] = {"name": name}
cast_data.append(actor_data)
actor_serializer = ActorSerializer(data=cast_data, many=True)
actor_serializer.is_valid(raise_exception=True)
actors: Actor = actor_serializer.save()
# Handle Film
film_data["director_id"] = director.id # director ID
film_data["cast"] = [actor.id for actor in actors] # list of actor IDs
types: list[type] = [type(field) for field in film_data.values()]
print(types, file=sys.stderr)
print(film_data, file=sys.stderr)
film_serializer = FilmSerializer(data=film_data)
film_serializer.is_valid(raise_exception=True)
film_serializer.save()
response: Response = Response(
{"detail": "Film added succesfully"}, status=status.HTTP_201_CREATED
)
except ValidationError as error:
response = Response(
{"error": error.message},
status=status.HTTP_400_BAD_REQUEST,
)
# Return serialized data (customize as needed)
return response
This is my serializer:
class FilmSerializer(serializers.ModelSerializer):
class Meta:
model = Film
# fields: str = "__all__"
fields: list[str] = [
"name",
"release",
"genre",
"description",
"duration",
"director_id",
"cast",
]
def validate_name(self, value) -> str:
print(f"Name validation {value}", file=sys.stderr)
if value is None:
raise serializers.ValidationError("Film name is required")
return value
def validate_release(self, value) -> str:
print(f"Date validation {value}", file=sys.stderr)
if value is None:
raise serializers.ValidationError("Release year is required")
try:
patterns: dict[str, str] = {
"%Y": r"^\d{4}$",
"%Y-%m": r"^\d{4}-\d{2}$",
"%Y-%m-%d": r"^\d{4}-\d{2}-\d{2}$",
"%m-%Y": r"^\d{2}-\d{4}$",
"%d-%m-%Y": r"^\d{2}-\d{2}-\d{4}$",
}
format_match: bool = False
for date_format, pattern in patterns.items():
if re.match(pattern, value):
format_match = True
print(f"Matched {date_format} format", file=sys.stderr)
return datetime.datetime.strptime(value, date_format)
if not format_match:
raise serializers.ValidationError(
"Invalid date format.",
"Accepted formats: %Y, %Y-%m, %Y-%m-%d, %m-%Y, %d-%m-%Y",
)
except ValueError:
raise serializers.ValidationError("Year must be a valid date")
return value
def validate_genre(self, value) -> str:
print(f"Genre validation {value}", file=sys.stderr)
if value is None:
raise serializers.ValidationError("Genre is required")
if value not in Film.GENRE_CHOICES:
raise serializers.ValidationError("Invalid genre")
return value
def validate_description(self, value) -> str:
print(f"Description validation {value}", file=sys.stderr)
if value is None:
raise serializers.ValidationError("Description is required")
if len(value) > 300:
raise serializers.ValidationError(
"Description cannot exceed 300 characters"
)
return value
def validate_director_id(self, value) -> str:
print(f"Director Id {value}", file=sys.stderr)
if value is None:
raise serializers.ValidationError("Director is required")
try:
Director.objects.get(pk=value)
except Director.DoesNotExist:
raise serializers.ValidationError("Invalid director ID")
return value
def validate_cast(self, value) -> str:
print(f"Cast {value}", file=sys.stderr)
if value is None:
raise serializers.ValidationError("At least one cast member is required")
try:
Actor.objects.filter(pk__in=value)
except Actor.DoesNotExist:
raise serializers.ValidationError("Invalid cast ID")
And this is my model:
class Film(models.Model):
GENRE_CHOICES: list[str] = [
"Action",
"Comedy",
"Crime",
"Doctumentary",
"Drama",
"Horror",
"Romance",
"Sci-Fi",
"Thriller",
"Western",
]
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=64, unique=True)
release = models.DateField()
genre = models.CharField(max_length=64)
description = models.TextField()
duration = models.FloatField()
director_id = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="director"
)
cast = models.ManyToManyField(User, related_name="cast")
The problem is that the release field is getting refused by a validation different from the one I have defined in the serializer, and the invalidation it is reporting should be avoided by the modifications in the value that my validate_release method is supposed to do.
The thing is that the validate_release method is not getting called, as can be seen in the terminal running the server:
### <class 'dict'>
[<class 'str'>, <class 'str'>, <class 'int'>, <class 'str'>, <class 'str'>, <class 'str'>, <class 'int'>, <class 'list'>]
{'name': 'Pulp Fiction', 'genre': 'Crime', 'duration': 154, 'description': "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", 'image_url': 'https://m.media-amazon.com/images/M/MV5BNGNhMDIzZTUtNTBlZi00MTRlLWFjM2ItYzViMjE3YzI5MjljXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg', 'release': '1994-00-00', 'director_id': 1, 'cast': [2, 3, 4, 5]}
Name validation Pulp Fiction
Genre validation Crime
Description validation The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.
Bad Request: /site-admin/add-film/
[04/May/2024 00:36:34] "POST /site-admin/add-film/ HTTP/1.1" 400 197
any ideas on how to fix this?
Because ModelSerializer
will map release
field to a DateField
. This way the validation is made against the default input date formats set by the framework.
In order to override that, you need to set your own valid inputs list (I will do it at settings.py, but of course can be anywhere).
DATE_INPUT_FORMATS = [
"%Y",
"%Y-%m",
"%m-%Y",
"%Y-%m-%d",
"%d-%m-%Y",
]
serializers.py
class FilmSerializer(serializers.ModelSerializer):
release = serializers.DateField(input_formats=settings.DATE_INPUT_FORMATS)
class Meta:
model = Film
fields = "__all__"