Merged in branch/iola/team-support from olau@iola.dk. This generalises and extends the group support for WGs and RGs so that also other groups, such as for instance the Edu Team, can have pages.

- Legacy-Id: 8013
This commit is contained in:
Henrik Levkowetz 2014-07-02 19:30:07 +00:00
commit 09f654b1f4
52 changed files with 1656 additions and 360 deletions

View file

@ -88,7 +88,7 @@ class WGField(DisplayField):
if raw or not document.group.type_id in ['wg','rg']:
return document.group.acronym
else:
return '<a href="%s">%s</a>' % (urlreverse('group_docs', kwargs=dict(group_type=document.group.type_id, acronym=document.group.acronym)), document.group.acronym) if (document.group and document.group.acronym != 'none') else ''
return '<a href="%s">%s</a>' % (urlreverse('group_home', kwargs=dict(group_type=document.group.type_id, acronym=document.group.acronym)), document.group.acronym) if (document.group and document.group.acronym != 'none') else ''
class ADField(DisplayField):

View file

@ -74,9 +74,10 @@ class DocumentInfo(models.Model):
return ext.lstrip(".").lower()
def get_file_path(self):
if self.type_id == "draft":
return settings.INTERNET_DRAFT_PATH
elif self.type_id in ("agenda", "minutes", "slides"):
elif self.type_id in ("agenda", "minutes", "slides") and self.meeting_related():
meeting = self.name.split("-")[1]
return os.path.join(settings.AGENDA_PATH, meeting, self.type_id) + "/"
elif self.type_id == "charter":
@ -86,18 +87,26 @@ class DocumentInfo(models.Model):
elif self.type_id == "statchg":
return settings.STATUS_CHANGE_PATH
else:
raise NotImplemented
return settings.DOCUMENT_PATH_PATTERN.format(doc=self)
def href(self):
meeting_related = self.meeting_related()
settings_var = settings.DOC_HREFS
if meeting_related:
settings_var = settings.MEETING_DOC_HREFS
try:
format = settings.DOC_HREFS[self.type_id]
format = settings_var[self.type_id]
except KeyError:
if len(self.external_url):
return self.external_url
return None
meeting = None
if self.type_id in ("agenda", "minutes", "slides"):
if meeting_related:
meeting = self.name.split("-")[1]
return format.format(doc=self,meeting=meeting)
def set_state(self, state):
@ -120,6 +129,9 @@ class DocumentInfo(models.Model):
"""Get state of type, or default state for document type if
not specified. Uses a local cache to speed multiple state
reads up."""
if self.pk == None: # states is many-to-many so not in database implies no state
return None
if state_type == None:
state_type = self.type_id
@ -158,6 +170,11 @@ class DocumentInfo(models.Model):
else:
return None
def meeting_related(self):
return(self.type_id in ("agenda", "minutes", "slides") and (
self.name.split("-")[1] == "interim"
or (self.session_set.exists() if isinstance(self, Document) else self.doc.session_set.exists())))
class Meta:
abstract = True
@ -270,12 +287,9 @@ class Document(DocumentInfo):
a = self.docalias_set.filter(name__startswith="rfc")
if a:
name = a[0].name
# elif self.type_id == "charter":
# if self.group.type.slug == "rg":
# top_org = "irtf"
# else:
# top_org = "ietf"
# return "charter-%s-%s" % (top_org, self.chartered_group.acronym)
elif self.type_id == "charter":
from ietf.doc.utils_charter import charter_name_for_group
return charter_name_for_group(self.chartered_group)
return name
def canonical_docalias(self):
@ -468,7 +482,11 @@ class DocHistoryAuthor(models.Model):
class DocHistory(DocumentInfo):
doc = models.ForeignKey(Document, related_name="history_set")
name = models.CharField(max_length=255) # WG charter canonical names can change if the group acronym changes
# the name here is used to capture the canonical name at the time
# - it would perhaps be more elegant to simply call the attribute
# canonical_name and replace the function on Document with a
# property
name = models.CharField(max_length=255)
related = models.ManyToManyField('DocAlias', through=RelatedDocHistory, blank=True)
authors = models.ManyToManyField(Email, through=DocHistoryAuthor, blank=True)
def __unicode__(self):

View file

@ -22,10 +22,6 @@ def expand_comma(value):
long comma-separated lists."""
return value.replace(",", ", ")
@register.filter(name='format_charter')
def format_charter(value):
return value.replace("\n\n", "</p><p>").replace("\n","<br/>\n")
@register.filter
def indent(value, numspaces=2):
replacement = "\n" + " " * int(numspaces)

View file

@ -217,6 +217,22 @@ class DocTestCase(TestCase):
r = self.client.get(urlreverse("doc_view", kwargs=dict(name='conflict-review-imaginary-irtf-submission')))
self.assertEqual(r.status_code, 200)
def test_document_material(self):
draft = make_test_data()
doc = Document.objects.create(
name="slides-testteam-test-slides",
rev="00",
title="Test Slides",
group=draft.group,
type_id="slides"
)
doc.set_state(State.objects.get(type="slides", slug="active"))
DocAlias.objects.create(name=doc.name, document=doc)
r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name)))
self.assertEqual(r.status_code, 200)
def test_document_ballot(self):
doc = make_test_data()
ballot = doc.active_ballot()

138
ietf/doc/tests_material.py Normal file
View file

@ -0,0 +1,138 @@
# Copyright The IETF Trust 2011, All Rights Reserved
import os, shutil
from StringIO import StringIO
from pyquery import PyQuery
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from ietf.doc.models import Document, State, DocAlias
from ietf.group.models import Group
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
class GroupMaterialTests(TestCase):
def setUp(self):
self.materials_dir = os.path.abspath("tmp-document-dir")
os.mkdir(self.materials_dir)
os.mkdir(os.path.join(self.materials_dir, "slides"))
settings.DOCUMENT_PATH_PATTERN = self.materials_dir + "/{doc.type_id}/"
def tearDown(self):
shutil.rmtree(self.materials_dir)
def create_slides(self):
group = Group.objects.create(type_id="team", acronym="testteam", name="Test Team", state_id="active")
doc = Document.objects.create(name="slides-testteam-test-file", rev="00", type_id="slides", group=group)
doc.set_state(State.objects.get(type="slides", slug="active"))
DocAlias.objects.create(name=doc.name, document=doc)
return doc
def test_choose_material_type(self):
group = Group.objects.create(type_id="team", acronym="testteam", name="Test Team", state_id="active")
url = urlreverse('ietf.doc.views_material.choose_material_type', kwargs=dict(acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue("Slides" in r.content)
def test_upload_slides(self):
group = Group.objects.create(type_id="team", acronym="testteam", name="Test Team", state_id="active")
url = urlreverse('group_new_material', kwargs=dict(acronym=group.acronym, doc_type="slides"))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
content = "%PDF-1.5\n..."
test_file = StringIO(content)
test_file.name = "unnamed.pdf"
# faulty post
r = self.client.post(url, dict(title="", name="", state="", material=test_file))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
test_file.seek(0)
# post
r = self.client.post(url, dict(title="Test File",
name="slides-%s-test-file" % group.acronym,
state=State.objects.get(type="slides", slug="active").pk,
material=test_file))
self.assertEqual(r.status_code, 302)
doc = Document.objects.get(name="slides-%s-test-file" % group.acronym)
self.assertEqual(doc.rev, "00")
self.assertEqual(doc.title, "Test File")
self.assertEqual(doc.get_state_slug(), "active")
with open(os.path.join(self.materials_dir, "slides", doc.name + "-" + doc.rev + ".pdf")) as f:
self.assertEqual(f.read(), content)
# check that posting same name is prevented
test_file.seek(0)
r = self.client.post(url, dict(title="Test File",
name=doc.name,
state=State.objects.get(type="slides", slug="active").pk,
material=test_file))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form ul.errorlist')) > 0)
def test_change_state(self):
doc = self.create_slides()
url = urlreverse('material_edit', kwargs=dict(name=doc.name, action="state"))
login_testing_unauthorized(self, "secretary", url)
# post
r = self.client.post(url, dict(state=State.objects.get(type="slides", slug="deleted").pk))
self.assertEqual(r.status_code, 302)
doc = Document.objects.get(name=doc.name)
self.assertEqual(doc.get_state_slug(), "deleted")
def test_edit_title(self):
doc = self.create_slides()
url = urlreverse('material_edit', kwargs=dict(name=doc.name, action="title"))
login_testing_unauthorized(self, "secretary", url)
# post
r = self.client.post(url, dict(title="New title"))
self.assertEqual(r.status_code, 302)
doc = Document.objects.get(name=doc.name)
self.assertEqual(doc.title, "New title")
def test_revise(self):
doc = self.create_slides()
url = urlreverse('material_edit', kwargs=dict(name=doc.name, action="revise"))
login_testing_unauthorized(self, "secretary", url)
content = "some text"
test_file = StringIO(content)
test_file.name = "unnamed.txt"
# post
r = self.client.post(url, dict(title="New title",
state=State.objects.get(type="slides", slug="active").pk,
material=test_file))
self.assertEqual(r.status_code, 302)
doc = Document.objects.get(name=doc.name)
self.assertEqual(doc.rev, "01")
self.assertEqual(doc.title, "New title")
self.assertEqual(doc.get_state_slug(), "active")
with open(os.path.join(self.materials_dir, "slides", doc.name + "-" + doc.rev + ".txt")) as f:
self.assertEqual(f.read(), content)

View file

@ -102,4 +102,5 @@ urlpatterns = patterns('',
(r'^(?P<name>charter-[A-Za-z0-9._+-]+)/', include('ietf.doc.urls_charter')),
(r'^(?P<name>[A-Za-z0-9._+-]+)/conflict-review/', include('ietf.doc.urls_conflict_review')),
(r'^(?P<name>[A-Za-z0-9._+-]+)/status-change/', include('ietf.doc.urls_status_change')),
(r'^(?P<name>[A-Za-z0-9._+-]+)/material/', include('ietf.doc.urls_material')),
)

View file

@ -0,0 +1,6 @@
from django.conf.urls import patterns, url
urlpatterns = patterns('ietf.doc.views_material',
url(r'^(?P<action>state|title|revise)/$', "edit_material", name="material_edit"),
)

View file

@ -5,6 +5,7 @@ import math
from django.conf import settings
from django.db.models.query import EmptyQuerySet
from django.forms import ValidationError
from ietf.utils import markup_txt
from ietf.doc.models import Document, DocHistory
@ -228,16 +229,13 @@ def add_links_in_new_revision_events(doc, events, diff_revisions):
def get_document_content(key, filename, split=True, markup=True):
f = None
try:
f = open(filename, 'rb')
raw_content = f.read()
with open(filename, 'rb') as f:
raw_content = f.read()
except IOError:
error = "Error; cannot read ("+key+")"
return error
finally:
if f:
f.close()
if markup:
return markup_txt.markup(raw_content, split)
else:
@ -397,3 +395,18 @@ def rebuild_reference_relations(doc):
ret['unfound']=list(unfound)
return ret
def check_common_doc_name_rules(name):
"""Check common rules for document names for use in forms, throws
ValidationError in case there's a problem."""
errors = []
if re.search("[^a-z0-9-]", name):
errors.append("The name may only contain digits, lowercase letters and dashes.")
if re.search("--", name):
errors.append("Please do not put more than one hyphen between any two words in the name.")
if re.search("-[0-9]{2}$", name):
errors.append("This name looks like ends in a version number. -00 will be added automatically. Please adjust the end of the name.")
if errors:
raise ValidationError(errors)

