-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #376 from dimagi/hy/program-list-creation-ui
Program List and Creation UI
- Loading branch information
Showing
17 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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("<int:pk>/edit", view=ProgramCreateOrUpdate.as_view(), name="edit"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{% extends "base.html" %} | ||
|
||
{% block breadcrumbs %} | ||
<nav class="mt-2 small" aria-label="breadcrumb"> | ||
<ol class="breadcrumb bg-white fw-bold"> | ||
<li class="breadcrumb-item"> | ||
<a href="{% url 'program:list' org_slug=request.org.slug %}"> | ||
Programs | ||
</a> | ||
</li> | ||
{% block breadcrumbs_inner %}{% endblock %} | ||
</ol> | ||
</nav> | ||
{% endblock %} | ||
|
||
{% block content %} | ||
<div class="row mt-5"> | ||
<div class="col-md-6 offset-md-3 p-4 rounded bg-white shadow "> | ||
{% block inner %}{% endblock %} | ||
</div> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{% extends "base.html" %} | ||
{% load static %} | ||
{% load crispy_forms_tags %} | ||
|
||
{% block content %} | ||
<h2 class="mb-2">Create Program</h2> | ||
<hr> | ||
{% include "partial_form.html" %} | ||
{% endblock content %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }} | ||
<li class="breadcrumb-item"> | ||
<a> | ||
{{ program.name }} | ||
</a> | ||
</li> | ||
<li class="breadcrumb-item active" aria-current="page">Edit Program</li> | ||
{% endblock %} | ||
|
||
{% block content %} | ||
<h2 class="mb-2">Edit Program</h2> | ||
<hr> | ||
<form method="post"> | ||
{% csrf_token %} | ||
{% crispy form %} | ||
</form> | ||
{% endblock content %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
{% extends "program/base.html" %} | ||
{% load static %} | ||
{% load sort_link %} | ||
|
||
{% block title %}{{ request.org }} - Program{% endblock %} | ||
|
||
{% block content %} | ||
<div class="container bg-white shadow"> | ||
<div class="mt-5 py-3"> | ||
<h1> Programs | ||
<span class="float-end"> | ||
<a class="btn btn-primary" | ||
href="{% url 'program:init' org_slug=request.org.slug %}" title="Add new program"><span | ||
class="bi bi-plus"></span> Add new | ||
</a> | ||
</span> | ||
</h1> | ||
</div> | ||
|
||
<div class="pb-4"> | ||
<table class="table border table-responsive"> | ||
<thead class="table-light"> | ||
<tr> | ||
<th>{% sort_link 'name' 'Name' %}</th> | ||
<th>{% sort_link 'start_date' 'Start Date' %}</th> | ||
<th>{% sort_link 'end_date' 'End Date' %}</th> | ||
<th>Delivery Type</th> | ||
<th>Manage</th> | ||
</tr> | ||
</thead> | ||
<tbody x-ref="tbody"> | ||
{% for program in page_obj %} | ||
<tr> | ||
<td>{{ program.name }}</td> | ||
<td>{{ program.start_date }}</td> | ||
<td>{{ program.end_date }}</td> | ||
<td>{{ program.delivery_type.name }}</td> | ||
<td width="300"> | ||
<div> | ||
<a class="btn btn-warning btn-sm" | ||
href="{% url 'program:edit' org_slug=request.org.slug pk=program.id %}"><span | ||
class="bi bi-pen"></span><span class="d-none d-md-inline"> Edit</span></a> | ||
</div> | ||
</td> | ||
</tr> | ||
{% empty %} | ||
<tr> | ||
<td colspan="3">No programs yet.</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
{% if page_obj.has_other_pages %} | ||
<nav aria-label="Page navigation"> | ||
<ul class="pagination justify-content-center"> | ||
{% if page_obj.has_previous %} | ||
<li class="page-item"> | ||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}"> | ||
<span aria-hidden="true">«</span> | ||
</a> | ||
</li> | ||
{% else %} | ||
<li class="page-item disabled"> | ||
<a class="page-link"> | ||
<span aria-hidden="true">«</span> | ||
</a> | ||
</li> | ||
{% endif %} | ||
|
||
{% for page_number in page_obj.paginator.page_range %} | ||
{% if page_obj.number == page_number %} | ||
<li class="page-item active"> | ||
{% else %} | ||
<li class="page-item"> | ||
{% endif %} | ||
<a class="page-link" href="?{% update_query_params page=page_number %}"> | ||
{{ page_number }} | ||
</a> | ||
</li> | ||
{% endfor %} | ||
|
||
{% if page_obj.has_next %} | ||
<li class="page-item"> | ||
<a class="page-link" | ||
href="?page={{ page_obj.next_page_number }}" aria-label="Next"> | ||
<span aria-hidden="true">»</span> | ||
</a> | ||
</li> | ||
{% else %} | ||
<li class="page-item disabled"> | ||
<a class="page-link"> | ||
<span aria-hidden="true">»</span> | ||
</a> | ||
</li> | ||
{% endif %} | ||
</ul> | ||
</nav> | ||
{% endif %} | ||
</div> | ||
{% endblock content %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters