Skip to content

Commit

Permalink
Merge pull request #376 from dimagi/hy/program-list-creation-ui
Browse files Browse the repository at this point in the history
Program List and Creation UI
  • Loading branch information
hemant10yadav authored Sep 18, 2024
2 parents 7d4f725 + 3e4ab97 commit dfdb609
Show file tree
Hide file tree
Showing 17 changed files with 359 additions and 0 deletions.
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions commcare_connect/program/apps.py
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")
50 changes: 50 additions & 0 deletions commcare_connect/program/forms.py
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)
39 changes: 39 additions & 0 deletions commcare_connect/program/migrations/0001_initial.py
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.
20 changes: 20 additions & 0 deletions commcare_connect/program/models.py
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.
10 changes: 10 additions & 0 deletions commcare_connect/program/urls.py
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"),
]
68 changes: 68 additions & 0 deletions commcare_connect/program/views.py
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
6 changes: 6 additions & 0 deletions commcare_connect/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
<a class="nav-link {% active_link "list || create || edit || detail" namespace='opportunity' %}"
href="{% url 'opportunity:list' request.org.slug %}">{% translate "Opportunities" %}</a>
</li>
{% if request.user.is_superuser %}
<li class="nav-item">
<a class="nav-link {% active_link "list || create || edit || detail" namespace='program' %}"
href="{% url 'program:list' request.org.slug %}">{% translate "Programs" %}</a>
</li>
{% endif %}
{% endif %}
{% endif %}
</ul>
Expand Down
22 changes: 22 additions & 0 deletions commcare_connect/templates/program/base.html
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 %}
9 changes: 9 additions & 0 deletions commcare_connect/templates/program/program_add.html
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 %}
24 changes: 24 additions & 0 deletions commcare_connect/templates/program/program_edit.html
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 %}
102 changes: 102 additions & 0 deletions commcare_connect/templates/program/program_list.html
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">&nbsp;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">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">
<span aria-hidden="true">&laquo;</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">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% endblock content %}
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
path("register/organization/", organization_create, name="organization_create"),
path("a/<slug:org_slug>/", include("commcare_connect.organization.urls")),
path("a/<slug:org_slug>/opportunity/", include("commcare_connect.opportunity.urls", namespace="opportunity")),
path("a/<slug:org_slug>/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)

Expand Down

0 comments on commit dfdb609

Please sign in to comment.