Skip to content

Commit a4daf26

Browse files
committed
Merge branch 'lars/5.6.3-facelift' into lars/5.6.4-facelift
Conflicts: ietf/doc/templatetags/ietf_filters.py ietf/doc/views_charter.py ietf/templates/doc/charter/edit_notify.html ietf/templates/doc/charter/edit_telechat_date.html ietf/templates/doc/document_ballot_content.html ietf/templates/doc/document_history.html ietf/templates/doc/edit_notify.html ietf/templates/doc/edit_telechat_date.html ietf/templates/doc/notify.html ietf/templates/group/concluded_groups.html - Legacy-Id: 8429
1 parent a5f49b3 commit a4daf26

File tree

282 files changed

+13445
-9345
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

282 files changed

+13445
-9345
lines changed

README

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
This is the "facelift" datatracker branch that uses Twitter Bootstrap for
2+
the UI.
3+
4+
You need to install a few new django extensions:
5+
https://pypi.python.org/pypi/django-widget-tweaks
6+
https://pypi.python.org/pypi/django-bootstrap3
7+
https://pypi.python.org/pypi/django-typogrify
8+
9+
The meta goal of this effort is: *** NO CHANGES TO THE PYTHON CODE ***
10+
11+
Whenever changes to the python code are made, they can only fix HTML bugs,
12+
or add comments (tagged with "FACELIFT") about functionality that can be
13+
removed once the facelift templates become default. Or they need to add
14+
functionality that is only called from the new facelift templates.
15+
16+
Javascript that is only used on one template goes into that template.
17+
Javascript that is used by more than one template goes into ietf.js.
18+
19+
CSS that is only used on one template goes into that template.
20+
CSS that is used by more than one template goes into ietf.css. No CSS in the
21+
templates or - god forbid - style tags! (And no CSS or HTML styling in
22+
python code!!)
23+
24+
Templates that use jquery or bootstrap plugins include the css in the pagehead
25+
block, and the js in the js block.

TODO

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Major pieces not facelifted: milestone editing, liaison editing, WG workflow customization
2+
3+
Use affix for navigation on active_wgs.html
4+
5+
Figure out why {% if debug %} does not work in the text templates under ietf/templates_facelift/community/public.
6+
7+
Make django generate HTML5 date inputs or use a js-based datepicker.
8+
9+
Deferring ballots does not work. (Seems to be an upstream bug.)
10+
11+
Make tables that are too wide to usefully work on small screens responsive. See http://getbootstrap.com/css/#tables-responsive

ietf/community/templatetags/community_tags.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django import template
22
from django.template.loader import render_to_string
3+
from django.conf import settings
34

45
from ietf.community.models import CommunityList
56
from ietf.group.models import Role
@@ -9,7 +10,7 @@
910

1011

1112
class CommunityListNode(template.Node):
12-
13+
1314
def __init__(self, user, var_name):
1415
self.user = user
1516
self.var_name = var_name
@@ -57,13 +58,13 @@ def show_field(field, doc):
5758

5859

5960
class CommunityListViewNode(template.Node):
60-
61+
6162
def __init__(self, clist):
6263
self.clist = clist
6364

6465
def render(self, context):
6566
clist = self.clist.resolve(context)
66-
if not clist.cached:
67+
if settings.DEBUG or not clist.cached:
6768
clist.cached = render_to_string('community/raw_view.html',
6869
{'cl': clist,
6970
'dc': clist.get_display_config()})

ietf/doc/templatetags/ballot_icon.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,35 +87,32 @@ def sort_key(t):
8787
positions = list(doc.active_ballot().active_ad_positions().items())
8888
positions.sort(key=sort_key)
8989

