Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,11 +533,10 @@ class SimplePhotoSerializer(serializers.ModelSerializer):
source="photo", use_url=True, read_only=True,
help_text="URL of the photo associated with the item. Note: This URL may change over time. Do not rely on it for permanent storage."
)
file = serializers.ImageField(required=True, source="photo", write_only=True)

class Meta:
model = Photo
fields = ("uuid", "url", "file")
fields = ("uuid", "url")
read_only_fields = (
"uuid",
)
Expand Down Expand Up @@ -736,19 +735,46 @@ class Meta:
read_only_fields = fields

class BaseReportWithPhotosSerializer(BaseReportSerializer):
photos = SimplePhotoSerializer(required=True, many=True)

def get_fields(self):
fields = super().get_fields()
request = self.context.get("request")

# Use different field behavior depending on request method
if request and request.method in ("POST", "PUT", "PATCH"):
# Write mode — accept uploaded image files
fields["photos"] = serializers.ListField(
child=serializers.ImageField(required=True),
write_only=True,
min_length=1,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max 3?

)
else:
# Read mode — return nested photo serializer
fields["photos"] = SimplePhotoSerializer(many=True, read_only=True)

return fields

@transaction.atomic
def create(self, validated_data):
photos = validated_data.pop("photos", [])

instance = super().create(validated_data)

# NOTE: do not use bulk here.
for photo in photos:
_ = Photo.objects.create(report=instance, **photo)
_ = Photo.objects.create(report=instance, photo=photo)

return instance

def to_representation(self, instance):
"""
Always serialize output using the read-only `photos` definition,
even if this serializer was initialized in write mode.
"""
# Rebind `photos` temporarily for output
self.fields["photos"] = SimplePhotoSerializer(many=True, read_only=True)
return super().to_representation(instance)

class Meta(BaseReportSerializer.Meta):
fields = BaseReportSerializer.Meta.fields + ("photos",)

Expand Down
6 changes: 3 additions & 3 deletions api/tests/integration/breeding_sites/create.tavern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ stages:
Authorization: "Bearer {app_user_token:s}"
method: "POST"
files:
photos[0]file: "{test_jpg_image_path}"
photos[1]file: "{test_png_image_path}"
photos[0]: "{test_jpg_image_path}"
photos[1]: "{test_png_image_path}"
data: &request_site_data
created_at: '2024-01-01T00:00:00Z'
sent_at: '2024-01-01T00:30:00Z'
Expand Down Expand Up @@ -112,6 +112,6 @@ stages:
request:
<<: *request_breeding_site
files:
photos[0]file: "{test_non_image_path}"
photos[0]: "{test_non_image_path}"
response:
status_code: 400
6 changes: 3 additions & 3 deletions api/tests/integration/observations/create.tavern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ stages:
Authorization: "Bearer {app_user_token:s}"
method: "POST"
files:
photos[0]file: "{test_jpg_image_path}"
photos[1]file: "{test_png_image_path}"
photos[0]: "{test_jpg_image_path}"
photos[1]: "{test_png_image_path}"
data: &request_site_data
created_at: '2024-01-01T00:00:00Z'
sent_at: '2024-01-01T00:30:00Z'
Expand Down Expand Up @@ -114,6 +114,6 @@ stages:
request:
<<: *request_observation
files:
photos[0]file: "{test_non_image_path}"
photos[0]: "{test_non_image_path}"
response:
status_code: 400
24 changes: 24 additions & 0 deletions tigaserver_app/migrations/0086_auto_20251016_1353.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.25 on 2025-10-16 13:53

from django.db import migrations, models
import tigaserver_app.models


class Migration(migrations.Migration):

dependencies = [
('tigaserver_app', '0085_auto_20250822_1214'),
]

operations = [
migrations.AlterField(
model_name='historicalreport',
name='report_id',
field=models.CharField(db_index=True, default=tigaserver_app.models.generate_report_id, help_text='4-digit alpha-numeric code generated on user phone to identify each unique report from that user. Digits should lbe randomly drawn from the set of all lowercase and uppercase alphabetic characters and 0-9, but excluding 0, o, and O to avoid confusion if we ever need user to be able to refer to a report ID in correspondence with MoveLab (as was previously the case when we had them sending samples).', max_length=4),
),
migrations.AlterField(
model_name='report',
name='report_id',
field=models.CharField(db_index=True, default=tigaserver_app.models.generate_report_id, help_text='4-digit alpha-numeric code generated on user phone to identify each unique report from that user. Digits should lbe randomly drawn from the set of all lowercase and uppercase alphabetic characters and 0-9, but excluding 0, o, and O to avoid confusion if we ever need user to be able to refer to a report ID in correspondence with MoveLab (as was previously the case when we had them sending samples).', max_length=4),
),
]
6 changes: 6 additions & 0 deletions tigaserver_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from PIL import Image
import pydenticon
import os
import random
from slugify import slugify
import string
from typing import List, Optional, Union
import uuid

Expand Down Expand Up @@ -781,6 +783,9 @@ class UUIDTaggedItem(GenericUUIDTaggedItemBase, TaggedItemBase):
class Meta(GenericUUIDTaggedItemBase.Meta, TaggedItemBase.Meta):
abstract = False

def generate_report_id():
return ''.join(random.choices(string.ascii_letters + string.digits, k=4))

class Report(TimeZoneModelMixin, models.Model):
TYPE_BITE = "bite"
TYPE_ADULT = "adult"
Expand Down Expand Up @@ -849,6 +854,7 @@ class Report(TimeZoneModelMixin, models.Model):
report_id = models.CharField(
max_length=4,
db_index=True,
default=generate_report_id,
help_text="4-digit alpha-numeric code generated on user phone to identify each unique report from that user. Digits should lbe randomly drawn from the set of all lowercase and uppercase alphabetic characters and 0-9, but excluding 0, o, and O to avoid confusion if we ever need user to be able to refer to a report ID in correspondence with MoveLab (as was previously the case when we had them sending samples).",
)

Expand Down