More select2 and test fixes.

- Legacy-Id: 19781
This commit is contained in:
Lars Eggert 2021-12-14 18:19:12 +00:00
parent e9fd78128c
commit c68446ae93
28 changed files with 572 additions and 571 deletions

View file

@ -145,7 +145,6 @@ class CustomApiTests(TestCase):
self.assertEqual(event.by, recman)
def test_api_upload_bluesheet(self):
return # FIXME-LARS
url = urlreverse('ietf.meeting.views.api_upload_bluesheet')
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
recman = recmanrole.person

View file

@ -4,34 +4,24 @@
import json
from typing import Type # pyflakes:ignore
from typing import Type # pyflakes:ignore
from django.utils.html import escape
from django.db import models # pyflakes:ignore
from django.db import models # pyflakes:ignore
from django.db.models import Q
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias
from ietf.doc.utils import uppercase_std_abbreviated_name
from ietf.utils.fields import SearchableField
def select2_id_doc_name(objs):
return [
{
"id": o.pk,
"text": escape(uppercase_std_abbreviated_name(o.name)),
}
for o in objs
]
def select2_id_name(objs):
return [
(o.pk, escape(uppercase_std_abbreviated_name(o.name))) for o in objs
]
return [{
"id": o.pk,
"text": escape(uppercase_std_abbreviated_name(o.name)),
} for o in objs]
def select2_id_doc_name_json(objs):
@ -39,11 +29,8 @@ def select2_id_doc_name_json(objs):
class SearchableDocumentsField(SearchableField):
"""
Server-based multi-select field for choosing documents using select2.js.
"""
model = Document # type: Type[models.Model]
"""Server-based multi-select field for choosing documents using select2.js. """
model = Document # type: Type[models.Model]
default_hint_text = "Type name to search for document"
def __init__(self, doc_type="draft", *args, **kwargs):
@ -59,41 +46,34 @@ class SearchableDocumentsField(SearchableField):
Accepts both names and pks as IDs
"""
names = [i for i in item_ids if not i.isdigit()]
ids = [i for i in item_ids if i.isdigit()]
objs = self.model.objects.filter(Q(name__in=names) | Q(id__in=ids))
names = [ i for i in item_ids if not i.isdigit() ]
ids = [ i for i in item_ids if i.isdigit() ]
objs = self.model.objects.filter(
Q(name__in=names)|Q(id__in=ids)
)
return self.doc_type_filter(objs)
def make_select2_data(self, model_instances):
"""Get select2 data items"""
self.choices = select2_id_name(set(model_instances))
# FIXME-LARS: this only works with one selection, not multiple
self.initial = [tup[0] for tup in self.choices]
return select2_id_doc_name(model_instances)
def ajax_url(self):
"""Get the URL for AJAX searches"""
return urlreverse(
"ietf.doc.views_search.ajax_select2_search_docs",
kwargs={
"doc_type": self.doc_type,
"model_name": self.model.__name__.lower(),
},
)
return urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={
"doc_type": self.doc_type,
"model_name": self.model.__name__.lower()
})
class SearchableDocumentField(SearchableDocumentsField):
"""Specialized to only return one Document"""
max_entries = 1
class SearchableDocAliasesField(SearchableDocumentsField):
"""Search DocAliases instead of Documents"""
model = DocAlias # type: Type[models.Model]
model = DocAlias # type: Type[models.Model]
def doc_type_filter(self, queryset):
"""Filter to include only desired doc type
@ -101,8 +81,6 @@ class SearchableDocAliasesField(SearchableDocumentsField):
"""
return queryset.filter(docs__type=self.doc_type)
class SearchableDocAliasField(SearchableDocAliasesField):
"""Specialized to only return one DocAlias"""
max_entries = 1
max_entries = 1

View file

@ -60,7 +60,6 @@ This test section has some text.
def test_bofreq_main_page(self):
return # FIXME-LARS
doc = BofreqFactory()
doc.save_with_history(doc.docevent_set.all())
self.write_bofreq_file(doc)
@ -167,7 +166,6 @@ This test section has some text.
self.client.logout()
def test_change_editors(self):
return # FIXME-LARS
doc = BofreqFactory()
previous_editors = list(bofreq_editors(doc))
acting_editor = previous_editors[0]
@ -210,7 +208,6 @@ This test section has some text.
def test_change_responsible(self):
return # FIXME-LARS
doc = BofreqFactory()
previous_responsible = list(bofreq_responsible(doc))
new_responsible = set(previous_responsible[1:])
@ -249,7 +246,6 @@ This test section has some text.
self.assertIn('BOF Request responsible leadership changed',outbox[0]['Subject'])
def test_change_responsible_validation(self):
return # FIXME-LARS
doc = BofreqFactory()
url = urlreverse('ietf.doc.views_bofreq.change_responsible', kwargs=dict(name=doc.name))
login_testing_unauthorized(self,'secretary',url)

View file

@ -719,7 +719,6 @@ def document_main(request, name, rev=None):
raise Http404("Document not found: %s" % (name + ("-%s"%rev if rev else "")))
def document_html(request, name, rev=None):
found = fuzzy_find_documents(name, rev)
num_found = found.documents.count()
@ -735,12 +734,14 @@ def document_html(request, name, rev=None):
if not os.path.exists(doc.get_file_name()):
raise Http404("File not found: %s" % doc.get_file_name())
if found.matched_rev or found.matched_name.startswith('rfc'):
rev = found.matched_rev
else:
rev = doc.rev
if rev:
doc = doc.history_set.filter(rev=rev).first() or doc.fake_history_obj(rev)
if doc.type_id in ['draft',]:
doc.supermeta = build_doc_supermeta_block(doc)
doc.meta = build_doc_meta_block(doc, settings.HTMLIZER_URL_PREFIX)
@ -1910,4 +1911,4 @@ def rfcdiff_latest_json(request, name, rev=None):
response['previous'] = f'rfc{match.group(2)}'
if not response:
raise Http404
return HttpResponse(json.dumps(response), content_type='application/json')
return HttpResponse(json.dumps(response), content_type='application/json')

View file

@ -198,7 +198,6 @@ class GroupPagesTests(TestCase):
self.assertEqual(len(q('#content a:contains("%s")' % group.acronym)), 1)
def test_group_documents(self):
return # FIXME-LARS
group = GroupFactory()
setup_default_community_list_for_group(group)
draft = WgDraftFactory(group=group)
@ -339,7 +338,6 @@ class GroupPagesTests(TestCase):
verify_cannot_edit_group(url, group, username)
def test_group_about_personnel(self):
return # FIXME-LARS
"""Correct personnel should appear on the group About page"""
group = GroupFactory()
for role_name in group.features.default_used_roles:
@ -582,7 +580,6 @@ class GroupEditTests(TestCase):
# self.assertEqual(Group.objects.get(acronym=group.acronym).name, "Test")
def test_edit_info(self):
return # FIXME-LARS
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'))
CharterFactory(group=group)
RoleFactory(group=group,name_id='chair',person__user__email='marschairman@example.org')
@ -719,7 +716,6 @@ class GroupEditTests(TestCase):
def test_edit_field(self):
return # FIXME-LARS
def _test_field(group, field_name, field_content, prohibited_form_names):
url = urlreverse('ietf.group.views.edit',
kwargs=dict(
@ -763,7 +759,6 @@ class GroupEditTests(TestCase):
_test_field(group, 'liaison_cc_contact_roles', 'user@example.com, other_user@example.com', ['liaison_contact'])
def test_edit_reviewers(self):
return # FIXME-LARS
group=GroupFactory(type_id='review',parent=GroupFactory(type_id='area'))
other_group=GroupFactory(type_id='review',parent=GroupFactory(type_id='area'))
review_req = ReviewRequestFactory(team=group)
@ -960,7 +955,6 @@ class GroupFormTests(TestCase):
self.assertEqual(actual, expected, 'unexpected value for {}'.format(attr))
def do_edit_roles_test(self, group):
return # FIXME-LARS
# get post_data for the group
orig_data = self._group_post_data(group)
@ -1000,7 +994,6 @@ class GroupFormTests(TestCase):
self.do_edit_roles_test(group)
def test_need_parent(self):
return # FIXME-LARS
"""GroupForm should enforce non-null parent when required"""
group = GroupFactory()
parent = group.parent
@ -1076,7 +1069,6 @@ class MilestoneTests(TestCase):
def test_milestone_sets(self):
return # FIXME-LARS
m1, m2, group = self.create_test_milestones()
for url in group_urlreverse_list(group, 'ietf.group.milestones.edit_milestones;current'):
@ -1097,7 +1089,6 @@ class MilestoneTests(TestCase):
self.assertContains(r, m2.desc)
def test_add_milestone(self):
return # FIXME-LARS
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
@ -1156,7 +1147,6 @@ class MilestoneTests(TestCase):
self.assertTrue('mars-wg@' in outbox[-1]['To'])
def test_add_milestone_as_chair(self):
return # FIXME-LARS
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
@ -1193,7 +1183,6 @@ class MilestoneTests(TestCase):
self.assertFalse(group.list_email in outbox[-1]['To'])
def test_accept_milestone(self):
return # FIXME-LARS
m1, m2, group = self.create_test_milestones()
m1.state_id = "review"
m1.save()
@ -1225,7 +1214,6 @@ class MilestoneTests(TestCase):
self.assertTrue("to active from review" in m.milestonegroupevent_set.all()[0].desc)
def test_delete_milestone(self):
return # FIXME-LARS
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
@ -1253,7 +1241,6 @@ class MilestoneTests(TestCase):
self.assertTrue("Deleted milestone" in m.milestonegroupevent_set.all()[0].desc)
def test_edit_milestone(self):
return # FIXME-LARS
m1, m2, group = self.create_test_milestones()
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
@ -1333,7 +1320,6 @@ class MilestoneTests(TestCase):
self.assertEqual(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add
def test_edit_sort(self):
return # FIXME-LARS
group = GroupFactory(uses_milestone_dates=False)
DatelessGroupMilestoneFactory(group=group,order=1)
DatelessGroupMilestoneFactory(group=group,order=0)
@ -1347,7 +1333,6 @@ class MilestoneTests(TestCase):
class DatelessMilestoneTests(TestCase):
def test_switch_to_dateless(self):
return # FIXME-LARS
ad_role = RoleFactory(group__type_id='area',name_id='ad')
ms = DatedGroupMilestoneFactory(group__parent=ad_role.group)
ad = ad_role.person
@ -1384,7 +1369,6 @@ class DatelessMilestoneTests(TestCase):
self.assertEqual(len(q('#uses_milestone_dates')),0)
def test_switch_to_dated(self):
return # FIXME-LARS
ad_role = RoleFactory(group__type_id='area',name_id='ad')
ms = DatelessGroupMilestoneFactory(group__parent=ad_role.group)
ad = ad_role.person
@ -1408,7 +1392,6 @@ class DatelessMilestoneTests(TestCase):
self.assertEqual(len(q('#uses_milestone_dates')),1)
def test_add_first_milestone(self):
return # FIXME-LARS
role = RoleFactory(name_id='chair',group__uses_milestone_dates=False)
group = role.group
chair = role.person
@ -1428,7 +1411,6 @@ class DatelessMilestoneTests(TestCase):
self.assertEqual(group.groupmilestone_set.count(),1)
def test_can_switch_date_types_for_initial_charter(self):
return # FIXME-LARS
ad_role = RoleFactory(group__type_id='area',name_id='ad')
ms = DatedGroupMilestoneFactory(group__parent=ad_role.group)
ad = ad_role.person
@ -1451,7 +1433,6 @@ class DatelessMilestoneTests(TestCase):
self.assertEqual(q('#switch-date-use-form button').attr('style'), None)
def test_edit_and_reorder_milestone(self):
return # FIXME-LARS
role = RoleFactory(name_id='chair',group__uses_milestone_dates=False)
group = role.group
@ -1661,7 +1642,6 @@ class MeetingInfoTests(TestCase):
def test_meeting_info(self):
return # FIXME-LARS
for url in group_urlreverse_list(self.group, 'ietf.group.views.meetings'):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@ -1852,7 +1832,6 @@ class AcronymValidationTests(TestCase):
self.assertTrue(form.is_valid())
def test_groupform_acronym_validation(self):
return # FIXME-LARS
form = GroupForm({'acronym':'shouldpass','name':'should pass','state':'active'},group_type='wg')
self.assertTrue(form.is_valid())
form = GroupForm({'acronym':'should-fail','name':'should fail','state':'active'},group_type='wg')

View file

@ -983,5 +983,4 @@ class ResetNextReviewerInTeamTests(TestCase):
self.assertEqual(r.status_code,302)
self.assertEqual(NextReviewerInTeam.objects.get(team=group).next_reviewer, reviewers[target_index].person)
self.client.logout()
target_index += 2
target_index += 2

View file

@ -330,7 +330,6 @@ class IESGAgendaTests(TestCase):
self.assertTrue(r.json())
def test_agenda(self):
return # FIXME-LARS
r = self.client.get(urlreverse("ietf.iesg.views.agenda"))
self.assertEqual(r.status_code, 200)

View file

@ -407,7 +407,6 @@ class IetfAuthTests(TestCase):
self.assertTrue(self.username_in_htpasswd_file(user.username))
def test_review_overview(self):
return # FIXME-LARS
review_req = ReviewRequestFactory()
assignment = ReviewAssignmentFactory(review_request=review_req,reviewer=EmailFactory(person__user__username='reviewer'))
RoleFactory(name_id='reviewer',group=review_req.team,person=assignment.reviewer.person)

View file

@ -8,62 +8,39 @@ from django.utils.html import escape
from django import forms
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
import debug # pyflakes:ignore
from ietf.ipr.models import IprDisclosureBase
from ietf.utils.fields import SearchableField
def select2_id_ipr_title(objs):
return [
{
"id": o.pk,
"text": escape("%s <%s>" % (o.title, o.time.date().isoformat())),
}
for o in objs
]
def select2_id_name(objs):
return [
(o.pk, escape("%s <%s>" % (o.title, o.time.date().isoformat())))
for o in objs
]
return [{
"id": o.pk,
"text": escape("%s <%s>" % (o.title, o.time.date().isoformat())),
} for o in objs]
def select2_id_ipr_title_json(value):
return json.dumps(select2_id_ipr_title(value))
class SearchableIprDisclosuresField(SearchableField):
"""
Server-based multi-select field for choosing documents using select2.js
"""
"""Server-based multi-select field for choosing documents using select2.js"""
model = IprDisclosureBase
default_hint_text = "Type in terms to search disclosure title"
def validate_pks(self, pks):
for pk in pks:
if not pk.isdigit():
raise forms.ValidationError(
"You must enter IPR ID(s) as integers (Unexpected value: %s)"
% pk
)
raise forms.ValidationError("You must enter IPR ID(s) as integers (Unexpected value: %s)" % pk)
def get_model_instances(self, item_ids):
for key in item_ids:
if not key.isdigit():
item_ids.remove(key)
return super(SearchableIprDisclosuresField, self).get_model_instances(
item_ids
)
return super(SearchableIprDisclosuresField, self).get_model_instances(item_ids)
def make_select2_data(self, model_instances):
self.choices = select2_id_name(set(model_instances))
# FIXME-LARS: this only works with one selection, not multiple
self.initial = [tup[0] for tup in self.choices]
return select2_id_ipr_title(model_instances)
def ajax_url(self):
return urlreverse("ietf.ipr.views.ajax_search")
return urlreverse('ietf.ipr.views.ajax_search')

View file

@ -206,7 +206,7 @@ class GenericDisclosureForm(forms.Form):
help_text = patent_number_help_text)
patent_inventor = forms.CharField(max_length=63, required=False, validators=[ validate_name ], help_text="Inventor name")
patent_title = forms.CharField(max_length=255, required=False, validators=[ validate_title ], help_text="Title of invention")
patent_date = forms.DateField(required=False, help_text="Date granted or applied for")
patent_date = DatepickerDateField(date_format="yyyy-mm-dd", required=False, help_text="Date granted or applied for")
patent_notes = forms.CharField(max_length=1024, required=False, widget=forms.Textarea)
has_patent_pending = forms.BooleanField(required=False)
@ -275,7 +275,7 @@ class IprDisclosureFormBase(forms.ModelForm):
help_text = patent_number_help_text)
patent_inventor = forms.CharField(max_length=63, required=True, validators=[ validate_name ], help_text="Inventor name")
patent_title = forms.CharField(max_length=255, required=True, validators=[ validate_title ], help_text="Title of invention")
patent_date = forms.DateField(required=True, help_text="Date granted or applied for")
patent_date = DatepickerDateField(date_format="yyyy-mm-dd", required=True, help_text="Date granted or applied for")
patent_notes = forms.CharField(max_length=4096, required=False, widget=forms.Textarea)
def __init__(self,*args,**kwargs):
@ -428,4 +428,4 @@ class SearchForm(forms.Form):
class StateForm(forms.Form):
state = forms.ModelChoiceField(queryset=IprDisclosureStateName.objects,label="New State",empty_label=None)
comment = forms.CharField(required=False, widget=forms.Textarea, help_text="You may add a comment to be included in the disclosure history.", strip=False)
private = forms.BooleanField(label="Private comment", required=False, help_text="If this box is checked the comment will not appear in the disclosure's public history view.")
private = forms.BooleanField(label="Private comment", required=False, help_text="If this box is checked the comment will not appear in the disclosure's public history view.")

View file

@ -370,6 +370,7 @@ class IprTests(TestCase):
"updates": "",
}
r = self.client.post(url, post_data, follow=True)
print(r)
self.assertContains(r, "Disclosure modified")
iprs = IprDisclosureBase.objects.filter(title__icontains=draft.name)
@ -455,7 +456,8 @@ class IprTests(TestCase):
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q("#id_updates").parents(".form-group").hasClass("is-invalid"))
# print(r.content)
self.assertTrue(q("#id_updates").parents(".row").hasClass("is-invalid"))
def test_addcomment(self):
ipr = HolderIprDisclosureFactory()
@ -707,4 +709,4 @@ Subject: test
removed_docevent = doc.docevent_set.filter(type='removed_related_ipr').first()
self.assertIn(ipr.title, removed_docevent.desc,
'IprDisclosure title does not appear in DocEvent desc when removed')

View file

@ -856,4 +856,4 @@ def update(request, id):
ipr = get_object_or_404(IprDisclosureBase,id=id)
child = ipr.get_child()
type = class_to_type[child.__class__.__name__]
return new(request, type, updates=id)
return new(request, type, updates=id)

View file

@ -13,31 +13,21 @@ from ietf.utils.fields import SearchableField
def select2_id_liaison(objs):
return [
{
"id": o.pk,
"text": "[{}] {}".format(o.pk, escape(o.title)),
}
for o in objs
]
def select2_id_name(objs):
return [(o.pk, "[{}] {}".format(o.pk, escape(o.title))) for o in objs]
return [{
"id": o.pk,
"text":"[{}] {}".format(o.pk, escape(o.title)),
} for o in objs]
def select2_id_liaison_json(objs):
return json.dumps(select2_id_liaison(objs))
def select2_id_group_json(objs):
return json.dumps([{"id": o.pk, "text": escape(o.acronym)} for o in objs])
return json.dumps([{ "id": o.pk, "text": escape(o.acronym) } for o in objs])
class SearchableLiaisonStatementsField(SearchableField):
"""Server-based multi-select field for choosing liaison statements using
select2.js."""
model = LiaisonStatement
default_hint_text = "Type in title to search for document"
@ -47,17 +37,10 @@ class SearchableLiaisonStatementsField(SearchableField):
raise forms.ValidationError("Unexpected value: %s" % pk)
def make_select2_data(self, model_instances):
self.choices = select2_id_name(set(model_instances))
# FIXME-LARS: this only works with one selection, not multiple
self.initial = [tup[0] for tup in self.choices]
return select2_id_liaison(model_instances)
def ajax_url(self):
return urlreverse(
"ietf.liaisons.views.ajax_select2_search_liaison_statements"
)
return urlreverse("ietf.liaisons.views.ajax_select2_search_liaison_statements")
def describe_failed_pks(self, failed_pks):
return "Could not recognize the following groups: {pks}.".format(
pks=", ".join(failed_pks)
)
return "Could not recognize the following groups: {pks}.".format(pks=", ".join(failed_pks))

View file

@ -851,4 +851,4 @@ class VolunteerForm(forms.ModelForm):
self.fields['nomcoms'].queryset = NomCom.objects.filter(is_accepting_volunteers=True).exclude(volunteer__person=person)
self.fields['nomcoms'].help_text = 'You may volunteer even if the datatracker does not currently calculate that you are eligible. Eligibility will be assessed when the selection process is performed.'
self.fields['affiliation'].help_text = 'Affiliation to show in the volunteer list'
self.fields['affiliation'].required = True
self.fields['affiliation'].required = True

View file

@ -1326,5 +1326,4 @@ def volunteers(request, year, public=False):
v.eligible = v.person in eligible
decorate_volunteers_with_qualifications(volunteers,nomcom=nomcom)
volunteers = sorted(volunteers,key=lambda v:(not v.eligible,v.person.last_name()))
return render(request, 'nomcom/volunteers.html', dict(year=year, nomcom=nomcom, volunteers=volunteers, public=public))
return render(request, 'nomcom/volunteers.html', dict(year=year, nomcom=nomcom, volunteers=volunteers, public=public))

View file

@ -7,33 +7,26 @@ import json
from collections import Counter
from urllib.parse import urlencode
from typing import Type # pyflakes:ignore
from typing import Type # pyflakes:ignore
from django import forms
from django.core.validators import validate_email
from django.db import models # pyflakes:ignore
from django.db import models # pyflakes:ignore
from django.urls import reverse as urlreverse
from django.utils.html import escape
import debug # pyflakes:ignore
import debug # pyflakes:ignore
from ietf.person.models import Email, Person
from ietf.utils.fields import SearchableField
def select2_id_name(objs, choices=False):
def select2_id_name(objs):
def format_email(e):
return escape("%s <%s>" % (e.person.name, e.address))
def format_person(p):
if p.name_count > 1:
return escape(
"%s (%s)"
% (
p.name,
p.email().address if p.email() else "no email address",
)
)
return escape('%s (%s)' % (p.name,p.email().address if p.email() else 'no email address'))
else:
return escape(p.name)
@ -43,14 +36,10 @@ def select2_id_name(objs, choices=False):
formatter = format_person
c = Counter([p.name for p in objs])
for p in objs:
p.name_count = c[p.name]
formatter = (
format_email if objs and isinstance(objs[0], Email) else format_person
)
if choices:
return [(o.pk, formatter(o)) for o in objs if o]
return [{"id": o.pk, "text": formatter(o)} for o in objs if o]
p.name_count = c[p.name]
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
return [{ "id": o.pk, "text": formatter(o) } for o in objs if o]
def select2_id_name_json(objs):
@ -58,37 +47,34 @@ def select2_id_name_json(objs):
class SearchablePersonsField(SearchableField):
"""Server-based multi-select field for choosing
persons/emails or just persons using select2.js.
The field operates on either Email or Person models. In the case
of Email models, the person name is shown next to the email
address.
The field uses a comma-separated list of primary keys in a
CharField element as its API with some extra attributes used by
the Javascript part.
If the field will be programmatically updated, any model instances
that may be added to the initial set should be included in the extra_prefetch
list. These can then be added by updating val() and triggering the 'change'
event on the select2 field in JavaScript.
"""
Server-based multi-select field for choosing persons/emails or just persons
using select2.js.
The field operates on either Email or Person models. In the case of Email
models, the person name is shown next to the email address.
If the field will be programmatically updated, any model instances that may
be added to the initial set should be included in the extra_prefetch list.
These can then be added by updating val() and triggering the 'change' event
on the select2 field in JavaScript.
"""
model = Person # type: Type[models.Model]
model = Person # type: Type[models.Model]
default_hint_text = "Type name to search for person."
def __init__(
self,
only_users=False, # only select persons who also have a user
all_emails=False, # select only active email addresses
extra_prefetch=None, # extra data records to include in prefetch
*args,
**kwargs
):
def __init__(self,
only_users=False, # only select persons who also have a user
all_emails=False, # select only active email addresses
extra_prefetch=None, # extra data records to include in prefetch
*args, **kwargs):
super(SearchablePersonsField, self).__init__(*args, **kwargs)
self.only_users = only_users
self.all_emails = all_emails
self.extra_prefetch = extra_prefetch or []
assert all(
[isinstance(obj, self.model) for obj in self.extra_prefetch]
)
assert all([isinstance(obj, self.model) for obj in self.extra_prefetch])
def validate_pks(self, pks):
"""Validate format of PKs"""
@ -97,20 +83,15 @@ class SearchablePersonsField(SearchableField):
raise forms.ValidationError("Unexpected value: %s" % pk)
def make_select2_data(self, model_instances):
# Include records needed by the initial value of the field plus any
# added via the extra_prefetch property.
prefetch_set = set(model_instances).union(
set(self.extra_prefetch)
) # eliminate duplicates
self.choices = select2_id_name(list(prefetch_set), True)
# FIXME-LARS: this only works with one selection, not multiple
self.initial = [tup[0] for tup in self.choices]
# Include records needed by the initial value of the field plus any added
# via the extra_prefetch property.
prefetch_set = set(model_instances).union(set(self.extra_prefetch)) # eliminate duplicates
return select2_id_name(list(prefetch_set))
def ajax_url(self):
url = urlreverse(
"ietf.person.views.ajax_select2_search",
kwargs={"model_name": self.model.__name__.lower()},
kwargs={ "model_name": self.model.__name__.lower() }
)
query_args = {}
if self.only_users:
@ -118,37 +99,30 @@ class SearchablePersonsField(SearchableField):
if self.all_emails:
query_args["a"] = "1"
if len(query_args) > 0:
url += "?%s" % urlencode(query_args)
url += '?%s' % urlencode(query_args)
return url
class SearchablePersonField(SearchablePersonsField):
"""Version of SearchablePersonsField specialized to a single object."""
max_entries = 1
class SearchableEmailsField(SearchablePersonsField):
"""Version of SearchablePersonsField with the defaults right for Emails."""
model = Email # type: Type[models.Model]
default_hint_text = (
"Type name or email to search for person and email address."
)
model = Email # type: Type[models.Model]
default_hint_text = "Type name or email to search for person and email address."
def validate_pks(self, pks):
for pk in pks:
validate_email(pk)
def get_model_instances(self, item_ids):
return self.model.objects.filter(pk__in=item_ids).select_related(
"person"
)
return self.model.objects.filter(pk__in=item_ids).select_related("person")
class SearchableEmailField(SearchableEmailsField):
"""Version of SearchableEmailsField specialized to a single object."""
max_entries = 1
@ -156,9 +130,8 @@ class PersonEmailChoiceField(forms.ModelChoiceField):
"""ModelChoiceField targeting Email and displaying choices with the
person name as well as the email address. Needs further
restrictions, e.g. on role, to useful."""
def __init__(self, *args, **kwargs):
if "queryset" not in kwargs:
if not "queryset" in kwargs:
kwargs["queryset"] = Email.objects.select_related("person")
self.label_with = kwargs.pop("label_with", None)

