diff --git a/commcare_connect/program/__init__.py b/commcare_connect/program/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/program/admin.py b/commcare_connect/program/admin.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/program/apps.py b/commcare_connect/program/apps.py new file mode 100644 index 00000000..d34beb7f --- /dev/null +++ b/commcare_connect/program/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class ProgramConfig(AppConfig): + name = "commcare_connect.program" + verbose_name = _("Program") diff --git a/commcare_connect/program/forms.py b/commcare_connect/program/forms.py new file mode 100644 index 00000000..7803e441 --- /dev/null +++ b/commcare_connect/program/forms.py @@ -0,0 +1,50 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Field, Layout, Row, Submit +from django import forms + +from commcare_connect.program.models import Program + +HALF_WIDTH_FIELD = "form-group col-md-6 mb-0" +DATE_INPUT = forms.DateInput(format="%Y-%m-%d", attrs={"type": "date", "class": "form-control"}) + + +class ProgramForm(forms.ModelForm): + class Meta: + model = Program + fields = ["name", "description", "delivery_type", "budget", "currency", "start_date", "end_date"] + widgets = {"start_date": DATE_INPUT, "end_date": DATE_INPUT} + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.layout = Layout( + Row(Field("name")), + Row(Field("description")), + Row(Field("delivery_type")), + Row( + Field("budget", wrapper_class=HALF_WIDTH_FIELD), + Field("currency", wrapper_class=HALF_WIDTH_FIELD), + ), + Row( + Field("start_date", wrapper_class=HALF_WIDTH_FIELD), + Field("end_date", wrapper_class=HALF_WIDTH_FIELD), + ), + Submit("submit", "Submit"), + ) + + def clean(self): + cleaned_data = super().clean() + start_date = cleaned_data.get("start_date") + end_date = cleaned_data.get("end_date") + + if start_date and end_date and end_date <= start_date: + self.add_error("end_date", "End date must be after the start date.") + return cleaned_data + + def save(self, commit=True): + instance = super().save(commit=False) + if not instance.pk: + instance.created_by = self.user.email + instance.modified_by = self.user.email + return super().save(commit=commit) diff --git a/commcare_connect/program/migrations/0001_initial.py b/commcare_connect/program/migrations/0001_initial.py new file mode 100644 index 00000000..b6ba5baf --- /dev/null +++ b/commcare_connect/program/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.5 on 2024-08-01 03:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("opportunity", "0053_assessment_opportunity_access_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Program", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_by", models.CharField(max_length=255)), + ("modified_by", models.CharField(max_length=255)), + ("date_created", models.DateTimeField(auto_now_add=True)), + ("date_modified", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255, unique=True)), + ("description", models.CharField()), + ("budget", models.IntegerField()), + ("currency", models.CharField(max_length=3)), + ("start_date", models.DateField()), + ("end_date", models.DateField()), + ( + "delivery_type", + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="opportunity.deliverytype"), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/commcare_connect/program/migrations/__init__.py b/commcare_connect/program/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/program/models.py b/commcare_connect/program/models.py new file mode 100644 index 00000000..57f8a132 --- /dev/null +++ b/commcare_connect/program/models.py @@ -0,0 +1,20 @@ +from django.db import models + +from commcare_connect.opportunity.models import DeliveryType +from commcare_connect.utils.db import BaseModel, slugify_uniquely + + +class Program(BaseModel): + name = models.CharField(max_length=255) + slug = models.SlugField(max_length=255, unique=True) + description = models.CharField() + delivery_type = models.ForeignKey(DeliveryType, on_delete=models.PROTECT) + budget = models.IntegerField() + currency = models.CharField(max_length=3) + start_date = models.DateField() + end_date = models.DateField() + + def save(self, *args, **kwargs): + if not self.id: + self.slug = slugify_uniquely(self.name, self.__class__) + super().save(*args, **kwargs) diff --git a/commcare_connect/program/tests.py b/commcare_connect/program/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/program/urls.py b/commcare_connect/program/urls.py new file mode 100644 index 00000000..c3a13228 --- /dev/null +++ b/commcare_connect/program/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from commcare_connect.program.views import ProgramCreateOrUpdate, ProgramList + +app_name = "program" +urlpatterns = [ + path("", view=ProgramList.as_view(), name="list"), + path("init/", view=ProgramCreateOrUpdate.as_view(), name="init"), + path("/edit", view=ProgramCreateOrUpdate.as_view(), name="edit"), +] diff --git a/commcare_connect/program/views.py b/commcare_connect/program/views.py new file mode 100644 index 00000000..6e66b076 --- /dev/null +++ b/commcare_connect/program/views.py @@ -0,0 +1,68 @@ +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.urls import reverse +from django.views.generic import ListView, UpdateView + +from commcare_connect.program.forms import ProgramForm +from commcare_connect.program.models import Program + + +class SuperUserMixin(LoginRequiredMixin, UserPassesTestMixin): + def test_func(self): + return self.request.user.is_superuser + + +class ProgramList(SuperUserMixin, ListView): + model = Program + paginate_by = 10 + allowed_orderings = { + "name": "name", + "-name": "-name", + "start_date": "start_date", + "-start_date": "-start_date", + "end_date": "end_date", + "-end_date": "-end_date", + } + default_ordering = "name" + + def get_queryset(self): + ordering = self.request.GET.get("sort", self.default_ordering) + ordering = self.allowed_orderings.get(ordering, self.default_ordering) + return Program.objects.all().order_by(ordering) + + +class ProgramCreateOrUpdate(SuperUserMixin, UpdateView): + model = Program + form_class = ProgramForm + + def get_object(self, queryset=None): + pk = self.kwargs.get("pk") + if pk: + return super().get_object(queryset) + return None + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["user"] = self.request.user + return kwargs + + def form_valid(self, form): + is_edit = self.object is not None + response = super().form_valid(form) + status = ("created", "updated")[is_edit] + message = f"Program '{self.object.name}' {status} successfully." + messages.success(self.request, message) + return response + + def get_success_url(self): + return reverse("program:list", kwargs={"org_slug": self.request.org.slug}) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["is_edit"] = self.object is not None + return context + + def get_template_names(self): + view = ("add", "edit")[self.object is not None] + template = f"program/program_{view}.html" + return template diff --git a/commcare_connect/templates/base.html b/commcare_connect/templates/base.html index 1e3358c9..29ce0481 100644 --- a/commcare_connect/templates/base.html +++ b/commcare_connect/templates/base.html @@ -59,6 +59,12 @@ {% translate "Opportunities" %} + {% if request.user.is_superuser %} + + {% endif %} {% endif %} {% endif %} diff --git a/commcare_connect/templates/program/base.html b/commcare_connect/templates/program/base.html new file mode 100644 index 00000000..02eeb169 --- /dev/null +++ b/commcare_connect/templates/program/base.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+
+ {% block inner %}{% endblock %} +
+
+{% endblock %} diff --git a/commcare_connect/templates/program/program_add.html b/commcare_connect/templates/program/program_add.html new file mode 100644 index 00000000..8dd68b94 --- /dev/null +++ b/commcare_connect/templates/program/program_add.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block content %} +