90-
edit_position_url = ""
91-
if has_role(user, "Area Director"):
92-
edit_position_url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))
93-
94-
title = "IESG positions (click to show more%s)" % (", right-click to edit position" if edit_position_url else "")
95-
96-
res = ['<a href="%s" data-popup="%s" data-edit="%s" title="%s" class="ballot-icon"><table>' % (
97-
urlreverse("doc_ballot", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
90+
res = ['<a href="%s" data-toggle="modal" data-target="#modal-%d" title="IESG positions (click to show more)" class="ballot-icon"><table>' % (
9891
urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
99-
edit_position_url,
100-
title
101-
)]
92+
ballot.pk)]
10293

10394
res.append("<tr>")
10495

10596
for i, (ad, pos) in enumerate(positions):
10697
if i > 0 and i % 5 == 0:
107-
res.append("</tr>")
108-
res.append("<tr>")
98+
res.append("</tr><tr>")
10999

110100
c = "position-%s" % (pos.pos.slug if pos else "norecord")
111101

112102
if user_is_person(user, ad):
113103
c += " my"
114104

115-
res.append('<td class="%s" />' % c)
105+
res.append('<td class="%s"></td>' % c)
106+
107+
# add sufficient table calls to last row to avoid HTML validation warning
108+
while (i + 1) % 5 != 0:
109+
res.append('<td class="empty"></td>')
110+
i = i + 1
116111

117-
res.append("</tr>")
118-
res.append("</table></a>")
112+
res.append("</tr></table></a>")
113+
# XXX FACELIFT: Loading via href will go away in bootstrap 4.
114+
# See http://getbootstrap.com/javascript/#modals-usage
115+
res.append('<div id="modal-%d" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"><div class="modal-dialog modal-lg"><div class="modal-content"></div></div></div>' % ballot.pk)
119116

120117
return "".join(res)
121118

@@ -137,7 +134,8 @@ def ballotposition(doc, user):
137134

138135

139136
@register.filter
140-
def state_age_colored(doc):
137+
# FACELIFT: added flavor argument for styling
138+
def state_age_colored(doc, flavor=""):
141139
if doc.type_id == 'draft':
142140
if not doc.get_state_slug() in ["active", "rfc"]:
143141
# Don't show anything for expired/withdrawn/replaced drafts
@@ -156,7 +154,7 @@ def state_age_colored(doc):
156154
except IndexError:
157155
state_date = datetime.date(1990,1,1)
158156
days = (datetime.date.today() - state_date).days
159-
# loosely based on
157+
# loosely based on
160158
# http://trac.tools.ietf.org/group/iesg/trac/wiki/PublishPath
161159
if iesg_state == "lc":
162160
goal1 = 30
@@ -180,16 +178,26 @@ def state_age_colored(doc):
180178
goal1 = 14
181179
goal2 = 28
182180
if days > goal2:
183-
class_name = "ietf-small ietf-highlight-r"
181+
if flavor == "facelift":
182+
class_name = "label label-danger"
183+
else:
184+
class_name = "ietf-small ietf-highlight-r"
184185
elif days > goal1:
185-
class_name = "ietf-small ietf-highlight-y"
186+
if flavor == "facelift":
187+
class_name = "label label-warning"
188+
else:
189+
class_name = "ietf-small ietf-highlight-y"
186190
else:
187191
class_name = "ietf-small"
188192
if days > goal1:
189193
title = ' title="Goal is &lt;%d days"' % (goal1,)
190194
else:
191195
title = ''
192-
return mark_safe('<span class="%s"%s>(for&nbsp;%d&nbsp;day%s)</span>' % (
193-
class_name, title, days, 's' if days != 1 else ''))
196+
# It's too bad that this function returns HTML; this makes it hard to
197+
# style. For the facelift, I therefore needed to add a new "flavor"
198+
# parameter, which is ugly.
199+
return mark_safe('<span class="%s"%s>%sfor %d day%s%s</span>' % (
200+
class_name, title, '(' if flavor != "facelift" else "", days,
201+
's' if days != 1 else '', '(' if flavor != "facelift" else "" ))
194202
else:
195203
return ""

ietf/doc/templatetags/ietf_filters.py

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ietf.doc.models import ConsensusDocEvent
1010
from django import template
1111
from django.utils.html import escape, fix_ampersands
12-
from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, urlize
12+
from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, urlize, striptags
1313
from django.template import resolve_variable
1414
from django.utils.safestring import mark_safe, SafeData
1515
from django.utils.html import strip_tags
@@ -48,12 +48,12 @@ def parse_email_list(value):
4848
u'<a href="mailto:joe@example.org">joe@example.org</a>, <a href="mailto:fred@example.com">fred@example.com</a>'
4949
5050
Parsing a non-string should return the input value, rather than fail:
51-
51+
5252
>>> parse_email_list(['joe@example.org', 'fred@example.com'])
5353
['joe@example.org', 'fred@example.com']
54-
54+
5555
Null input values should pass through silently:
56-
56+
5757
>>> parse_email_list('')
5858
''
5959
@@ -87,7 +87,7 @@ def fix_angle_quotes(value):
8787
if "<" in value:
8888
value = re.sub("<([\w\-\.]+@[\w\-\.]+)>", "&lt;\1&gt;", value)
8989
return value
90-
90+
9191
# there's an "ahref -> a href" in GEN_UTIL
9292
# but let's wait until we understand what that's for.
9393
@register.filter(name='make_one_per_line')
@@ -99,7 +99,7 @@ def make_one_per_line(value):
9999
'a\\nb\\nc'
100100
101101
Pass through non-strings:
102-
102+
103103
>>> make_one_per_line([1, 2])
104104
[1, 2]
105105
@@ -110,7 +110,7 @@ def make_one_per_line(value):
110110
return re.sub(", ?", "\n", value)
111111
else:
112112
return value
113-
113+
114114
@register.filter(name='timesum')
115115
def timesum(value):
116116
"""
@@ -222,7 +222,7 @@ def rfclink(string):
222222
URL for that RFC.
223223
"""
224224
string = str(string);
225-
return "http://tools.ietf.org/html/rfc" + string;
225+
return "//tools.ietf.org/html/rfc" + string;
226226

227227
@register.filter(name='urlize_ietf_docs', is_safe=True, needs_autoescape=True)
228228
def urlize_ietf_docs(string, autoescape=None):
@@ -274,7 +274,7 @@ def truncate_ellipsis(text, arg):
274274
return escape(text[:num-1])+"&hellip;"
275275
else:
276276
return escape(text)
277-
277+
278278
@register.filter
279279
def split(text, splitter=None):
280280
return text.split(splitter)
@@ -375,7 +375,7 @@ def linebreaks_lf(text):
375375
@register.filter(name='clean_whitespace')
376376
def clean_whitespace(text):
377377
"""
378-
Map all ASCII control characters (0x00-0x1F) to spaces, and
378+
Map all ASCII control characters (0x00-0x1F) to spaces, and
379379
remove unnecessary spaces.
380380
"""
381381
text = re.sub("[\000-\040]+", " ", text)
@@ -384,7 +384,7 @@ def clean_whitespace(text):
384384
@register.filter(name='unescape')
385385
def unescape(text):
386386
"""
387-
Unescape &nbsp;/&gt;/&lt;
387+
Unescape &nbsp;/&gt;/&lt;
388388
"""
389389
text = text.replace("&gt;", ">")
390390
text = text.replace("&lt;", "<")
@@ -423,7 +423,7 @@ def has_role(user, role_names):
423423
@register.filter
424424
def stable_dictsort(value, arg):
425425
"""
426-
Like dictsort, except it's stable (preserves the order of items
426+
Like dictsort, except it's stable (preserves the order of items
427427
whose sort key is the same). See also bug report
428428
http://code.djangoproject.com/ticket/12110
429429
"""
@@ -448,10 +448,10 @@ def format_history_text(text):
448448
if text.startswith("This was part of a ballot set with:"):
449449
full = urlize_ietf_docs(full)
450450

451-
full = mark_safe(keep_spacing(linebreaksbr(urlize(sanitize_html(full)))))
451+
full = mark_safe(keep_spacing(linebreaksbr(urlize_html(sanitize_html(full)))))
452452
snippet = truncatewords_html(full, 25)
453453
if snippet != full:
454-
return mark_safe(u'<div class="snippet">%s<span class="show-all">[show all]</span></div><div style="display:none" class="full">%s</div>' % (snippet, full))
454+
return mark_safe(u'<div class="snippet">%s<button class="btn btn-xs btn-default show-all"><span class="fa fa-caret-down"></span></button></div><div class="hidden full">%s</div>' % (snippet, full))
455455
return full
456456

457457
@register.filter
@@ -512,3 +512,66 @@ def consensus(doc):
512512
else:
513513
return "Unknown"
514514

515+
# FACELIFT: The following filters are only used by the facelift UI:
516+
517+
@register.filter
518+
def pos_to_label(text):
519+
"""Return a valid Bootstrap3 label type for a ballot position."""
520+
return {
521+
'Yes': 'success',
522+
'No Objection': 'info',
523+
'Abstain': 'warning',
524+
'Discuss': 'danger',
525+
'Block': 'danger',
526+
'Recuse': 'default',
527+
}.get(str(text), 'blank')
528+
529+
@register.filter
530+
def capfirst_allcaps(text):
531+
from django.template import defaultfilters
532+
"""Like capfirst, except it doesn't lowercase words in ALL CAPS."""
533+
result = text
534+
i = False
535+
for token in re.split("(\W+)", striptags(text)):
536+
if not re.match("^[A-Z]+$", token):
537+
if not i:
538+
result = result.replace(token, token.capitalize())
539+
i = True
540+
else:
541+
result = result.replace(token, token.lower())
542+
return result
543+
544+
@register.filter
545+
def lower_allcaps(text):
546+
from django.template import defaultfilters
547+
"""Like lower, except it doesn't lowercase words in ALL CAPS."""
548+
result = text
549+
i = False
550+
for token in re.split("(\W+)", striptags(text)):
551+
if not re.match("^[A-Z]+$", token):
552+
result = result.replace(token, token.lower())
553+
return result
554+
555+
# See https://djangosnippets.org/snippets/2072/ and
556+
# https://stackoverflow.com/questions/9939248/how-to-prevent-django-basic-inlines-from-autoescaping
557+
@register.filter
558+
def urlize_html(html, autoescape=False):
559+
"""
560+
Returns urls found in an (X)HTML text node element as urls via Django urlize filter.
561+
"""
562+
try:
563+
from BeautifulSoup import BeautifulSoup
564+
from django.utils.html import urlize
565+
except ImportError:
566+
if settings.DEBUG:
567+
raise template.TemplateSyntaxError, "Error in urlize_html The Python BeautifulSoup libraries aren't installed."
568+
return html
569+
else:
570+
soup = BeautifulSoup(html)
571+
572+
textNodes = soup.findAll(text=True)
573+
for textNode in textNodes:
574+
urlizedText = urlize(textNode, autoescape=autoescape)
575+
textNode.replaceWith(BeautifulSoup(urlizedText))
576+
577+
return str(soup)

ietf/doc/templatetags/wg_menu.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,32 @@
4343
'rai':'RAI'
4444
}
4545

