Skip to content

Commit fd829c2

Browse files
authored
Merge pull request dimagi#981 from dimagi/ze/progress-map-page
Expanding on Progress Map Page
2 parents 592df62 + ebd22f5 commit fd829c2

File tree

6 files changed

+134
-21
lines changed

6 files changed

+134
-21
lines changed

commcare_connect/microplanning/tests/__init__.py

Whitespace-only changes.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from datetime import date
4+
from types import SimpleNamespace
5+
from unittest import mock
6+
7+
import pytest
8+
from django.test import Client
9+
from django.urls import reverse
10+
11+
from commcare_connect.flags.flag_names import MICROPLANNING
12+
from commcare_connect.flags.models import Flag
13+
from commcare_connect.microplanning import views as microplanning_views
14+
15+
16+
@pytest.mark.django_db
17+
class TestGetMetricsForMicroplanning:
18+
def test_end_date_missing(self):
19+
opp = SimpleNamespace(end_date=None)
20+
metrics = microplanning_views.get_metrics_for_microplanning(opp)
21+
assert metrics == [{"name": "Days Remaining", "value": "--"}]
22+
23+
def test_end_date_in_future(self):
24+
with mock.patch.object(microplanning_views, "localdate", return_value=date(2026, 1, 1)):
25+
opp = SimpleNamespace(end_date=date(2026, 1, 11))
26+
metrics = microplanning_views.get_metrics_for_microplanning(opp)
27+
assert metrics == [{"name": "Days Remaining", "value": 10}]
28+
29+
def test_end_date_in_past(self):
30+
with mock.patch.object(microplanning_views, "localdate", return_value=date(2026, 1, 1)):
31+
opp = SimpleNamespace(end_date=date(2025, 12, 30))
32+
metrics = microplanning_views.get_metrics_for_microplanning(opp)
33+
assert metrics == [{"name": "Days Remaining", "value": 0}]
34+
35+
36+
@pytest.mark.django_db
37+
class TestMicroplanningHomeView:
38+
def url(self, org_slug: str, opp_id: str):
39+
return reverse("microplanning:microplanning_home", args=(org_slug, opp_id))
40+
41+
def test_success(self, client: Client, settings, organization, org_user_admin, opportunity):
42+
settings.MAPBOX_TOKEN = "test-mapbox-token"
43+
44+
flag, _ = Flag.objects.get_or_create(name=MICROPLANNING)
45+
flag.opportunities.add(opportunity)
46+
47+
client.force_login(org_user_admin)
48+
response = client.get(self.url(organization.slug, str(opportunity.opportunity_id)))
49+
50+
assert response.status_code == 200
51+
assert any(t.name == "microplanning/home.html" for t in response.templates)
52+
53+
def test_flag_disabled(self, client: Client, organization, org_user_admin, opportunity):
54+
client.force_login(org_user_admin)
55+
response = client.get(self.url(organization.slug, str(opportunity.opportunity_id)))
56+
assert response.status_code == 404
57+
58+
def test_unauthenticated(self, client: Client, organization, org_user_member, opportunity):
59+
flag, _ = Flag.objects.get_or_create(name=MICROPLANNING)
60+
flag.opportunities.add(opportunity)
61+
62+
client.force_login(org_user_member)
63+
response = client.get(self.url(organization.slug, str(opportunity.opportunity_id)))
64+
assert response.status_code == 404

commcare_connect/microplanning/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from django.conf import settings
22
from django.shortcuts import render
3+
from django.utils.timezone import localdate
4+
from django.utils.translation import gettext as _
35
from django.views.decorators.http import require_GET
46

57
from commcare_connect.flags.decorators import require_flag_for_opp
@@ -12,10 +14,22 @@
1214
@opportunity_required
1315
@require_flag_for_opp(MICROPLANNING)
1416
def microplanning_home(request, *args, **kwargs):
17+
opportunity = request.opportunity
1518
return render(
1619
request,
1720
template_name="microplanning/home.html",
1821
context={
1922
"mapbox_api_key": settings.MAPBOX_TOKEN,
23+
"opportunity": opportunity,
24+
"metrics": get_metrics_for_microplanning(opportunity),
2025
},
2126
)
27+
28+
29+
def get_metrics_for_microplanning(opportunity):
30+
return [
31+
{
32+
"name": _("Days Remaining"),
33+
"value": max((opportunity.end_date - localdate()).days, 0) if opportunity.end_date else "--",
34+
},
35+
]

commcare_connect/templates/microplanning/home.html

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,54 @@
1212
{% endblock javascript %}
1313

