Skip to content

Commit

Permalink
mass_update base action
Browse files Browse the repository at this point in the history
  • Loading branch information
saxix committed Oct 15, 2024
1 parent 706bbf4 commit 6c557c6
Show file tree
Hide file tree
Showing 15 changed files with 3,794 additions and 267 deletions.
233 changes: 6 additions & 227 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,232 +1,11 @@
# syntax=docker/dockerfile:1
ARG PYTHON_VER=3.12
ARG PKG_DIR=/code/__pypackages__/${PYTHON_VER}
ARG CHECKSUM
ARG VERSION=0.1.0
ARG BUILD_DATE=not_provided
FROM python:3.12-slim-bookworm
COPY uv.lock pyproject.toml
COPY conf /conf
COPY bin /usr/local/bin/

ARG APATH=${PKG_DIR}/bin
ARG APYTHONPATH=${PKG_DIR}/lib/
RUN pip imstall uv \
&& uv sync

FROM python:${PYTHON_VER}-slim-bookworm AS python_base
ARG APATH
ENV APATH=$APATH
ARG APYTHONPATH
ENV APYTHONPATH=$APYTHONPATH
ARG PKG_DIR
ENV PKG_DIR=$PKG_DIR

ARG CHECKSUM
ENV CHECKSUM=$CHECKSUM
ARG VERSION
ENV VERSION=$VERSION
ARG BUILD_DATE
ENV BUILD_DATE=$BUILD_DATE
ARG SOURCE_COMMIT
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ARG GITHUB_SERVER_URL
ENV GITHUB_SERVER_URL=$GITHUB_SERVER_URL
ARG GITHUB_REPOSITORY
ENV GITHUB_REPOSITORY=$GITHUB_REPOSITORY

ARG GOSU_VERSION=1.17
ARG GOSU_SHA256=bbc4136d03ab138b1ad66fa4fc051bafc6cc7ffae632b069a53657279a450de3
ARG TINI_VERSION=0.19.0
ARG TINI_SHA256=93dcc18adc78c65a028a84799ecf8ad40c936fdfc5f2a57b1acda5a8117fa82c
ARG WAITFOR_IT_VERSION=2.4.1
ARG WAITFOR_IT_MD5=cd67c8e45436c4a7b2b707d7a5b15a66