View file

@ -1,187 +1,226 @@
$(document).ready(function () {
var idCounter = -1;
var milestonesForm = $('#milestones-form');
var group_uses_milestone_dates = ( $('#uses_milestone_dates').length > 0 );
var milestone_order_has_changed = false;
var switch_date_use_form = $("#switch-date-use-form")
$(document)
.ready(function () {
var idCounter = -1;
var milestonesForm = $('#milestones-form');
var group_uses_milestone_dates = ($('#uses_milestone_dates')
.length > 0);
var milestone_order_has_changed = false;
var switch_date_use_form = $("#switch-date-use-form");
// make sure we got the lowest number for idCounter
milestonesForm.find('.edit-milestone input[name$="-id"]').each(function () {
var v = +this.value;
if (!isNaN(v) && v < idCounter)
idCounter = v - 1;
});
// make sure we got the lowest number for idCounter
milestonesForm.find('.edit-milestone input[name$="-id"]')
.each(function () {
var v = +this.value;
if (!isNaN(v) && v < idCounter)
idCounter = v - 1;
});
function setChanged() {
$(this).closest(".edit-milestone").addClass("changed");
setSubmitButtonState();
if (switch_date_use_form) {
switch_date_use_form.hide();
}
}
milestonesForm.on("change", '.edit-milestone select,.edit-milestone input,.edit-milestone textarea', setChanged);
milestonesForm.on("click", '.edit-milestone .select2 input', setChanged);
// the required stuff seems to trip up many browsers with dynamic forms
milestonesForm.find("input").prop("required", false);
function setSubmitButtonState() {
var action, label;
if ( milestonesForm.find("input[name$=delete]:visible").length > 0 || milestone_order_has_changed )
action = "review";
else
action = "save";
milestonesForm.find("input[name=action]").val(action);
var submit = milestonesForm.find("[type=submit]");
submit.text(submit.data("label" + action));
if (milestonesForm.find(".edit-milestone.changed,.edit-milestone.delete").length > 0 || action == "review")
submit.show();
else
submit.hide();
}
milestonesForm.find(".milestone").click(function () {
var row = $(this), editRow = row.next(".edit-milestone");
row.hide();
editRow.show();
editRow.find('input[name$="desc"]').focus();
setSubmitButtonState();
// collapse unchanged rows
milestonesForm.find(".milestone").not(this).each(function () {
var e = $(this).next('.edit-milestone');
if (e.is(":visible") && !e.hasClass("changed")) {
$(this).show();
e.hide();
}
});
});
milestonesForm.find(".add-milestone").click(function() {
var template = $("#extratemplatecontainer .extratemplate");
var templateclone = template.clone();
$("#dragdropcontainer").append(templateclone);
var new_milestone = $("#dragdropcontainer > div:last")
var new_edit_milestone = new_milestone.find(".edit-milestone");
var new_edit_milestone_order = $("#dragdropcontainer > div").length
new_milestone.removeClass("extratemplate")
new_milestone.addClass("draggable")
new_milestone.addClass("milestonerow")
var newId = idCounter;
--idCounter;
var prefix = "m" + newId;
new_edit_milestone.find('input[name="prefix"]').val(prefix);
new_edit_milestone.find('input[name="order"]').val(new_edit_milestone_order);
new_edit_milestone.find("input,select,textarea").each(function () {
if (this.name == "prefix")
return;
if (this.name == "id")
this.value = "" + idCounter;
this.name = prefix + "-" + this.name;
this.id = prefix + "-" + this.id;
});
new_edit_milestone.find("label").each(function () {
if (this.htmlFor)
this.htmlFor = prefix + "-" + this.htmlFor;
});
new_edit_milestone.removeClass("template");
new_edit_milestone.show();
new_edit_milestone.find(".select2-field").each(function () {
window.setupSelect2Field($(this)); // from select2-field.js
});
if ( ! group_uses_milestone_dates ) {
setOrderControlValue();
}
});
function setResolvedState() {
var resolved = $(this).is(":checked");
var label = $(this).closest(".edit-milestone").find("label[for=" + this.id + "]");
var reason = $(this).closest(".edit-milestone").find("[name$=resolved]");
if (resolved) {
reason.closest(".form-group").show();
if (!reason.val())
reason.val(reason.data("default"));
}
else {
reason.closest(".form-group").hide();
reason.val("");
}
}
milestonesForm.find(".edit-milestone [name$=resolved_checkbox]").each(setResolvedState);
milestonesForm.on("change", ".edit-milestone [name$=resolved_checkbox]", setResolvedState);
function setDeleteState() {
var edit = $(this).closest(".edit-milestone"), row = edit.prev(".milestone");
if ($(this).is(":checked")) {
if (+edit.find('input[name$="id"]').val() < 0) {
edit.remove();
setSubmitButtonState();
}
else {
row.addClass("delete");
edit.addClass("delete");
}
}
else {
row.removeClass("delete");
edit.removeClass("delete");
}
}
function setOrderControlValue() {
$("#dragdropcontainer > div").each(function(index){
var prefix = $(this).find('input[name="prefix"]').val();
$(this).find('input[name="'+prefix+'-order"]').val(index)
})
}
milestonesForm.find(".edit-milestone [name$=delete]").each(setDeleteState);
milestonesForm.on("change", ".edit-milestone input[name$=delete]", setDeleteState);
milestonesForm.find('.edit-milestone .is-invalid').each(function () {
$(this).closest(".edit-milestone").prev().click();
});
setSubmitButtonState();
if ( ! group_uses_milestone_dates) {
setOrderControlValue();
function onEnd(event) {
milestone_order_has_changed = true;
function setChanged() {
$(this)
.closest(".edit-milestone")
.addClass("changed");
setSubmitButtonState();
setOrderControlValue();
if (switch_date_use_form) {
switch_date_use_form.hide();
}
switch_date_use_form.addClass("visually-hidden");
}
}
var options = {
animation: 150,
draggable: ".draggable",
onEnd: function(event) {onEnd(event)}
};
milestonesForm.on("change", '.edit-milestone select,.edit-milestone input,.edit-milestone textarea', setChanged);
milestonesForm.on("click", '.edit-milestone .select2 input', setChanged);
var el = document.getElementById('dragdropcontainer');
var sortable = new Sortable(el, options);
}
});
// the required stuff seems to trip up many browsers with dynamic forms
milestonesForm.find("input")
.prop("required", false);
function setSubmitButtonState() {
var action;
if (milestonesForm.find("input[name$=delete]:visible")
.length > 0 || milestone_order_has_changed)
action = "review";
else
action = "save";
milestonesForm.find("input[name=action]")
.val(action);
var submit = milestonesForm.find("[type=submit]");
submit.text(submit.data("label" + action));
if (milestonesForm.find(".edit-milestone.changed,.edit-milestone.delete")
.length > 0 || action == "review")
submit.removeClass("visually-hidden");
else
submit.addClass("visually-hidden");
}
milestonesForm.find(".milestone")
.on("click", function () {
var row = $(this),
editRow = row.next(".edit-milestone");
row.addClass("visually-hidden");
editRow.removeClass("visually-hidden");
editRow.find('input[name$="desc"]')
.focus();
setSubmitButtonState();
// collapse unchanged rows
milestonesForm.find(".milestone")
.not(this)
.each(function () {
var e = $(this)
.next('.edit-milestone');
if (e.is(":visible") && !e.hasClass("changed")) {
$(this)
.removeClass("visually-hidden");
e.addClass("visually-hidden");
}
});
});
milestonesForm.find(".add-milestone")
.on("click", function () {
var template = $("#extratemplatecontainer .extratemplate");
var templateclone = template.clone();
$("#dragdropcontainer")
.append(templateclone);
var new_milestone = $("#dragdropcontainer > div:last");
var new_edit_milestone = new_milestone.find(".edit-milestone");
var new_edit_milestone_order = $("#dragdropcontainer > div")
.length;
new_milestone.removeClass("extratemplate");
new_milestone.addClass("draggable");
new_milestone.addClass("milestonerow");
var newId = idCounter;
--idCounter;
var prefix = "m" + newId;
new_edit_milestone.find('input[name="prefix"]')
.val(prefix);
new_edit_milestone.find('input[name="order"]')
.val(new_edit_milestone_order);
new_edit_milestone.find("input,select,textarea")
.each(function () {
if (this.name == "prefix")
return;
if (this.name == "id")
this.value = "" + idCounter;
this.name = prefix + "-" + this.name;
this.id = prefix + "-" + this.id;
});
new_edit_milestone.find("label")
.each(function () {
if (this.htmlFor)
this.htmlFor = prefix + "-" + this.htmlFor;
});
new_edit_milestone.removeClass("template");
new_edit_milestone.removeClass("visually-hidden");
new_edit_milestone.find(".select2-field")
.each(function () {
window.setupSelect2Field($(this)); // from select2-field.js
});
if (!group_uses_milestone_dates) {
setOrderControlValue();
}
});
function setResolvedState() {
var resolved = $(this)
.is(":checked");
// var label = $(this)
// .closest(".edit-milestone")
// .find("label[for=" + this.id + "]");
var reason = $(this)
.closest(".edit-milestone")
.find("[name$=resolved]");
if (resolved) {
reason.closest(".form-group")
.removeClass("visually-hidden");
if (!reason.val())
reason.val(reason.data("default"));
} else {
reason.closest(".form-group")
.addClass("visually-hidden");
reason.val("");
}
}
milestonesForm.find(".edit-milestone [name$=resolved_checkbox]")
.each(setResolvedState);
milestonesForm.on("change", ".edit-milestone [name$=resolved_checkbox]", setResolvedState);
function setDeleteState() {
var edit = $(this)
.closest(".edit-milestone");
var row = edit.prev(".milestone");
if ($(this)
.is(":checked")) {
if (+edit.find('input[name$="id"]')
.val() < 0) {
edit.remove();
setSubmitButtonState();
} else {
row.addClass("delete");
edit.addClass("delete");
}
} else {
row.removeClass("delete");
edit.removeClass("delete");
}
}
function setOrderControlValue() {
$("#dragdropcontainer > div")
.each(function (index) {
var prefix = $(this)
.find('input[name="prefix"]')
.val();
$(this)
.find('input[name="' + prefix + '-order"]')
.val(index);
});
}
milestonesForm.find(".edit-milestone [name$=delete]")
.each(setDeleteState);
milestonesForm.on("change", ".edit-milestone input[name$=delete]", setDeleteState);
milestonesForm.find('.edit-milestone .is-invalid')
.each(function () {
$(this)
.closest(".edit-milestone")
.prev()
.trigger("click");
});
setSubmitButtonState();
if (!group_uses_milestone_dates) {
setOrderControlValue();
var options = {
animation: 150,
draggable: ".draggable",
onEnd: function () {
milestone_order_has_changed = true;
setSubmitButtonState();
setOrderControlValue();
if (switch_date_use_form) {
switch_date_use_form.addClass("visually-hidden");
}
}
};
var el = document.getElementById('dragdropcontainer');
Sortable.create(el, options);
}
});

