Skip to content

Commit af62277

Browse files
committed
Force people to consider adding a tag when creating a patch
1 parent fa58274 commit af62277

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

pgcommitfest/commitfest/forms.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ def __init__(self, data, commitfest=None, *args, **kwargs):
8080
self.fields[f].widget.attrs = {"class": "input-medium"}
8181

8282

83+
# Tags that get their own checkbox buttons as shortcuts (also appear in selectize dropdown)
84+
CHECKBOX_TAG_NAMES = ["Bugfix", "Security", "Performance"]
85+
86+
8387
class PatchForm(forms.ModelForm):
8488
selectize_fields = {
8589
"authors": "/lookups/user",
@@ -112,6 +116,13 @@ def __init__(self, *args, **kwargs):
112116
self.fields["authors"].widget.attrs["class"] = "add-user-picker"
113117
self.fields["reviewers"].widget.attrs["class"] = "add-user-picker"
114118

119+
# Cache checkbox tags for use in JavaScript syncing and template rendering
120+
self.checkbox_tags = list(
121+
Tag.objects.filter(name__in=CHECKBOX_TAG_NAMES).values(
122+
"id", "name", "color"
123+
)
124+
)
125+
115126
# Selectize multiple fields -- don't pre-populate everything
116127
for field, url in list(self.selectize_fields.items()):
117128
if url is None:
@@ -157,6 +168,13 @@ class NewPatchForm(PatchForm):
157168
widget=ThreadPickWidget,
158169
)
159170

171+
# "No tags apply" checkbox to force users to explicitly opt out of tagging
172+
no_tags_apply = forms.BooleanField(
173+
required=False,
174+
label="No tags apply",
175+
help_text="Check this if none of the tags above apply to this patch",
176+
)
177+
160178
def __init__(self, *args, **kwargs):
161179
request = kwargs.pop("request", None)
162180
super(NewPatchForm, self).__init__(*args, **kwargs)
@@ -165,6 +183,24 @@ def __init__(self, *args, **kwargs):
165183
self.fields["authors"].queryset = User.objects.filter(pk=request.user.id)
166184
self.fields["authors"].initial = [request.user.id]
167185

186+
def clean(self):
187+
cleaned_data = super().clean()
188+
189+
has_tags = bool(cleaned_data.get("tags"))
190+
no_tags = cleaned_data.get("no_tags_apply")
191+
192+
if no_tags and has_tags:
193+
raise ValidationError(
194+
"You cannot select 'No tags apply' while also selecting tags."
195+
)
196+
197+
if not no_tags and not has_tags:
198+
raise ValidationError(
199+
"Please select at least one tag, or check 'No tags apply' if none are applicable."
200+
)
201+
202+
return cleaned_data
203+
168204
def clean_threadmsgid(self):
169205
try:
170206
_archivesAPI("/message-id.json/%s" % self.cleaned_data["threadmsgid"])

