Skip to content

Commit d7966fd

Browse files
frjowes-otf
andauthored
Set project start date to now and add field in create project form to set the end date (#4495)
Fixes #4494 - [x] Set project start date to now and add field in create project form to set the end date - [x] Show dates on project detail view. - [x] Make it possible to edit the start and end dates on the project detail view. - [x] Expose dates in project table. --------- Co-authored-by: Wes Appler <wes@opentech.fund>
1 parent e636c57 commit d7966fd

22 files changed

Lines changed: 276 additions & 72 deletions

File tree

hypha/apply/funds/tests/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ def test_can_create_project(self):
329329
"project_create_form": "",
330330
"project_lead": self.user.id,
331331
"project_initial_status": CONTRACTING,
332+
"project_end": timezone.now().date(),
332333
"submission": self.submission.id,
333334
},
334335
view_name="create_project",

hypha/apply/funds/views/submission_edit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ def post(self, *args, **kwargs):
468468
return render(
469469
self.request,
470470
"funds/modals/create_project_form.html",
471-
context={"form": form, "value": _("Confirm"), "object": self.object},
471+
context={"form": form, "value": _("Confirm"), "object": self.submission},
472472
status=400,
473473
)
474474

hypha/apply/projects/forms/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SkipPAFApprovalProcessForm,
1919
StaffUploadContractForm,
2020
SubmitContractDocumentsForm,
21+
UpdateProjectDatesForm,
2122
UpdateProjectLeadForm,
2223
UpdateProjectTitleForm,
2324
UploadContractDocumentForm,
@@ -44,6 +45,7 @@
4445
"UploadContractDocumentForm",
4546
"StaffUploadContractForm",
4647
"UploadDocumentForm",
48+
"UpdateProjectDatesForm",
4749
"UpdateProjectLeadForm",
4850
"CreateInvoiceForm",
4951
"ChangeInvoiceStatusForm",

hypha/apply/projects/forms/project.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from datetime import date
2+
13
from django import forms
4+
from django.conf import settings
25
from django.contrib.auth import get_user_model
36
from django.db.models import Count, Q
47
from django.utils.text import slugify
@@ -10,6 +13,7 @@
1013
get_project_default_status,
1114
get_project_status_options,
1215
)
16+
from hypha.apply.projects.templatetags.project_tags import show_start_date
1317
from hypha.apply.stream_forms.fields import SingleFileField
1418
from hypha.apply.stream_forms.forms import StreamBaseForm
1519
from hypha.apply.users.roles import STAFF_GROUP_NAME
@@ -86,16 +90,20 @@ class ProjectCreateForm(forms.Form):
8690
)
8791

8892
project_lead = forms.ModelChoiceField(
89-
label=_("Select Project Lead"), queryset=User.objects.all()
93+
label=_("Select project lead"), queryset=User.objects.all()
9094
)
9195

9296
# Set the initial value to the settings default if valid, otherwise fall back to draft
9397
project_initial_status = forms.ChoiceField(
94-
label=_("Initial Project Status"),
98+
label=_("Initial project status"),
9599
choices=get_project_status_options(),
96100
initial=get_project_default_status(),
97101
)
98102

103+
project_end = forms.DateField(
104+
label=_("Project end date"),
105+
)
106+
99107
def __init__(self, *args, instance=None, **kwargs):
100108
super().__init__(*args, **kwargs)
101109

@@ -119,7 +127,24 @@ def save(self, *args, **kwargs):
119127
submission = self.cleaned_data["submission"]
120128
lead = self.cleaned_data["project_lead"]
121129
status = self.cleaned_data["project_initial_status"]
122-
return Project.create_from_submission(submission, lead=lead, status=status)
130+
end_date = self.cleaned_data["project_end"]
131+
132+
start_date = None
133+
134+
if not settings.PROJECTS_START_AFTER_CONTRACTING or status in [
135+
INVOICING_AND_REPORTING,
136+
CLOSING,
137+
COMPLETE,
138+
]:
139+
start_date = date.today()
140+
141+
return Project.create_from_submission(
142+
submission,
143+
lead=lead,
144+
status=status,
145+
end_date=end_date,
146+
start_date=start_date,
147+
)
123148

124149

125150
class MixedMetaClass(type(StreamBaseForm), type(forms.ModelForm)):
@@ -444,3 +469,28 @@ class Meta:
444469

445470
def __init__(self, *args, user=None, **kwargs):
446471
super().__init__(*args, **kwargs)
472+
473+
474+
class UpdateProjectDatesForm(forms.ModelForm):
475+
class Meta:
476+
fields = ["proposed_start", "proposed_end"]
477+
model = Project
478+
479+
def clean(self):
480+
cleaned_data = super().clean()
481+
if (
482+
show_start_date(self.instance)
483+
and cleaned_data["proposed_start"] >= cleaned_data["proposed_end"]
484+
):
485+
self.add_error(
486+
"proposed_end", _("The end date must be after the start date.")
487+
)
488+
489+
def __init__(self, *args, user=None, **kwargs):
490+
super().__init__(*args, **kwargs)
491+
# Only show the start date field if relevant
492+
if not show_start_date(self.instance):
493+
proposed_start = self.fields["proposed_start"]
494+
proposed_start.disabled = True
495+
proposed_start.required = False
496+
proposed_start.widget = proposed_start.hidden_widget()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Generated by Django 4.2.20 on 2025-04-10 17:27
2+
3+
import datetime
4+
from django.db import migrations, models
5+
6+
7+
def migration_set_project_start_date(apps, schema_editor):
8+
Project = apps.get_model("application_projects", "Project")
9+
for project in Project.objects.all():
10+
if not project.proposed_start:
11+
project.proposed_start = project.created_at
12+
project.save(update_fields=["proposed_start"])
13+
14+
15+
class Migration(migrations.Migration):
16+
dependencies = [
17+
("application_projects", "0099_remove_reportconfig_project_and_more"),
18+
]
19+
20+
operations = [
21+
migrations.RunPython(migration_set_project_start_date),
22+
migrations.AlterField(
23+
model_name="project",
24+
name="proposed_end",
25+
field=models.DateField(null=True, verbose_name="Proposed end date"),
26+
),
27+
migrations.AlterField(
28+
model_name="project",
29+
name="proposed_start",
30+
field=models.DateField(
31+
default=datetime.date.today,
32+
null=True,
33+
verbose_name="Proposed start date",
34+
),
35+
),
36+
]

hypha/apply/projects/models/project.py

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from datetime import date
23

34
from django import forms
45
from django.apps import apps
@@ -20,7 +21,7 @@
2021
Value,
2122
When,
2223
)
23-
from django.db.models.functions import Cast, Coalesce
24+
from django.db.models.functions import Coalesce
2425
from django.db.models.signals import post_delete
2526
from django.dispatch.dispatcher import receiver
2627
from django.urls import reverse
@@ -164,21 +165,6 @@ def with_outstanding_reports(self):
164165
)
165166
)
166167

167-
def with_start_date(self):
168-
return self.annotate(
169-
start=Cast(
170-
Subquery(
171-
Contract.objects.filter(
172-
project=OuterRef("pk"),
173-
)
174-
.approved()
175-
.order_by("approved_at")
176-
.values("approved_at")[:1]
177-
),
178-
models.DateField(),
179-
)
180-
)
181-
182168
def for_table(self):
183169
return (
184170
self.with_amount_paid()
@@ -259,8 +245,10 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
259245
decimal_places=2,
260246
validators=[MinValueValidator(limit_value=0)],
261247
)
262-
proposed_start = models.DateTimeField(_("Proposed Start Date"), null=True)
263-
proposed_end = models.DateTimeField(_("Proposed End Date"), null=True)
248+
proposed_start = models.DateField(
249+
_("Proposed start date"), null=True, default=date.today
250+
)
251+
proposed_end = models.DateField(_("Proposed end date"), null=True)
264252

265253
status = models.TextField(choices=PROJECT_STATUS_CHOICES, default=DRAFT)
266254

@@ -321,7 +309,9 @@ def get_address_display(self):
321309
return "" # todo: need to figure out
322310

323311
@classmethod
324-
def create_from_submission(cls, submission, lead=None, status=None):
312+
def create_from_submission(
313+
cls, submission, lead=None, status=None, end_date=None, start_date=None
314+
):
325315
"""
326316
Create a Project from the given submission.
327317
@@ -357,26 +347,18 @@ def create_from_submission(cls, submission, lead=None, status=None):
357347
title=submission.title,
358348
status=status,
359349
lead=lead if lead else None,
350+
proposed_end=end_date,
351+
proposed_start=start_date,
360352
value=submission.form_data.get("value", 0),
361353
)
362354

363-
@property
364-
def start_date(self):
365-
# Assume project starts when OTF are happy with the first signed contract
366-
first_approved_contract = (
367-
self.contracts.approved().order_by("approved_at").first()
368-
)
369-
if not first_approved_contract:
370-
return None
371-
return first_approved_contract.approved_at.date()
372-
373355
@property
374356
def end_date(self):
375357
# Aiming for the proposed end date as the last day of the project
376358
# If still ongoing assume today is the end
377359
if self.proposed_end:
378360
return max(
379-
self.proposed_end.date(),
361+
self.proposed_end,
380362
timezone.now().date(),
381363
)
382364
return timezone.now().date()

hypha/apply/projects/reports/models.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ def for_table(self):
5959
project_start_date=Subquery(
6060
Project.objects.filter(
6161
pk=OuterRef("project_id"),
62-
)
63-
.with_start_date()
64-
.values("start")[:1]
62+
).values("proposed_start")[:1]
6563
),
6664
start=Case(
6765
When(
@@ -176,7 +174,7 @@ def start_date(self):
176174
if last_report:
177175
return last_report.end_date + relativedelta(days=1)
178176

179-
return self.project.start_date
177+
return self.project.proposed_start
180178

181179

182180
class ReportVersion(BaseStreamForm, AccessFormData, models.Model):
@@ -329,14 +327,14 @@ def current_due_report(self):
329327
return None
330328

331329
# Project not started - no reporting required
332-
if not self.project.start_date:
330+
if not self.project.proposed_start:
333331
return None
334332

335333
today = timezone.now().date()
336334

337335
last_report = self.last_report()
338336

339-
schedule_date = self.schedule_start or self.project.start_date
337+
schedule_date = self.schedule_start or self.project.proposed_start
340338

341339
if last_report:
342340
# Frequency is one time and last report exists - no reporting required anymore

hypha/apply/projects/reports/tests/test_models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def test_no_report_creates_report(self):
8080
# combined => 31th + 1 month = 30th - 1 day = 29th (wrong)
8181
# separate => 31th - 1 day = 30th + 1 month = 30th (correct)
8282
next_due = (
83-
report.project.start_date - relativedelta(days=1) + relativedelta(months=1)
83+
report.project.proposed_start
84+
- relativedelta(days=1)
85+
+ relativedelta(months=1)
8486
)
8587
assert Report.objects.count() == 1
8688
assert report.end_date == next_due

hypha/apply/projects/reports/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def get_form_kwargs(self, **kwargs):
462462
}
463463
else:
464464
kwargs["initial"] = {
465-
"start": self.project.start_date,
465+
"start": self.project.proposed_start,
466466
}
467467
return kwargs
468468

hypha/apply/projects/tables.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class BaseProjectsTable(tables.Table):
141141
fund = tables.Column(verbose_name=_("Fund"), accessor="submission__page")
142142
reporting = tables.Column(verbose_name=_("Reporting"), accessor="pk")
143143
last_payment_request = tables.DateColumn()
144+
end_date = tables.DateColumn(verbose_name=_("End date"), accessor="proposed_end")
144145

145146
def order_reporting(self, qs, is_descending):
146147
direction = "-" if is_descending else ""
@@ -176,7 +177,7 @@ class Meta:
176177
"fund",
177178
"reporting",
178179
"last_payment_request",
179-
"created_at",
180+
"end_date",
180181
]
181182
model = Project
182183
template_name = "application_projects/tables/table.html"
@@ -192,7 +193,7 @@ class Meta:
192193
"lead",
193194
"reporting",
194195
"last_payment_request",
195-
"created_at",
196+
"end_date",
196197
]
197198
model = Project
198199
orderable = False
@@ -254,10 +255,10 @@ class Meta:
254255
"fund",
255256
"reporting",
256257
"last_payment_request",
257-
"created_at",
258+
"end_date",
258259
]
259260
model = Project
260261
orderable = True
261-
order_by = ("-created_at",)
262+
order_by = ("end_date",)
262263
template_name = "application_projects/tables/table.html"
263264
attrs = {"class": "projects-table"}

0 commit comments

Comments
 (0)