Skip to content

Commit a4d5323

Browse files
authored
Merge pull request #5725 from opensafely-core/mikerkelly/project-create/form-template-required-attrsf
Add front-end validation to the project create form
2 parents 27dca42 + 32db3e1 commit a4d5323

6 files changed

Lines changed: 49 additions & 9 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 5.2.12 on 2026-03-23 17:11
2+
3+
import re
4+
5+
import django.core.validators
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
dependencies = [
11+
("jobserver", "0025_alter_project_copilot_support_ends_at"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="project",
17+
name="number",
18+
field=models.CharField(
19+
blank=True,
20+
help_text="Project ID can be found in the All Projects spreadsheet. Enter a whole number or use the format POS-20YY-NNNN (for example, POS-2026-2001).",
21+
max_length=20,
22+
null=True,
23+
validators=[
24+
django.core.validators.RegexValidator(
25+
re.compile("^[1-9][0-9]*$|^POS-20[0-9]{2}-[1-9][0-9]{3}$"),
26+
"Enter a whole number or use the format POS-20YY-NNNN (for example, POS-2026-2001).",
27+
)
28+
],
29+
verbose_name="Project ID",
30+
),
31+
),
32+
]

jobserver/models/project.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
NUMBER_REGEX = re.compile(NUMBER_PATTERN)
3131
# Either format, wrapping each with ^$ anchors to require full match.
3232
NUMBER_PATTERN_FULLMATCH = rf"^{DIGITS_PATTERN}$|^{POS_FORMAT_PATTERN}$"
33+
NUMBER_REGEX_DESCRIPTION = (
34+
"Enter a whole number or use the format POS-20YY-NNNN (for example, POS-2026-2001)."
35+
)
3336
NUMBER_REGEX_VALIDATOR = RegexValidator(
34-
re.compile(NUMBER_PATTERN_FULLMATCH),
35-
"Enter a whole number or use the format POS-20YY-NNNN (for example, POS-2026-2001).",
37+
re.compile(NUMBER_PATTERN_FULLMATCH), NUMBER_REGEX_DESCRIPTION
3638
)
3739

3840

@@ -123,7 +125,10 @@ class Statuses(models.TextChoices):
123125
blank=True,
124126
validators=[NUMBER_REGEX_VALIDATOR],
125127
verbose_name="Project ID",
126-
help_text="Project ID can be found in the All Projects spreadsheet.",
128+
help_text=(
129+
"Project ID can be found in the All Projects spreadsheet. "
130+
+ NUMBER_REGEX_DESCRIPTION
131+
),
127132
)
128133

129134
copilot_support_ends_at = models.DateTimeField(null=True, blank=True)

staff/views/projects.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from jobserver.authorization.permissions import Permission
2424
from jobserver.authorization.utils import roles_for
2525
from jobserver.models import AuditableEvent, Org, Project, ProjectMembership, User
26+
from jobserver.models.project import NUMBER_PATTERN
2627

2728
from ..forms import (
2829
ProjectAddMemberForm,
@@ -47,7 +48,8 @@ def get_context_data(self, **kwargs):
4748
return super().get_context_data(**kwargs) | {
4849
"can_create_org": has_permission(
4950
user=self.request.user, permission=Permission.ORG_CREATE
50-
)
51+
),
52+
"number_field_regex": NUMBER_PATTERN,
5153
}
5254

5355
def get_initial(self):

templates/_components/form/input.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
<div class="relative">
3939
<input
40-
{% attrs autocapitalize autocomplete autocorrect inputmode name=input_name placeholder required value=input_value|default_if_none:""|escape %}
40+
{% attrs autocapitalize autocomplete autocorrect inputmode name=input_name placeholder pattern required value=input_value|default_if_none:""|escape %}
4141
class="
4242
mt-1 block w-full rounded-md border-slate-300 text-slate-900 shadow-sm placeholder:text-slate-400
4343
user-invalid:border-bn-ribbon-600 user-invalid:ring-1 user-invalid:ring-bn-ribbon-600

templates/_components/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ <h3>Input</h3>
396396
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode"><code>inputmode</code></a></li>
397397
<li><code>label</code>: [string] - overwrite the label passed from Django</li>
398398
<li><code>name</code>: [string] - set the HTML name for the input element</li>
399+
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#pattern"><code>pattern</code></a></li>
399400
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#placeholder"><code>placeholder</code></a></li>
400401
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required"><code>required</code></a></li>
401402
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types"><code>type</code></a></li>

templates/staff/project/create.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ <h3 class="sr-only">Project data</h3>
6464
{% #card title="Project information" container=True %}
6565
{% #form_fieldset class="w-full items-stretch gap-y-6" %}
6666
{% form_legend text="Project information" class="sr-only" %}
67-
{% form_input field=form.number %}
68-
{% form_input field=form.name %}
67+
{% form_input field=form.number pattern=number_field_regex %}
68+
{% form_input field=form.name required %}
6969
{% /form_fieldset %}
7070
{% /card %}
7171

@@ -83,14 +83,14 @@ <h3 class="sr-only">Project data</h3>
8383
</p>
8484
{% endif %}
8585
{% endfragment %}
86-
{% form_select field=form.orgs choices=form.fields.orgs.choices selected=form.orgs.value|default_if_none:'' hint_text=select_hint_text hint_extra_class="max-w-full" %}
86+
{% form_select field=form.orgs choices=form.fields.orgs.choices selected=form.orgs.value|default_if_none:'' hint_text=select_hint_text hint_extra_class="max-w-full" required %}
8787
{% /form_fieldset %}
8888
{% /card %}
8989

9090
{% #card title="Co-pilot details" container=True %}
9191
{% #form_fieldset class="w-full items-stretch gap-y-6" %}
9292
{% form_legend text="Co-pilot details" class="sr-only" %}
93-
{% form_select field=form.copilot choices=form.fields.copilot.choices selected=form.copilot.value %}
93+
{% form_select field=form.copilot choices=form.fields.copilot.choices selected=form.copilot.value required %}
9494
{% /form_fieldset %}
9595
{% /card %}
9696
{% #button variant="success" type="submit" class="self-start" %}

0 commit comments

Comments
 (0)