1414
{% block content %}
15-
<h2 class="mb-4 text-brand-deep-purple font-medium">{% translate "Select Opportunity Area" %}</h2>
16-
<div x-ref="mapContainer" class="w-full grid grid-cols-12 gap-2 h-[max(calc(100vh-150px),200px)]"
17-
x-data="mapController()">
18-
<div id="map-wrapper" class="relative h-full shadow-md" :class="panelOpen ? 'col-span-9' : 'col-span-12'">
19-
<div id="map" class="w-full h-full"></div>
20-
<button id="toggle-sidebar" title="{% translate 'Toggle Settings Panel' %}"
21-
class="absolute top-4 right-4 z-50 bg-white p-2 rounded shadow-md hover:cursor-pointer"
22-
@click="togglePanel()" aria-label="{% translate 'Toggle Settings Panel' %}" :aria-expanded="panelOpen"
23-
aria-controls="sidebar" type="button">
24-
<i x-show="panelOpen" class="fa fa-chevron-right"></i>
25-
<i x-show="!panelOpen" class="fa fa-chevron-left"></i>
26-
</button>
15+
<div class="min-w-0">
16+
<h2 class="mb-4 text-brand-deep-purple font-medium">{% translate "Select Opportunity Area" %}</h2>
17+
18+
<!-- Top header section -->
19+
<header class="mb-4">
20+
<div class="w-full rounded-lg bg-brand-indigo p-5 shadow-sm sm:p-6">
21+
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
22+
<div class="min-w-0 flex-1">
23+
<h1 class="multi-line-clamp-2 text-2xl font-semibold text-white break-words hyphens-auto leading-snug">
24+
{{ opportunity.name }}
25+
</h1>
26+
</div>
27+
28+
<div class="w-full border-t border-white/20 pt-4 lg:w-auto lg:border-t-0 lg:border-l lg:border-white/20 lg:pl-8 lg:pt-0">
29+
<div class="grid w-full grid-cols-2 gap-2 sm:grid-cols-3 lg:w-auto lg:max-w-3xl xl:grid-cols-6">
30+
{% for metric in metrics %}
31+
<div class="rounded-md border border-brand-sky/30 bg-brand-cornflower-blue/25 p-3 text-center">
32+
<div class="text-xl font-semibold text-white">
33+
{{ metric.value|default:"--" }}
34+
</div>
35+
<div class="mt-0.5 text-xs text-white/80">{{ metric.name }}</div>
36+
</div>
37+
{% endfor %}
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
</header>
43+
44+
<div class="flex flex-col gap-2 xl:flex-row xl:items-stretch xl:h-[max(calc(100vh-300px),200px)]" x-data="mapController()">
45+
<!-- Map section -->
46+
<div id="map-wrapper" x-ref="mapContainer" class="relative min-w-0 flex-1 shadow-md h-[max(calc(100vh-300px),200px)] xl:h-full">
47+
<div id="map" class="w-full h-full"></div>
48+
</div>
49+
50+
<!-- Sidebars (stacked) -->
51+
<div class="w-full xl:w-96 shrink-0 flex flex-col gap-2 min-h-0 xl:h-full">
52+
<aside id="sidebar" class="bg-white shadow-md p-4 overflow-auto xl:flex-1 min-h-0">
53+
<h2 class="text-lg font-semibold mb-2">{% translate "Settings" %}</h2>
54+
<!-- Sidebar content goes here -->
55+
</aside>
56+
57+
<aside id="right-sidebar" class="bg-white shadow-md p-4 overflow-auto xl:flex-1 min-h-0">
58+
<h2 class="text-lg font-semibold mb-2">{% translate "Modify Work Area" %}</h2>
59+
<!-- Outer sidebar content goes here -->
60+
</aside>
61+
</div>
2762
</div>
28-
<aside id="sidebar" class="col-span-3 h-full bg-white shadow-md p-4 overflow-auto" x-transition x-show="panelOpen">
29-
<h2 class="text-lg font-semibold mb-2">{% translate "Settings" %}</h2>
30-
<!-- Sidebar content goes here -->
31-
</aside>
3263
</div>
3364
{% include "microplanning/map_handler.html" %}
3465
{% endblock %}

commcare_connect/templates/microplanning/map_handler.html

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script>
22
document.addEventListener("alpine:init", () => {
33
Alpine.data("mapController", () => ({
4-
panelOpen: true,
54
map: null,
65

76
init() {
@@ -35,10 +34,6 @@
3534
this.map.resize();
3635
});
3736
observer.observe($mapContainer);
38-
},
39-
40-
togglePanel() {
41-
this.panelOpen = !this.panelOpen;
4237
}
4338
}));
4439
});

tailwind/tailwind.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,3 +757,12 @@ select:disabled,
757757
.textinput:disabled {
758758
@apply pointer-events-none opacity-50;
759759
}
760+
761+
/* Multi-line truncation helper (2 lines). */
762+
.multi-line-clamp-2 {
763+
display: -webkit-box;
764+
-webkit-box-orient: vertical;
765+
line-clamp: 2;
766+
-webkit-line-clamp: 2;
767+
overflow: hidden;
768+
}

0 commit comments

Comments
 (0)