Summary: Resolve person/email/document multiselect issue by importing

select2 and switching the widgets over to using that. Port the
milestones editing page to Bootstrap.
 - Legacy-Id: 8713
This commit is contained in:
Ole Laursen 2014-11-25 16:47:48 +00:00
parent c7342d2f30
commit cebc979282
37 changed files with 1853 additions and 516 deletions

View file

@ -8,17 +8,16 @@ import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias
def tokeninput_id_doc_name_json(objs):
return json.dumps([{ "id": o.pk, "name": escape(o.name) } for o in objs])
def select2_id_doc_name_json(objs):
return json.dumps([{ "id": o.pk, "text": escape(o.name) } for o in objs])
class AutocompletedDocumentsField(forms.CharField):
"""Tokenizing autocompleted multi-select field for choosing
documents using jquery.tokeninput.js.
class SearchableDocumentsField(forms.CharField):
"""Server-based multi-select field for choosing documents using
select2.js.
The field uses a comma-separated list of primary keys in a
CharField element as its API, the tokeninput Javascript adds some
selection magic on top of this so we have to pass it a JSON
representation of ids and user-understandable labels."""
CharField element as its API with some extra attributes used by
the Javascript part."""
def __init__(self,
max_entries=None, # max number of selected objs
@ -31,39 +30,45 @@ class AutocompletedDocumentsField(forms.CharField):
self.doc_type = doc_type
self.model = model
super(AutocompletedDocumentsField, self).__init__(*args, **kwargs)
super(SearchableDocumentsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "tokenized-field"
self.widget.attrs["data-hint-text"] = hint_text
self.widget.attrs["class"] = "select2-field"
self.widget.attrs["data-placeholder"] = hint_text
if self.max_entries != None:
self.widget.attrs["data-max-entries"] = self.max_entries
def parse_tokenized_value(self, value):
def parse_select2_value(self, value):
return [x.strip() for x in value.split(",") if x.strip()]
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
value = self.model.objects.filter(pk__in=pks, type=self.doc_type)
pks = self.parse_select2_value(value)
value = self.model.objects.filter(pk__in=pks)
filter_args = {}
if self.model == DocAlias:
filter_args["document__type"] = self.doc_type
else:
filter_args["type"] = self.doc_type
value = value.filter(**filter_args)
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = tokeninput_id_doc_name_json(value)
self.widget.attrs["data-pre"] = select2_id_doc_name_json(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"] = urlreverse("ajax_tokeninput_search_docs", kwargs={
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_select2_search_docs", kwargs={
"doc_type": self.doc_type,
"model_name": self.model.__name__.lower()
})
return ",".join(o.pk for o in value)
return u",".join(unicode(o.pk) for o in value)
def clean(self, value):
value = super(AutocompletedDocumentsField, self).clean(value)
pks = self.parse_tokenized_value(value)
value = super(SearchableDocumentsField, self).clean(value)
pks = self.parse_select2_value(value)
objs = self.model.objects.filter(pk__in=pks)
@ -77,7 +82,7 @@ class AutocompletedDocumentsField(forms.CharField):
return objs
class AutocompletedDocAliasField(AutocompletedDocumentsField):
class SearchableDocAliasesField(SearchableDocumentsField):
def __init__(self, model=DocAlias, *args, **kwargs):
super(AutocompletedDocAliasField, self).__init__(model=model, *args, **kwargs)
super(SearchableDocAliasesField, self).__init__(model=model, *args, **kwargs)

View file

@ -127,7 +127,7 @@ class SearchTestCase(TestCase):
draft = make_test_data()
# Document
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
url = urlreverse("ajax_select2_search_docs", kwargs={
"model_name": "document",
"doc_type": "draft",
})
@ -139,7 +139,7 @@ class SearchTestCase(TestCase):
# DocAlias
doc_alias = draft.docalias_set.get()
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
url = urlreverse("ajax_select2_search_docs", kwargs={
"model_name": "docalias",
"doc_type": "draft",
})

View file

@ -1227,8 +1227,7 @@ class ChangeReplacesTests(TestCase):
# Post that says replacea replaces base a
self.assertEqual(self.basea.get_state().slug,'active')
repljson='{"%d":"%s"}'%(DocAlias.objects.get(name=self.basea.name).id,self.basea.name)
r = self.client.post(url, dict(replaces=repljson))
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id)))
self.assertEqual(r.status_code, 302)
self.assertEqual(RelatedDocument.objects.filter(relationship__slug='replaces',source=self.replacea).count(),1)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
@ -1236,23 +1235,20 @@ class ChangeReplacesTests(TestCase):
# Post that says replaceboth replaces both base a and base b
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replaceboth.name))
self.assertEqual(self.baseb.get_state().slug,'expired')
repljson='{"%d":"%s","%d":"%s"}'%(DocAlias.objects.get(name=self.basea.name).id,self.basea.name,
DocAlias.objects.get(name=self.baseb.name).id,self.baseb.name)
r = self.client.post(url, dict(replaces=repljson))
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id) + "," + str(DocAlias.objects.get(name=self.baseb.name).id)))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
# Post that undoes replaceboth
repljson='{}'
r = self.client.post(url, dict(replaces=repljson))
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
# Post that undoes replacea
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replacea.name))
r = self.client.post(url, dict(replaces=repljson))
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')

View file

