Skip to content

Commit e0def5b

Browse files
Add base64 image field
1 parent 8fe71e7 commit e0def5b

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ dependencies = [
5252
"unicef-security>=1.5.1",
5353
"django-pghistory>=3.5.4",
5454
"django-anymail[mailjet]>=13.0",
55+
"pillow>=11.2.1",
5556
]
5657
[project.scripts]
5758
celery-monitor = "country_workspace.__monitor__:run"

src/country_workspace/utils/flex_fields.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
from base64 import b64encode
12
import hashlib
23
import json
34
from typing import TYPE_CHECKING, Generator
45

6+
from django import forms
7+
from django.core.files.uploadedfile import UploadedFile
8+
59
from hope_flex_fields.models import DataChecker
610

11+
from country_workspace.contrib.kobo.api.data.helpers import VALUE_FORMAT
12+
713
if TYPE_CHECKING:
814
from country_workspace.models.base import Validable
915

@@ -21,3 +27,23 @@ def get_obj_checksum(obj: "Validable") -> str:
2127
if obj.flex_files:
2228
h.update(obj.flex_files[:8192]) # is this enough ?
2329
return h.hexdigest()
30+
31+
32+
class Base64ImageInput(forms.ClearableFileInput):
33+
template_name = "workspace/base64_image_widget.html"
34+
35+
def is_initial(self, value: str | None) -> bool:
36+
# we need to override this as base method looks for url
37+
return bool(value)
38+
39+
40+
class Base64ImageField(forms.ImageField):
41+
widget = Base64ImageInput
42+
43+
def clean(self, data: UploadedFile, initial: str | None = None) -> str | None:
44+
if cleaned_data := super().clean(data, initial):
45+
content = b64encode(cleaned_data.read()).decode()
46+
return VALUE_FORMAT.format(mimetype=data.content_type, content=content)
47+
48+
# if we return cleaned_data here, False will be stored, so we return None explicitly
49+
return None
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by HCW 0.1.0 on 2025 04 15 09:25:45
2+
from concurrency.utils import fqn
3+
from hope_flex_fields.models import FieldDefinition
4+
from hope_flex_fields.registry import field_registry
5+
from hope_flex_fields.utils import get_kwargs_from_field_class, get_common_attrs
6+
from packaging.version import Version
7+
8+
from country_workspace.utils.flex_fields import Base64ImageField
9+
10+
_script_for_version = Version("0.1.0")
11+
12+
13+
def forward() -> None:
14+
field_registry.register(Base64ImageField)
15+
FieldDefinition.objects.get_or_create(
16+
name=Base64ImageField.__name__,
17+
field_type=fqn(Base64ImageField),
18+
defaults={"attrs": get_kwargs_from_field_class(Base64ImageField, get_common_attrs())},
19+
)
20+
21+
22+
def backward() -> None:
23+
FieldDefinition.objects.get(
24+
name=Base64ImageField.__name__,
25+
field_type=fqn(Base64ImageField),
26+
).delete()
27+
28+
29+
class Scripts:
30+
requires = []
31+
operations = [(forward, backward)]

src/country_workspace/workspaces/admin/hh_ind.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from ..models import CountryHousehold, CountryIndividual
2323
from .cleaners import actions
2424
from .cleaners.validate import validate_program
25-
25+
from ...utils.flex_fields import Base64ImageField
2626

2727
if TYPE_CHECKING:
2828
from hope_flex_fields.forms import FlexForm
@@ -225,13 +225,13 @@ def _changeform_view(
225225
elif not self.has_view_or_change_permission(request, obj):
226226
raise PermissionDenied
227227

228-
if obj.flex_fields:
228+
if hasattr(obj, "flex_fields"):
229229
initials = {k.replace("flex_fields__", ""): v for k, v in obj.flex_fields.items()}
230230
else:
231231
initials = {}
232232
if request.method == "POST":
233233
if obj:
234-
form: "FlexForm" = form_class(request.POST, prefix="flex_field", initial=initials)
234+
form: "FlexForm" = form_class(request.POST, request.FILES, prefix="flex_field", initial=initials)
235235
form_valid = form.is_valid()
236236
if form_valid or "_save_invalid" in request.POST:
237237
obj.flex_fields = form.cleaned_data
@@ -248,6 +248,7 @@ def _changeform_view(
248248
context["show_save_invalid"] = True
249249
context["checker_form"] = form
250250
context["has_change_permission"] = self.has_change_permission(request)
251+
context["has_file_field"] = any(isinstance(field, Base64ImageField) for field in form.fields.values())
251252

252253
return TemplateResponse(request, self.change_form_template, context)
253254

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% if widget.is_initial %}
2+
{{ widget.initial_text }}:
3+
<img src="{{ widget.value }}" style="max-height: 100px; width: auto">
4+
{% if not widget.required %}
5+
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"
6+
{% if widget.attrs.disabled %} disabled{% endif %}
7+
{% if widget.attrs.checked %} checked{% endif %}>
8+
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>
9+
{% endif %}<br>
10+
{{ widget.input_text }}:
11+
{% endif %}
12+
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>

uv.lock

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)