View file

@ -185,10 +185,10 @@ $(document)
.attr("data-bs-target", "#righthand-nav")
.scrollspy("refresh");
$(window)
.on("activate.bs.scrollspy", function () {
console.log("X");
});
// $(window)
// .on("activate.bs.scrollspy", function () {
// console.log("X");
// });
}
});

View file

@ -1,55 +1,72 @@
$(document).ready(function() {
var form = $(".ipr-form");
$(document)
.ready(function () {
var form = $(".ipr-form");
var template = form.find('.draft-row.template');
$('.draft-add-row')
.on("click", function () {
var template = form.find('.draft-row.template');
var el = template.clone(true)
.removeClass("template visually-hidden");
var templateData = template.clone();
var totalField = $('#id_iprdocrel_set-TOTAL_FORMS');
var total = +totalField.val();
$('.draft-add-row').click(function() {
var el = template.clone(true);
var totalField = $('#id_iprdocrel_set-TOTAL_FORMS');
var total = +totalField.val();
el.find("*[for*=iprdocrel], *[id*=iprdocrel], *[name*=iprdocrel]")
.not(".visually-hidden")
.each(function () {
var x = $(this);
["for", "id", "name"].forEach(function (at) {
var val = x.attr(at);
if (val && val.match("iprdocrel")) {
x.attr(at, val.replace('-1-', '-' + total + '-'));
}
});
});
++total;
el.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('');
});
totalField.val(total);
el.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
template.before(el);
el.find(".select2-field")
.each(function () {
setupSelect2Field($(this));
});
});
++total;
function updateRevisions() {
if ($(this)
.hasClass("template"))
return;
totalField.val(total);
var selectbox = $(this)
.find('[name$="document"]');
template.before(el);
el.removeClass("template");
el.find(".select2-field").each(function () {
setupSelect2Field($(this));
});
});
function updateRevisions() {
var selectbox = $(this).find('[name$="document"]');
if (selectbox.val()) {
var name = selectbox.select2("data").text;
if (name.toLowerCase().substring(0, 3) == "rfc")
$(this).find('[name$=revisions]').val("").hide();
else
$(this).find('[name$=revisions]').show();
if (selectbox.val()) {
var name = selectbox.select2("data")[0]
.text;
var prefix = name.toLowerCase()
.substring(0, 3);
if (prefix == "rfc" || prefix == "bcp" || prefix == "std")
$(this)
.find('[name$=revisions]')
.val("")
.hide();
else
$(this)
.find('[name$=revisions]')
.show();
}
}
}
form.on("change", ".select2-field", function () {
$(this).closest(".draft-row").each(updateRevisions);
});
form.on("change", ".select2-field", function () {
$(this)
.closest(".draft-row")
.each(updateRevisions);
});
// add a little bit of delay to let the select2 box have time to do its magic
setTimeout(function () {
form.find(".draft-row").each(updateRevisions);
}, 10);
});
// add a little bit of delay to let the select2 box have time to do its magic
setTimeout(function () {
form.find(".draft-row")
.each(updateRevisions);
}, 10);
});