View file

@ -9,6 +9,13 @@ from ietf.person.models import Person
from ietf.utils.history import find_history_active_at
from ietf.utils.mail import send_mail_text
def charter_name_for_group(group):
if group.type_id == "rg":
top_org = "irtf"
else:
top_org = "ietf"
return "charter-%s-%s" % (top_org, group.acronym)
def next_revision(rev):
if rev == "":

View file

@ -30,7 +30,7 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os, datetime, urllib, json
import os, datetime, urllib, json, glob
from django.http import HttpResponse, Http404
from django.shortcuts import render_to_response, get_object_or_404, redirect
@ -51,7 +51,7 @@ from ietf.community.models import CommunityList
from ietf.doc.mails import email_ad
from ietf.doc.views_status_change import RELATION_SLUGS as status_change_relationships
from ietf.group.models import Role
from ietf.group.utils import can_manage_group_type
from ietf.group.utils import can_manage_group_type, can_manage_materials
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
from ietf.name.models import StreamName, BallotPositionName
from ietf.person.models import Email
@ -240,7 +240,7 @@ def document_main(request, name, rev=None):
elif group.type_id in ("rg", "wg"):
submission = "%s %s" % (group.acronym, group.type)
if group.type_id == "wg":
submission = "<a href=\"%s\">%s</a>" % (urlreverse("group_docs", kwargs=dict(group_type=doc.group.type_id, acronym=doc.group.acronym)), submission)
submission = "<a href=\"%s\">%s</a>" % (urlreverse("group_home", kwargs=dict(group_type=doc.group.type_id, acronym=doc.group.acronym)), submission)
if doc.stream_id and doc.get_state_slug("draft-stream-%s" % doc.stream_id) == "c-adopt":
submission = "candidate for %s" % submission
@ -490,6 +490,48 @@ def document_main(request, name, rev=None):
),
context_instance=RequestContext(request))
if doc.type_id in ("slides", "agenda", "minutes"):
can_manage_material = can_manage_materials(request.user, doc.group)
if doc.meeting_related():
# disallow editing meeting-related stuff through this
# interface for the time being
can_manage_material = False
basename = doc.canonical_name() # meeting materials are unversioned at the moment
if doc.external_url:
# we need to remove the extension for the globbing below to work
basename = os.path.splitext(doc.external_url)[0]
else:
basename = "%s-%s" % (doc.canonical_name(), doc.rev)
pathname = os.path.join(doc.get_file_path(), basename)
content = None
other_types = []
globs = glob.glob(pathname + ".*")
for g in globs:
extension = os.path.splitext(g)[1]
t = os.path.splitext(g)[1].lstrip(".")
url = doc.href()
if not url.endswith("/") and not url.endswith(extension):
url += extension
if extension == ".txt":
content = get_document_content(basename, pathname + extension, split=False)
t = "plain text"
other_types.append((t, url))
return render_to_response("doc/document_material.html",
dict(doc=doc,
top=top,
content=content,
revisions=revisions,
snapshot=snapshot,
can_manage_material=can_manage_material,
other_types=other_types,
),
context_instance=RequestContext(request))
raise Http404

174
ietf/doc/views_material.py Normal file
View file