Create Program

+
+{% include "partial_form.html" %} +{% endblock content %} diff --git a/commcare_connect/templates/program/program_edit.html b/commcare_connect/templates/program/program_edit.html new file mode 100644 index 00000000..1f3517ce --- /dev/null +++ b/commcare_connect/templates/program/program_edit.html @@ -0,0 +1,24 @@ +{% extends "program/base.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block title %}{{ request.org }} - {{ program.name }}{% endblock %} + +{% block breadcrumbs_inner %} +{{ block.super }} + + +{% endblock %} + +{% block content %} +

Edit Program

+
+
+ {% csrf_token %} + {% crispy form %} +
+{% endblock content %} diff --git a/commcare_connect/templates/program/program_list.html b/commcare_connect/templates/program/program_list.html new file mode 100644 index 00000000..bedfc688 --- /dev/null +++ b/commcare_connect/templates/program/program_list.html @@ -0,0 +1,102 @@ +{% extends "program/base.html" %} +{% load static %} +{% load sort_link %} + +{% block title %}{{ request.org }} - Program{% endblock %} + +{% block content %} +
+
+

Programs + + Add new + + +

+
+ +
+ + + + + + + + + + + + {% for program in page_obj %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% sort_link 'name' 'Name' %}{% sort_link 'start_date' 'Start Date' %}{% sort_link 'end_date' 'End Date' %}Delivery TypeManage
{{ program.name }}{{ program.start_date }}{{ program.end_date }}{{ program.delivery_type.name }} +
+  Edit +
+
No programs yet.
+
+ + {% if page_obj.has_other_pages %} + + {% endif %} +
+{% endblock content %} diff --git a/config/settings/base.py b/config/settings/base.py index 681d8778..a70bae57 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -72,6 +72,7 @@ "commcare_connect.form_receiver", "commcare_connect.opportunity", "commcare_connect.organization", + "commcare_connect.program", "commcare_connect.reports", "commcare_connect.users", "commcare_connect.web", diff --git a/config/urls.py b/config/urls.py index 9eb3609b..9eca1758 100644 --- a/config/urls.py +++ b/config/urls.py @@ -22,6 +22,7 @@ path("register/organization/", organization_create, name="organization_create"), path("a//", include("commcare_connect.organization.urls")), path("a//opportunity/", include("commcare_connect.opportunity.urls", namespace="opportunity")), + path("a//program/", include("commcare_connect.program.urls", namespace="program")), path("admin_reports/", include("commcare_connect.reports.urls")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)