View file

@ -14,12 +14,20 @@ $.fn.select2.defaults.set("escapeMarkup", function (m) {
// Copyright The IETF Trust 2015-2021, All Rights Reserved
// JS for ietf.utils.fields.SearchableField subclasses
function setupSelect2Field(e) {
var url = e.data("ajax--url");
if (!url)
window.setupSelect2Field = function (e) {
var url = e.data("ajax-url");
if (!url) {
console.log("data-ajax-url missing, not enabling select2 on field", e);
return;
}
var maxEntries = e.data("max-entries");
var options = e.data("pre");
for (var id in options) {
e.append(new Option(options[id].text, options[id].id, true, true));
}
// e.trigger("change");
e.select2({
multiple: maxEntries !== 1,
maximumSelectionSize: maxEntries,
@ -43,7 +51,7 @@ function setupSelect2Field(e) {
}
}
});
}
};
$(document)
.ready(function () {

View file

@ -1 +1,3 @@
import "sortablejs";
import { Sortable } from "sortablejs";
window.Sortable = Sortable;

View file

@ -22,8 +22,6 @@
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-secondary float-end" href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">Back</a>
</form>
{% endblock %}

View file

@ -4,128 +4,127 @@
{% load static %}
{% load django_bootstrap5 %}
{% load misc_filters %}
{% block pagehead %}
{{ all_forms|merge_media:'css' }}
<link rel="stylesheet" href="{% static 'ietf/css/datepicker.css' %}">
{% endblock %}
{% block pagehead %}{{ all_forms|merge_media:'css' }}{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
{% origin %}
<h1>{{ title }}</h1>
<noscript>This page depends on Javascript being enabled to work properly.</noscript>
<p>Links:
<a href="{{ group.about_url }}">{{ group.acronym }} {{ group.type.name }}</a>
<form method="post" id="switch-date-use-form">
<a class="btn btn-primary" href="{{ group.about_url }}">{{ group.acronym }} {{ group.type.name }}</a>
{% if group.charter %}
- <a href="{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}">{{ group.charter.canonical_name }}</a>
<a class="btn btn-primary"
href="{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}">
{{ group.charter.canonical_name }}
</a>
{% endif %}
{% if can_change_uses_milestone_dates %}
{% csrf_token %}
<button class="btn btn-primary"
type="submit"
name="action"
value="switch"
{% if milestone_set == 'charter' and not group.charter.rev == '00-00' %} style="display:none;"{% endif %}>
{% if group.uses_milestone_dates %}
Stop
{% else %}
Start
{% endif %}
using milestone dates
</button>
{% endif %}
</form>
<p class="help-block my-3">
{% if forms %}Click a milestone to edit it.{% endif %}
{% if forms and not group.uses_milestone_dates %}Drag and drop milestones to reorder them.{% endif %}
{% if needs_review %}
Note that as {{ group.type.name }} Chair you cannot edit descriptions of existing
milestones and milestones you add are subject to review by the {{ reviewer }}.
{% endif %}
</p>
<div class="container-fluid">
{% if can_change_uses_milestone_dates %}
<div class="col-sm-12">
<form method="post" id="switch-date-use-form">{% csrf_token %}
<button class="btn btn-primary" type="submit" name="action" value="switch"{% if milestone_set == 'charter' and not group.charter.rev == '00-00' %} style="display:none;"{% endif %}>
{% if group.uses_milestone_dates %}Stop{% else %}Start{% endif %} using milestone dates
</button>
</form>
</div>
{% endif %}
<div class="col-sm-12">
<p class="help-block">
{% if forms %}Click a milestone to edit it.{% endif %}
{% if forms and not group.uses_milestone_dates %}Drag and drop milestones to reorder them.{% endif %}
{% if needs_review %}
Note that as {{ group.type.name }} Chair you cannot edit descriptions of existing
milestones and milestones you add are subject to review by the {{ reviewer }}.
{% endif %}
</p>
{% if can_reset %}
<p>
You can <a href="{% url 'ietf.group.milestones.reset_charter_milestones' group_type=group.type_id acronym=group.acronym %}">reset
this list</a> to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}.
</p>
{% endif %}
{% if form_errors %}
<p class="alert alert-danger">There were errors, see below.</p>
{% endif %}
</div>
</div>
<form method="post" id="milestones-form">{% csrf_token %}
<div id="dragdropcontainer" class="container-fluid">
{% if can_reset %}
<p>
You can
<a href="{% url 'ietf.group.milestones.reset_charter_milestones' group_type=group.type_id acronym=group.acronym %}">
reset
this list
</a>
to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}.
</p>
{% endif %}
{% if form_errors %}
<p class="alert alert-danger">
There were errors, see below.
</p>
{% endif %}
<form method="post" id="milestones-form">
{% csrf_token %}
<div id="dragdropcontainer">
{% for form in forms %}
<div class="row milestonerow draggable">
<span class="milestone{% if form.delete.data %} delete{% endif %}">
<span class="due handle col-sm-1">
<div class="milestonerow draggable">
<hr>
<div class="row milestone{% if form.delete.data %} delete{% endif %}">
<div class="due handle col-md-2 col-form-label col-form-label-md">
{% if form.milestone.resolved %}
<span class="badge bg-success">{{ form.milestone.resolved }}</span>
{% else %}
{% if group.uses_milestone_dates %}{{ form.milestone.due|date:"M Y" }}{% endif %}
<b>{% if group.uses_milestone_dates %}{{ form.milestone.due|date:"M Y" }}{% endif %}</b>
{% endif %}
</span>
<span class="col-sm-11">
<span>{{ form.milestone.desc }}
{% if form.needs_review %}<span title="This milestone is not active yet, awaiting {{ reviewer }} acceptance" class="badge bg-warning">Awaiting accept</span>{% endif %}
</div>
<div class="col-md-10 col-form-label col-form-label-md">
<b>{{ form.milestone.desc }}</b>
{% if form.needs_review %}
<span title="This milestone is not active yet, awaiting {{ reviewer }} acceptance"
class="badge bg-warning">
Awaiting accept
</span>
{% endif %}
{% if form.changed %}<span class="badge bg-info">Changed</span>{% endif %}
{% if form.delete.data %}<span class="badge bg-danger">Deleted</span>{% endif %}
</span>
{% for d in form.docs_names %}
<div class="doc">{{ d }}</div>
{% endfor %}
</span>
</span>
<span class="edit-milestone{% if form.changed %} changed{% endif %}">
<span colspan="2">{% include "group/milestone_form.html" %}</span>
</span>
{% for d in form.docs_names %}<div class="doc">{{ d }}</div>{% endfor %}
</div>
</div>
<div class="visually-hidden row edit-milestone{% if form.changed %} changed{% endif %}">
{% include "group/milestone_form.html" %}
</div>
</div>
{% endfor %}
</div>
<div class="row extrabuttoncontainer">
<div class="col-sm-1"></div>
<div class="col-sm-11"><button type="button" class="btn btn-primary add-milestone">Add extra {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for {{ reviewer }} review{% endif %}</button></div>
</div>
<div id="extratemplatecontainer">
<div id="extratemplatecontainer" class="visually-hidden">
<div class="row extratemplate">
<div class="edit-milestone template"><div colspan="2">{% include "group/milestone_form.html" with form=empty_form %}</div></div>
<hr>
<div class="edit-milestone template">
{% include "group/milestone_form.html" with form=empty_form %}
</div>
</div>
</div>
<a class="btn btn-secondary float-end" href="{% if milestone_set == "charter" %}{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}{% else %}{{ group.about_url }}{% endif %}">Cancel</a>
<button style="display:none" class="btn btn-primary" type="submit" data-labelsave="Save" data-labelreview="Review changes">Save</button>
<div class="row extrabuttoncontainer">
<div class="offset-md-2">
<button type="button" class="btn btn-primary add-milestone">
Add extra
{% if milestone_set == "chartering" %}charter{% endif %}
milestone
{% if needs_review %}for {{ reviewer }} review{% endif %}
</button>
</div>
</div>
<a class="btn btn-secondary float-end"
href="{% if milestone_set == "charter" %}{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}{% else %}{{ group.about_url }}{% endif %}">
Cancel
</a>
<button class="btn btn-primary hidden"
type="submit"
data-labelsave="Save"
data-labelreview="Review changes">
Save
</button>
<input type="hidden" name="action" value="save">
</form>
{% if group.uses_milestone_dates %}
<div id="uses_milestone_dates"></div>
{% endif %}
{% if group.uses_milestone_dates %}<div id="uses_milestone_dates"></div>{% endif %}
{% endblock %}
{% block js %}
{{ all_forms|merge_media:'js' }}
<script src="{% static 'ietf/js/datepicker.js' %}"></script>
{% if not group.uses_milestone_dates %}
<script src="{% static 'ietf/js/sortable.js' %}"></script>
{% endif %}
<script src="{% static 'ietf/js/edit-milestones.js' %}"></script>
{% endblock %}
{% endblock %}

View file

@ -1,9 +1,8 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
{# bs5ok #}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% origin %}
{# assumes group, form, needs_review, reviewer are in the context #}
{% load django_bootstrap5 %}
<div class="form-horizontal">
<input type="hidden" name="prefix" value="{{ form.prefix|default:"" }}"/>
{% bootstrap_form form layout='horizontal' %}
</div>
<input type="hidden" name="prefix" value="{{ form.prefix|default:"" }}"/>
{% bootstrap_form form layout='horizontal' %}

View file

@ -2,7 +2,7 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load ietf_filters ipr_filters django_bootstrap5 widget_tweaks %}
{% load ietf_filters ipr_filters django_bootstrap5 %}
{% block title %}{% if form.instance %}Edit IPR #{{ form.instance.id }}{% else %}New IPR{% endif %}{% endblock %}
@ -136,9 +136,9 @@
{{ draft_formset.management_form }}
{% for draft_form in draft_formset %}
<div class="form-group draft-row {% if forloop.last %}template{% endif %}">
<div class="form-group row draft-row {% if forloop.last %}template visually-hidden{% endif %}">
<label class="col-md-2 col-form-label" for="{{ draft_form.document.id_for_label }}">
<label class="col-md-2 fw-bold" for="{{ draft_form.document.id_for_label }}">
{{ draft_form.document.label }}
</label>
@ -148,18 +148,37 @@
</div>
<div class="col-md-2">
{% render_field draft_form.revisions class="form-control" placeholder="Revisions, e.g. 04-07" %}
<label class="sr-only" for="{{ draft_form.revisions.id_for_label }}">{{ draft_form.revisions.label }}</label>
{% bootstrap_field draft_form.revisions class="form-control" placeholder="Revisions, e.g., 04-07" show_help=False show_label=False %}
<label class="visually-hidden" for="{{ draft_form.revisions.id_for_label }}">{{ draft_form.revisions.label }}</label>
</div>
<div class="col-md-2">
{% render_field draft_form.sections class="form-control" placeholder="Sections" %}
<label class="sr-only" for="{{ draft_form.sections.id_for_label }}">{{ draft_form.sections.label }}</label>
{% bootstrap_field draft_form.sections class="form-control" placeholder="Sections" show_help=False show_label=False %}
<label class="visually-hidden" for="{{ draft_form.sections.id_for_label }}">{{ draft_form.sections.label }}</label>
</div>
</div>
{% endfor %}
<div class="form-group">
{% comment %}
{% for draft_form in draft_formset %}
<div class="form-group row draft-row {% if forloop.last %}template{% endif %}">
<div class="col-md-2 fw-bold">
{% bootstrap_label draft_form.document.label %}
</div>
<div class="col-md-6">
{% bootstrap_field draft_form.document label_class="visually-hidden" show_help=False %}
</div>
<div class="col-md-2">
{% bootstrap_field draft_form.revisions placeholder="Revisions, e.g., 04-07" label_class="visually-hidden" show_help=False %}
</div>
<div class="col-md-2">
{% bootstrap_field draft_form.sections placeholder="Sections" label_class="visually-hidden" show_help=False %}
</div>
</div>
{% endfor %}
{% endcomment %}
<div class="form-group mb-3">
<label class="col-md-2 col-form-label"></label>
<div class="col-md-10"><a class="draft-add-row btn btn-primary"><span class="bi bi-plus" aria-hidden="true"></span> Add more</a></div>
</div>
@ -255,8 +274,7 @@
{% bootstrap_field form.notes layout='horizontal' %}
<button class="btn btn-primary" type="submit" name="submit" value="Submit">Submit</button>
{% bootstrap_button button_type="submit" name="submit" content="Submit" %}
</form>
@ -266,4 +284,4 @@
{{ form.media.js }}
<script src="{% static 'ietf/js/datepicker.js' %}"></script>
<script src="{% static 'ietf/js/ipr-edit.js' %}"></script>
{% endblock %}
{% endblock %}

View file

@ -1,7 +1,7 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load ietf_filters %}
<table class="table table-sm table-striped tablesorter">
<table class="table table-sm table-striped tablesorter ipr-table">
<thead>
<tr>
<th data-sort="date">Date</th>

View file

@ -197,7 +197,9 @@ class SearchableField(forms.MultipleChoiceField):
specific default_hint_text as well.
"""
widget = Select2Multiple
# model = None # must be filled in by subclass
model = None # type:Optional[Type[models.Model]]
# max_entries = None # may be overridden in __init__
max_entries = None # type: Optional[int]
default_hint_text = 'Type a value to search'
@ -216,6 +218,21 @@ class SearchableField(forms.MultipleChoiceField):
if self.max_entries is not None:
self.widget.attrs["data-max-entries"] = self.max_entries
def make_select2_data(self, model_instances):
"""Get select2 data items
Should return an array of dicts, each with at least 'id' and 'text' keys.
"""
raise NotImplementedError('Must implement make_select2_data')
def ajax_url(self):
"""Get the URL for AJAX searches
Doing this in the constructor is difficult because the URL patterns may not have been
fully constructed there yet.
"""
raise NotImplementedError('Must implement ajax_url')
def get_model_instances(self, item_ids):
"""Get model instances corresponding to item identifiers in select2 field value
@ -223,6 +240,14 @@ class SearchableField(forms.MultipleChoiceField):
"""
return self.model.objects.filter(pk__in=item_ids)
def validate_pks(self, pks):
"""Validate format of PKs
Base implementation does nothing, but subclasses may override if desired.
Should raise a forms.ValidationError in case of a failed validation.
"""
pass
def describe_failed_pks(self, failed_pks):
"""Format error message to display when non-existent PKs are referenced"""
return ('Could not recognize the following {model_name}s: {pks}. '
@ -232,34 +257,45 @@ class SearchableField(forms.MultipleChoiceField):
)
def prepare_value(self, value):
self.widget.attrs["data-pre"] = json.dumps({
d['id']: d for d in self.make_select2_data(value)
})
result = super(SearchableField, self).prepare_value(value)
if not value:
value = ""
if isinstance(value, int):
value = str(value)
if type(value) in (str, list):
value = self.get_model_instances(value)
if isinstance(value, self.model):
value = [value]
if value.count() > 0:
self.widget.attrs["data-pre"] = json.dumps({
d['id']: d for d in self.make_select2_data(value)
})
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax--url"] = self.ajax_url()
self.widget.attrs["data-ajax-url"] = self.ajax_url()
return super(SearchableField, self).prepare_value(value)
return result
def clean(self, value):
def clean(self, pks):
try:
objs = self.model.objects.filter(pk__in=value)
objs = self.model.objects.filter(pk__in=pks)
except ValueError as e:
raise forms.ValidationError('Unexpected field value; {}'.format(e))
found_pks = [ o.pk for o in objs ]
failed_pks = [ x for x in value if x not in found_pks ]
found_pks = [ str(o.pk) for o in objs ]
failed_pks = [ x for x in pks if x not in found_pks ]
if failed_pks:
raise forms.ValidationError(self.describe_failed_pks(failed_pks))
if self.max_entries != None and len(objs) > self.max_entries:
raise forms.ValidationError('You can select at most {} {}.'.format(
self.max_entries,
'entry' if self.max_entries == 1 else 'entries',
'entry' if self.max_entries == 1 else 'entries',
))
return objs
return objs.first() if self.max_entries == 1 else objs
class IETFJSONField(jsonfield.fields.forms.JSONField):

View file

@ -376,7 +376,8 @@ def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=F
if save:
message.sent = datetime.datetime.now()
message.save()
show_that_mail_was_sent(request,'Email was sent',msg,bcc)
if settings.SERVER_MODE != 'development':
show_that_mail_was_sent(request,'Email was sent',msg,bcc)
except smtplib.SMTPException as e:
log_smtp_exception(e)
build_warning_message(request, e)
@ -627,4 +628,4 @@ def get_payload_text(msg, decode=True, default_charset="utf-8"):
payload = msg.get_payload(decode=decode)
payload = payload.decode(str(charset))
return payload