@ -0,0 +1,174 @@
# views for managing group materials (slides, ...)
import os
import datetime
import re
from django import forms
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponseForbidden, Http404
from django.utils.html import mark_safe
from django.utils.text import slugify
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias, DocTypeName, DocEvent, State
from ietf.doc.models import NewRevisionDocEvent, save_document_in_history
from ietf.doc.utils import add_state_change_event, check_common_doc_name_rules
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
@login_required
def choose_material_type(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if not group.features.has_materials:
raise Http404
return render(request, 'doc/material/choose_material_type.html', {
'group': group,
'material_types': DocTypeName.objects.filter(slug__in=group.features.material_types),
})
def name_for_material(doc_type, group, title):
return "%s-%s-%s" % (doc_type.slug, group.acronym, slugify(title))
class UploadMaterialForm(forms.Form):
title = forms.CharField(max_length=Document._meta.get_field("title").max_length)
name = forms.CharField(max_length=Document._meta.get_field("name").max_length)
state = forms.ModelChoiceField(State.objects.all(), empty_label=None)
material = forms.FileField(label='File', help_text="PDF or text file (ASCII/UTF-8)")
def __init__(self, doc_type, action, group, doc, *args, **kwargs):
super(UploadMaterialForm, self).__init__(*args, **kwargs)
self.fields["state"].queryset = self.fields["state"].queryset.filter(type=doc_type)
self.doc_type = doc_type
self.action = action
self.group = group
if action == "new":
self.fields["state"].widget = forms.HiddenInput()
self.fields["state"].queryset = self.fields["state"].queryset.filter(slug="active")
self.fields["state"].initial = self.fields["state"].queryset[0].pk
self.fields["name"].initial = u"%s-%s-" % (doc_type.slug, group.acronym)
else:
del self.fields["name"]
self.fields["title"].initial = doc.title
self.fields["state"].initial = doc.get_state().pk if doc.get_state() else None
if doc.get_state_slug() == "deleted":
self.fields["state"].help_text = "Note: If you wish to revise this document, you may wish to change the state so it's not deleted."
if action == "title":
del self.fields["state"]
del self.fields["material"]
elif action == "state":
del self.fields["title"]
del self.fields["material"]
def clean_name(self):
name = self.cleaned_data["name"].strip().rstrip("-")
check_common_doc_name_rules(name)
if not re.search("^%s-%s-[a-z0-9]+" % (self.doc_type.slug, self.group.acronym), name):
raise forms.ValidationError("The name must start with %s-%s- followed by descriptive dash-separated words." % (self.doc_type.slug, self.group.acronym))
existing = Document.objects.filter(type=self.doc_type, name=name)
if existing:
url = urlreverse("material_edit", kwargs={ 'name': existing[0].name, 'action': 'revise' })
raise forms.ValidationError(mark_safe("Can't upload: %s with name %s already exists. Choose another title and name for what you're uploading or <a href=\"%s\">revise the existing %s</a>." % (self.doc_type.name, name, url, name)))
return name
@login_required
def edit_material(request, name=None, acronym=None, action=None, doc_type=None):
# the materials process is not very developed, so at the moment we
# handle everything through the same view/form
if action == "new":
group = get_object_or_404(Group, acronym=acronym)
if not group.features.has_materials:
raise Http404
doc = None
document_type = get_object_or_404(DocTypeName, slug=doc_type)
else:
doc = get_object_or_404(Document, name=name)
group = doc.group
document_type = doc.type
if not can_manage_materials(request.user, group):
return HttpResponseForbidden("You don't have permission to access this view")
if request.method == 'POST':
form = UploadMaterialForm(document_type, action, group, doc, request.POST, request.FILES)
if form.is_valid():
if action == "new":
doc = Document()
doc.type = document_type
doc.group = group
doc.rev = "00"
doc.name = name_for_material(doc.type, doc.group, form.cleaned_data["title"])
prev_rev = None
else:
save_document_in_history(doc)
prev_rev = doc.rev
prev_title = doc.title
prev_state = doc.get_state()
if "title" in form.cleaned_data:
doc.title = form.cleaned_data["title"]
doc.time = datetime.datetime.now()
if "material" in form.fields:
if action != "new":
doc.rev = "%02d" % (int(doc.rev) + 1)
f = form.cleaned_data["material"]
file_ext = os.path.splitext(f.name)[1]
with open(os.path.join(doc.get_file_path(), doc.name + "-" + doc.rev + file_ext), 'wb+') as dest:
for chunk in f.chunks():
dest.write(chunk)
doc.save()
if action == "new":
DocAlias.objects.get_or_create(name=doc.name, document=doc)
if prev_rev != doc.rev:
e = NewRevisionDocEvent(type="new_revision", doc=doc, rev=doc.rev)
e.time = doc.time
e.by = request.user.person
e.desc = "New version available: <b>%s-%s</b>" % (doc.name, doc.rev)
e.save()
if prev_title != doc.title:
e = DocEvent(doc=doc, by=request.user.person, type='changed_document')
e.desc = u"Changed title to <b>%s</b>" % doc.title
if prev_title:
e.desc += u" from %s" % prev_title
e.time = doc.time
e.save()
if "state" in form.cleaned_data and form.cleaned_data["state"] != prev_state:
doc.set_state(form.cleaned_data["state"])
add_state_change_event(doc, request.user.person, prev_state, form.cleaned_data["state"])
return redirect("doc_view", name=doc.name)
else:
form = UploadMaterialForm(document_type, action, group, doc)
return render(request, 'doc/material/edit_material.html', {
'group': group,
'form': form,
'action': action,
'document_type': document_type,
'doc_name': doc.name if doc else "",
})

View file

@ -1,24 +1,23 @@
# edit/create view for groups
import re
import os
import datetime
import shutil
from django import forms
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponseForbidden
from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect
from django.utils.html import mark_safe
from django.http import Http404, HttpResponse
from django.contrib.auth.decorators import login_required
import debug # pyflakes:ignore
from ietf.doc.models import DocAlias, DocTagName, Document, State, save_document_in_history
from ietf.doc.models import Document, DocAlias, DocTagName, State
from ietf.doc.utils import get_tags_for_stream_id
from ietf.doc.utils_charter import charter_name_for_group
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
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.forms import EmailsField
from ietf.person.models import Person, Email
@ -134,12 +133,7 @@ def format_urls(urls, fs="\n"):
return fs.join(res)
def get_or_create_initial_charter(group, group_type):
if group_type == "rg":
top_org = "irtf"
else:
top_org = "ietf"
charter_name = "charter-%s-%s" % (top_org, group.acronym)
charter_name = charter_name_for_group(group)
try:
charter = Document.objects.get(docalias__name=charter_name)
@ -166,6 +160,9 @@ def submit_initial_charter(request, group_type, acronym=None):
return HttpResponseForbidden("You don't have permission to access this view")
group = get_object_or_404(Group, acronym=acronym)
if not group.features.has_chartering_process:
raise Http404
if not group.charter:
group.charter = get_or_create_initial_charter(group, group_type)
group.save()
@ -188,6 +185,9 @@ def edit(request, group_type=None, acronym=None, action="edit"):
else:
raise Http404
if not group_type and group:
group_type = group.type_id
if request.method == 'POST':
form = GroupForm(request.POST, group=group, confirmed=request.POST.get("confirmed", False), group_type=group_type)
if form.is_valid():
@ -233,8 +233,6 @@ def edit(request, group_type=None, acronym=None, action="edit"):
changes.append(desc(name, clean[attr], v))
setattr(group, attr, clean[attr])
prev_acronym = group.acronym
# update the attributes, keeping track of what we're doing
diff('name', "Name")
diff('acronym', "Acronym")
@ -245,17 +243,6 @@ def edit(request, group_type=None, acronym=None, action="edit"):
diff('list_subscribe', "Mailing list subscribe address")
diff('list_archive', "Mailing list archive")
if not new_group and group.acronym != prev_acronym and group.charter:
save_document_in_history(group.charter)
DocAlias.objects.get_or_create(
name="charter-ietf-%s" % group.acronym,
document=group.charter,
)
old = os.path.join(group.charter.get_file_path(), 'charter-ietf-%s-%s.txt' % (prev_acronym, group.charter.rev))
if os.path.exists(old):
new = os.path.join(group.charter.get_file_path(), 'charter-ietf-%s-%s.txt' % (group.acronym, group.charter.rev))
shutil.copy(old, new)
# update roles
for attr, slug, title in [('chairs', 'chair', "Chairs"), ('secretaries', 'secr', "Secretaries"), ('techadv', 'techadv', "Tech Advisors"), ('delegates', 'delegate', "Delegates")]:
new = clean[attr]
@ -295,7 +282,7 @@ def edit(request, group_type=None, acronym=None, action="edit"):
if action=="charter":
return redirect('charter_submit', name=group.charter.name, option="initcharter")
return redirect('group_charter', group_type=group.type_id, acronym=group.acronym)
return HttpResponseRedirect(group.about_url())
else: # form.is_valid()
if not new_group:
init = dict(name=group.name,
@ -328,11 +315,11 @@ class ConcludeForm(forms.Form):
instructions = forms.CharField(widget=forms.Textarea(attrs={'rows': 30}), required=True)
@login_required
def conclude(request, group_type, acronym):
def conclude(request, acronym, group_type=None):
"""Request the closing of group, prompting for instructions."""
group = get_object_or_404(Group, type=group_type, acronym=acronym)
group = get_group_or_404(acronym, group_type)
if not can_manage_group_type(request.user, group_type):
if not can_manage_group_type(request.user, group.type_id):
return HttpResponseForbidden("You don't have permission to access this view")
if request.method == 'POST':
@ -347,17 +334,22 @@ def conclude(request, group_type, acronym):
e.desc = "Requested closing group"
e.save()
return redirect('group_charter', group_type=group.type_id, acronym=group.acronym)
return redirect(group.features.about_page, group_type=group_type, acronym=group.acronym)
else:
form = ConcludeForm()
return render(request, 'group/conclude.html',
dict(form=form, group=group))
return render(request, 'group/conclude.html', {
'form': form,
'group': group,
'group_type': group_type,
})
@login_required
def customize_workflow(request, group_type, acronym):
group = get_object_or_404(Group, type=group_type, acronym=acronym)
group = get_group_or_404(acronym, group_type)
if not group.features.customize_workflow:
raise Http404
if (not has_role(request.user, "Secretariat") and
not group.role_set.filter(name="chair", person__user=request.user)):
return HttpResponseForbidden("You don't have permission to access this view")

25
ietf/group/features.py Normal file
View file

@ -0,0 +1,25 @@
class GroupFeatures(object):
"""Configuration of group pages and processes to have this collected
in one place rather than scattered over the group page views."""
has_milestones = False
has_chartering_process = False
has_documents = False # i.e. drafts/RFCs
has_materials = False
customize_workflow = False
about_page = "group_about"
default_tab = about_page
material_types = ["slides"]
def __init__(self, group):
if group.type_id in ("wg", "rg"):
self.has_milestones = True
self.has_chartering_process = True
self.has_documents = True
self.customize_workflow = True
self.default_tab = "group_docs"
elif group.type_id in ("team",):
self.has_materials = True
if self.has_chartering_process:
self.about_page = "group_charter"

View file

@ -22,7 +22,7 @@ class GroupChangesFeed(Feed):
def link(self, obj):
if not obj:
raise FeedDoesNotExist
return urlreverse('group_charter', kwargs=dict(group_type=obj.type_id, acronym=obj.acronym))
return obj.about_url()
def description(self, obj):
return self.title(obj)
@ -40,7 +40,7 @@ class GroupChangesFeed(Feed):
if isinstance(obj, DocEvent):
return urlreverse("doc_view", kwargs={'name': obj.doc_id })
elif isinstance(obj, GroupEvent):
return urlreverse('group_charter', kwargs=dict(group_type=obj.group.type_id, acronym=obj.group.acronym))
return obj.group.about_url()
def item_pubdate(self, obj):
return obj.time

View file

@ -35,22 +35,25 @@
import os
import itertools
from tempfile import mkstemp
from collections import OrderedDict
from django.shortcuts import get_object_or_404, render
from django.shortcuts import render
from django.template.loader import render_to_string
from django.http import HttpResponse, Http404
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from django.views.decorators.cache import cache_page
from django.db.models import Q
from django.utils.safestring import mark_safe
from ietf.doc.views_search import SearchForm, retrieve_search_results
from ietf.doc.models import State, DocAlias, RelatedDocument
from ietf.doc.models import Document, State, DocAlias, RelatedDocument
from ietf.doc.utils import get_chartering_type
from ietf.doc.templatetags.ietf_filters import clean_whitespace
from ietf.group.models import Group, Role, ChangeStateGroupEvent
from ietf.name.models import GroupTypeName
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
from ietf.group.utils import can_manage_materials, get_group_or_404
from ietf.utils.pipe import pipe
def roles(group, role_name):
@ -58,10 +61,31 @@ def roles(group, role_name):
def fill_in_charter_info(group, include_drafts=False):
group.areadirector = group.ad.role_email("ad", group.parent) if group.ad else None
group.chairs = roles(group, "chair")
group.techadvisors = roles(group, "techadv")
group.editors = roles(group, "editor")
group.secretaries = roles(group, "secr")
personnel = {}
for r in Role.objects.filter(group=group).select_related("email", "person", "name"):
if r.name_id not in personnel:
personnel[r.name_id] = []
personnel[r.name_id].append(r)
if group.parent and group.parent.type_id == "area" and group.ad and "ad" not in personnel:
ad_roles = list(Role.objects.filter(group=group.parent, name="ad", person=group.ad))
if ad_roles:
personnel["ad"] = ad_roles
group.personnel = []
for role_name_slug, roles in personnel.iteritems():
label = roles[0].name.name
if len(roles) > 1:
if label.endswith("y"):
label = label[:-1] + "ies"
else:
label += "s"
group.personnel.append((role_name_slug, label, roles))
group.personnel.sort(key=lambda t: t[2][0].name.order)
milestone_state = "charter" if group.state_id == "proposed" else "active"
group.milestones = group.groupmilestone_set.filter(state=milestone_state).order_by('due')
@ -70,18 +94,6 @@ def fill_in_charter_info(group, include_drafts=False):
else:
group.charter_text = u"Not chartered yet."
if include_drafts:
aliases = DocAlias.objects.filter(document__type="draft", document__group=group).select_related('document').order_by("name")
group.drafts = []
group.rfcs = []
for a in aliases:
if a.name.startswith("draft"):
group.drafts.append(a)
else:
group.rfcs.append(a)
a.rel = RelatedDocument.objects.filter(source=a.document).distinct()
a.invrel = RelatedDocument.objects.filter(target=a).distinct()
def extract_last_name(role):
return role.person.name_parts()[3]
@ -113,6 +125,32 @@ def wg_summary_acronym(request, group_type):
'groups': groups },
content_type='text/plain; charset=UTF-8')
def fill_in_wg_roles(group):
def get_roles(slug, default):
for role_slug, label, roles in group.personnel:
if slug == role_slug:
return roles
return default
group.chairs = get_roles("chair", [])
ads = get_roles("ad", [])
group.areadirector = ads[0] if ads else None
group.techadvisors = get_roles("techadv", [])
group.editors = get_roles("editor", [])
group.secretaries = get_roles("secr", [])
def fill_in_wg_drafts(group):
aliases = DocAlias.objects.filter(document__type="draft", document__group=group).select_related('document').order_by("name")
group.drafts = []
group.rfcs = []
for a in aliases:
if a.name.startswith("draft"):
group.drafts.append(a)
else:
group.rfcs.append(a)
a.rel = RelatedDocument.objects.filter(source=a.document).distinct()
a.invrel = RelatedDocument.objects.filter(target=a).distinct()
def wg_charters(request, group_type):
if group_type != "wg":
raise Http404
@ -121,7 +159,9 @@ def wg_charters(request, group_type):
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("name")
for group in area.groups:
fill_in_charter_info(group, include_drafts=True)
fill_in_charter_info(group)
fill_in_wg_roles(group)
fill_in_wg_drafts(group)
group.area = area
return render(request, 'group/1wg-charters.txt',
{ 'areas': areas },
@ -137,7 +177,9 @@ def wg_charters_by_acronym(request, group_type):
groups = Group.objects.filter(type="wg", state="active").exclude(parent=None).order_by("acronym")
for group in groups:
fill_in_charter_info(group, include_drafts=True)
fill_in_charter_info(group)
fill_in_wg_roles(group)
fill_in_wg_drafts(group)
group.area = areas.get(group.parent_id)
return render(request, 'group/1wg-charters-by-acronym.txt',
{ 'groups': groups },
@ -216,29 +258,63 @@ def concluded_groups(request):
return render(request, 'group/concluded_groups.html',
dict(group_types=group_types))
def construct_group_menu_context(request, group, selected, others):
def get_group_materials(group):
return Document.objects.filter(group=group, type__in=group.features.material_types, session=None).exclude(states__slug="deleted")
def construct_group_menu_context(request, group, selected, group_type, others):
"""Return context with info for the group menu filled in."""
kwargs = dict(acronym=group.acronym)
if group_type:
kwargs["group_type"] = group_type
# menu entries
entries = []
if group.features.has_documents:
entries.append(("Documents", urlreverse("ietf.group.info.group_documents", kwargs=kwargs)))
if group.features.has_chartering_process:
entries.append(("Charter", urlreverse("group_charter", kwargs=kwargs)))
else:
entries.append(("About", urlreverse("group_about", kwargs=kwargs)))
if group.features.has_materials and get_group_materials(group).exists():
entries.append(("Materials", urlreverse("ietf.group.info.materials", kwargs=kwargs)))
entries.append(("History", urlreverse("ietf.group.info.history", kwargs=kwargs)))
if group.features.has_documents:
entries.append(("Dependency Graph", urlreverse("ietf.group.info.dependencies_pdf", kwargs=kwargs)))
if group.list_archive.startswith("http:") or group.list_archive.startswith("https:") or group.list_archive.startswith("ftp:"):
entries.append((mark_safe("List Archive &raquo;"), group.list_archive))
if group.has_tools_page():
entries.append((mark_safe("Tools %s Page &raquo;" % group.type.name), "https://tools.ietf.org/%s/%s/" % (group.type_id, group.acronym)))
# actions
actions = []
is_chair = group.has_role(request.user, "chair")
can_manage = can_manage_group_type(request.user, group.type_id)
if group.state_id != "proposed" and (is_chair or can_manage):
actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
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)))
if group.state_id != "conclude" and can_manage:
actions.append((u"Edit group", urlreverse("group_edit", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
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)))
if is_chair or can_manage:
actions.append((u"Customize workflow", urlreverse("ietf.group.edit.customize_workflow", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
if group.type_id in ("rg", "wg") and group.state_id != "conclude" and can_manage:
actions.append((u"Edit group", urlreverse("group_edit", kwargs=kwargs)))
if group.features.customize_workflow and (is_chair or can_manage):
actions.append((u"Customize workflow", urlreverse("ietf.group.edit.customize_workflow", kwargs=kwargs)))
if group.state_id in ("active", "dormant") and can_manage:
actions.append((u"Request closing group", urlreverse("ietf.group.edit.conclude", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
actions.append((u"Request closing group", urlreverse("ietf.group.edit.conclude", kwargs=kwargs)))
d = {
"group": group,
"selected": selected,
"selected_menu_entry": selected,
"menu_entries": entries,
"menu_actions": actions,
"group_type": group_type,
}
d.update(others)
@ -278,22 +354,33 @@ def search_for_group_documents(group):
return docs, meta, docs_related, meta_related
def group_documents(request, group_type, acronym):
group = get_object_or_404(Group, type=group_type, acronym=acronym)
def group_home(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
kwargs = dict(acronym=group.acronym)
if group_type:
kwargs["group_type"] = group_type
return HttpResponseRedirect(urlreverse(group.features.default_tab, kwargs=kwargs))
def group_documents(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_documents:
raise Http404
docs, meta, docs_related, meta_related = search_for_group_documents(group)
return render(request, 'group/group_documents.html',
construct_group_menu_context(request, group, "documents", {
construct_group_menu_context(request, group, "documents", group_type, {
'docs': docs,
'meta': meta,
'docs_related': docs_related,
'meta_related': meta_related
}))
def group_documents_txt(request, group_type, acronym):
def group_documents_txt(request, acronym, group_type=None):
"""Return tabulator-separated rows with documents for group."""
group = get_object_or_404(Group, type=group_type, acronym=acronym)
group = get_group_or_404(acronym, group_type)
if not group.features.has_documents:
raise Http404
docs, meta, docs_related, meta_related = search_for_group_documents(group)
@ -315,44 +402,53 @@ def group_documents_txt(request, group_type, acronym):
return HttpResponse(u"\n".join(rows), content_type='text/plain; charset=UTF-8')
def group_about(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
def group_charter(request, group_type, acronym):
group = get_object_or_404(Group, type=group_type, acronym=acronym)
fill_in_charter_info(group, include_drafts=False)
group.delegates = roles(group, "delegate")
fill_in_charter_info(group)
e = group.latest_event(type__in=("changed_state", "requested_close",))
requested_close = group.state_id != "conclude" and e and e.type == "requested_close"
long_group_types = dict(
wg="Working Group",
rg="Research Group",
)
can_manage = can_manage_group_type(request.user, group.type_id)
return render(request, 'group/group_charter.html',
construct_group_menu_context(request, group, "charter", {
return render(request, 'group/group_about.html',
construct_group_menu_context(request, group, "charter" if group.features.has_chartering_process else "about", group_type, {
"milestones_in_review": group.groupmilestone_set.filter(state="review"),
"milestone_reviewer": milestone_reviewer_for_group_type(group_type),
"requested_close": requested_close,
"long_group_type":long_group_types.get(group_type, "Group"),
"can_manage": can_manage,
}))
def history(request, group_type, acronym):
group = get_object_or_404(Group, type=group_type, acronym=acronym)
def history(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
events = group.groupevent_set.all().select_related('by').order_by('-time', '-id')
return render(request, 'group/history.html',
construct_group_menu_context(request, group, "history", {
"events": events,
}))
construct_group_menu_context(request, group, "history", group_type, {
"events": events,
}))
def materials(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_materials:
raise Http404
docs = get_group_materials(group).order_by("type__order", "-time").select_related("type")
doc_types = OrderedDict()
for d in docs:
if d.type not in doc_types:
doc_types[d.type] = []
doc_types[d.type].append(d)
return render(request, 'group/materials.html',
construct_group_menu_context(request, group, "materials", group_type, {
"doc_types": doc_types.items(),
"can_manage_materials": can_manage_materials(request.user, group)
}))
def nodename(name):
return name.replace('-','_')
@ -470,19 +566,21 @@ def make_dot(group):
dict( nodes=nodes, edges=edges )
)
def dependencies_dot(request, group_type, acronym):
group = get_object_or_404(Group, type=group_type, acronym=acronym)
def dependencies_dot(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_documents:
raise Http404
return HttpResponse(make_dot(group),
content_type='text/plain; charset=UTF-8'
)
@cache_page ( 60 * 60 )
def dependencies_pdf(request, group_type, acronym):
def dependencies_pdf(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_documents:
raise Http404
group = get_object_or_404(Group, type=group_type, acronym=acronym)
dothandle,dotname = mkstemp()
os.close(dothandle)
dotfile = open(dotname,"w")

View file

@ -22,7 +22,7 @@ def email_iesg_secretary_re_charter(request, group, subject, text):
"group/email_iesg_secretary_re_charter.txt",
dict(text=text,
group=group,
group_url=settings.IDTRACKER_BASE_URL + urlreverse('group_charter', kwargs=dict(group_type=group.type_id, acronym=group.acronym)),
group_url=settings.IDTRACKER_BASE_URL + group.about_url(),
charter_url=settings.IDTRACKER_BASE_URL + urlreverse('doc_view', kwargs=dict(name=group.charter.name)) if group.charter else "[no charter]",
)
)
@ -31,7 +31,7 @@ def email_milestones_changed(request, group, changes):
def wrap_up_email(to, text):
text = wrap(strip_tags(text), 70)
text += "\n\n"
text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("group_charter", kwargs=dict(group_type=group.type_id, acronym=group.acronym)))
text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + group.about_url())
send_mail_text(request, to, None,
u"Milestones changed for %s %s" % (group.acronym, group.type.name),
@ -121,7 +121,7 @@ def email_milestones_due(group, early_warning_days):
milestones=milestones,
today=today,
early_warning_days=early_warning_days,
url=settings.IDTRACKER_BASE_URL + urlreverse("group_charter", kwargs=dict(group_type=group.type_id, acronym=group.acronym))
url=settings.IDTRACKER_BASE_URL + group.about_url(),
))
def groups_needing_milestones_due_reminder(early_warning_days):
@ -146,7 +146,7 @@ def email_milestones_overdue(group):
"group/reminder_milestones_overdue.txt",
dict(group=group,
milestones=milestones,
url=settings.IDTRACKER_BASE_URL + urlreverse("group_charter", kwargs=dict(group_type=group.type_id, acronym=group.acronym))
url=settings.IDTRACKER_BASE_URL + group.about_url(),
))
def groups_needing_milestones_overdue_reminder(grace_period=30):

View file

@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
from south.db import db
from south.v2 import SchemaMigration
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Group.description'
db.add_column(u'group_group', 'description',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'GroupHistory.description'
db.add_column(u'group_grouphistory', 'description',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Group.description'
db.delete_column(u'group_group', 'description')
# Deleting field 'GroupHistory.description'
db.delete_column(u'group_grouphistory', 'description')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'doc.document': {
'Meta': {'object_name': 'Document'},
'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': u"orm['person.Person']"}),
'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['person.Email']", 'symmetrical': 'False', 'through': u"orm['doc.DocumentAuthor']", 'blank': 'True'}),
'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}),
'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}),
'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': u"orm['person.Person']"}),
'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}),
'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StreamName']", 'null': 'True', 'blank': 'True'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'})
},
u'doc.documentauthor': {
'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'},
'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '1'})
},
u'doc.state': {
'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': u"orm['doc.State']"}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.StateType']"}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'doc.statetype': {
'Meta': {'object_name': 'StateType'},
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'})
},
u'group.changestategroupevent': {
'Meta': {'ordering': "['-time', 'id']", 'object_name': 'ChangeStateGroupEvent', '_ormbases': [u'group.GroupEvent']},
u'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupStateName']"})
},
u'group.group': {
'Meta': {'object_name': 'Group'},
'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True', 'blank': 'True'}),
'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': u"orm['doc.Document']"}),
'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupStateName']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupTypeName']", 'null': 'True'}),
'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'})
},
u'group.groupevent': {
'Meta': {'ordering': "['-time', 'id']", 'object_name': 'GroupEvent'},
'by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}),
'desc': ('django.db.models.fields.TextField', [], {}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'group.grouphistory': {
'Meta': {'object_name': 'GroupHistory'},
'acronym': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True', 'blank': 'True'}),
'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupStateName']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupTypeName']", 'null': 'True'}),
'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'})
},
u'group.groupmilestone': {
'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestone'},
'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}),
'due': ('django.db.models.fields.DateField', [], {}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupMilestoneStateName']"}),
'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
u'group.groupmilestonehistory': {
'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestoneHistory'},
'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}),
'due': ('django.db.models.fields.DateField', [], {}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'milestone': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': u"orm['group.GroupMilestone']"}),
'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupMilestoneStateName']"}),
'time': ('django.db.models.fields.DateTimeField', [], {})
},
u'group.groupstatetransitions': {
'Meta': {'object_name': 'GroupStateTransitions'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_groupstatetransitions_states'", 'symmetrical': 'False', 'to': u"orm['doc.State']"}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.State']"})
},
u'group.groupurl': {
'Meta': {'object_name': 'GroupURL'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
},
u'group.milestonegroupevent': {
'Meta': {'ordering': "['-time', 'id']", 'object_name': 'MilestoneGroupEvent', '_ormbases': [u'group.GroupEvent']},
u'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}),
'milestone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.GroupMilestone']"})
},
u'group.role': {
'Meta': {'object_name': 'Role'},
'email': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.RoleName']"}),
'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"})
},
u'group.rolehistory': {
'Meta': {'object_name': 'RoleHistory'},
'email': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.GroupHistory']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.RoleName']"}),
'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"})
},
u'name.doctagname': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.doctypename': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.groupmilestonestatename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.groupstatename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.grouptypename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.intendedstdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.rolename': {
'Meta': {'ordering': "['order']", 'object_name': 'RoleName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.stdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.streamname': {
'Meta': {'ordering': "['order']", 'object_name': 'StreamName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'person.email': {
'Meta': {'object_name': 'Email'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}),
'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
u'person.person': {
'Meta': {'object_name': 'Person'},
'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}),
'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['group']

View file

@ -5,14 +5,15 @@ import calendar
import json
from django import forms
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, 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.utils import get_chartering_type
from ietf.group.models import Group, GroupMilestone, MilestoneGroupEvent
from ietf.group.utils import save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type
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
@ -25,7 +26,7 @@ def parse_doc_names(s):
class MilestoneForm(forms.Form):
id = forms.IntegerField(required=True, widget=forms.HiddenInput)
desc = forms.CharField(max_length=500, label="Milestone", required=True)
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)
resolved_checkbox = forms.BooleanField(required=False, label="Resolved")
@ -39,6 +40,8 @@ class MilestoneForm(forms.Form):
required=False, initial="noaction", widget=forms.RadioSelect)
def __init__(self, *args, **kwargs):
kwargs["label_suffix"] = ""
m = self.milestone = kwargs.pop("instance", None)
self.needs_review = kwargs.pop("needs_review", False)
@ -53,7 +56,7 @@ class MilestoneForm(forms.Form):
desc=m.desc,
due_month=m.due.month,
due_year=m.due.year,
resolved_checkbox="on" if m.resolved else False,
resolved_checkbox=bool(m.resolved),
resolved=m.resolved,
docs=",".join(m.docs.values_list("pk", flat=True)),
delete=False,
@ -108,17 +111,19 @@ class MilestoneForm(forms.Form):
return r
@login_required
def edit_milestones(request, group_type, acronym, milestone_set="current"):
def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
# milestones_set + needs_review: we have several paths into this view
# management (IRTF chair/AD/...)/Secr. -> all actions on current + add new
# group chair -> limited actions on current + add new for review
# (re)charter -> all actions on existing in state charter + add new in state charter
#
# For charters we store the history on the charter document to not confuse people.
group = get_object_or_404(Group, type=group_type, acronym=acronym)
group = get_group_or_404(acronym, group_type)
if not group.features.has_milestones:
raise Http404
needs_review = False
if not can_manage_group_type(request.user, group_type):
if not can_manage_group_type(request.user, group.type_id):
if group.role_set.filter(name="chair", person__user=request.user):
if milestone_set == "current":
needs_review = True
@ -306,7 +311,7 @@ def edit_milestones(request, group_type, acronym, milestone_set="current"):
if milestone_set == "charter":
return redirect('doc_view', name=group.charter.canonical_name())
else:
return redirect('group_charter', group_type=group.type_id, acronym=group.acronym)
return HttpResponseRedirect(group.about_url())
else:
for m in milestones:
forms.append(MilestoneForm(instance=m, needs_review=needs_review))
@ -332,8 +337,10 @@ def edit_milestones(request, group_type, acronym, milestone_set="current"):
@login_required
def reset_charter_milestones(request, group_type, acronym):
"""Reset charter milestones to the currently in-use milestones."""
group = get_object_or_404(Group, type=group_type, acronym=acronym)
group = get_group_or_404(acronym, group_type)
if not group.features.has_milestones:
raise Http404
if (not can_manage_group_type(request.user, group_type) and
not group.role_set.filter(name="chair", person__user=request.user)):
return HttpResponseForbidden("You are not chair of this group.")

View file

@ -17,6 +17,7 @@ class GroupInfo(models.Model):
state = models.ForeignKey(GroupStateName, null=True)
type = models.ForeignKey(GroupTypeName, null=True)
parent = models.ForeignKey('Group', blank=True, null=True)
description = models.TextField(blank=True)
ad = models.ForeignKey(Person, verbose_name="AD", blank=True, null=True)
list_email = models.CharField(max_length=64, blank=True)
list_subscribe = models.CharField(max_length=255, blank=True)
@ -35,6 +36,21 @@ class GroupInfo(models.Model):
res += " %s (%s)" % (self.type, self.acronym)
return res
@property
def features(self):
if not hasattr(self, "features_cache"):
from ietf.group.features import GroupFeatures
self.features_cache = GroupFeatures(self)
return self.features_cache
def about_url(self):
# bridge gap between group-type prefixed URLs and /group/ ones
from django.core.urlresolvers import reverse as urlreverse
kwargs = { 'acronym': self.acronym }
if self.type_id in ("wg", "rg"):
kwargs["group_type"] = self.type_id
return urlreverse(self.features.about_page, kwargs=kwargs)
class Meta:
abstract = True

View file

@ -183,7 +183,7 @@ class GroupPagesTests(TestCase):
due=datetime.date.today() + datetime.timedelta(days=100))
milestone.docs.add(draft)
url = urlreverse('ietf.group.info.group_charter', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
url = group.about_url()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(group.name in r.content)
@ -192,6 +192,50 @@ class GroupPagesTests(TestCase):
self.assertTrue(milestone.desc in r.content)
self.assertTrue(milestone.docs.all()[0].name in r.content)
def test_group_about(self):
make_test_data()
group = Group.objects.create(
type_id="team",
acronym="testteam",
name="Test Team",
description="The test team is testing.",
state_id="active",
)
url = group.about_url()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(group.name in r.content)
self.assertTrue(group.acronym in r.content)
self.assertTrue(group.description in r.content)
def test_materials(self):
make_test_data()
group = Group.objects.create(type_id="team", acronym="testteam", name="Test Team", state_id="active")
doc = Document.objects.create(
name="slides-testteam-test-slides",
rev="00",
title="Test Slides",
group=group,
type_id="slides",
)
doc.set_state(State.objects.get(type="slides", slug="active"))
DocAlias.objects.create(name=doc.name, document=doc)
url = urlreverse("group_materials", kwargs={ 'acronym': group.acronym })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(doc.title in r.content)
self.assertTrue(doc.name in r.content)
# try deleting the document and check it's gone
doc.set_state(State.objects.get(type="slides", slug="deleted"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(doc.title not in r.content)
def test_history(self):
draft = make_test_data()
group = draft.group

View file

@ -7,6 +7,28 @@ urlpatterns = patterns('',
(r'^chartering/$', 'ietf.group.info.chartering_groups'),
(r'^chartering/create/(?P<group_type>(wg|rg))/$', 'ietf.group.edit.edit', {'action': "charter"}, "group_create"),
(r'^concluded/$', 'ietf.group.info.concluded_groups'),
# FIXME: the things below are duplicated in urls_info.py while we
# figure out whether to serve everything from /group/<acronym>,
# need to unify these at some point
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/$', 'ietf.group.info.group_home', None, "group_home"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/history/$', 'ietf.group.info.history'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/deps/dot/$', 'ietf.group.info.dependencies_dot'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/deps/pdf/$', 'ietf.group.info.dependencies_pdf'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/init-charter/', 'ietf.group.edit.submit_initial_charter'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/edit/$', 'ietf.group.edit.edit', {'action': "edit"}, "group_edit"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/conclude/$', 'ietf.group.edit.conclude'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "current"}, "group_edit_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "charter"}, "group_edit_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', 'ietf.group.milestones.reset_charter_milestones', None, "group_reset_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', 'ietf.group.milestones.ajax_search_docs', None, "group_ajax_search_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', 'ietf.group.edit.customize_workflow'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/about/(?P<group_type>.)?$', 'ietf.group.info.group_about', None, 'group_about'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/materials/new/$', 'ietf.doc.views_material.choose_material_type'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/materials/new/(?P<doc_type>[\w-]+)/$', 'ietf.doc.views_material.edit_material', { 'action': "new" }, "group_new_material"),
)

View file

@ -19,8 +19,9 @@ urlpatterns = patterns('',
(r'^bofs/$', info.bofs),
(r'^bofs/create/$', edit.edit, {'action': "create"}, "bof_create"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/documents/txt/$', info.group_documents_txt),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/$', info.group_documents, None, "group_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/charter/$', info.group_charter, None, 'group_charter'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/$', info.group_home, None, "group_home"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/documents/$', info.group_documents, None, "group_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/charter/$', info.group_about, None, 'group_charter'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/history/$', info.history),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/deps/dot/$', info.dependencies_dot),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/deps/pdf/$', info.dependencies_pdf),

View file

@ -1,5 +1,7 @@
import os
from django.shortcuts import get_object_or_404
from ietf.group.models import Group, RoleHistory
from ietf.person.models import Email
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
@ -105,3 +107,13 @@ def milestone_reviewer_for_group_type(group_type):
else:
return "Area Director"
def can_manage_materials(user, group):
return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr"))
def get_group_or_404(acronym, group_type):
"""Helper to overcome the schism between group-type prefixed URLs and generic."""
possible_groups = Group.objects.all()
if group_type:
possible_groups = possible_groups.filter(type=group_type)
return get_object_or_404(possible_groups, acronym=acronym)

View file

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
from south.v2 import DataMigration
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
orm.RoleName.objects.filter(slug="chair").update(order=1)
orm.RoleName.objects.filter(slug="ad").update(order=2)
orm.RoleName.objects.filter(slug="pre-ad").update(order=3)
orm.RoleName.objects.filter(slug="editor").update(order=5)
orm.RoleName.objects.filter(slug="secr").update(order=6)
orm.RoleName.objects.filter(slug="techadv").update(order=4)
orm.RoleName.objects.filter(slug="execdir").update(order=2)
orm.RoleName.objects.filter(slug="admdir").update(order=3)
orm.RoleName.objects.filter(slug="liaiman").update(order=4)
orm.RoleName.objects.filter(slug="auth").update(order=5)
orm.RoleName.objects.filter(slug="delegate").update(order=6)
orm.RoleName.objects.filter(slug="atlarge").update(order=10)
orm.RoleName.objects.filter(slug="member").update(order=7)
orm.RoleName.objects.filter(slug="liaison").update(order=11)
orm.RoleName.objects.filter(slug="advisor").update(order=4)
orm.RoleName.objects.filter(slug="announce").update(order=12)
def backwards(self, orm):
"Write your backwards methods here."
models = {
u'name.ballotpositionname': {
'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'},
'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.constraintname': {
'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.dbtemplatetypename': {
'Meta': {'ordering': "['order']", 'object_name': 'DBTemplateTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.docrelationshipname': {
'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'revname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.docremindertypename': {
'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.doctagname': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.doctypename': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.draftsubmissionstatename': {
'Meta': {'ordering': "['order']", 'object_name': 'DraftSubmissionStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': u"orm['name.DraftSubmissionStateName']"}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.feedbacktype': {
'Meta': {'ordering': "['order']", 'object_name': 'FeedbackType'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.groupmilestonestatename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.groupstatename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.grouptypename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.intendedstdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.liaisonstatementpurposename': {
'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.meetingtypename': {
'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.nomineepositionstate': {
'Meta': {'ordering': "['order']", 'object_name': 'NomineePositionState'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.rolename': {
'Meta': {'ordering': "['order']", 'object_name': 'RoleName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.sessionstatusname': {
'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.stdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.streamname': {
'Meta': {'ordering': "['order']", 'object_name': 'StreamName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.timeslottypename': {
'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
}
}
complete_apps = ['name']
symmetrical = True

View file

@ -24,7 +24,7 @@ from ietf.utils.mail import send_mail
ROLODEX_URL = getattr(settings, 'ROLODEX_URL', None)
def get_group_or_404(year):
def get_nomcom_group_or_404(year):
return get_object_or_404(Group,
acronym__icontains=year,
state__slug='active',
@ -121,7 +121,7 @@ class EditMembersFormPreview(FormPreview):
@method_decorator(role_required("Nomcom Chair", "Nomcom Advisor"))
def __call__(self, request, *args, **kwargs):
year = kwargs['year']
group = get_group_or_404(year)
group = get_nomcom_group_or_404(year)
self.state['group'] = group
self.state['rolodex_url'] = ROLODEX_URL
groups = group.nomcom_set.all()
@ -225,7 +225,7 @@ class EditChairFormPreview(FormPreview):
@method_decorator(role_required("Secretariat"))
def __call__(self, request, *args, **kwargs):
year = kwargs['year']
group = get_group_or_404(year)
group = get_nomcom_group_or_404(year)
self.state['group'] = group
self.state['rolodex_url'] = ROLODEX_URL
self.group = group

View file

@ -72,7 +72,7 @@ class GroupModelForm(forms.ModelForm):
class Meta:
model = Group
fields = ('acronym','name','type','state','parent','ad','list_email','list_subscribe','list_archive','comments')
fields = ('acronym','name','type','state','parent','ad','list_email','list_subscribe','list_archive','description','comments')
def __init__(self, *args, **kwargs):
super(GroupModelForm, self).__init__(*args, **kwargs)

View file

@ -45,7 +45,11 @@
<tr><td>Email Address:</td><td>{{ group.list_email }}</td></tr>
<tr><td>Email Subscription:</td><td>{{ group.list_subscribe }}</td></tr>
<tr><td>Email Archive:</td><td>{{ group.list_archive }}</td></tr>
{% if group.features.has_chartering_process %}
<tr><td>Charter:</td><td><a href="{% url "groups_charter" acronym=group.acronym %}">View Charter</a></td></tr>
{% else %}
<tr><td>Description:</td><td>{{ group.description }}</td></tr>
{% endif %}
<tr><td>Comments:</td><td>{{ group.comments }}</td></tr>
<tr><td>Last Modified Date:</td><td>{{ group.time }}</td></tr>
</table>

View file

@ -256,6 +256,7 @@ DATETIME_FORMAT = "Y-m-d H:i"
# Override this in settings_local.py if needed
# *_PATH variables ends with a slash/ .
DOCUMENT_PATH_PATTERN = '/a/www/ietf-ftp/{doc.type_id}/'
INTERNET_DRAFT_PATH = '/a/www/ietf-ftp/internet-drafts/'
INTERNET_DRAFT_PDF_PATH = '/a/www/ietf-datatracker/pdf/'
RFC_PATH = '/a/www/ietf-ftp/rfc/'
@ -276,7 +277,6 @@ INTERNET_DRAFT_ARCHIVE_DIR = '/a/www/www6s/draft-archive'
# Ideally, more of these would be local -- but since we don't support
# versions right now, we'll point to external websites
DOC_HREFS = {
"agenda": "/meeting/{meeting}/agenda/{doc.group.acronym}/",
#"charter": "/doc/{doc.name}-{doc.rev}/",
"charter": "http://www.ietf.org/charter/{doc.name}-{doc.rev}.txt",
#"draft": "/doc/{doc.name}-{doc.rev}/",
@ -285,6 +285,11 @@ DOC_HREFS = {
# who understands this better can take care of it.
#"liai-att": None
#"liaison": None
"slides": 'http://www.ietf.org/slides/{doc.name}-{doc.rev}',
}
MEETING_DOC_HREFS = {
"agenda": "/meeting/{meeting}/agenda/{doc.group.acronym}/",
"minutes": "http://www.ietf.org/proceedings/{meeting}/minutes/{doc.external_url}",
"slides": "http://www.ietf.org/proceedings/{meeting}/slides/{doc.external_url}",
}

View file

@ -11,21 +11,14 @@
{% block content %}
{{ top|safe }}
<div class="snapshots">
Snapshots:
<span class="revisions">
{% for rev in revisions %}
<a {% if rev != doc.rev %}href="{% url "doc_view" name=doc.name %}{% if not forloop.last %}{{ rev }}/{% endif %}"{% endif %}>{{ rev }}</a>
{% endfor %}
</span>
</div>
{% include "doc/revisions_list.html" %}
<div class="ietf-box metabox">
<div>
{% if snapshot %}Snapshot of{% endif %}
{% if doc.get_state_slug != "approved" %}Proposed{% endif %}
Charter for "{{ group.name }}"
(<a {% if group.type_id == "wg" or group.type_id == "rg" %}href="{% url "ietf.group.info.group_charter" group_type=group.type_id acronym=group.acronym %}"{% endif %}>{{ group.acronym }}</a>) {{ group.type.name }}
(<a href="{{ group.about_url }}">{{ group.acronym }}</a>) {{ group.type.name }}
</div>
<table id="metatable" width="100%">

View file

@ -11,14 +11,7 @@
{% block content %}
{{ top|safe }}
<div class="snapshots">
Versions:
<span class="revisions">
{% for rev in revisions %}
<a {% if rev != doc.rev %}href="{% url "doc_view" name=doc.name %}{% if not forloop.last %}{{ rev }}/{% endif %}"{% endif %}>{{ rev }}</a>
{% endfor %}
</span>
</div>
{% include "doc/revisions_list.html" %}
<div class="ietf-box metabox">
<div>

View file

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% load ietf_filters %}
{% block title %}{{ doc.canonical_name }}-{{ doc.rev }}{% endblock %}
{% block pagehead %}
<link rel="stylesheet" type="text/css" href="/css/doc.css"></link>
{% endblock %}
{% block content %}
{{ top|safe }}
{% include "doc/revisions_list.html" %}
<div class="ietf-box metabox">
<div>
{% if snapshot %}Snapshot of{% endif %} {% if doc.meeting_related %}Meeting{% endif %} {{ doc.type.name }} for <a href="{{ doc.group.about_url }}">{{ doc.group.acronym }}</a> group
</div>
<table id="metatable" width="100%">
<tr>
<td>Title:</td>
<td>
<a {% if not snapshot and can_manage_material %} class="editlink" href="{% url "material_edit" name=doc.name action="title" name=doc.name %}"{% endif %}>{{ doc.title }}</a>
</td>
</tr>
<tr>
<td>State:</td>
<td>
<a title="{{ doc.get_state.desc }}"{% if not snapshot and can_manage_material %} class="editlink" href="{% url "material_edit" name=doc.name action="state" %}"{% endif %}>{{ doc.get_state.name }}</a>
</td>
</tr>
{% if other_types %}
<tr>
<td>Other versions:</td>
<td>
{% for t, url in other_types %}
<a href="{{ url }}">{{ t }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
</td>
</tr>
{% endif %}
<tr>
<td>Last updated:</td>
<td>{{ doc.time|date:"Y-m-d" }}</td>
</tr>
{% if not snapshot and can_manage_material %}
<tr><td colspan="2">
<a class="button" href="{% url "material_edit" name=doc.name action="revise" %}">Upload New Revision</a>
</td><tr/>
{% endif %}
</table>
</div>
{% if doc.rev and content != None %}
<h3>{{ doc.title }}</h3>
<div class="markup_draft">
{{ content|fill:"80"|safe|linebreaksbr|keep_spacing|sanitize_html|safe }}
</div>
{% else %}
<p>Not available as plain text.</p>
{% if other_types %}
<p class="download-instead"><a href="{{ other_types.0.1 }}">Download as {{ other_types.0.0.upper }}</a></p>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -11,14 +11,7 @@
{% block content %}
{{ top|safe }}
<div class="snapshots">
Versions:
<span class="revisions">
{% for rev in revisions %}
<a {% if rev != doc.rev %}href="{% url "doc_view" name=doc.name %}{% if not forloop.last %}{{ rev }}/{% endif %}"{% endif %}>{{ rev }}</a>
{% endfor %}
</span>
</div>
{% include "doc/revisions_list.html" %}
<div class="ietf-box metabox">
<div>

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Upload Material for Group {{ group.acronym }}{% endblock %}
{% block morecss %}
{{ block.super }}
.material-types li { margin-bottom: 0.5em; }
{% endblock %}
{% block content %}
{% load ietf_filters %}
<h1>Upload Material for Group {{ group.acronym }}</h1>
<p>Select what kind of material you wish to upload:</p>
<ul class="material-types">
{% for t in material_types %}
<li><a href="{% url "group_new_material" acronym=group.acronym doc_type=t.slug %}">{{ t.name }}</a></li>
{% endfor %}
</ul>
{% endblock content %}

View file

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}{% if action == "new" or action == "revise" %}Upload{% else %}Edit{% endif %} {{ document_type.name }} for Group {{ group.acronym }}{% endblock %}
{% block morecss %}
{{ block.super }}
form.upload-material td { padding-bottom: 0.6em; }
form.upload-material #id_title, form.upload-material #id_name { width: 30em; }
form.upload-material .submit-row td { padding-top: 1em; text-align: right; }
{% endblock %}
{% block content %}
{% load ietf_filters %}
<h1>{% if action == "new" or action == "revise" %}Upload{% else %}Edit{% endif %} {{ document_type.name }} for Group {{ group.acronym }}</h1>
{% if action == "new" %}
<p>
Below you can upload a file for the group
<a href="{% url "group_materials" acronym=group.acronym %}">{{ group.acronym }}</a>.
The file will appear under the materials tab in the group pages.
</p>
<h3>Upload</h3>
{% elif action == "revise" %}
<p>
Below you can upload a new revision of {{ doc_name }} for the group
<a href="{% url "group_materials" acronym=group.acronym %}">{{ group.acronym }}</a>.
</p>
<h3>Upload New Revision</h3>
{% endif %}
<form class="upload-material" method="post" enctype="multipart/form-data" data-nameprefix="{{ document_type.slug }}-{{ group.acronym }}-">{% csrf_token %}
<table>
{{ form.as_table }}
<tr class="submit-row">
<td colspan="2">
<a class="button" href="{% if doc_name %}{% url "doc_view" name=doc_name %}{% else %}{% url "group_materials" acronym=group.acronym %}{% endif %}">Cancel</a>
<input class="submit button" type="submit" value="{% if action == "new" or action == "revise" %}Upload{% else %}Save{% endif %}" />
</td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
jQuery(document).ready(function () {
jQuery("form.upload-material input#id_title").on("change keyup", function () {
var v = jQuery(this).val();
var slug = jQuery(this).parents("form").data("nameprefix");
slug += v.toLowerCase().replace(/ /g,'-').replace(/[-]+/g, '-').replace(/[^a-z-]+/g,'');
jQuery(this).parents("form").find("input#id_name").val(slug);
});
});
{% endblock %}

View file

@ -0,0 +1,8 @@
<div class="snapshots">
Versions:
<span class="revisions">
{% for rev in revisions %}
<a {% if rev != doc.rev %}href="{% url "doc_view" name=doc.name %}{% if not forloop.last %}{{ rev }}/{% endif %}"{% endif %}>{{ rev }}</a>
{% endfor %}
</span>
</div>

View file

@ -22,7 +22,7 @@
<table class="ietf-wg-table">
{% for group in groups %}
<tr>
<td width="10%;"><a href="{% url "ietf.group.info.group_documents" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td width="10%;"><a href="{% url "ietf.group.info.group_home" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td width="50%">{{ group.name }}</td>
<td width="40%">{% for chair in group.chairs %}<a href="mailto:{{ chair.email.address }}">{{ chair.person.plain_name }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
</tr>

View file

@ -80,7 +80,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<table class="ietf-wg-table">
{% for group in area.groups %}
<tr>
<td width="10%;"><a href="{% url "ietf.group.info.group_documents" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td width="10%;"><a href="{% url "ietf.group.info.group_home" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td width="1%">{% for ad in area.ads %}{% if ad.person_id == group.ad_id %}<span title="AD for {{ group.acronym }}: {{ ad.person }}" class="square bgcolor{{forloop.counter}}"></span>{% endif %}{% endfor %}</td>
<td width="50%">{{ group.name }}</td>
<td width="39%">{% for chair in group.chairs %}<a href="mailto:{{ chair.email.address }}">{{ chair.person.plain_name }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>

View file

@ -25,7 +25,7 @@
{% for g in groups %}
<tr class="{{ forloop.counter|divisibleby:2|yesno:"oddrow,evenrow" }}">
<td class="acronym">
<a href="{% url "group_charter" group_type=g.type_id acronym=g.acronym %}">{{ g.acronym }}</a>
<a href="{{ g.about_url }}">{{ g.acronym }}</a>
</td>
<td class="title">
<a {%comment%}href="{% url "doc_view" name=g.charter.name %}"{%endcomment%}>{{ g.name }}</a>

View file

@ -33,7 +33,7 @@
{% for g in t.chartering_groups %}
<tr class="{{ forloop.counter|divisibleby:2|yesno:"oddrow,evenrow" }}">
<td class="acronym">
<a href="{% url "group_charter" group_type=g.type_id acronym=g.acronym %}">{{ g.acronym }}</a>
<a href="{{ g.about_url }}">{{ g.acronym }}</a>
</td>
<td class="title">
<a href="{% url "doc_view" name=g.charter.name %}">{{ g.name }}</a>

View file

@ -29,7 +29,7 @@ form.conclude .actions {
{{ form.as_table }}
<tr>
<td colspan="2" class="actions">
<a class="button" href="{% url "group_charter" group_type=group.type_id acronym=group.acronym %}">Cancel</a>
<a class="button" href="{{ group.about_url }}">Cancel</a>
<input class="button" type="submit" value="Send request"/>
</td>
</tr>

View file

@ -25,7 +25,7 @@
{% for g in t.concluded_groups %}
<tr>
<td>
<a href="{% url "group_charter" group_type=g.type_id acronym=g.acronym %}">{{ g.acronym }}</a>
<a href="{{ group.about_url }}">{{ g.acronym }}</a>
</td>
<td>{{ g.name }}
<span class="active-period">({% if g.start_date %}{{ g.start_date|date:"M. Y" }}{% else %}?{% endif %}

View file

@ -34,7 +34,7 @@
<h1>Customize Workflow for {{ group.acronym }} {{ group.type.name }}</h1>
<p>Below you can customize the draft states and tags used in the
<a href="{% url "group_charter" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }} {{ group.type.name }}</a>. Note that some states are
<a href="{{ group.about_url }}">{{ group.acronym }} {{ group.type.name }}</a>. Note that some states are
mandatory for group operation and cannot be deactivated.</p>
{% if group.type_id == "wg" %}

View file

@ -69,7 +69,7 @@ so. New accounts can be <a href="{% url "create_account" %}">created here</a>.</
<td></td>
<td class="actions">
{% if action == "edit" %}
<a class="button" href="{% url "group_charter" group_type=group.type_id acronym=group.acronym %}">Cancel</a>
<a class="button" href="{{ group.about_url }}">Cancel</a>
<input class="button" type="submit" value="Save"/>
{% else %}
{% if action == "charter" %}

View file

@ -42,7 +42,7 @@ tr.milestone.add { font-style: italic; }
<noscript>This page depends on Javascript being enabled to work properly.</noscript>
<p>Links:
<a href="{% url "group_charter" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }} {{ group.type.name }}</a>
<a href="{{ g.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 %}
@ -92,7 +92,7 @@ this list</a> to the milestones currently in use for the {{ group.acronym }} {{
</table>
<div class="actions">
<a class="button" href="{% if milestone_set == "charter" %}{% url "doc_view" name=group.charter.canonical_name %}{% else %}{% url "group_charter" group_type=group.type_id acronym=group.acronym %}{% endif %}">Cancel</a>
<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"/>
<input type="hidden" name="action" value="save">
</div>

View file

@ -0,0 +1,127 @@
{% extends "group/group_base.html" %}
{% load ietf_filters %}
{% block group_subtitle %}Charter{% endblock %}
{% block group_content %}
<div class="ietf-box ietf-group-details">
{% if group.state_id == "conclude" %}
<span class="ietf-concluded-warning">Note: The data for concluded {{ group.type.name }}s
is occasionally incorrect.</span>
{% endif %}
<table>
<tr><th colspan="2">Group</th></tr>
<tr valign="top">
<td style="width:14ex;">Name:</td>
<td>{{ group.name }}</td>
</tr>
<tr><td>Acronym:</td><td>{{ group.acronym }}</td></tr>
{% if group.parent and group.parent.type_id == "area" %}
<tr><td>{{ group.parent.type.name }}:</td><td>{{ group.parent.name }} ({{ group.parent.acronym }})</td></tr>
{% endif %}
<tr>
<td>State:</td>
<td>{{ group.state.name }}
{% if requested_close %}
(but in the process of being closed)
{% endif %}
</td>
</tr>
{% if group.features.has_chartering_process %}
<tr>
<td>Charter:</td>
<td>
{% if group.charter %}
<a href="{% url "doc_view" name=group.charter.name %}">{{ group.charter.name }}-{{ group.charter.rev }}</a> ({{ group.charter.get_state.name }})
{% else %}
none
{% if can_manage %}
- <a href="{% url "ietf.group.edit.submit_initial_charter" group_type=group.type_id acronym=group.acronym %}">Submit Charter</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endif %}
{% with group.groupurl_set.all as urls %}
{% if urls %}
<tr>
<td>More info:</td>
<td>
{% for url in urls %}
<a href="{{ url.url }}">{{ url.name }}</a>{% if not forloop.last %}<br>{% endif %}
{% endfor %}
</td>
</tr>
{% endif %}
{% endwith %}
<tr><th colspan="2">Personnel</th></tr>
{% for slug, label, roles in group.personnel %}
<tr valign="top">
<td>{{ label }}:</td>
<td>
{% for r in roles %}
<a href="mailto:{{ r.email.address }}">{{ r.person.plain_name }} &lt;{{ r.email.address }}&gt;</a><br/>
{% endfor %}
</td>
</tr>
{% endfor %}
{% if group.list_email %}
<tr><th colspan="2">Mailing List</th></tr>
<tr><td>Address:</td><td>{{ group.list_email|urlize }}</td></tr>
<tr><td>To Subscribe:</td><td>{{ group.list_subscribe|urlize }}</td></tr>
<tr><td>Archive:</td><td>{{ group.list_archive|urlize }}</td></tr>
{% endif %}
{% if group.state_id != "conclude" %}
<tr><th colspan="2">Jabber Chat</th></tr>
<tr>
<td>Room Address:</td>
<td><a href="xmpp:{{ group.acronym }}@jabber.ietf.org">xmpp:{{ group.acronym }}@jabber.ietf.org</a></td>
</tr>
<tr>
<td>Logs:</td>
<td><a href="http://jabber.ietf.org/logs/{{ group.acronym }}/">http://jabber.ietf.org/logs/{{ group.acronym }}/</a></td>
</tr>
{% endif %}
</table>
</div>
{% if group.features.has_chartering_process %}
<h2>Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ group.type.desc.title }}</h2>
<p>{{ group.charter_text|linebreaks }}</p>
{% else %}
<h2>About</h2>
<p>{{ group.description|default:"No description yet."|linebreaks }}</p>
{% endif %}
{% if group.features.has_milestones %}
<h2>{% if group.state_id == "proposed" %}Proposed{% endif %} Milestones</h2>
{% include "group/milestones.html" with milestones=group.milestones %}
{% if milestones_in_review %}
<p>+ {{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }}
currently in {{ milestone_reviewer }} review.</p>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -66,16 +66,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<div class="ietf-navset">
<div>
<a {% if selected == "documents" %}class="selected"{% else %}href="{% url "ietf.group.info.group_documents" group_type=group.type_id acronym=group.acronym %}"{% endif %}>Documents</a> |
<a {% if selected == "charter" %}class="selected"{% else %}href="{% url "ietf.group.info.group_charter" group_type=group.type_id acronym=group.acronym %}"{% endif %}>Charter</a> |
<a {% if selected == "history" %}class="selected"{% else %}href="{% url "ietf.group.info.history" group_type=group.type_id acronym=group.acronym %}"{% endif %}>History</a>
| <a href="{% url 'ietf.group.info.dependencies_pdf' group_type=group.type_id acronym=group.acronym %}">Dependency Graph</a>
{% if group.list_archive|startswith:"http:" or group.list_archive|startswith:"https:" or group.list_archive|startswith:"ftp:" %}
| <a href="{{ group.list_archive }}">List Archive &raquo;</a>
{% endif %}
{% if group.has_tools_page %}
| <a href="http://tools.ietf.org/{{ group.type_id }}/{{ group.acronym }}/">Tools {{ group.type.name }} Page &raquo;</a>
{% endif %}
{% for name, url in menu_entries %}
<a {% if selected_menu_entry == name.lower %}class="selected"{% else %}href="{{ url }}"{% endif %}>{{ name }}</a> {% if not forloop.last %} | {% endif %}
{% endfor %}
</div>
{% if menu_actions %}

View file

@ -1,168 +0,0 @@
{% extends "group/group_base.html" %}
{% load ietf_filters %}
{% block group_subtitle %}Charter{% endblock %}
{% block morecss %}
{{ block.super }}
h2 a.button { margin-left: 0.5em; font-size: 13px; }
{% endblock %}
{% block group_content %}
<div class="ietf-box ietf-group-details">
{% if group.state_id == "conclude" %}
<span class="ietf-concluded-warning">Note: The data for concluded {{ group.type.name }}s
is occasionally incorrect.</span>
{% endif %}
<table>
<tr><th colspan="2">Group</th></tr>
<tr valign="top">
<td style="width:14ex;">Name:</td>
<td>{{ group.name }}</td>
</tr>
<tr><td>Acronym:</td><td>{{ group.acronym }}</td></tr>
{% if group.parent and group.parent.type_id == "area" %}
<tr><td>{{ group.parent.type.name }}:</td><td>{{ group.parent.name }} ({{ group.parent.acronym }})</td></tr>
{% endif %}
<tr>
<td>State:</td>
<td>{{ group.state.name }}
{% if requested_close %}
(but in the process of being closed)
{% endif %}
</td>
</tr>
<tr>
<td>Charter:</td>
<td>
{% if group.charter %}
<a href="{% url "doc_view" name=group.charter.name %}">{{ group.charter.name }}-{{ group.charter.rev }}</a> ({{ group.charter.get_state.name }})
{% else %}
none
{% if can_manage %}
- <a href="{% url "ietf.group.edit.submit_initial_charter" group_type=group.type_id acronym=group.acronym %}">Submit Charter</a>
{% endif %}
{% endif %}
</td>
</tr>
<tr><th colspan="2">Personnel</th></tr>
<tr valign="top">
<td>Chair{{ group.chairs|pluralize }}:</td>
<td>
{% for chair in group.chairs %}
<a href="mailto:{{ chair.email.address }}">{{ chair.person.plain_name }} &lt;{{ chair.email.address }}&gt;</a><br/>
{% endfor %}
</td>
</tr>
{% if group.parent.type_id == "area" %}
<tr><td>Area Director:</td>
<td>
{% if group.areadirector %}
<a href="mailto:{{ group.areadirector.address }}">{{ group.areadirector.person.plain_name }} &lt;{{ group.areadirector.address }}&gt;</a>
{% else %}?{% endif %}
</td>
</tr>
{% endif %}
{% if group.techadvisors %}
<tr>
<td>Tech Advisor{{ group.techadvisors|pluralize }}:</td>
<td>
{% for techadvisor in group.techadvisors %}
<a href="mailto:{{ techadvisor.email.address }}">{{ techadvisor.person.plain_name }} &lt;{{ techadvisor.email.address }}&gt;</a><br/>
{% endfor %}
</td>
</tr>
{% endif %}
{% if group.editors %}
<tr>
<td>Editor{{ group.editors|pluralize }}:</td>
<td>
{% for editor in group.editors %}
<a href="mailto:{{ editor.email.address }}">{{ editor.person.plain_name }} &lt;{{ editor.email.address }}&gt;</a><br/>
{% endfor %}
</td>
</tr>
{% endif %}
{% if group.secretaries %}
<tr>
<td>Secretar{{ group.secretaries|pluralize:"y,ies" }}:</td>
<td>
{% for secretary in group.secretaries %}
<a href="mailto:{{ secretary.email.address }}">{{ secretary.person.plain_name }} &lt;{{ secretary.email.address }}&gt;</a><br/>
{% endfor %}
</td>
</tr>
{% endif %}
{% if group.delegates %}
<tr>
<td>Delegate{{ group.delegates|pluralize }}:</td>
<td>
{% for delegate in group.delegates %}
<a href="mailto:{{ delegate.email.address }}">{{ delegate.person.plain_name }} &lt;{{ delegate.email.address }}&gt;</a><br/>
{% endfor %}
</td>
</tr>
{% endif %}
<tr><th colspan="2">Mailing List</th></tr>
<tr><td>Address:</td><td>{{ group.list_email|urlize }}</td></tr>
<tr><td>To Subscribe:</td><td>{{ group.list_subscribe|urlize }}</td></tr>
<tr><td>Archive:</td><td>{{ group.list_archive|urlize }}</td></tr>
{% if group.state_id != "conclude" %}
<tr><th colspan="2">Jabber Chat</th></tr>
<tr>
<td>Room Address:</td>
<td><a href="xmpp:{{ group.acronym }}@jabber.ietf.org">xmpp:{{ group.acronym }}@jabber.ietf.org</a></td>
</tr>
<tr>
<td>Logs:</td>
<td><a href="http://jabber.ietf.org/logs/{{ group.acronym }}/">http://jabber.ietf.org/logs/{{ group.acronym }}/</a></td>
</tr>
{% endif %}
</table>
</div>
{% with group.groupurl_set.all as urls %}
{% if urls %}
<p>In addition to the charter, there is additional information about this group on the Web at:
{% for url in urls %}
<a href="{{ url.url }}">{{ url.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% endwith %}
<h2>Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ long_group_type }}</h2>
<p>{{ group.charter_text|escape|format_charter|safe }}</p>
<h2>{% if group.state_id == "proposed" %}Proposed{% endif %} Milestones</h2>
{% include "group/milestones.html" with milestones=group.milestones %}
{% if milestones_in_review %}
<p>+ {{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }}
currently in {{ milestone_reviewer }} review.</p>
{% endif %}
{% endblock %}

View file

@ -13,7 +13,7 @@
{% for ad in group.area.ads %} {{ ad.person.plain_name }} <{{ ad.email.address }}>
{% endfor %}
{% if group.areadirector %} {{ group.area.name }} Advisor:
{{ group.areadirector.person.plain_name }} <{{ group.areadirector.address }}>
{{ group.areadirector.person.plain_name }} <{{ group.areadirector.email.address }}>
{% endif %}{% if group.techadvisors %}
Tech Advisor{{ group.techadvisors|pluralize }}:
{% for techadvisor in group.techadvisors %} {{ techadvisor.person.plain_name }} <{{ techadvisor.email.address }}>

View file

@ -0,0 +1,41 @@
{% extends "group/group_base.html" %}
{% load ietf_filters %}
{% block group_subtitle %}Materials{% endblock %}
{% block morecss %}
{{ block.super }}
.materials .edit-options { float: right; margin-left: 2em; font-style: italic; }
{% endblock %}
{% block group_content %}
{% load ietf_filters %}
{% if doc_types %}
{% for doc_type, docs in doc_types %}
<h2>{{ doc_type.name }}</h2>
<table class="ietf-table ietf-doctable materials">
<tr>
<th>Title</th>
<th>Rev.</th>
<th>Date</th>
</tr>
{% for d in docs %}
<tr class="{% cycle "evenrow" "oddrow" %}">
<td><a class="title-link" href="{% url "doc_view" name=d.name %}">{{ d.title }}</a></td>
<td>{{ d.rev }}</td>
<td>{{ d.time|date:"Y-m-d" }}</td>
</tr>
{% endfor %}
</table>
{% endfor %}
{% else %}
<h2>Materials</h2>
<p>No materials uploaded.</p>
{% endif %}
{% endblock %}

View file

@ -15,6 +15,7 @@ from ietf.utils.mail import send_mail_text, send_mail_mime, outbox
class PyFlakesTestCase(TestCase):
def test_pyflakes(self):
self.maxDiff = None
path = os.path.join(settings.BASE_DIR)
warnings = []
warnings = pyflakes.checkPaths([path], verbosity=0)

View file

@ -177,7 +177,7 @@ form table th {
vertical-align: top;
}
form table .help {
form table .help, form table .helptext {
font-style: italic;
font-size: 11px;
}

View file

@ -46,3 +46,5 @@ h3 a.edit { font-weight: normal; font-size: 13px; display: inline-block; margin-
h4 { margin-bottom: 0; }
h4 + p { margin-top: 0; max-width: 400px; }
p.download-instead a { font-size: 20px; font-weight: bold; color: #2647a0; }