@ -49,7 +49,7 @@ urlpatterns = patterns('',
url(r'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),
url(r'^tokeninputsearch/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_tokeninput_search_docs, name="ajax_tokeninput_search_docs"),
url(r'^select2search/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_select2_search_docs, name="ajax_select2_search_docs"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?$', views_doc.document_main, name="doc_view"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/history/$', views_doc.document_history, name="doc_history"),

View file

@ -1,6 +1,6 @@
# changing state and metadata on Internet Drafts
import datetime, json
import datetime
from django import forms
from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
@ -24,13 +24,14 @@ from ietf.doc.utils import ( add_state_change_event, can_adopt_draft,
get_tags_for_stream_id, nice_consensus,
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify )
from ietf.doc.lastcall import request_last_call
from ietf.doc.fields import SearchableDocAliasesField
from ietf.group.models import Group, Role
from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person
from ietf.ietfauth.utils import role_required
from ietf.message.models import Message
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName
from ietf.person.fields import AutocompletedEmailField
from ietf.person.fields import SearchableEmailField
from ietf.person.models import Person, Email
from ietf.secr.lib.template import jsonapi
from ietf.utils.mail import send_mail, send_mail_message
@ -307,35 +308,21 @@ def collect_email_addresses(emails, doc):
return emails
class ReplacesForm(forms.Form):
replaces = forms.CharField(max_length=512,widget=forms.HiddenInput)
replaces = SearchableDocAliasesField(required=False)
comment = forms.CharField(widget=forms.Textarea, required=False)
def __init__(self, *args, **kwargs):
self.doc = kwargs.pop('doc')
super(ReplacesForm, self).__init__(*args, **kwargs)
drafts = {}
for d in self.doc.related_that_doc("replaces"):
drafts[d.id] = d.document.name
self.initial['replaces'] = json.dumps(drafts)
self.initial['replaces'] = self.doc.related_that_doc("replaces")
def clean_replaces(self):
data = self.cleaned_data['replaces'].strip()
if data:
ids = [int(x) for x in json.loads(data)]
else:
return []
objects = []
for id in ids:
try:
d = DocAlias.objects.get(pk=id)
except DocAlias.DoesNotExist:
raise forms.ValidationError("ERROR: %s not found for id %d" % DocAlias._meta.verbos_name, id)
for d in self.cleaned_data['replaces']:
if d.document == self.doc:
raise forms.ValidationError("ERROR: A draft can't replace itself")
raise forms.ValidationError("A draft can't replace itself")
if d.document.type_id == "draft" and d.document.get_state_slug() == "rfc":
raise forms.ValidationError("ERROR: A draft can't replace an RFC")
objects.append(d)
return objects
raise forms.ValidationError("A draft can't replace an RFC")
return self.cleaned_data['replaces']
def replaces(request, name):
"""Change 'replaces' set of a Document of type 'draft' , notifying parties
@ -942,7 +929,7 @@ def edit_shepherd_writeup(request, name):
context_instance=RequestContext(request))
class ShepherdForm(forms.Form):
shepherd = AutocompletedEmailField(required=False, only_users=True)
shepherd = SearchableEmailField(required=False, only_users=True)
def edit_shepherd(request, name):
"""Change the shepherd for a Document"""
@ -968,7 +955,7 @@ def edit_shepherd(request, name):
c.desc = "Document shepherd changed to "+ (doc.shepherd.person.name if doc.shepherd else "(None)")
c.save()
if doc.shepherd.formatted_email() not in doc.notify:
if doc.shepherd and doc.shepherd.formatted_email() not in doc.notify:
login = request.user.person
addrs = doc.notify
if addrs:

View file

@ -45,7 +45,7 @@ from ietf.community.models import CommunityList
from ietf.doc.models import ( Document, DocAlias, State, RelatedDocument, DocEvent,
LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
from ietf.doc.expire import expirable_draft
from ietf.doc.fields import tokeninput_id_doc_name_json
from ietf.doc.fields import select2_id_doc_name_json
from ietf.group.models import Group
from ietf.idindex.index import active_drafts_index_by_group
from ietf.ipr.models import IprDocAlias
@ -629,7 +629,7 @@ def index_active_drafts(request):
return render_to_response("doc/index_active_drafts.html", { 'groups': groups }, context_instance=RequestContext(request))
def ajax_tokeninput_search_docs(request, model_name, doc_type):
def ajax_select2_search_docs(request, model_name, doc_type):
if model_name == "docalias":
model = DocAlias
else:
@ -652,4 +652,4 @@ def ajax_tokeninput_search_docs(request, model_name, doc_type):
objs = qs.distinct().order_by("name")[:20]
return HttpResponse(tokeninput_id_doc_name_json(objs), content_type='application/json')
return HttpResponse(select2_id_doc_name_json(objs), content_type='application/json')

View file

@ -19,7 +19,7 @@ from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStat
from ietf.group.utils import save_group_in_history, can_manage_group_type
from ietf.group.utils import get_group_or_404
from ietf.ietfauth.utils import has_role
from ietf.person.fields import AutocompletedEmailsField
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Person, Email
from ietf.group.mails import email_iesg_secretary_re_charter
@ -29,11 +29,11 @@ class GroupForm(forms.Form):
name = forms.CharField(max_length=255, label="Name", required=True)
acronym = forms.CharField(max_length=10, label="Acronym", required=True)
state = forms.ModelChoiceField(GroupStateName.objects.all(), label="State", required=True)
chairs = AutocompletedEmailsField(label="Chairs", required=False, only_users=True)
secretaries = AutocompletedEmailsField(label="Secretarias", required=False, only_users=True)
techadv = AutocompletedEmailsField(label="Technical Advisors", required=False, only_users=True)
delegates = AutocompletedEmailsField(label="Delegates", required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - at most %s persons at a given time." % MAX_GROUP_DELEGATES))
chairs = SearchableEmailsField(label="Chairs", required=False, only_users=True)
secretaries = SearchableEmailsField(label="Secretarias", required=False, only_users=True)
techadv = SearchableEmailsField(label="Technical Advisors", required=False, only_users=True)
delegates = SearchableEmailsField(label="Delegates", required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - at most %s persons at a given time." % MAX_GROUP_DELEGATES))
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), label="Shepherding AD", empty_label="(None)", required=False)
parent = forms.ModelChoiceField(Group.objects.filter(state="active").order_by('name'), empty_label="(None)", required=False)
list_email = forms.CharField(max_length=64, required=False)

View file

@ -303,7 +303,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
if group.features.has_milestones:
if group.state_id != "proposed" and (is_chair or can_manage):
actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
if group.features.has_materials and can_manage_materials(request.user, group):
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))

View file

@ -2,99 +2,72 @@
import datetime
import calendar
import json
from django import forms
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect, Http404
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect, Http404
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from ietf.doc.models import Document, DocEvent
from ietf.doc.models import DocEvent
from ietf.doc.utils import get_chartering_type
from ietf.doc.fields import AutocompletedDocumentsField
from ietf.doc.fields import SearchableDocumentsField
from ietf.group.models import GroupMilestone, MilestoneGroupEvent
from ietf.group.utils import (save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type,
get_group_or_404)
from ietf.name.models import GroupMilestoneStateName
from ietf.group.mails import email_milestones_changed
def json_doc_names(docs):
return json.dumps([{"id": doc.pk, "name": doc.name } for doc in docs])
def parse_doc_names(s):
return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft")
from ietf.utils.fields import DatepickerDateField
class MilestoneForm(forms.Form):
id = forms.IntegerField(required=True, widget=forms.HiddenInput)
desc = forms.CharField(max_length=500, label="Milestone:", required=True)
due_month = forms.TypedChoiceField(choices=(), required=True, coerce=int)
due_year = forms.TypedChoiceField(choices=(), required=True, coerce=int)
desc = forms.CharField(max_length=500, label="Milestone", required=True)
due = DatepickerDateField(date_format="MM yyyy", picker_settings={"min-view-mode": "months", "autoclose": "1", "view-mode": "years" }, required=True)
docs = SearchableDocumentsField(label="Drafts", required=False, help_text="Any drafts that the milestone concerns.")
resolved_checkbox = forms.BooleanField(required=False, label="Resolved")
resolved = forms.CharField(max_length=50, required=False)
resolved = forms.CharField(label="Resolved as", max_length=50, required=False)
delete = forms.BooleanField(required=False, initial=False)
docs = AutocompletedDocumentsField(required=False)
accept = forms.ChoiceField(choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")),
review = forms.ChoiceField(label="Review action", help_text="Choose whether to accept or reject the proposed changes.",
choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")),
required=False, initial="noaction", widget=forms.RadioSelect)
def __init__(self, *args, **kwargs):
kwargs["label_suffix"] = ""
def __init__(self, needs_review, reviewer, *args, **kwargs):
m = self.milestone = kwargs.pop("instance", None)
self.needs_review = kwargs.pop("needs_review", False)
can_review = not self.needs_review
can_review = not needs_review
if m:
self.needs_review = m.state_id == "review"
needs_review = m.state_id == "review"
if not "initial" in kwargs:
kwargs["initial"] = {}
kwargs["initial"].update(dict(id=m.pk,
desc=m.desc,
due_month=m.due.month,
due_year=m.due.year,
due=m.due,
resolved_checkbox=bool(m.resolved),
resolved=m.resolved,
docs=",".join(m.docs.values_list("pk", flat=True)),
docs=m.docs.all(),
delete=False,
accept="noaction" if can_review and self.needs_review else None,
review="noaction" if can_review and needs_review else "",
))
kwargs["prefix"] = "m%s" % m.pk
super(MilestoneForm, self).__init__(*args, **kwargs)
# set choices for due date
this_year = datetime.date.today().year
self.fields["resolved"].widget.attrs["data-default"] = "Done"
self.fields["due_month"].choices = [(month, datetime.date(this_year, month, 1).strftime("%B")) for month in range(1, 13)]
if needs_review and self.milestone and self.milestone.state_id != "review":
self.fields["desc"].widget.attrs["readonly"] = True
years = [ y for y in range(this_year, this_year + 10)]
self.changed = False
initial = self.initial.get("due_year")
if initial and initial not in years:
years.insert(0, initial)
if not (needs_review and can_review):
self.fields["review"].widget = forms.HiddenInput()
self.fields["due_year"].choices = zip(years, map(str, years))
# figure out what to prepopulate many-to-many field with
pre = ""
if not self.is_bound:
pre = self.initial.get("docs", "")
else:
pre = self["docs"].data or ""
# this is ugly, but putting it on self["docs"] is buggy with a
# bound/unbound form in Django 1.2
self.docs_names = parse_doc_names(pre)
self.docs_prepopulate = json_doc_names(self.docs_names)
# calculate whether we've changed
self.changed = self.is_bound and (not self.milestone or any(unicode(self[f].data) != unicode(self.initial[f]) for f in self.fields.iterkeys()))
self.needs_review = needs_review
def clean_resolved(self):
r = self.cleaned_data["resolved"].strip()
@ -134,14 +107,17 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
title = "Edit charter milestones for %s %s" % (group.acronym, group.type.name)
milestones = group.groupmilestone_set.filter(state="charter")
reviewer = milestone_reviewer_for_group_type(group_type)
forms = []
milestones_dict = dict((str(m.id), m) for m in milestones)
def due_month_year_to_date(c):
y = c["due_year"]
m = c["due_month"]
return datetime.date(y, m, calendar.monthrange(y, m)[1])
y = c["due"].year
m = c["due"].month
first_day, last_day = calendar.monthrange(y, m)
return datetime.date(y, m, last_day)
def set_attributes_from_form(f, m):
c = f.cleaned_data
@ -153,10 +129,24 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
m.state = GroupMilestoneStateName.objects.get(slug="active")
elif milestone_set == "charter":
m.state = GroupMilestoneStateName.objects.get(slug="charter")
m.desc = c["desc"]
m.due = due_month_year_to_date(c)
m.resolved = c["resolved"]
def milestone_changed(f, m):
# we assume that validation has run
if not m or not f.is_valid():
return True
c = f.cleaned_data
return (c["desc"] != m.desc or
due_month_year_to_date(c) != m.due or
c["resolved"] != m.resolved or
set(c["docs"]) != set(m.docs.all()) or
c.get("review") in ("accept", "reject")
)
def save_milestone_form(f):
c = f.cleaned_data
@ -180,14 +170,14 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
changes = ['Changed %s' % named_milestone]
if m.state_id == "review" and not needs_review and c["accept"] != "noaction":
if m.state_id == "review" and not needs_review and c["review"] != "noaction":
if not history:
history = save_milestone_in_history(m)
if c["accept"] == "accept":
if c["review"] == "accept":
m.state_id = "active"
changes.append("set state to active from review, accepting new milestone")
elif c["accept"] == "reject":
elif c["review"] == "reject":
m.state_id = "deleted"
changes.append("set state to deleted from review, rejecting new milestone")
@ -257,8 +247,6 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
elif m.state_id == "review":
return 'Added %s for review, due %s' % (named_milestone, m.due.strftime("%B %Y"))
finished_milestone_text = "Done"
form_errors = False
if request.method == 'POST':
@ -269,22 +257,23 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
# new milestones have non-existing ids so instance end up as None
instance = milestones_dict.get(request.POST.get(prefix + "-id", ""), None)
f = MilestoneForm(request.POST, prefix=prefix, instance=instance,
needs_review=needs_review)
f = MilestoneForm(needs_review, reviewer, request.POST, prefix=prefix, instance=instance)
forms.append(f)
form_errors = form_errors or not f.is_valid()
f.changed = milestone_changed(f, f.milestone)
if f.is_valid() and f.cleaned_data.get("review") in ("accept", "reject"):
f.needs_review = False
action = request.POST.get("action", "review")
if action == "review":
for f in forms:
if not f.is_valid():
continue
# let's fill in the form milestone so we can output it in the template
if not f.milestone:
f.milestone = GroupMilestone()
set_attributes_from_form(f, f.milestone)
if f.is_valid():
# let's fill in the form milestone so we can output it in the template
if not f.milestone:
f.milestone = GroupMilestone()
set_attributes_from_form(f, f.milestone)
elif action == "save" and not form_errors:
changes = []
for f in forms:
@ -311,11 +300,11 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
return HttpResponseRedirect(group.about_url())
else:
for m in milestones:
forms.append(MilestoneForm(instance=m, needs_review=needs_review))
forms.append(MilestoneForm(needs_review, reviewer, instance=m))
can_reset = milestone_set == "charter" and get_chartering_type(group.charter) == "rechartering"
empty_form = MilestoneForm(needs_review=needs_review)
empty_form = MilestoneForm(needs_review, reviewer)
forms.sort(key=lambda f: f.milestone.due if f.milestone else datetime.date.max)
@ -326,9 +315,8 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
form_errors=form_errors,
empty_form=empty_form,
milestone_set=milestone_set,
finished_milestone_text=finished_milestone_text,
needs_review=needs_review,
reviewer=milestone_reviewer_for_group_type(group_type),
reviewer=reviewer,
can_reset=can_reset))
@login_required

View file

@ -1,7 +1,6 @@
import os
import shutil
import calendar
import json
import datetime
from pyquery import PyQuery
@ -530,23 +529,21 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': "-1",
'm-1-desc': "", # no description
'm-1-due_month': str(due.month),
'm-1-due_year': str(due.year),
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': ",".join(docs),
'action': "save",
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
# add
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': "-1",
'm-1-desc': "Test 3",
'm-1-due_month': str(due.month),
'm-1-due_year': str(due.year),
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': ",".join(docs),
'action': "save",
@ -580,8 +577,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': -1,
'm-1-desc': "Test 3",
'm-1-due_month': str(due.month),
'm-1-due_year': str(due.year),
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': "",
'action': "save",
@ -612,11 +608,10 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': m1.desc,
'm1-due_month': str(m1.due.month),
'm1-due_year': str(m1.due.year),
'm1-due': m1.due.strftime("%B %Y"),
'm1-resolved': m1.resolved,
'm1-docs': ",".join(m1.docs.values_list("name", flat=True)),
'm1-accept': "accept",
'm1-review': "accept",
'action': "save",
})
self.assertEqual(r.status_code, 302)
@ -639,8 +634,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': m1.desc,
'm1-due_month': str(m1.due.month),
'm1-due_year': str(m1.due.year),
'm1-due': m1.due.strftime("%B %Y"),
'm1-resolved': "",
'm1-docs': ",".join(m1.docs.values_list("name", flat=True)),
'm1-delete': "checked",
@ -670,15 +664,14 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': "", # no description
'm1-due_month': str(due.month),
'm1-due_year': str(due.year),
'm1-due': due.strftime("%B %Y"),
'm1-resolved': "",
'm1-docs': ",".join(docs),
'action': "save",
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
self.assertEqual(m.due, m1.due)
@ -688,8 +681,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': "Test 2 - changed",
'm1-due_month': str(due.month),
'm1-due_year': str(due.year),
'm1-due': due.strftime("%B %Y"),
'm1-resolved': "Done",
'm1-resolved_checkbox': "checked",
'm1-docs': ",".join(docs),

View file

@ -10,7 +10,7 @@ from ietf.group.models import Group, GroupEvent, Role
from ietf.group.utils import save_group_in_history
from ietf.ietfauth.utils import has_role
from ietf.name.models import StreamName
from ietf.person.fields import AutocompletedEmailsField
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Email
import debug # pyflakes:ignore
@ -33,7 +33,7 @@ def stream_documents(request, acronym):
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable }, context_instance=RequestContext(request))
class StreamEditForm(forms.Form):
delegates = AutocompletedEmailsField(required=False, only_users=True)
delegates = SearchableEmailsField(required=False, only_users=True)
def stream_edit(request, acronym):
group = get_object_or_404(Group, acronym=acronym)

View file

@ -17,7 +17,7 @@ from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEM
get_user_email, validate_private_key, validate_public_key,
get_or_create_nominee, create_feedback_email)
from ietf.person.models import Email
from ietf.person.fields import AutocompletedEmailField
from ietf.person.fields import SearchableEmailField
from ietf.utils.fields import MultiEmailField
from ietf.utils.mail import send_mail
@ -657,7 +657,7 @@ class PositionForm(BaseNomcomForm, forms.ModelForm):
fieldsets = [('Position', ('name', 'description',
'is_open', 'incumbent'))]
incumbent = AutocompletedEmailField(required=False)
incumbent = SearchableEmailField(required=False)
class Meta:
model = Position

View file

@ -8,7 +8,7 @@ import debug # pyflakes:ignore
from ietf.person.models import Email, Person
def tokeninput_id_name_json(objs):
def select2_id_name_json(objs):
def format_email(e):
return escape(u"%s <%s>" % (e.person.name, e.address))
def format_person(p):
@ -16,19 +16,19 @@ def tokeninput_id_name_json(objs):
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
return json.dumps([{ "id": o.pk, "name": formatter(o) } for o in objs])
return json.dumps([{ "id": o.pk, "text": formatter(o) } for o in objs])
class AutocompletedPersonsField(forms.CharField):
"""Tokenizing autocompleted multi-select field for choosing
persons/emails or just persons using jquery.tokeninput.js.
class SearchablePersonsField(forms.CharField):
"""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.
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, the tokeninput Javascript adds some
selection magic on top of this so we have to pass it a JSON
representation of ids and user-understandable labels."""
CharField element as its API with some extra attributes used by
the Javascript part."""
def __init__(self,
max_entries=None, # max number of selected objs
@ -41,38 +41,38 @@ class AutocompletedPersonsField(forms.CharField):
self.only_users = only_users
self.model = model
super(AutocompletedPersonsField, self).__init__(*args, **kwargs)
super(SearchablePersonsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "tokenized-field"
self.widget.attrs["data-hint-text"] = hint_text
self.widget.attrs["class"] = "select2-field"
self.widget.attrs["data-placeholder"] = hint_text
if self.max_entries != None:
self.widget.attrs["data-max-entries"] = self.max_entries
def parse_tokenized_value(self, value):
def parse_select2_value(self, value):
return [x.strip() for x in value.split(",") if x.strip()]
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
pks = self.parse_select2_value(value)
value = self.model.objects.filter(pk__in=pks).select_related("person")
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = tokeninput_id_name_json(value)
self.widget.attrs["data-pre"] = select2_id_name_json(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"] = urlreverse("ajax_tokeninput_search", kwargs={ "model_name": self.model.__name__.lower() })
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_select2_search_person_email", kwargs={ "model_name": self.model.__name__.lower() })
if self.only_users:
self.widget.attrs["data-ajax-url"] += "?user=1" # require a Datatracker account
return ",".join(e.address for e in value)
return u",".join(e.address for e in value)
def clean(self, value):
value = super(AutocompletedPersonsField, self).clean(value)
pks = self.parse_tokenized_value(value)
value = super(SearchablePersonsField, self).clean(value)
pks = self.parse_select2_value(value)
objs = self.model.objects.filter(pk__in=pks)
if self.model == Email:
@ -92,32 +92,32 @@ class AutocompletedPersonsField(forms.CharField):
return objs
class AutocompletedPersonField(AutocompletedPersonsField):
"""Version of AutocompletedPersonsField specialized to a single object."""
class SearchablePersonField(SearchablePersonsField):
"""Version of SearchablePersonsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedPersonField, self).__init__(*args, **kwargs)
super(SearchablePersonField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedPersonField, self).clean(value).first()
return super(SearchablePersonField, self).clean(value).first()
class AutocompletedEmailsField(AutocompletedPersonsField):
"""Version of AutocompletedPersonsField with the defaults right for Emails."""
class SearchableEmailsField(SearchablePersonsField):
"""Version of SearchablePersonsField with the defaults right for Emails."""
def __init__(self, model=Email, hint_text="Type in name or email to search for person and email address.",
*args, **kwargs):
super(AutocompletedEmailsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
super(SearchableEmailsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
class AutocompletedEmailField(AutocompletedEmailsField):
"""Version of AutocompletedEmailsField specialized to a single object."""
class SearchableEmailField(SearchableEmailsField):
"""Version of SearchableEmailsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedEmailField, self).__init__(*args, **kwargs)
super(SearchableEmailField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedEmailField, self).clean(value).first()
return super(SearchableEmailField, self).clean(value).first()

View file

@ -11,7 +11,7 @@ class PersonTests(TestCase):
draft = make_test_data()
person = draft.ad
r = self.client.get(urlreverse("ietf.person.views.ajax_tokeninput_search", kwargs={ "model_name": "email"}), dict(q=person.name))
r = self.client.get(urlreverse("ietf.person.views.ajax_select2_search", kwargs={ "model_name": "email"}), dict(q=person.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], person.email_address())

View file

@ -2,6 +2,6 @@ from django.conf.urls import patterns
from ietf.person import ajax
urlpatterns = patterns('',
(r'^search/(?P<model_name>(person|email))/$', "ietf.person.views.ajax_tokeninput_search", None, 'ajax_tokeninput_search'),
(r'^search/(?P<model_name>(person|email))/$', "ietf.person.views.ajax_select2_search", None, 'ajax_select2_search_person_email'),
(r'^(?P<personid>[a-z0-9]+).json$', ajax.person_json),
)

View file

@ -2,9 +2,9 @@ from django.http import HttpResponse
from django.db.models import Q
from ietf.person.models import Email, Person
from ietf.person.fields import tokeninput_id_name_json
from ietf.person.fields import select2_id_name_json
def ajax_tokeninput_search(request, model_name):
def ajax_select2_search(request, model_name):
if model_name == "email":
model = Email
else:
@ -39,6 +39,11 @@ def ajax_tokeninput_search(request, model_name):
if only_users:
objs = objs.exclude(user=None)
objs = objs.distinct()[:10]
try:
page = int(request.GET.get("p", 1)) - 1
except ValueError:
page = 0
return HttpResponse(tokeninput_id_name_json(objs), content_type='application/json')
objs = objs.distinct()[page:page + 10]
return HttpResponse(select2_id_name_json(objs), content_type='application/json')

View file

@ -8,7 +8,7 @@ from ietf.doc.models import Document, DocAlias, State
from ietf.name.models import IntendedStdLevelName, DocRelationshipName
from ietf.group.models import Group
from ietf.person.models import Person, Email
from ietf.person.fields import AutocompletedEmailField
from ietf.person.fields import SearchableEmailField
from ietf.secr.groups.forms import get_person
@ -132,7 +132,7 @@ class EditModelForm(forms.ModelForm):
iesg_state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft-iesg'),required=False)
group = GroupModelChoiceField(required=True)
review_by_rfc_editor = forms.BooleanField(required=False)
shepherd = AutocompletedEmailField(required=False, only_users=True)
shepherd = SearchableEmailField(required=False, only_users=True)
class Meta:
model = Document

View file

@ -2,7 +2,7 @@ from django import forms
from ietf.group.models import Group
from ietf.meeting.models import ResourceAssociation
from ietf.person.fields import AutocompletedPersonsField
from ietf.person.fields import SearchablePersonsField
# -------------------------------------------------
@ -67,7 +67,7 @@ class SessionForm(forms.Form):
wg_selector3 = forms.ChoiceField(choices=WG_CHOICES,required=False)
third_session = forms.BooleanField(required=False)
resources = forms.MultipleChoiceField(choices=[(x.pk,x.desc) for x in ResourceAssociation.objects.all()], widget=forms.CheckboxSelectMultiple,required=False)
bethere = AutocompletedPersonsField(label="Must be present", required=False)
bethere = SearchablePersonsField(label="Must be present", required=False)
def __init__(self, *args, **kwargs):
super(SessionForm, self).__init__(*args, **kwargs)

View file

@ -7,9 +7,58 @@
<link rel="stylesheet" type="text/css" href="{{ SECR_STATIC_URL }}css/jquery.ui.autocomplete.css" />
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery-ui-1.8.1.custom.min.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
<script>
// this is copy-pasted from ietf.js, we should include that here too instead of this
$(document).ready(function () {
$(".select2-field").each(function () {
var e = $(this);
if (e.data("ajax-url")) {
var maxEntries = e.data("max-entries");
var multiple = maxEntries != 1;
var prefetched = e.data("pre");
e.select2({
multiple: multiple,
minimumInputLength: 2,
width: "off",
allowClear: true,
maximumSelectionSize: maxEntries,
ajax: {
url: e.data("ajax-url"),
dataType: "json",
quietMillis: 250,
data: function (term, page) {
return {
q: term,
p: page
};
},
results: function (results) {
return {
results: results,
more: results.length == 10
};
}
},
escapeMarkup: function (m) {
return m;
},
initSelection: function (element, cb) {
if (!multiple && prefetched.length > 0)
cb(prefetched[0]);
else
cb(prefetched);
},
dropdownCssClass: "bigdrop"
});
}
});
});
</script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}

View file

@ -5,9 +5,58 @@
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/sessions.js"></script>
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
<script>
// this is copy-pasted from ietf.js, we should include that here too instead of this
$(document).ready(function () {
$(".select2-field").each(function () {
var e = $(this);
if (e.data("ajax-url")) {
var maxEntries = e.data("max-entries");
var multiple = maxEntries != 1;
var prefetched = e.data("pre");
e.select2({
multiple: multiple,
minimumInputLength: 2,
width: "off",
allowClear: true,
maximumSelectionSize: maxEntries,
ajax: {
url: e.data("ajax-url"),
dataType: "json",
quietMillis: 250,
data: function (term, page) {
return {
q: term,
p: page
};
},
results: function (results) {
return {
results: results,
more: results.length == 10
};
}
},
escapeMarkup: function (m) {
return m;
},
initSelection: function (element, cb) {
if (!multiple && prefetched.length > 0)
cb(prefetched[0]);
else
cb(prefetched);
},
dropdownCssClass: "bigdrop"
});
}
});
});
</script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}

View file

@ -5,9 +5,58 @@
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/sessions.js"></script>
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
<script>
// this is copy-pasted from ietf.js, we should include that here too instead of this
$(document).ready(function () {
$(".select2-field").each(function () {
var e = $(this);
if (e.data("ajax-url")) {
var maxEntries = e.data("max-entries");
var multiple = maxEntries != 1;
var prefetched = e.data("pre");
e.select2({
multiple: multiple,
minimumInputLength: 2,
width: "off",
allowClear: true,
maximumSelectionSize: maxEntries,
ajax: {
url: e.data("ajax-url"),
dataType: "json",
quietMillis: 250,
data: function (term, page) {
return {
q: term,
p: page
};
},
results: function (results) {
return {
results: results,
more: results.length == 10
};
}
},
escapeMarkup: function (m) {
return m;
},
initSelection: function (element, cb) {
if (!multiple && prefetched.length > 0)
cb(prefetched[0]);
else
cb(prefetched);
},
dropdownCssClass: "bigdrop"
});
}
});
});
</script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}

View file

@ -7,8 +7,8 @@ Change document shepherd for {{ doc.name }}-{{ doc.rev }}
{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="/facelift/css/tokenfield-typeahead.min.css">
<link rel="stylesheet" href="/facelift/css/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
{% endblock %}
{% block content %}
@ -19,7 +19,7 @@ Change document shepherd for {{ doc.name }}-{{ doc.rev }}
{% bootstrap_messages %}
<form class="tokenized-form" role="form" enctype="multipart/form-data" method="post">
<form role="form" enctype="multipart/form-data" method="post">
{% csrf_token %}
{% bootstrap_form form %}
@ -31,6 +31,5 @@ Change document shepherd for {{ doc.name }}-{{ doc.rev }}
{% endblock %}
{% block js %}
<script src="/facelift/js/lib/typeahead.bundle.min.js"></script>
<script src="/facelift/js/lib/bootstrap-tokenfield.min.js"></script>
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
{% endblock %}

View file

@ -7,23 +7,17 @@
{% bootstrap_messages %}
{% block pagehead %}
<link rel="stylesheet" href="/facelift/css/tokenfield-typeahead.min.css">
<link rel="stylesheet" href="/facelift/css/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
{% endblock %}
{% block content %}
<h1>Change documents replaced by<br><small>{{ doc }}</small></h1>
<form class="tokenized-form" role="form" method="post">
<form role="form" method="post">
{% csrf_token %}
<div class="form-group">
<label>{{ form.replaces.label }}</label>
<input type="text" class="form-control tokenized-field" data-ajax-url="{% url "doc_ajax_internet_draft" %}?term=" data-display="label" data-io="#id_replaces" data-format="json">
<span class="help-block">Enter draft names, separated with commas.</span>
</div>
{% bootstrap_form form %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Save</button>
@ -34,6 +28,5 @@
{% endblock %}
{% block js %}
<script src="/facelift/js/lib/typeahead.bundle.min.js"></script>
<script src="/facelift/js/lib/bootstrap-tokenfield.min.js"></script>
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
{% endblock %}

View file

@ -10,7 +10,7 @@
{% bootstrap_messages %}
<form class="tokenized-form" role="form" method="post">
<form role="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}

View file

@ -12,20 +12,18 @@
{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="/facelift/css/tokenfield-typeahead.min.css">
<link rel="stylesheet" href="/facelift/css/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
{% endblock %}
{% block content %}
<h1>
{% if action == "edit" %}
Edit {{ group.type.name }} {{ group.acronym }}
{% elif action == "charter" %}
Start chartering new group
{% else %}
{% if action == "charter" %}
Start chartering new group
{% else %}
Create new group or BOF
{% endif %}
Create new group or BOF
{% endif %}
</h1>
@ -35,7 +33,7 @@
chairs and delegates, need a datatracker account to actually do
so. New accounts can be <a href="{% url "create_account" %}">created here</a>.</p>
<form class="tokenized-form form-horizontal" role="form" method="post">
<form class="form-horizontal" role="form" method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
@ -43,18 +41,15 @@ so. New accounts can be <a href="{% url "create_account" %}">created here</a>.</
{% if action == "edit" %}
<button class="btn btn-primary" type="submit">Submit</button>
<a class="btn btn-default pull-right" href="{{ group.about_url }}">Back</a>
{% else %}
{% if action == "charter" %}
<button class="btn btn-primary" type="submit">Start chartering</button>
{% else %}
<button class="btn btn-primary" type="submit">Create group or BOF</button>
{% endif %}
{% endif %}
{% endbuttons %}
{% elif action == "charter" %}
<button class="btn btn-primary" type="submit">Start chartering</button>
{% else %}
<button class="btn btn-primary" type="submit">Create group or BOF</button>
{% endif %}
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
<script src="/facelift/js/lib/typeahead.bundle.min.js"></script>
<script src="/facelift/js/lib/bootstrap-tokenfield.min.js"></script>
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
{% endblock %}

View file

@ -1,111 +1,99 @@
{% extends "base.html" %}
{% extends "ietf.html" %}
{% load bootstrap3 %}
{% block pagehead %}
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
<link rel="stylesheet" href="/facelift/css/datepicker3.css">
{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block morecss %}
tr.milestone td { padding: 0.2em 0; cursor: pointer; vertical-align: top; }
tr.milestone:hover { background-color: #e8f0fa; }
td.due { width: 5em; }
.milestone.changed { font-weight: bold; }
.milestone .note { font-style: italic; display: inline-block; margin-left: 0.5em; color: #2647a0; }
.milestone .doc { display: block; padding-left: 1em; }
.edit-milestone { display: none; }
.milestone.delete, .edit-milestone.delete, .edit-milestone.delete input { color: #aaa !important; }
.edit-milestone table { margin: 1em 0; }
.edit-milestone table td { padding: 0.1em; }
.edit-milestone .desc input { width: 50em; }
.edit-milestone .due input { width: 6em; }
.edit-milestone input[type=checkbox] { vertical-align: middle; margin: 0 0.2em 0 0.8em;}
.edit-milestone .resolved label { vertical-align: middle; }
.edit-milestone .delete label { vertical-align: middle; }
.edit-milestone .accept ul { display: inline-block; margin: 0; padding: 0; }
.edit-milestone .accept ul li { list-style: none; display: inline-block; margin: 0; padding: 0; padding-left: 0.4em; }
.edit-milestone .accept ul li label { vertical-align: middle; }
.edit-milestone .accept ul li input { margin: 0; padding: 0; vertical-align: middle; }
.edit-milestone .docs td { vertical-align: top; }
ul.errorlist { border-width: 0px; padding: 0px; margin: 0px; display: inline-block; }
ul.errorlist li { color: #a00; margin: 0px; padding: 0px; list-style: none; }
p.help { font-style: italic; }
p.error { color: #a00; font-size: larger; }
tr.milestone.add { font-style: italic; }
{% endblock %}
{% block pagehead %}
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
{% endblock %}
{% bootstrap_messages %}
{% block content %}
{% load ietf_filters %}
<h1>{{ title }}</h1>
<noscript>This page depends on Javascript being enabled to work properly.</noscript>
<p>Links:
<a href="{{ g.about_url }}">{{ group.acronym }} {{ group.type.name }}</a>
<a href="{{ group.about_url }}">{{ group.acronym }} {{ group.type.name }}</a>
{% if group.charter %}
- <a href="{% url "doc_view" name=group.charter.canonical_name %}">{{ group.charter.canonical_name }}</a>
{% endif %}
</p>
<p class="help">{% if forms %}Click a milestone to edit it.{% endif %}
<p class="help-block">
{% if forms %}Click a milestone to edit it.{% 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 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 "group_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 }}.
You can <a href="{% url "group_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="error">There were errors, see below.</p>
<p class="alert alert-danger">There were errors, see below.</p>
{% endif %}
<form action="" method="post" id="milestones-form">{% csrf_token %}
<table cellspacing="0" cellpadding="0">
{% for form in forms %}
<tr class="milestone{% if form.delete.data %} delete{% endif %}">
<td class="due">{% if form.milestone.resolved %}{{ form.milestone.resolved }}{% else %}{{ form.milestone.due|date:"M Y" }}{% endif %}</td>
<td>
<div>{{ form.milestone.desc }}
{% if form.needs_review %}<span class="note">awaiting accept</span>{% endif %}
{% if form.changed %}<span class="note">changed</span>{% endif %}
</div>
<form method="post" id="milestones-form">{% csrf_token %}
<table class="table">
{% for d in form.docs_names %}
<div class="doc">{{ d }}</div>
{% for form in forms %}
<tr class="milestone{% if form.delete.data %} delete{% endif %}">
<td class="due">
{% if form.milestone.resolved %}
<span class="label label-success">{{ form.milestone.resolved }}</span>
{% else %}
{{ form.milestone.due|date:"M Y" }}
{% endif %}
</td>
<td>
<div>{{ form.milestone.desc }}
{% if form.needs_review %}<span title="This milestone is not active yet, awaiting {{ reviewer }} acceptance" class="label label-warning">Awaiting accept</span>{% endif %}
{% if form.changed %}<span class="label label-info">Changed</span>{% endif %}
</div>
{% for d in form.docs_names %}
<div class="doc">{{ d }}</div>
{% endfor %}
</td>
</tr>
<tr class="edit-milestone{% if form.changed %} changed{% endif %}">
<td colspan="2">{% include "group/milestone_form.html" %}</td>
</tr>
{% endfor %}
</td>
</tr>
<tr class="edit-milestone{% if form.changed %} changed{% endif %}"><td colspan="2">{% include "group/milestone_form.html" %}</td></tr>
{% endfor %}
<tr class="milestone add"><td></td><td>Add {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for {{ reviewer }} review{% endif %}</td></tr>
<tr class="edit-milestone template"><td colspan="2">{% include "group/milestone_form.html" with form=empty_form %}</td></tr>
</table>
<tr>
<td></td>
<td><button type="button" class="btn btn-default add-milestone">Add extra {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for {{ reviewer }} review{% endif %}</button></td>
</tr>
<div class="actions">
<a class="button" href="{% if milestone_set == "charter" %}{% url "doc_view" name=group.charter.canonical_name %}{% else %}{{ group.about_url }}{% endif %}">Cancel</a>
<input class="button" type="submit" data-labelsave="Save" data-labelreview="Review changes" value="Save" style="display:none"/>
<tr class="edit-milestone template"><td colspan="2">{% include "group/milestone_form.html" with form=empty_form %}</td></tr>
</table>
{% buttons %}
<a class="btn btn-default pull-right" href="{% if milestone_set == "charter" %}{% url "doc_view" 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>
<input type="hidden" name="action" value="save">
</div>
{% endbuttons %}
</form>
{% endblock %}
{% block content_end %}
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/lib/json2.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<script>
var finishedMilestoneText = "{{ finished_milestone_text|escapejs }}";
</script>
<script type="text/javascript" src="/js/edit-milestones.js"></script>
{% block js %}
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
<script src="/facelift/js/lib/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="/facelift/js/edit-milestones.js"></script>
{% endblock %}

View file

@ -1,42 +1,9 @@
{# assumes group, form, needs_review, reviewer are in the context #}
<input type="hidden" name="prefix" value="{{ form.prefix|default:"" }}"/>
{{ form.id }}
<table cellspacing="0" cellpadding="0">
<tr>
<td>{{ form.desc.label_tag }}</td>
<td>
<span class="desc">
{% if needs_review and form.milestone and form.milestone.state_id != "review" %}
{{ form.milestone.desc }} {{ form.desc.as_hidden }}
{% else %}
{{ form.desc }}
{% endif %}
</span>
<span class="delete">{{ form.delete }} {{ form.delete.label_tag }}</span>
</td>
</tr>
{% if form.desc.errors %}<tr><td></td><td colspan="2">{{ form.desc.errors }}</td></tr>{% endif %}
<tr>
<td>Due date:</td>
<td><span class="due">{{ form.due_month }} {{ form.due_year }}</span> {{ form.due_month.errors }} {{ form.due_year.errors }}
<span class="resolved">{{ form.resolved_checkbox }} {{ form.resolved_checkbox.label_tag }} {{ form.resolved }}</span>
{{ form.resolved.errors }}
</td>
</tr>
<tr class="docs">
<td>Drafts:</td>
<td>{{ form.docs }}
{{ form.docs.errors }}
</td>
</tr>
{% if form.needs_review %}
<tr class="needs-review">
<td>Review:</td>
<td class="accept">
This milestone is not active yet, awaiting
{{ reviewer }} acceptance{% if needs_review %}.{% else %}: {{ form.accept }}{% endif %}
</td>
</tr>
{% endif %}
</table>
{% load bootstrap3 %}
<div role="form" class="form-horizontal">
<input type="hidden" name="prefix" value="{{ form.prefix|default:"" }}"/>
{% bootstrap_form form layout='horizontal' %}
</div>

View file

@ -6,8 +6,8 @@
{% block title %}Manage {{ group.name }} RFC stream{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="/facelift/css/tokenfield-typeahead.min.css">
<link rel="stylesheet" href="/facelift/css/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
{% endblock %}
{% block content %}
@ -29,12 +29,12 @@
datatracker account. New accounts can be <a href="{% url "create_account" %}">created here</a>.
</p>
<form class="tokenized-form" action="" role="form" method="post">
<form action="" role="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<input type="submit" class="btn btn-primary" value="Save">
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-default pull-right" href="{% url "ietf.group.views_stream.streams" %}{{ group.acronym }}">Back</a>
{% endbuttons %}
@ -42,6 +42,5 @@
{% endblock %}
{% block js %}
<script src="/facelift/js/lib/typeahead.bundle.min.js"></script>
<script src="/facelift/js/lib/bootstrap-tokenfield.min.js"></script>
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
{% endblock %}

View file

@ -1,8 +1,8 @@
{% extends "nomcom/nomcom_private_base.html" %}
{% block pagehead %}
<link rel="stylesheet" href="/facelift/css/tokenfield-typeahead.min.css">
<link rel="stylesheet" href="/facelift/css/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/facelift/css/lib/select2.css">
<link rel="stylesheet" href="/facelift/css/lib/select2-bootstrap.css">
{% endblock %}
{% load bootstrap3 %}
@ -27,6 +27,5 @@
{% endblock %}
{% block content_end %}
<script src="/facelift/js/lib/typeahead.bundle.min.js"></script>
<script src="/facelift/js/lib/bootstrap-tokenfield.min.js"></script>
<script src="/facelift/js/lib/select2-3.5.2.min.js"></script>
{% endblock %}

View file

@ -206,3 +206,17 @@ pre { line-height: 1.214; }
.navbar-dev .navbar-link:hover {
color: #ecdbff;
}
/* milestone editing */
#milestones-form .milestone {
cursor: pointer;
}
#milestones-form .milestone:hover {
background-color: #f5f5f5;
}
#milestones-form .edit-milestone {
display: none;
}
#milestones-form .milestone.delete, #milestones-form .edit-milestone.delete, #milestones-form .edit-milestone.delete input {
color: #aaa !important;
}

View file

@ -0,0 +1,497 @@
/**
* Select2 Bootstrap 3 CSS v1.4.1
* Tested with Bootstrap v3.2.0 and Select2 v3.3.2, v3.4.1-v3.4.5, v3.5.1, master
* in latest Chrome, Safari, Firefox, Opera (Mac) and IE8-IE11
* MIT License
*/
/**
* Reset Bootstrap 3 .form-control styles which - if applied to the
* original <select>-element the Select2-plugin may be run against -
* are copied to the .select2-container.
*
* 1. Overwrite .select2-container's original display:inline-block
* with Bootstrap 3's default for .form-control, display:block;
* courtesy of @juristr (@see https://github.com/fk/select2-bootstrap-css/pull/1)
*/
.select2-container.form-control {
background: transparent;
border: none;
display: block;
/* 1 */
margin: 0;
padding: 0;
}
/**
* Adjust Select2 inputs to fit Bootstrap 3 default .form-control appearance.
*/
.select2-container .select2-choices .select2-search-field input,
.select2-container .select2-choice,
.select2-container .select2-choices {
background: none;
padding: 0;
border-color: #cccccc;
border-radius: 4px;
color: #555555;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: white;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.select2-search input {
border-color: #cccccc;
border-radius: 4px;
color: #555555;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: white;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.select2-container .select2-choices .select2-search-field input {
-webkit-box-shadow: none;
box-shadow: none;
}
/**
* Adjust Select2 input heights to match the Bootstrap default.
*/
.select2-container .select2-choice {
height: 34px;
line-height: 1.42857;
}
/**
* Address Multi Select2's height which - depending on how many elements have been selected -
* may grown higher than their initial size.
*/
.select2-container.select2-container-multi.form-control {
height: auto;
}
/**
* Address Bootstrap 3 control sizing classes
* @see http://getbootstrap.com/css/#forms-control-sizes
*/
.select2-container.input-sm .select2-choice,
.input-group-sm .select2-container .select2-choice {
height: 30px;
line-height: 1.5;
border-radius: 3px;
}
.select2-container.input-lg .select2-choice,
.input-group-lg .select2-container .select2-choice {
height: 46px;
line-height: 1.33;
border-radius: 6px;
}
.select2-container-multi .select2-choices .select2-search-field input {
height: 32px;
}
.select2-container-multi.input-sm .select2-choices .select2-search-field input,
.input-group-sm .select2-container-multi .select2-choices .select2-search-field input {
height: 28px;
}
.select2-container-multi.input-lg .select2-choices .select2-search-field input,
.input-group-lg .select2-container-multi .select2-choices .select2-search-field input {
height: 44px;
}
/**
* Adjust height and line-height for .select2-search-field amd multi-select Select2 widgets.
*
* 1. Class repetition to address missing .select2-chosen in Select2 < 3.3.2.
*/
.select2-container-multi .select2-choices .select2-search-field input {
margin: 0;
}
.select2-chosen,
.select2-choice > span:first-child,
.select2-container .select2-choices .select2-search-field input {
padding: 6px 12px;
}
.input-sm .select2-chosen,
.input-group-sm .select2-chosen,
.input-sm .select2-choice > span:first-child,
.input-group-sm .select2-choice > span:first-child,
.input-sm .select2-choices .select2-search-field input,
.input-group-sm .select2-choices .select2-search-field input {
padding: 5px 10px;
}
.input-lg .select2-chosen,
.input-group-lg .select2-chosen,
.input-lg .select2-choice > span:first-child,
.input-group-lg .select2-choice > span:first-child,
.input-lg .select2-choices .select2-search-field input,
.input-group-lg .select2-choices .select2-search-field input {
padding: 10px 16px;
}
.select2-container-multi .select2-choices .select2-search-choice {
margin-top: 5px;
margin-bottom: 3px;
}
.select2-container-multi.input-sm .select2-choices .select2-search-choice,
.input-group-sm .select2-container-multi .select2-choices .select2-search-choice {
margin-top: 3px;
margin-bottom: 2px;
}
.select2-container-multi.input-lg .select2-choices .select2-search-choice,
.input-group-lg .select2-container-multi .select2-choices .select2-search-choice {
line-height: 24px;
}
/**
* Adjust the single Select2's dropdown arrow button appearance.
*
* 1. For Select2 v.3.3.2.
*/
.select2-container .select2-choice .select2-arrow,
.select2-container .select2-choice div {
border-left: 1px solid #cccccc;
background: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.select2-dropdown-open .select2-choice .select2-arrow,
.select2-dropdown-open .select2-choice div {
border-left-color: transparent;
background: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
/**
* Adjust the dropdown arrow button icon position for the single-select Select2 elements
* to make it line up vertically now that we increased the height of .select2-container.
*
* 1. Class repetition to address missing .select2-chosen in Select2 v.3.3.2.
*/
.select2-container .select2-choice .select2-arrow b,
.select2-container .select2-choice div b {
background-position: 0 3px;
}
.select2-dropdown-open .select2-choice .select2-arrow b,
.select2-dropdown-open .select2-choice div b {
background-position: -18px 3px;
}
.select2-container.input-sm .select2-choice .select2-arrow b,
.input-group-sm .select2-container .select2-choice .select2-arrow b,
.select2-container.input-sm .select2-choice div b,
.input-group-sm .select2-container .select2-choice div b {
background-position: 0 1px;
}
.select2-dropdown-open.input-sm .select2-choice .select2-arrow b,
.input-group-sm .select2-dropdown-open .select2-choice .select2-arrow b,
.select2-dropdown-open.input-sm .select2-choice div b,
.input-group-sm .select2-dropdown-open .select2-choice div b {
background-position: -18px 1px;
}
.select2-container.input-lg .select2-choice .select2-arrow b,
.input-group-lg .select2-container .select2-choice .select2-arrow b,
.select2-container.input-lg .select2-choice div b,
.input-group-lg .select2-container .select2-choice div b {
background-position: 0 9px;
}
.select2-dropdown-open.input-lg .select2-choice .select2-arrow b,
.input-group-lg .select2-dropdown-open .select2-choice .select2-arrow b,
.select2-dropdown-open.input-lg .select2-choice div b,
.input-group-lg .select2-dropdown-open .select2-choice div b {
background-position: -18px 9px;
}
/**
* Address Bootstrap's validation states and change Select2's border colors and focus states.
* Apply .has-warning, .has-danger or .has-succes to #select2-drop to match Bootstraps' colors.
*/
.has-warning .select2-choice,
.has-warning .select2-choices {
border-color: #8a6d3b;
}
.has-warning .select2-container-active .select2-choice,
.has-warning .select2-container-multi.select2-container-active .select2-choices {
border-color: #66512c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
}
.has-warning.select2-drop-active {
border-color: #66512c;
}
.has-warning.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #66512c;
}
.has-error .select2-choice,
.has-error .select2-choices {
border-color: #a94442;
}
.has-error .select2-container-active .select2-choice,
.has-error .select2-container-multi.select2-container-active .select2-choices {
border-color: #843534;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
}
.has-error.select2-drop-active {
border-color: #843534;
}
.has-error.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #843534;
}
.has-success .select2-choice,
.has-success .select2-choices {
border-color: #3c763d;
}
.has-success .select2-container-active .select2-choice,
.has-success .select2-container-multi.select2-container-active .select2-choices {
border-color: #2b542c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
}
.has-success.select2-drop-active {
border-color: #2b542c;
}
.has-success.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #2b542c;
}
/**
* Make Select2's active-styles - applied to .select2-container when the widget receives focus -
* fit Bootstrap 3's .form-element:focus appearance.
*/
.select2-container-active .select2-choice,
.select2-container-multi.select2-container-active .select2-choices {
border-color: #66afe9;
outline: none;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.select2-drop-active {
border-color: #66afe9;
}
.select2-drop-auto-width,
.select2-drop.select2-drop-above.select2-drop-active {
border-top-color: #66afe9;
}
/**
* Select2 widgets in Bootstrap Input Groups
*
* When Select2 widgets are combined with other elements using Bootstrap 3's
* "Input Group" component, we don't want specific edges of the Select2 container
* to have a border-radius.
*
* In Bootstrap 2, input groups required a markup where these style adjustments
* could be bound to a CSS-class identifying if the additional elements are appended,
* prepended or both.
*
* Bootstrap 3 doesn't rely on these classes anymore, so we have to use our own.
* Use .select2-bootstrap-prepend and .select2-bootstrap-append on a Bootstrap 3 .input-group
* to let the contained Select2 widget know which edges should not be rounded as they are
* directly followed by another element.
*
* @see http://getbootstrap.com/components/#input-groups
*/
.input-group.select2-bootstrap-prepend [class^="select2-choice"] {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
.input-group.select2-bootstrap-append [class^="select2-choice"] {
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.select2-dropdown-open [class^="select2-choice"] {
border-bottom-right-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-top-right-radius: 0 !important;
border-top-left-radius: 0 !important;
border-bottom-right-radius: 4px !important;
border-bottom-left-radius: 4px !important;
}
.input-group.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
.input-group.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.input-group.input-group-sm.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-bottom-right-radius: 3px !important;
}
.input-group.input-group-lg.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-bottom-right-radius: 6px !important;
}
.input-group.input-group-sm.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-bottom-left-radius: 3px !important;
}
.input-group.input-group-lg.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-bottom-left-radius: 6px !important;
}
/**
* Adjust Select2's choices hover and selected styles to match Bootstrap 3's default dropdown styles.
*/
.select2-results .select2-highlighted {
color: white;
background-color: #428bca;
}
/**
* Adjust alignment of Bootstrap 3 buttons in Bootstrap 3 Input Groups to address
* Multi Select2's height which - depending on how many elements have been selected -
* may grown higher than their initial size.
*/
.select2-bootstrap-append .select2-container-multiple,
.select2-bootstrap-append .input-group-btn,
.select2-bootstrap-append .input-group-btn .btn,
.select2-bootstrap-prepend .select2-container-multiple,
.select2-bootstrap-prepend .input-group-btn,
.select2-bootstrap-prepend .input-group-btn .btn {
vertical-align: top;
}
/**
* Make Multi Select2's choices match Bootstrap 3's default button styles.
*/
.select2-container-multi .select2-choices .select2-search-choice {
color: #555555;
background: white;
border-color: #cccccc;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: none;
box-shadow: none;
}
.select2-container-multi .select2-choices .select2-search-choice-focus {
background: #ebebeb;
border-color: #adadad;
color: #333333;
-webkit-box-shadow: none;
box-shadow: none;
}
/**
* Address Multi Select2's choice close-button vertical alignment.
*/
.select2-search-choice-close {
margin-top: -7px;
top: 50%;
}
/**
* Adjust the single Select2's clear button position (used to reset the select box
* back to the placeholder value and visible once a selection is made
* activated by Select2's "allowClear" option).
*/
.select2-container .select2-choice abbr {
top: 50%;
}
/**
* Adjust "no results" and "selection limit" messages to make use
* of Bootstrap 3's default "Alert" style.
*
* @see http://getbootstrap.com/components/#alerts-default
*/
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-selection-limit {
background-color: #fcf8e3;
color: #8a6d3b;
}
/**
* Address disabled Select2 styles.
*
* 1. For Select2 v.3.3.2.
* 2. Revert border-left:0 inherited from Select2's CSS to prevent the arrow
* from jumping when switching from disabled to enabled state and vice versa.
*/
.select2-container.select2-container-disabled .select2-choice,
.select2-container.select2-container-disabled .select2-choices {
cursor: not-allowed;
background-color: #eeeeee;
border-color: #cccccc;
}
.select2-container.select2-container-disabled .select2-choice .select2-arrow,
.select2-container.select2-container-disabled .select2-choice div,
.select2-container.select2-container-disabled .select2-choices .select2-arrow,
.select2-container.select2-container-disabled .select2-choices div {
background-color: transparent;
border-left: 1px solid transparent;
/* 2 */
}
/**
* Address Select2's loading indicator position - which should not stick
* to the right edge of Select2's search input.
*
* 1. in .select2-search input
* 2. in Multi Select2's .select2-search-field input
* 3. in the status-message of infinite-scroll with remote data (@see http://ivaynberg.github.io/select2/#infinite)
*
* These styles alter Select2's default background-position of 100%
* and supply the new background-position syntax to browsers which support it:
*
* 1. Android, Safari < 6/Mobile, IE<9: change to a relative background-position of 99%
* 2. Chrome 25+, Firefox 13+, IE 9+, Opera 10.5+: use the new CSS3-background-position syntax
*
* @see http://www.w3.org/TR/css3-background/#background-position
*
* @todo Since both Select2 and Bootstrap 3 only support IE8 and above,
* we could use the :after-pseudo-element to display the loading indicator.
* Alternatively, we could supply an altered loading indicator image which already
* contains an offset to the right.
*/
.select2-search input.select2-active,
.select2-container-multi .select2-choices .select2-search-field input.select2-active,
.select2-more-results.select2-active {
background-position: 99%;
/* 4 */
background-position: right 4px center;
/* 5 */
}
/**
* To support Select2 pre v3.4.2 in combination with Bootstrap v3.2.0,
* ensure that .select2-offscreen width, height and position can not be overwritten.
*
* This adresses changes in Bootstrap somewhere after the initial v3.0.0 which -
* in combination with Select2's pre-v3.4.2 CSS missing the "!important" after
* the following rules - allow Bootstrap to overwrite the latter, which results in
* the original <select> element Select2 is replacing not be properly being hidden
* when used in a "Bootstrap Input Group with Addon".
**/
.select2-offscreen,
.select2-offscreen:focus {
width: 1px !important;
height: 1px !important;
position: absolute !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,704 @@
/*
Version: 3.5.2 Timestamp: Sat Nov 1 14:43:36 EDT 2014
*/
.select2-container {
margin: 0;
position: relative;
display: inline-block;
/* inline-block for ie7 */
zoom: 1;
*display: inline;
vertical-align: middle;
}
.select2-container,
.select2-drop,
.select2-search,
.select2-search input {
/*
Force border-box so that % widths fit the parent
container without overlap because of margin/padding.
More Info : http://www.quirksmode.org/css/box.html
*/
-webkit-box-sizing: border-box; /* webkit */
-moz-box-sizing: border-box; /* firefox */
box-sizing: border-box; /* css3 */
}
.select2-container .select2-choice {
display: block;
height: 26px;
padding: 0 0 0 8px;
overflow: hidden;
position: relative;
border: 1px solid #aaa;
white-space: nowrap;
line-height: 26px;
color: #444;
text-decoration: none;
border-radius: 4px;
background-clip: padding-box;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: #fff;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));
background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);
background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);
background-image: linear-gradient(to top, #eee 0%, #fff 50%);
}
html[dir="rtl"] .select2-container .select2-choice {
padding: 0 8px 0 0;
}
.select2-container.select2-drop-above .select2-choice {
border-bottom-color: #aaa;
border-radius: 0 0 4px 4px;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff));
background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%);
background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
background-image: linear-gradient(to bottom, #eee 0%, #fff 90%);
}
.select2-container.select2-allowclear .select2-choice .select2-chosen {
margin-right: 42px;
}
.select2-container .select2-choice > .select2-chosen {
margin-right: 26px;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
float: none;
width: auto;
}
html[dir="rtl"] .select2-container .select2-choice > .select2-chosen {
margin-left: 26px;
margin-right: 0;
}
.select2-container .select2-choice abbr {
display: none;
width: 12px;
height: 12px;
position: absolute;
right: 24px;
top: 8px;
font-size: 1px;
text-decoration: none;
border: 0;
background: url('select2.png') right top no-repeat;
cursor: pointer;
outline: 0;
}
.select2-container.select2-allowclear .select2-choice abbr {
display: inline-block;
}
.select2-container .select2-choice abbr:hover {
background-position: right -11px;
cursor: pointer;
}
.select2-drop-mask {
border: 0;
margin: 0;
padding: 0;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 9998;
/* styles required for IE to work */
background-color: #fff;
filter: alpha(opacity=0);
}
.select2-drop {
width: 100%;
margin-top: -1px;
position: absolute;
z-index: 9999;
top: 100%;
background: #fff;
color: #000;
border: 1px solid #aaa;
border-top: 0;
border-radius: 0 0 4px 4px;
-webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
}
.select2-drop.select2-drop-above {
margin-top: 1px;
border-top: 1px solid #aaa;
border-bottom: 0;
border-radius: 4px 4px 0 0;
-webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
}
.select2-drop-active {
border: 1px solid #5897fb;
border-top: none;
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid #5897fb;
}
.select2-drop-auto-width {
border-top: 1px solid #aaa;
width: auto;
}
.select2-drop-auto-width .select2-search {
padding-top: 4px;
}
.select2-container .select2-choice .select2-arrow {
display: inline-block;
width: 18px;
height: 100%;
position: absolute;
right: 0;
top: 0;
border-left: 1px solid #aaa;
border-radius: 0 4px 4px 0;
background-clip: padding-box;
background: #ccc;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0);
background-image: linear-gradient(to top, #ccc 0%, #eee 60%);
}
html[dir="rtl"] .select2-container .select2-choice .select2-arrow {
left: 0;
right: auto;
border-left: none;
border-right: 1px solid #aaa;
border-radius: 4px 0 0 4px;
}
.select2-container .select2-choice .select2-arrow b {
display: block;
width: 100%;
height: 100%;
background: url('select2.png') no-repeat 0 1px;
}
html[dir="rtl"] .select2-container .select2-choice .select2-arrow b {
background-position: 2px 1px;
}
.select2-search {
display: inline-block;
width: 100%;
min-height: 26px;
margin: 0;
padding-left: 4px;
padding-right: 4px;
position: relative;
z-index: 10000;
white-space: nowrap;
}
.select2-search input {
width: 100%;
height: auto !important;
min-height: 26px;
padding: 4px 20px 4px 5px;
margin: 0;
outline: 0;
font-family: sans-serif;
font-size: 1em;
border: 1px solid #aaa;
border-radius: 0;
-webkit-box-shadow: none;
box-shadow: none;
background: #fff url('select2.png') no-repeat 100% -22px;
background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
}
html[dir="rtl"] .select2-search input {
padding: 4px 5px 4px 20px;
background: #fff url('select2.png') no-repeat -37px -22px;
background: url('select2.png') no-repeat -37px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
background: url('select2.png') no-repeat -37px -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat -37px -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat -37px -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
}
.select2-drop.select2-drop-above .select2-search input {
margin-top: 4px;
}
.select2-search input.select2-active {
background: #fff url('select2-spinner.gif') no-repeat 100%;
background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
}
.select2-container-active .select2-choice,
.select2-container-active .select2-choices {
border: 1px solid #5897fb;
outline: none;
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
box-shadow: 0 0 5px rgba(0, 0, 0, .3);
}
.select2-dropdown-open .select2-choice {
border-bottom-color: transparent;
-webkit-box-shadow: 0 1px 0 #fff inset;
box-shadow: 0 1px 0 #fff inset;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: #eee;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee));
background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%);
background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
background-image: linear-gradient(to top, #fff 0%, #eee 50%);
}
.select2-dropdown-open.select2-drop-above .select2-choice,
.select2-dropdown-open.select2-drop-above .select2-choices {
border: 1px solid #5897fb;
border-top-color: transparent;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee));
background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%);
background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
background-image: linear-gradient(to bottom, #fff 0%, #eee 50%);
}
.select2-dropdown-open .select2-choice .select2-arrow {
background: transparent;
border-left: none;
filter: none;
}
html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow {
border-right: none;
}
.select2-dropdown-open .select2-choice .select2-arrow b {
background-position: -18px 1px;
}
html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow b {
background-position: -16px 1px;
}
.select2-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
/* results */
.select2-results {
max-height: 200px;
padding: 0 0 0 4px;
margin: 4px 4px 4px 0;
position: relative;
overflow-x: hidden;
overflow-y: auto;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html[dir="rtl"] .select2-results {
padding: 0 4px 0 0;
margin: 4px 0 4px 4px;
}
.select2-results ul.select2-result-sub {
margin: 0;
padding-left: 0;
}
.select2-results li {
list-style: none;
display: list-item;
background-image: none;
}
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: bold;
}
.select2-results .select2-result-label {
padding: 3px 7px 4px;
margin: 0;
cursor: pointer;
min-height: 1em;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.select2-results-dept-1 .select2-result-label { padding-left: 20px }
.select2-results-dept-2 .select2-result-label { padding-left: 40px }
.select2-results-dept-3 .select2-result-label { padding-left: 60px }
.select2-results-dept-4 .select2-result-label { padding-left: 80px }
.select2-results-dept-5 .select2-result-label { padding-left: 100px }
.select2-results-dept-6 .select2-result-label { padding-left: 110px }
.select2-results-dept-7 .select2-result-label { padding-left: 120px }
.select2-results .select2-highlighted {
background: #3875d7;
color: #fff;
}
.select2-results li em {
background: #feffde;
font-style: normal;
}
.select2-results .select2-highlighted em {
background: transparent;
}
.select2-results .select2-highlighted ul {
background: #fff;
color: #000;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: #f4f4f4;
display: list-item;
padding-left: 5px;
}
/*
disabled look for disabled choices in the results dropdown
*/
.select2-results .select2-disabled.select2-highlighted {
color: #666;
background: #f4f4f4;
display: list-item;
cursor: default;
}
.select2-results .select2-disabled {
background: #f4f4f4;
display: list-item;
cursor: default;
}
.select2-results .select2-selected {
display: none;
}
.select2-more-results.select2-active {
background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%;
}
.select2-results .select2-ajax-error {
background: rgba(255, 50, 50, .2);
}
.select2-more-results {
background: #f4f4f4;
display: list-item;
}
/* disabled styles */
.select2-container.select2-container-disabled .select2-choice {
background-color: #f4f4f4;
background-image: none;
border: 1px solid #ddd;
cursor: default;
}
.select2-container.select2-container-disabled .select2-choice .select2-arrow {
background-color: #f4f4f4;
background-image: none;
border-left: 0;
}
.select2-container.select2-container-disabled .select2-choice abbr {
display: none;
}
/* multiselect */
.select2-container-multi .select2-choices {
height: auto !important;
height: 1%;
margin: 0;
padding: 0 5px 0 0;
position: relative;
border: 1px solid #aaa;
cursor: text;
overflow: hidden;
background-color: #fff;
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff));
background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%);
background-image: linear-gradient(to bottom, #eee 1%, #fff 15%);
}
html[dir="rtl"] .select2-container-multi .select2-choices {
padding: 0 0 0 5px;
}
.select2-locked {
padding: 3px 5px 3px 5px !important;
}
.select2-container-multi .select2-choices {
min-height: 26px;
}
.select2-container-multi.select2-container-active .select2-choices {
border: 1px solid #5897fb;
outline: none;
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
box-shadow: 0 0 5px rgba(0, 0, 0, .3);
}
.select2-container-multi .select2-choices li {
float: left;
list-style: none;
}
html[dir="rtl"] .select2-container-multi .select2-choices li
{
float: right;
}
.select2-container-multi .select2-choices .select2-search-field {
margin: 0;
padding: 0;
white-space: nowrap;
}
.select2-container-multi .select2-choices .select2-search-field input {
padding: 5px;
margin: 1px 0;
font-family: sans-serif;
font-size: 100%;
color: #666;
outline: 0;
border: 0;
-webkit-box-shadow: none;
box-shadow: none;
background: transparent !important;
}
.select2-container-multi .select2-choices .select2-search-field input.select2-active {
background: #fff url('select2-spinner.gif') no-repeat 100% !important;
}
.select2-default {
color: #999 !important;
}
.select2-container-multi .select2-choices .select2-search-choice {
padding: 3px 5px 3px 18px;
margin: 3px 0 3px 5px;
position: relative;
line-height: 13px;
color: #333;
cursor: default;
border: 1px solid #aaaaaa;
border-radius: 3px;
-webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
background-clip: padding-box;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: #e4e4e4;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee));
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
background-image: linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
}
html[dir="rtl"] .select2-container-multi .select2-choices .select2-search-choice
{
margin: 3px 5px 3px 0;
padding: 3px 18px 3px 5px;
}
.select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
cursor: default;
}
.select2-container-multi .select2-choices .select2-search-choice-focus {
background: #d4d4d4;
}
.select2-search-choice-close {
display: block;
width: 12px;
height: 13px;
position: absolute;
right: 3px;
top: 4px;
font-size: 1px;
outline: none;
background: url('select2.png') right top no-repeat;
}
html[dir="rtl"] .select2-search-choice-close {
right: auto;
left: 3px;
}
.select2-container-multi .select2-search-choice-close {
left: 3px;
}
html[dir="rtl"] .select2-container-multi .select2-search-choice-close {
left: auto;
right: 2px;
}
.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
background-position: right -11px;
}
.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
background-position: right -11px;
}
/* disabled styles */
.select2-container-multi.select2-container-disabled .select2-choices {
background-color: #f4f4f4;
background-image: none;
border: 1px solid #ddd;
cursor: default;
}
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
padding: 3px 5px 3px 5px;
border: 1px solid #ddd;
background-image: none;
background-color: #f4f4f4;
}
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none;
background: none;
}
/* end multiselect */
.select2-result-selectable .select2-match,
.select2-result-unselectable .select2-match {
text-decoration: underline;
}
.select2-offscreen, .select2-offscreen:focus {
clip: rect(0 0 0 0) !important;
width: 1px !important;
height: 1px !important;
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
position: absolute !important;
outline: 0 !important;
left: 0px !important;
top: 0px !important;
}
.select2-display-none {
display: none;
}
.select2-measure-scrollbar {
position: absolute;
top: -10000px;
left: -10000px;
width: 100px;
height: 100px;
overflow: scroll;
}
/* Retina-ize icons */
@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) {
.select2-search input,
.select2-search-choice-close,
.select2-container .select2-choice abbr,
.select2-container .select2-choice .select2-arrow b {
background-image: url('select2x2.png') !important;
background-repeat: no-repeat !important;
background-size: 60px 40px !important;
}
.select2-search input {
background-position: 100% -21px !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

View file

@ -0,0 +1,140 @@
$(document).ready(function () {
var idCounter = -1;
var milestonesForm = $('#milestones-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;
});
function setChanged() {
$(this).closest(".edit-milestone").addClass("changed");
setSubmitButtonState();
}
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)
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").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() {
// move Add milestone row and duplicate hidden template
var row = $(this).closest("tr"), editRow = row.next(".edit-milestone");
row.closest("table").append(row).append(editRow.clone());
// fixup template
var newId = idCounter;
--idCounter;
var prefix = "m" + newId;
editRow.find('input[name="prefix"]').val(prefix);
editRow.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;
});
editRow.find("label").each(function () {
if (this.htmlFor)
this.htmlFor = prefix + "-" + this.htmlFor;
});
editRow.removeClass("template");
editRow.show();
editRow.find(".select2-field").each(function () {
window.setupSelect2Field($(this)); // from ietf.js
});
});
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");
}
}
milestonesForm.find(".edit-milestone [name$=delete]").each(setDeleteState);
milestonesForm.on("change", ".edit-milestone input[name$=delete]", setDeleteState);
milestonesForm.find('.edit-milestone .has-error').each(function () {
$(this).closest(".edit-milestone").prev().click();
});
setSubmitButtonState();
});

View file

@ -192,160 +192,59 @@ $(".snippet .show-all").click(function () {
// }
// });
function setupSelect2Field(e) {
var url = e.data("ajax-url");
if (!url)
return;
function to_disp(t) {
// typehead/tokenfield don't fully deal with HTML entities
return $('<div/>').html(t).text().replace(/[<>"]/g, function (m) {
return {
'<': '(',
'>': ')',
'"': ''
}[m];
});
var maxEntries = e.data("max-entries");
var multiple = maxEntries != 1;
var prefetched = e.data("pre");
e.select2({
multiple: multiple,
minimumInputLength: 2,
width: "off",
allowClear: true,
maximumSelectionSize: maxEntries,
ajax: {
url: url,
dataType: "json",
quietMillis: 250,
data: function (term, page) {
return {
q: term,
p: page
};
},
results: function (results) {
return {
results: results,
more: results.length == 10
};
}
},
escapeMarkup: function (m) {
return m;
},
initSelection: function (element, cb) {
if (!multiple && prefetched.length > 0)
cb(prefetched[0]);
else
cb(prefetched);
},
dropdownCssClass: "bigdrop"
});
}
$(document).ready(function () {
$(".select2-field").each(function () {
if ($(this).closest(".template").length > 0)
return;
$(".tokenized-form").submit(function (e) {
$(this).find(".tokenized-field").each(function () {
var f = $(this);
var io = f.data("io");
var format = f.data("format");
var t = f.tokenfield("getTokens");
var v = $.map(t, function(o) { return o["value"]; });
if (format === "json") {
v = JSON.stringify(v);
} else if (format === "csv") {
v = v.join(", ");
} else {
console.log(io, "unknown format");
v = v.join(" ");
}
f.val(v);
if (io) {
$(io).val(v);
}
});
});
$(".tokenized-field").each(function () {
// autocomplete interferes with the token popup
$(this).attr("autocomplete", "off");
// in which field ID are we expected to place the result
// (we also read the prefill information from there)
var io = $(this).data("io");
var raw = "";
if (io) {
raw = $(io).val();
} else {
io = "#" + this.id;
raw = $(this).val();
}
console.log("io: ", io);
console.log(io, "raw", raw);
$(this).data("io", io);
// which field of the JSON are we supposed to display
var display = $(this).data("display");
if (!display) {
display = "name";
}
console.log(io, "display", display);
$(this).data("display", display);
// which field of the JSON are we supposed to return
var result = $(this).data("result");
if (!result) {
result = "id";
}
console.log(io, "result", result);
$(this).data("result", result);
// what kind of data are we returning (json or csv)
var format = $(this).data("format");
if (!format) {
format = "csv";
}
console.log(io, "format", format);
$(this).data("format", format);
// make tokens to prefill the input
if (raw) {
raw = $.parseJSON(raw);
var pre = [];
if (!raw[0] || !raw[0][display]) {
$.each(raw, function(k, v) {
var obj = {};
obj["value"] = k;
obj["label"] = to_disp(v);
pre.push(obj);
});
} else {
for (var i in raw) {
var obj = {};
obj["value"] = raw[i][result];
obj["label"] = to_disp(raw[i][display]);
pre.push(obj);
}
}
$(this).val(pre);
}
console.log(io, "pre", pre);
// check if the ajax-url contains a query parameter, add one if not
var url = $(this).data("ajax-url");
if (url.indexOf("?") === -1) {
url += "?q=";
}
$(this).data("ajax-url", url);
console.log(io, "ajax-url", url);
var bh = new Bloodhound({
datumTokenizer: function (d) {
return Bloodhound.tokenizers.nonword(d[display]);
},
queryTokenizer: Bloodhound.tokenizers.nonword,
limit: 20,
remote: {
url: url + "%QUERY",
filter: function (data) {
return $.map($.grep(data, function (n, i) {
return true;
}), function (n, i) {
n["label"] = to_disp(n[display]);
n["value"] = n[result];
return n;
});
}
}
});
bh.initialize();
$(this).tokenfield({
typeahead: [{
highlight: true,
minLength: 3,
hint: true
}, {
source: bh.ttAdapter(),
displayKey: "label"
}],
beautify: true,
delimiter: [',', ';']
}).tokenfield("setTokens", pre);
// only allow tokens from the popup to be added to the field, no free text
$(this).on('tokenfield:createtoken', function (event) {
var existingTokens = $(this).tokenfield('getTokens');
$.each(existingTokens, function(index, token) {
if (event.attrs.id === undefined) {
event.preventDefault();
}
});
setupSelect2Field($(this));
});
});
// Use the Bootstrap3 tooltip plugin for all elements with a title attribute
$('[title][title!=""]').tooltip();

File diff suppressed because one or more lines are too long