46+
# FACELIFT: Function is called with "facelift" flavor from the new UI code.
47+
# The old code (and flavoring) can be remove eventually.
4648
@register.simple_tag
47-
def wg_menu():
48-
res = cache.get('base_left_wgmenu')
49+
def wg_menu(flavor=""):
50+
res = cache.get('wgmenu' + flavor)
4951
if res:
5052
return res
5153

5254
areas = Group.objects.filter(type="area", state="active").order_by('acronym')
53-
groups = Group.objects.filter(type="wg", state="active", parent__in=areas).order_by("acronym")
55+
wgs = Group.objects.filter(type="wg", state="active", parent__in=areas).order_by("acronym")
56+
rgs = Group.objects.filter(type="rg", state="active").order_by("acronym")
5457

5558
for a in areas:
5659
a.short_area_name = area_short_names.get(a.acronym) or a.name
5760
if a.short_area_name.endswith(" Area"):
5861
a.short_area_name = a.short_area_name[:-len(" Area")]
5962

60-
a.active_groups = [g for g in groups if g.parent_id == a.id]
63+
a.active_groups = [g for g in wgs if g.parent_id == a.id]
6164

6265
areas = [a for a in areas if a.active_groups]
6366

64-
res = render_to_string('base/wg_menu.html', {'areas':areas})
65-
cache.set('base_left_wgmenu', res, 30*60)
67+
if flavor == "facelift":
68+
res = render_to_string('base/menu_wg.html', {'areas':areas, 'rgs':rgs})
69+
elif flavor == "modal":
70+
res = render_to_string('base/menu_wg_modal.html', {'areas':areas, 'rgs':rgs})
71+
else:
72+
res = render_to_string('base/wg_menu.html', {'areas':areas, 'rgs':rgs})
73+
cache.set('wgmenu' + flavor, res, 30*60)
6674
return res

0 commit comments

Comments
 (0)