pgcommitfest/commitfest/templates/base_form.html

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@
1414
{%endif%}
1515
{%for field in form%}
1616
{%if not field.is_hidden%}
17+
{%if field.name == 'tags' and form.checkbox_tags %}
18+
{# Render the tags field with checkbox shortcuts #}
19+
<div class="form-group">
20+
<label class="control-label">Tags</label>
21+
<div class="col-lg-12 controls">
22+
{# Checkbox shortcuts for common tags #}
23+
<div class="btn-group flex-wrap tag-checkbox-group mb-2" role="group" aria-label="Quick tag selection">
24+
<button type="button" class="btn btn-sm btn-outline-secondary" disabled>Shortcuts:</button>
25+
{%for tag in form.checkbox_tags%}
26+
<input type="checkbox" class="btn-check tag-shortcut-checkbox" data-tag-id="{{tag.id}}" data-tag-name="{{tag.name}}" id="tag_shortcut_{{tag.id}}" autocomplete="off">
27+
<label class="btn btn-sm btn-outline-primary d-inline-flex align-items-center" for="tag_shortcut_{{tag.id}}"><span class="tag-color" style="background-color: {{tag.color}}; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; margin-top: 1px;"></span>{{tag.name}}</label>
28+
{%endfor%}
29+
</div>
30+
{# The actual selectize field for all tags #}
31+
{{field}}
32+
{%if field.help_text%}<br/>{{field.help_text|safe}}{%endif%}
33+
{# "No tags apply" checkbox below the selectize (only for new patches) #}
34+
{%if form.no_tags_apply %}
35+
<div class="form-check mt-2">
36+
<input type="checkbox" class="form-check-input" name="no_tags_apply" id="id_no_tags_apply" {% if form.no_tags_apply.value %}checked{% endif %}>
37+
<label class="form-check-label" for="id_no_tags_apply">{{form.no_tags_apply.label}}</label>
38+
{%if form.no_tags_apply.help_text%}<small class="form-text text-muted d-block">{{form.no_tags_apply.help_text}}</small>{%endif%}
39+
</div>
40+
{%endif%}
41+
</div>
42+
</div>
43+
{%elif field.name == 'no_tags_apply' %}
44+
{# Skip - rendered as part of the tags group above #}
45+
{%else%}
1746
<div class="form-group">
1847
{{field|label_class:"control-label"}}
1948
<div class="col-lg-12 controls">
@@ -32,6 +61,7 @@
3261
{%elif not field.name in form.selectize_fields%}{{field|field_class:"form-control"}}{%else%}{{field}}{%endif%}
3362
{%if field.help_text%}<br/>{{field.help_text|safe}}{%endif%}</div>
3463
</div>
64+
{%endif%}
3565
{%else%}
3666
{{field}}
3767
{%endif%}
@@ -92,5 +122,63 @@ <h3 class="modal-title">Search user</h3>
92122
$('#searchUserSearchField').focus();
93123
});
94124
{%endif%}
125+
126+
// Tag shortcut checkbox synchronization with selectize
127+
(function() {
128+
var $tagsSelect = $('#id_tags');
129+
if (!$tagsSelect.length || !$tagsSelect[0].selectize) return;
130+
131+
var selectize = $tagsSelect[0].selectize;
132+
var $checkboxes = $('.tag-shortcut-checkbox');
133+
var $noTagsCheckbox = $('#id_no_tags_apply');
134+
135+
// Initialize checkbox state from current selectize values
136+
function syncCheckboxesFromSelectize() {
137+
var selectedIds = selectize.getValue();
138+
if (typeof selectedIds === 'string') {
139+
selectedIds = selectedIds ? selectedIds.split(',') : [];
140+
}
141+
selectedIds = selectedIds.map(function(id) { return String(id); });
142+
143+
$checkboxes.each(function() {
144+
var tagId = String($(this).data('tag-id'));
145+
$(this).prop('checked', selectedIds.indexOf(tagId) !== -1);
146+
});
147+
148+
// Uncheck "no tags apply" if any tags are selected
149+
if ($noTagsCheckbox.length && selectedIds.length > 0) {
150+
$noTagsCheckbox.prop('checked', false);
151+
}
152+
}
153+
154+
// Sync on selectize change
155+
selectize.on('change', syncCheckboxesFromSelectize);
156+
157+
// Handle checkbox clicks
158+
$checkboxes.on('change', function() {
159+
var tagId = String($(this).data('tag-id'));
160+
if ($(this).is(':checked')) {
161+
selectize.addItem(tagId, true);
162+
if ($noTagsCheckbox.length) {
163+
$noTagsCheckbox.prop('checked', false);
164+
}
165+
} else {
166+
selectize.removeItem(tagId, true);
167+
}
168+
});
169+
170+
// Handle "no tags apply" checkbox (only exists for new patches)
171+
if ($noTagsCheckbox.length) {
172+
$noTagsCheckbox.on('change', function() {
173+
if ($(this).is(':checked')) {
174+
selectize.clear(true);
175+
$checkboxes.prop('checked', false);
176+
}
177+
});
178+
}
179+
180+
// Initial sync
181+
syncCheckboxesFromSelectize();
182+
})();
95183
</script>
96184
{%endblock%}

0 commit comments

Comments
 (0)