feat: add django-channel and daphne

This commit is contained in:
2026-05-25 00:42:34 +02:00
parent f543cdcd5c
commit c2602dbc02
8 changed files with 1158 additions and 43 deletions
+44 -7
View File
@@ -1,5 +1,20 @@
from django.contrib import admin
from .models import Submission, SubmissionFile
from .models import Submission, SubmissionFile, ImageCrop
class ImageCropInline(admin.TabularInline):
model = ImageCrop
extra = 1
fields = [
"label",
"x1",
"y1",
"x2",
"y2",
"ocr_status",
"created_at",
]
readonly_fields = ["created_at"]
class SubmissionFileInline(admin.TabularInline):
@@ -9,15 +24,12 @@ class SubmissionFileInline(admin.TabularInline):
"original_filename",
"file_size",
"content_type",
"ocr_processed",
"ocr_error",
"ocr_status",
"created_at",
]
readonly_fields = [
"file_size",
"content_type",
"ocr_processed",
"ocr_error",
"created_at",
]
@@ -54,12 +66,13 @@ class SubmissionFileAdmin(admin.ModelAdmin):
"submission_id",
"file_size",
"content_type",
"ocr_processed",
"ocr_status",
"created_at",
]
list_filter = ["content_type", "ocr_processed", "created_at"]
list_filter = ["content_type", "ocr_status", "created_at"]
search_fields = ["original_filename", "submission__id"]
readonly_fields = ["file_size", "content_type", "created_at", "updated_at"]
inlines = [ImageCropInline]
def get_queryset(self, request):
return super().get_queryset(request).select_related("submission")
@@ -67,3 +80,27 @@ class SubmissionFileAdmin(admin.ModelAdmin):
@admin.display(description="Submission ID")
def submission_id(self, obj):
return str(obj.submission.id)[:8]
@admin.register(ImageCrop)
class ImageCropAdmin(admin.ModelAdmin):
list_display = [
"label_or_coords",
"file_name",
"ocr_status",
"created_at",
]
list_filter = ["ocr_status", "created_at"]
search_fields = ["label", "submission_file__original_filename"]
readonly_fields = ["created_at", "updated_at"]
def get_queryset(self, request):
return super().get_queryset(request).select_related("submission_file")
@admin.display(description="Crop")
def label_or_coords(self, obj):
return obj.label or f"({obj.x1}, {obj.y1}, {obj.x2}, {obj.y2})"
@admin.display(description="File")
def file_name(self, obj):
return obj.submission_file.original_filename
@@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2026-05-24 21:34
# Generated by Django 6.0.5 on 2026-05-24 22:37
import django.db.models.deletion
import submissions.models
@@ -27,6 +27,8 @@ class Migration(migrations.Migration):
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"notes",
models.TextField(
@@ -35,8 +37,6 @@ class Migration(migrations.Migration):
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
@@ -49,6 +49,9 @@ class Migration(migrations.Migration):
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="SubmissionFile",
@@ -62,6 +65,8 @@ class Migration(migrations.Migration):
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"file",
models.FileField(
@@ -85,26 +90,25 @@ class Migration(migrations.Migration):
models.CharField(help_text="MIME type of the file", max_length=100),
),
(
"ocr_processed",
models.BooleanField(
default=False,
help_text="Whether OCR has been processed for this file",
"ocr_status",
models.CharField(
choices=[
("to_process", "To Process"),
("processing", "Processing"),
("processed", "Processed"),
("error", "Error processing"),
],
default="to_process",
help_text="OCR processing status for the entire file",
max_length=20,
),
),
(
"ocr_raw_data",
models.JSONField(
blank=True, help_text="Raw OCR data as JSON", null=True
),
),
(
"ocr_error",
models.TextField(
blank=True, help_text="OCR processing error message"
blank=True, help_text="Raw OCR data for the entire file"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"submission",
models.ForeignKey(
@@ -120,4 +124,71 @@ class Migration(migrations.Migration):
"ordering": ["file", "created_at"],
},
),
migrations.CreateModel(
name="ImageCrop",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("x1", models.PositiveIntegerField(help_text="Top-left x coordinate")),
("y1", models.PositiveIntegerField(help_text="Top-left y coordinate")),
(
"x2",
models.PositiveIntegerField(help_text="Bottom-right x coordinate"),
),
(
"y2",
models.PositiveIntegerField(help_text="Bottom-right y coordinate"),
),
(
"label",
models.CharField(
blank=True,
help_text="Optional label for this crop region",
max_length=255,
),
),
(
"ocr_status",
models.CharField(
choices=[
("to_process", "To Process"),
("processing", "Processing"),
("processed", "Processed"),
("error", "Error processing"),
],
default="to_process",
help_text="OCR processing status for this crop",
max_length=20,
),
),
(
"ocr_raw_data",
models.TextField(
blank=True, help_text="Raw OCR data for this crop"
),
),
(
"submission_file",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="crops",
to="submissions.submissionfile",
),
),
],
options={
"verbose_name": "Image Crop",
"verbose_name_plural": "Image Crops",
"ordering": ["created_at"],
},
),
]
+69 -18
View File
@@ -6,6 +6,13 @@ import uuid
from accounts.models import CustomUser
class OCRStatus(models.TextChoices):
TO_PROCESS = "to_process", _("To Process")
PROCESSING = "processing", _("Processing")
PROCESSED = "processed", _("Processed")
ERROR = "error", _("Error processing")
def submission_file_upload_path(instance, filename):
ext = filename.split(".")[-1] if "." in filename else ""
@@ -14,9 +21,18 @@ def submission_file_upload_path(instance, filename):
return f"submissions/{instance.submission.id}/{new_filename}"
class Submission(models.Model):
class BaseModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Submission(BaseModel):
user = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
@@ -33,14 +49,8 @@ class Submission(models.Model):
help_text="Optional notes about the submission",
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SubmissionFile(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class SubmissionFile(BaseModel):
submission = models.ForeignKey(
Submission,
on_delete=models.CASCADE,
@@ -53,7 +63,8 @@ class SubmissionFile(models.Model):
)
original_filename = models.CharField(
max_length=255, help_text=_("Original filename as uploaded by user")
max_length=255,
help_text=_("Original filename as uploaded by user"),
)
file_size = models.PositiveIntegerField(help_text=_("File size in bytes"))
content_type = models.CharField(
@@ -62,17 +73,16 @@ class SubmissionFile(models.Model):
)
# OCR metadata
ocr_processed = models.BooleanField(
default=False, help_text="Whether OCR has been processed for this file"
ocr_status = models.CharField(
max_length=20,
choices=OCRStatus.choices,
default=OCRStatus.TO_PROCESS,
help_text=_("OCR processing status for the entire file"),
)
ocr_raw_data = models.JSONField(
null=True, blank=True, help_text="Raw OCR data as JSON"
ocr_raw_data = models.TextField(
blank=True,
help_text=_("Raw OCR data for the entire file"),
)
ocr_error = models.TextField(blank=True, help_text="OCR processing error message")
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["file", "created_at"]
@@ -89,3 +99,44 @@ class SubmissionFile(models.Model):
self.file_size = self.file.size
super().save(*args, **kwargs)
class ImageCrop(BaseModel):
submission_file = models.ForeignKey(
SubmissionFile,
on_delete=models.CASCADE,
related_name="crops",
)
# Crop coordinates (pixels): x1, y1, x2, y2
x1 = models.PositiveIntegerField(help_text=_("Top-left x coordinate"))
y1 = models.PositiveIntegerField(help_text=_("Top-left y coordinate"))
x2 = models.PositiveIntegerField(help_text=_("Bottom-right x coordinate"))
y2 = models.PositiveIntegerField(help_text=_("Bottom-right y coordinate"))
# Label/name for the crop
label = models.CharField(
max_length=255,
blank=True,
help_text=_("Optional label for this crop region"),
)
# OCR results for this crop
ocr_status = models.CharField(
max_length=20,
choices=OCRStatus.choices,
default=OCRStatus.TO_PROCESS,
help_text=_("OCR processing status for this crop"),
)
ocr_raw_data = models.TextField(
blank=True,
help_text=_("Raw OCR data for this crop"),
)
class Meta:
ordering = ["created_at"]
verbose_name = "Image Crop"
verbose_name_plural = "Image Crops"
def __str__(self):
return f"{self.label or f'Crop ({self.x1}, {self.y1}, {self.x2}, {self.y2})'}"
+68
View File
@@ -0,0 +1,68 @@
from django.tasks import task
from submissions.models import ImageCrop, SubmissionFile, OCRStatus
from PIL import Image
import pytesseract
def _process_ocr_file(file) -> bool:
if file.ocr_status != OCRStatus.TO_PROCESS:
return False
file.ocr_status = OCRStatus.PROCESSING
file.save(update_fields=["ocr_status"])
try:
file.ocr_raw_data = pytesseract.image_to_string(Image.open(file.file))
except Exception:
file.ocr_status = OCRStatus.ERROR
file.save(update_fields=["ocr_status"])
return False
finally:
file.ocr_status = OCRStatus.PROCESSED
file.save(update_fields=["ocr_status", "ocr_raw_data"])
return True
@task
def process_submission_files():
"""Process uploaded submission files (OCR, validation, etc.)"""
for file in SubmissionFile.objects.filter(ocr_status=OCRStatus.TO_PROCESS):
process_submission_file.enqueue(str(file.id))
@task
def process_submission_file(file_id: str):
file = SubmissionFile.objects.filter(id=file_id).first()
if not file:
return
_process_ocr_file(file)
process_crops.enqueue(file_id)
@task
def process_crops(file_id: str):
file = SubmissionFile.objects.filter(id=file_id).first()
if not file:
return
for crop in file.crops.all():
process_crop.enqueue(str(crop.id))
@task
def process_crop(crop_id: str):
crop = ImageCrop.objects.filter(id=crop_id).first()
if not crop:
return
_process_ocr_file(crop)