More select2 and test fixes.
- Legacy-Id: 19781
This commit is contained in:
parent
e9fd78128c
commit
c68446ae93
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
|
@ -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.")
|
|
@ -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')
|
||||
|
||||
|
|
@ -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)
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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))
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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");
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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 () {
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
import "sortablejs";
|
||||
import { Sortable } from "sortablejs";
|
||||
|
||||
window.Sortable = Sortable;
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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 %}
|
|
@ -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' %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
Loading…
Reference in a new issue