From c68446ae9383545a6cfbfb6b52e245eda1f70185 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 14 Dec 2021 18:19:12 +0000 Subject: [PATCH] More select2 and test fixes. - Legacy-Id: 19781 --- ietf/api/tests.py | 1 - ietf/doc/fields.py | 64 +-- ietf/doc/tests_bofreq.py | 4 - ietf/doc/views_doc.py | 5 +- ietf/group/tests_info.py | 21 - ietf/group/tests_review.py | 3 +- ietf/iesg/tests.py | 1 - ietf/ietfauth/tests.py | 1 - ietf/ipr/fields.py | 41 +- ietf/ipr/forms.py | 6 +- ietf/ipr/tests.py | 6 +- ietf/ipr/views.py | 2 +- ietf/liaisons/fields.py | 31 +- ietf/nomcom/forms.py | 2 +- ietf/nomcom/views.py | 3 +- ietf/person/fields.py | 107 ++--- ietf/static/js/edit-milestones.js | 399 ++++++++++-------- ietf/static/js/ietf.js | 8 +- ietf/static/js/ipr-edit.js | 105 +++-- ietf/static/js/select2.js | 16 +- ietf/static/js/sortable.js | 4 +- ietf/templates/doc/bofreq/change_editors.html | 2 - ietf/templates/group/edit_milestones.html | 191 +++++---- ietf/templates/group/milestone_form.html | 13 +- ietf/templates/ipr/details_edit.html | 40 +- ietf/templates/ipr/ipr_table.html | 2 +- ietf/utils/fields.py | 60 ++- ietf/utils/mail.py | 5 +- 28 files changed, 572 insertions(+), 571 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 24fd60eca..27cb37935 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -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 diff --git a/ietf/doc/fields.py b/ietf/doc/fields.py index c7db63b87..aeaa6f393 100644 --- a/ietf/doc/fields.py +++ b/ietf/doc/fields.py @@ -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 \ No newline at end of file + max_entries = 1 diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index cf496bca6..495fbe18c 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -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) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index f2ccb3ec9..2e50b4e49 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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') \ No newline at end of file diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index ab1bd38c8..712dd305c 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -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') diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py index 7af311959..f6f3450f4 100644 --- a/ietf/group/tests_review.py +++ b/ietf/group/tests_review.py @@ -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 \ No newline at end of file diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 2476ab18a..ed102dcbc 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -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) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 509b7b205..a7447f1cd 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -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) diff --git a/ietf/ipr/fields.py b/ietf/ipr/fields.py index 27bc662b3..c3491c940 100644 --- a/ietf/ipr/fields.py +++ b/ietf/ipr/fields.py @@ -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") \ No newline at end of file + return urlreverse('ietf.ipr.views.ajax_search') \ No newline at end of file diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index 792128592..06248e2a7 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -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.") \ No newline at end of file diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 500e5b36f..ddc3598d2 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -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') - + \ No newline at end of file diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 5d2d8393f..386178e31 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -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) \ No newline at end of file diff --git a/ietf/liaisons/fields.py b/ietf/liaisons/fields.py index fef0c82b2..e7762bb09 100644 --- a/ietf/liaisons/fields.py +++ b/ietf/liaisons/fields.py @@ -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) - ) \ No newline at end of file + return "Could not recognize the following groups: {pks}.".format(pks=", ".join(failed_pks)) diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 642ec5d5f..6221201c6 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -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 \ No newline at end of file diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 5ddeec24d..98d0f8157 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -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)) \ No newline at end of file diff --git a/ietf/person/fields.py b/ietf/person/fields.py index 6d5493b30..fafcc7151 100644 --- a/ietf/person/fields.py +++ b/ietf/person/fields.py @@ -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) diff --git a/ietf/static/js/edit-milestones.js b/ietf/static/js/edit-milestones.js index 5ff50efca..40633692d 100644 --- a/ietf/static/js/edit-milestones.js +++ b/ietf/static/js/edit-milestones.js @@ -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); - } -}); \ No newline at end of file + // 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); + } + }); \ No newline at end of file diff --git a/ietf/static/js/ietf.js b/ietf/static/js/ietf.js index 9ce48497e..00d250de0 100644 --- a/ietf/static/js/ietf.js +++ b/ietf/static/js/ietf.js @@ -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"); + // }); } }); diff --git a/ietf/static/js/ipr-edit.js b/ietf/static/js/ipr-edit.js index c1a735e82..313e86017 100644 --- a/ietf/static/js/ipr-edit.js +++ b/ietf/static/js/ipr-edit.js @@ -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); + }); \ No newline at end of file diff --git a/ietf/static/js/select2.js b/ietf/static/js/select2.js index 0bd501fa5..aff7ad719 100644 --- a/ietf/static/js/select2.js +++ b/ietf/static/js/select2.js @@ -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 () { diff --git a/ietf/static/js/sortable.js b/ietf/static/js/sortable.js index b1d65b9c4..3a21c358d 100644 --- a/ietf/static/js/sortable.js +++ b/ietf/static/js/sortable.js @@ -1 +1,3 @@ -import "sortablejs"; \ No newline at end of file +import { Sortable } from "sortablejs"; + +window.Sortable = Sortable; \ No newline at end of file diff --git a/ietf/templates/doc/bofreq/change_editors.html b/ietf/templates/doc/bofreq/change_editors.html index e13b9820d..ebad71345 100644 --- a/ietf/templates/doc/bofreq/change_editors.html +++ b/ietf/templates/doc/bofreq/change_editors.html @@ -22,8 +22,6 @@ Back - - {% endblock %} diff --git a/ietf/templates/group/edit_milestones.html b/ietf/templates/group/edit_milestones.html index 55b790839..99c0b26ca 100644 --- a/ietf/templates/group/edit_milestones.html +++ b/ietf/templates/group/edit_milestones.html @@ -4,128 +4,127 @@ {% load static %} {% load django_bootstrap5 %} {% load misc_filters %} - -{% block pagehead %} - {{ all_forms|merge_media:'css' }} - -{% endblock %} - +{% block pagehead %}{{ all_forms|merge_media:'css' }}{% endblock %} {% block title %}{{ title }}{% endblock %} - {% block content %} {% origin %}

