from django import forms
from django.contrib.auth.models import User
from django.forms import ValidationError
from django.forms.widgets import HiddenInput
from django.http import Http404
from .ajax import _archivesAPI
from .models import MailThread, Patch, PatchOnCommitFest, Tag, TargetVersion
from .widgets import ThreadPickWidget
class CommitFestFilterForm(forms.Form):
selectize_fields = {
"author": "/lookups/user",
"reviewer": "/lookups/user",
"tag": None,
}
text = forms.CharField(max_length=50, required=False)
status = forms.ChoiceField(required=False)
targetversion = forms.ChoiceField(required=False)
tag = forms.MultipleChoiceField(required=False, label="Tag (type to search)")
author = forms.ChoiceField(required=False, label="Author (type to search)")
reviewer = forms.ChoiceField(required=False, label="Reviewer (type to search)")
sortkey = forms.IntegerField(required=False)
def __init__(self, data, commitfest=None, *args, **kwargs):
super(CommitFestFilterForm, self).__init__(data, *args, **kwargs)
self.commitfest = commitfest
# Update selectize_fields with cf parameter if commitfest is provided
if commitfest:
self.selectize_fields = {
"author": f"/lookups/user?cf={commitfest.id}",
"reviewer": f"/lookups/user?cf={commitfest.id}",
"tag": None,
}
self.fields["sortkey"].widget = forms.HiddenInput()
c = [(-1, "* All")] + list(PatchOnCommitFest._STATUS_CHOICES)
self.fields["status"] = forms.ChoiceField(choices=c, required=False)
userchoices = [(-1, "* All"), (-2, "* None"), (-3, "* Yourself")]
selected_user_ids = set()
if data and "author" in data:
try:
selected_user_ids.add(int(data["author"]))
except ValueError:
pass
if data and "reviewer" in data:
try:
selected_user_ids.add(int(data["reviewer"]))
except ValueError:
pass
if selected_user_ids:
userchoices.extend(
(u.id, f"{u.first_name} {u.last_name} ({u.username})")
for u in User.objects.filter(pk__in=selected_user_ids)
)
self.fields["targetversion"] = forms.ChoiceField(
choices=[("-1", "* All"), ("-2", "* None")]
+ [(v.id, v.version) for v in TargetVersion.objects.all()],
required=False,
label="Target version",
)
self.fields["author"].choices = userchoices
self.fields["reviewer"].choices = userchoices
self.fields["tag"].choices = list(Tag.objects.all().values_list("id", "name"))
for f in (
"status",
"author",
"reviewer",
):
self.fields[f].widget.attrs = {"class": "input-medium"}
# Tags that get their own checkbox buttons as shortcuts (also appear in selectize dropdown)
CHECKBOX_TAG_NAMES = ["Bugfix", "Security", "Performance"]
class PatchForm(forms.ModelForm):
selectize_fields = {
"authors": "/lookups/user",
"reviewers": "/lookups/user",
"tags": None,
}
class Meta:
model = Patch
exclude = (
"commitfests",
"mailthread_set",
"modified",
"lastmail",
"subscribers",
# TODO: Remove once we fully get rid of topics
"topic",
)
def __init__(self, *args, **kwargs):
super(PatchForm, self).__init__(*args, **kwargs)
self.fields["authors"].help_text = "Enter part of name to see list"
self.fields["reviewers"].help_text = "Enter part of name to see list"
self.fields["committer"].label_from_instance = lambda x: (
"%s %s (%s)"
% (
x.user.first_name,
x.user.last_name,
x.user.username,
)
)
self.fields["authors"].widget.attrs["class"] = "add-user-picker"
self.fields["reviewers"].widget.attrs["class"] = "add-user-picker"
# Cache checkbox tags for use in JavaScript syncing and template rendering
self.checkbox_tags = list(
Tag.objects.filter(name__in=CHECKBOX_TAG_NAMES).values(
"id", "name", "color"
)
)
# Selectize multiple fields -- don't pre-populate everything
for field, url in list(self.selectize_fields.items()):
if url is None:
continue
# If this is a postback of a selectize field, it may contain ids that are not currently
# stored in the field. They must still be among the *allowed* values of course, which
# are handled by the existing queryset on the field.
if self.instance.pk:
# If this object isn't created yet, then it by definition has no related
# objects, so just bypass the collection of values since it will cause
# errors.
vals = [o.pk for o in getattr(self.instance, field).all()]
else:
vals = []
if "data" in kwargs and str(field) in kwargs["data"]:
vals.extend([x for x in kwargs["data"].getlist(field)])
self.fields[field].widget.attrs["data-selecturl"] = url
self.fields[field].queryset = self.fields[field].queryset.filter(
pk__in=set(vals)
)
self.fields[field].label_from_instance = lambda u: (
f"{u.get_full_name()} ({u.username})"
)
# Only allow modifying reviewers and committers if the patch is closed.
if (
self.instance.id is None
or self.instance.current_patch_on_commitfest().is_open
):
del self.fields["committer"]
del self.fields["reviewers"]
class NewPatchForm(PatchForm):
# Put threadmsgid first
field_order = ["threadmsgid"]
threadmsgid = forms.CharField(
max_length=200,
required=True,
label="Specify thread msgid",
widget=ThreadPickWidget,
)
# "No tags apply" checkbox to force users to explicitly opt out of tagging
no_tags_apply = forms.BooleanField(
required=False,
label="No tags apply",
help_text="Check this if none of the tags above apply to this patch",
)
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
super(NewPatchForm, self).__init__(*args, **kwargs)
if request:
self.fields["authors"].queryset = User.objects.filter(pk=request.user.id)
self.fields["authors"].initial = [request.user.id]
def clean(self):
cleaned_data = super().clean()
has_tags = bool(cleaned_data.get("tags"))
no_tags = cleaned_data.get("no_tags_apply")
if no_tags and has_tags:
raise ValidationError(
"You cannot select 'No tags apply' while also selecting tags."
)
if not no_tags and not has_tags:
raise ValidationError(
"Please select at least one tag, or check 'No tags apply' if none are applicable."
)
return cleaned_data
def clean_threadmsgid(self):
try:
_archivesAPI("/message-id.json/%s" % self.cleaned_data["threadmsgid"])
except Http404:
raise ValidationError("Message not found in archives")
except Exception:
raise ValidationError("Error in API call to validate thread")
return self.cleaned_data["threadmsgid"]
def _fetch_thread_choices(patch):
for mt in patch.mailthread_set.order_by("-latestmessage"):
ti = sorted(
_archivesAPI("/message-id.json/%s" % mt.messageid),
key=lambda x: x["date"],
reverse=True,
)
yield [
mt.subject,
[
(
"%s,%s" % (mt.messageid, t["msgid"]),
"From %s at %s" % (t["from"], t["date"]),
)
for t in ti
],
]
review_state_choices = (
(0, "Tested"),
(1, "Passed"),
)
def reviewfield(label):
return forms.MultipleChoiceField(
choices=review_state_choices,
label=label,
widget=forms.CheckboxSelectMultiple,
required=False,
)
class CommentForm(forms.Form):
responseto = forms.ChoiceField(choices=[], required=True, label="In response to")
# Specific checkbox fields for reviews
review_installcheck = reviewfield("make installcheck-world")
review_implements = reviewfield("Implements feature")
review_spec = reviewfield("Spec compliant")
review_doc = reviewfield("Documentation")
message = forms.CharField(required=True, widget=forms.Textarea)
newstatus = forms.ChoiceField(
choices=PatchOnCommitFest.OPEN_STATUS_CHOICES(), label="New status"
)
def __init__(self, patch, poc, is_review, *args, **kwargs):
super(CommentForm, self).__init__(*args, **kwargs)
self.is_review = is_review
self.fields["responseto"].choices = _fetch_thread_choices(patch)
self.fields["newstatus"].initial = poc.status
if not is_review:
del self.fields["review_installcheck"]
del self.fields["review_implements"]
del self.fields["review_spec"]
del self.fields["review_doc"]
def clean_responseto(self):
try:
(threadid, respid) = self.cleaned_data["responseto"].split(",")
self.thread = MailThread.objects.get(messageid=threadid)
self.respid = respid
except MailThread.DoesNotExist:
raise ValidationError("Selected thread appears to no longer exist")
except Exception:
raise ValidationError("Invalid message selected")
return self.cleaned_data["responseto"]
def clean(self):
if self.is_review:
for fn, f in self.fields.items():
if fn.startswith("review_") and fn in self.cleaned_data:
if (
"1" in self.cleaned_data[fn]
and "0" not in self.cleaned_data[fn]
):
self.errors[fn] = (
("Cannot pass a test without performing it!"),
)
return self.cleaned_data
class BulkEmailForm(forms.Form):
reviewers = forms.CharField(required=False, widget=HiddenInput())
authors = forms.CharField(required=False, widget=HiddenInput())
subject = forms.CharField(required=True)
body = forms.CharField(required=True, widget=forms.Textarea)
confirm = forms.BooleanField(required=True, label="Check to confirm sending")
def __init__(self, *args, **kwargs):
super(BulkEmailForm, self).__init__(*args, **kwargs)