RUN set -x \
&& buildDeps=" \
wget \
" \
&& apt-get update && apt-get install -y --no-install-recommends ${buildDeps} \
&& rm -rf /var/lib/apt/lists/* \
&& wget --quiet -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-amd64" \
&& echo "$GOSU_SHA256 /usr/local/bin/gosu" | sha256sum --check --status \
&& chmod +x /usr/local/bin/gosu \
&& wget --quiet -O /usr/local/bin/tini "https://github.com/krallin/tini/releases/download/v$TINI_VERSION/tini-amd64" \
&& echo "$TINI_SHA256 /usr/local/bin/tini" | sha256sum --check --status \
&& chmod +x /usr/local/bin/tini \
&& wget --quiet -O /usr/local/bin/waitforit "https://github.com/maxcnunes/waitforit/releases/download/v$WAITFOR_IT_VERSION/waitforit-linux_amd64" \
&& echo "$WAITFOR_IT_MD5 /usr/local/bin/waitforit" | md5sum --check --status \
&& chmod +x /usr/local/bin/waitforit \
&& apt-get purge -y --auto-remove $buildDeps


RUN \
--mount=type=cache,target=/var/cache/apt \
apt-get clean \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
postgresql-client \
libgl1 \
libglib2.0-0 \
libffi8 \
libgif-dev \
libjpeg-dev \
libmagic1 \
libopenblas-dev \
libpng16-16 \
libxml2 \
libwebp-dev \
mime-support \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN groupadd --gid 1024 app \
&& adduser --disabled-login --disabled-password --no-create-home --ingroup app -q user \
&& echo $CHECKSUM > /CHECKSUM


COPY docker/bin/* /usr/local/bin/
COPY docker/conf/* /conf/

FROM python_base AS build_deps

RUN set -x \
&& buildDeps="build-essential \
cmake \
curl \
gcc \
git \
libgdal-dev \
libgif-dev \
libjpeg-dev \
liblapack-dev \
libopenblas-dev \
libpng-dev \
libpq-dev \
libwebp-dev \
libssl-dev \
libxml2-dev \
python3-dev \
zlib1g-dev \
" \
&& apt-get update \
&& apt-get install -y --no-install-recommends $buildDeps \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

ENV PATH=${APATH}:${PATH} \
PYTHONPATH=${APYTHONPATH}:/code/app/src \
PYTHONDONTWRITEBYTCODE=1


FROM build_deps AS python_dev_deps
ARG CHECKSUM
ENV CHECKSUM=$CHECKSUM
ARG VERSION
ENV VERSION=$VERSION
ARG BUILD_DATE
ENV BUILD_DATE=$BUILD_DATE
ARG DISTRO
ENV DISTRO=$DISTRO
ARG SOURCE_COMMIT
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ARG GITHUB_SERVER_URL
ENV GITHUB_SERVER_URL=$GITHUB_SERVER_URL
ARG GITHUB_REPOSITORY
ENV GITHUB_REPOSITORY=$GITHUB_REPOSITORY


LABEL date=$BUILD_DATE
LABEL version=$VERSION
LABEL checksum=$CHECKSUM
LABEL distro="test"

#COPY pyproject.toml pdm.lock ./
#COPY docker/conf/config.toml /etc/xdg/pdm/config.toml
COPY . /code
WORKDIR /code

RUN set -x \
&& pip install -U pip pdm \
&& mkdir -p $PKG_DIR \
&& pdm sync --no-editable -v --no-self

RUN <<EOF cat> /RELEASE
{"version": "$VERSION",
"commit": "$SOURCE_COMMIT",
"date": "$BUILD_DATE",
"distro": "test",
"checksum": "$CHECKSUM",
"source": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/tree/${SOURCE_COMMIT:-master}/"
}
EOF

FROM build_deps AS python_prod_deps
ARG PKG_DIR
ARG CHECKSUM
ENV CHECKSUM=$CHECKSUM
ARG VERSION
ENV VERSION=$VERSION
ARG BUILD_DATE
ENV BUILD_DATE=$BUILD_DATE
ARG SOURCE_COMMIT
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ARG GITHUB_SERVER_URL
ENV GITHUB_SERVER_URL=$GITHUB_SERVER_URL
ARG GITHUB_REPOSITORY
ENV GITHUB_REPOSITORY=$GITHUB_REPOSITORY

LABEL date=$BUILD_DATE
LABEL version=$VERSION
LABEL checksum=$CHECKSUM
LABEL distro="builder-prod"

COPY docker/conf/config.toml /etc/xdg/pdm/config.toml
#COPY pyproject.toml pdm.lock /README.md /LICENSE ./
#COPY ./src /code/src

COPY . /code
WORKDIR /code

RUN set -x \
&& pip install -U pip pdm \
&& mkdir -p $PKG_DIR \
&& pdm sync --no-editable -v --prod


FROM python_base AS dist

ARG PKG_DIR
ARG CHECKSUM
ENV CHECKSUM=$CHECKSUM
ARG VERSION
ENV VERSION=$VERSION
ARG BUILD_DATE
ENV BUILD_DATE=$BUILD_DATE
ARG SOURCE_COMMIT
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ARG GITHUB_SERVER_URL
ENV GITHUB_SERVER_URL=$GITHUB_SERVER_URL
ARG GITHUB_REPOSITORY
ENV GITHUB_REPOSITORY=$GITHUB_REPOSITORY


WORKDIR /code
COPY --chown=user:app --from=python_prod_deps /code/__pypackages__ /code/__pypackages__
COPY --chown=user:app --from=python_prod_deps /code/README.md /code/LICENSE /

ENV PATH=${APATH}:${PATH} \
PYTHONPATH=${APYTHONPATH} \
PYTHONDBUFFERED=1 \
PYTHONDONTWRITEBYTCODE=1

RUN <<EOF cat> /RELEASE
{"version": "$VERSION",
"commit": "$SOURCE_COMMIT",
"date": "$BUILD_DATE",
"distro": "dist",
"checksum": "$CHECKSUM",
"source": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/tree/${SOURCE_COMMIT:-master}/"
}
EOF

VOLUME /var/run/app/
EXPOSE 8000
ENTRYPOINT exec docker-entrypoint.sh "$0" "$@"
CMD ["run"]
Expand Down
5 changes: 4 additions & 1 deletion src/country_workspace/management/commands/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ def handle(self, *args: Any, **options: Any) -> None:
)

if settings.HOPE_API_TOKEN:
sync_all()
print("Syncing online")
with vcr.use_cassette(test_utils_dir.parent / "sync_all.yaml", record_mode=RecordMode.ALL):
sync_all()
else:
print("Syncing using cassette")
with vcr.use_cassette(test_utils_dir.parent / "sync_all.yaml", record_mode=RecordMode.NONE):
sync_all()

Expand Down
5 changes: 5 additions & 0 deletions src/country_workspace/types.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import TypeVar, Union

from country_workspace.models import Household, Individual

Beneficiary = Union[Household, Individual]
1 change: 1 addition & 0 deletions src/country_workspace/workspaces/admin/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .mass_update import mass_update # noqa
129 changes: 129 additions & 0 deletions src/country_workspace/workspaces/admin/actions/mass_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from typing import TYPE_CHECKING, Any, Callable

from django import forms
from django.db import transaction
from django.db.models import QuerySet
from django.forms import MultiValueField, widgets
from django.shortcuts import render
from django.utils.text import slugify
from django.utils.translation import gettext as _

from hope_flex_fields.fields import FlexFormMixin
from strategy_field.utils import fqn

if TYPE_CHECKING:
from hope_flex_fields.models import DataChecker

from country_workspace.types import Beneficiary
from country_workspace.workspaces.admin.hh_ind import CountryHouseholdIndividualBaseAdmin

MassUpdateFunc = Callable[[Any, Any], Any]
FormOperations = dict[str, tuple[str, str]]
Operation = tuple[Any, str, MassUpdateFunc]
Operations = dict[str, Operation]


class OperationManager:
COMMON = [
("set", None),
("set null", lambda old_value: None),
]

def __init__(
self,
):
self._dict: dict[str, "Operation"] = dict()
self._cache = {}

def register(self, target: Any, name: str, func: "MassUpdateFunc"):
unique = slugify(f"{fqn(target)}_{name}_{func.__name__}")
self._dict[unique] = (target, name, func)

def get_function_by_id(self, id) -> "MassUpdateFunc":
return self._dict.get(id)[2]

def get_choices_for_target(self, target):
ret = []
if target not in self._cache:
for id, attrs in self._dict.items():
if issubclass(target, attrs[0]):
ret.append([id, attrs[1]])
self._cache[target] = ret
return self._cache[target]


operations = OperationManager()
operations.register(forms.Field, "set", lambda old_value, new_value: new_value)
operations.register(forms.Field, "set null", lambda old_value, new_value: None)


class MassUpdateWidget(widgets.MultiWidget):
template_name = "actions/massupdatewidget.html"
is_required = False

def __init__(self, field: FlexFormMixin, attrs=None):
_widgets = (
widgets.CheckboxInput(),
widgets.Select(choices=operations.get_choices_for_target(field.flex_field.field.field_type)),
field.widget,
)
super().__init__(_widgets, attrs)

def decompress(self, value):
if value:
return [value, "", ""]
return [None, None, None]


class MassUpdateField(MultiValueField):
widget = MassUpdateWidget

def __init__(self, *, field, **kwargs):
field.required = False
fields = (forms.BooleanField(required=False), forms.CharField(required=False), field)
self.widget = MassUpdateWidget(field)
super().__init__(fields, require_all_fields=False, required=False, **kwargs)

def compress(self, data_list):
return data_list


class MassUpdateForm(forms.Form):
action = forms.CharField()
select_across = forms.BooleanField()
_selected_action = forms.CharField()

def __init__(self, *args, **kwargs):
checker: "DataChecker" = kwargs.pop("checker")
super().__init__(*args, **kwargs)
for name, fld in checker.get_form()().fields.items():
self.fields[f"flex_fields__{name}"] = MassUpdateField(field=fld)

def get_selected(self) -> "FormOperations":
ret = {}
for k, v in self.cleaned_data.items():
if k.startswith("flex_fields__") and v[0]:
ret[k.replace("flex_fields__", "")] = v[1:]
return ret


def mass_update_impl(records: "QuerySet[Beneficiary]", config: "FormOperations") -> None:
with transaction.atomic():
for record in records:
for field_name, attrs in config.items():
op, new_value = attrs
old_value = record.flex_fields[field_name]
func = operations.get_function_by_id(op)
record.flex_fields[field_name] = func(old_value, new_value)
record.save()


def mass_update(model_admin: "CountryHouseholdIndividualBaseAdmin", request, queryset):
ctx = model_admin.get_common_context(request, title=_("Mass update"))
ctx["checker"] = checker = model_admin.get_checker(request)
form = MassUpdateForm(request.POST, checker=checker)
ctx["form"] = form
if "_apply" in request.POST:
if form.is_valid():
mass_update_impl(queryset.all(), form.get_selected())
return render(request, "actions/mass_update.html", ctx)
File renamed without changes.
Loading

0 comments on commit 6c557c6

Please sign in to comment.