{{ title }}

- - - -

Links: - {{ group.acronym }} {{ group.type.name }} +

+ {{ group.acronym }} {{ group.type.name }} {% if group.charter %} - - {{ group.charter.canonical_name }} + + {{ group.charter.canonical_name }} + + {% endif %} + {% if can_change_uses_milestone_dates %} + {% csrf_token %} + + {% endif %} +
+

+ {% 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 %}

- -
- {% if can_change_uses_milestone_dates %} -
-
{% csrf_token %} - -
-
- {% endif %} - - -
-

- {% 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 %} -

- - {% if can_reset %} -

- You can reset - this list to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}. -

- {% endif %} - - {% if form_errors %} -

There were errors, see below.

- {% endif %} -
-
- -
{% csrf_token %} -
- + {% if can_reset %} +

+ You can + + reset + this list + + to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}. +

+ {% endif %} + {% if form_errors %} +

+ There were errors, see below. +

+ {% endif %} + + {% csrf_token %} +
{% for form in forms %} -
- - +
+
+
+
{% if form.milestone.resolved %} {{ form.milestone.resolved }} {% else %} - {% if group.uses_milestone_dates %}{{ form.milestone.due|date:"M Y" }}{% endif %} + {% if group.uses_milestone_dates %}{{ form.milestone.due|date:"M Y" }}{% endif %} {% endif %} - - - {{ form.milestone.desc }} - {% if form.needs_review %}Awaiting accept{% endif %} +
+
+ {{ form.milestone.desc }} + {% if form.needs_review %} + + Awaiting accept + + {% endif %} {% if form.changed %}Changed{% endif %} {% if form.delete.data %}Deleted{% endif %} - - - {% for d in form.docs_names %} -
{{ d }}
- {% endfor %} - - - - - {% include "group/milestone_form.html" %} - + {% for d in form.docs_names %}
{{ d }}
{% endfor %} +
+
+
+ {% include "group/milestone_form.html" %} +
{% endfor %}
-
-
-
-
-
+
-
{% include "group/milestone_form.html" with form=empty_form %}
+
+
+ {% include "group/milestone_form.html" with form=empty_form %} +
- - - - Cancel - - +
+
+ +
+
+ + Cancel + + - - - - - {% if group.uses_milestone_dates %} -
- {% endif %} - + {% if group.uses_milestone_dates %}
{% endif %} {% endblock %} - {% block js %} {{ all_forms|merge_media:'js' }} - {% if not group.uses_milestone_dates %} {% endif %} -{% endblock %} - +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/group/milestone_form.html b/ietf/templates/group/milestone_form.html index 4332483f4..24a812b04 100644 --- a/ietf/templates/group/milestone_form.html +++ b/ietf/templates/group/milestone_form.html @@ -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 %} - -
- - - {% bootstrap_form form layout='horizontal' %} -
+ +{% bootstrap_form form layout='horizontal' %} diff --git a/ietf/templates/ipr/details_edit.html b/ietf/templates/ipr/details_edit.html index 9360d7408..1e2c71d57 100644 --- a/ietf/templates/ipr/details_edit.html +++ b/ietf/templates/ipr/details_edit.html @@ -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 %} -
+
-
- {% render_field draft_form.revisions class="form-control" placeholder="Revisions, e.g. 04-07" %} - + {% bootstrap_field draft_form.revisions class="form-control" placeholder="Revisions, e.g., 04-07" show_help=False show_label=False %} +
- {% render_field draft_form.sections class="form-control" placeholder="Sections" %} - + {% bootstrap_field draft_form.sections class="form-control" placeholder="Sections" show_help=False show_label=False %} +
{% endfor %} -
+ {% comment %} + {% for draft_form in draft_formset %} +
+
+ {% bootstrap_label draft_form.document.label %} +
+
+ {% bootstrap_field draft_form.document label_class="visually-hidden" show_help=False %} +
+
+ {% bootstrap_field draft_form.revisions placeholder="Revisions, e.g., 04-07" label_class="visually-hidden" show_help=False %} +
+
+ {% bootstrap_field draft_form.sections placeholder="Sections" label_class="visually-hidden" show_help=False %} +
+
+ {% endfor %} + {% endcomment %} + + @@ -255,8 +274,7 @@ {% bootstrap_field form.notes layout='horizontal' %} - - + {% bootstrap_button button_type="submit" name="submit" content="Submit" %} @@ -266,4 +284,4 @@ {{ form.media.js }} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/ipr/ipr_table.html b/ietf/templates/ipr/ipr_table.html index 405c0c9b8..886fe053e 100644 --- a/ietf/templates/ipr/ipr_table.html +++ b/ietf/templates/ipr/ipr_table.html @@ -1,7 +1,7 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %}{% origin %} {% load ietf_filters %} - +
diff --git a/ietf/utils/fields.py b/ietf/utils/fields.py index 8c670f00b..905f600c3 100644 --- a/ietf/utils/fields.py +++ b/ietf/utils/fields.py @@ -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): diff --git a/ietf/utils/mail.py b/ietf/utils/mail.py index d1949210a..c18981d41 100644 --- a/ietf/utils/mail.py +++ b/ietf/utils/mail.py @@ -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 - + \ No newline at end of file
Date