diff --git a/ietf/community/display.py b/ietf/community/display.py
index ede88ecc7..a6f293a41 100644
--- a/ietf/community/display.py
+++ b/ietf/community/display.py
@@ -88,7 +88,7 @@ class WGField(DisplayField):
if raw or not document.group.type_id in ['wg','rg']:
return document.group.acronym
else:
- return '%s' % (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 '%s' % (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):
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 916c326c4..ab5872510 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -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):
diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py
index 2a0b0e132..2d03d55f4 100644
--- a/ietf/doc/templatetags/ietf_filters.py
+++ b/ietf/doc/templatetags/ietf_filters.py
@@ -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", "
").replace("\n","
\n")
-
@register.filter
def indent(value, numspaces=2):
replacement = "\n" + " " * int(numspaces)
diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py
index 8b9088f92..19e01887f 100644
--- a/ietf/doc/tests.py
+++ b/ietf/doc/tests.py
@@ -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()
diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py
new file mode 100644
index 000000000..4c175415e
--- /dev/null
+++ b/ietf/doc/tests_material.py
@@ -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)
+
diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py
index fb43df6e8..8b7a253af 100644
--- a/ietf/doc/urls.py
+++ b/ietf/doc/urls.py
@@ -102,4 +102,5 @@ urlpatterns = patterns('',
(r'^(?Pcharter-[A-Za-z0-9._+-]+)/', include('ietf.doc.urls_charter')),
(r'^(?P[A-Za-z0-9._+-]+)/conflict-review/', include('ietf.doc.urls_conflict_review')),
(r'^(?P[A-Za-z0-9._+-]+)/status-change/', include('ietf.doc.urls_status_change')),
+ (r'^(?P[A-Za-z0-9._+-]+)/material/', include('ietf.doc.urls_material')),
)
diff --git a/ietf/doc/urls_material.py b/ietf/doc/urls_material.py
new file mode 100644
index 000000000..01aa922fd
--- /dev/null
+++ b/ietf/doc/urls_material.py
@@ -0,0 +1,6 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('ietf.doc.views_material',
+ url(r'^(?Pstate|title|revise)/$', "edit_material", name="material_edit"),
+)
+
diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py
index 8decb7431..061466b05 100644
--- a/ietf/doc/utils.py
+++ b/ietf/doc/utils.py
@@ -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)
diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py
index 1b60ae4e7..2de5055a8 100644
--- a/ietf/doc/utils_charter.py
+++ b/ietf/doc/utils_charter.py
@@ -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 == "":
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index 755a19b5d..9af1fc5fc 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -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 = "%s" % (urlreverse("group_docs", kwargs=dict(group_type=doc.group.type_id, acronym=doc.group.acronym)), submission)
+ submission = "%s" % (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
diff --git a/ietf/doc/views_material.py b/ietf/doc/views_material.py
new file mode 100644
index 000000000..ae8efa1c7
--- /dev/null
+++ b/ietf/doc/views_material.py
@@ -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 revise the existing %s." % (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: %s-%s" % (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 %s" % 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 "",
+ })
diff --git a/ietf/group/edit.py b/ietf/group/edit.py
index dc47babb6..a65372c63 100644
--- a/ietf/group/edit.py
+++ b/ietf/group/edit.py
@@ -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")
diff --git a/ietf/group/features.py b/ietf/group/features.py
new file mode 100644
index 000000000..98b5ba2da
--- /dev/null
+++ b/ietf/group/features.py
@@ -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"
diff --git a/ietf/group/feeds.py b/ietf/group/feeds.py
index e54c6a2bf..cda603e23 100644
--- a/ietf/group/feeds.py
+++ b/ietf/group/feeds.py
@@ -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
diff --git a/ietf/group/info.py b/ietf/group/info.py
index 16922c60b..9c0a2e262 100644
--- a/ietf/group/info.py
+++ b/ietf/group/info.py
@@ -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 »"), group.list_archive))
+ if group.has_tools_page():
+ entries.append((mark_safe("Tools %s Page »" % 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")
diff --git a/ietf/group/mails.py b/ietf/group/mails.py
index aa418c2b5..c398f7385 100644
--- a/ietf/group/mails.py
+++ b/ietf/group/mails.py
@@ -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):
diff --git a/ietf/group/migrations/0007_auto__add_field_group_description__add_field_grouphistory_description.py b/ietf/group/migrations/0007_auto__add_field_group_description__add_field_grouphistory_description.py
new file mode 100644
index 000000000..3291e98ec
--- /dev/null
+++ b/ietf/group/migrations/0007_auto__add_field_group_description__add_field_grouphistory_description.py
@@ -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']
diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py
index 6b4a0083e..1778e316c 100644
--- a/ietf/group/milestones.py
+++ b/ietf/group/milestones.py
@@ -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.")
diff --git a/ietf/group/models.py b/ietf/group/models.py
index d699cf554..ee372e02b 100644
--- a/ietf/group/models.py
+++ b/ietf/group/models.py
@@ -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
diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py
index 4e1b26b3f..802eb6442 100644
--- a/ietf/group/tests_info.py
+++ b/ietf/group/tests_info.py
@@ -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
diff --git a/ietf/group/urls.py b/ietf/group/urls.py
index ea049c7e5..283eb38cd 100644
--- a/ietf/group/urls.py
+++ b/ietf/group/urls.py
@@ -7,6 +7,28 @@ urlpatterns = patterns('',
(r'^chartering/$', 'ietf.group.info.chartering_groups'),
(r'^chartering/create/(?P(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/,
+ # need to unify these at some point
+ (r'^(?P[a-zA-Z0-9-._]+)/$', 'ietf.group.info.group_home', None, "group_home"),
+ (r'^(?P[a-zA-Z0-9-._]+)/documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
+ (r'^(?P[a-zA-Z0-9-._]+)/charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
+ (r'^(?P[a-zA-Z0-9-._]+)/history/$', 'ietf.group.info.history'),
+ (r'^(?P[a-zA-Z0-9-._]+)/deps/dot/$', 'ietf.group.info.dependencies_dot'),
+ (r'^(?P[a-zA-Z0-9-._]+)/deps/pdf/$', 'ietf.group.info.dependencies_pdf'),
+ (r'^(?P[a-zA-Z0-9-._]+)/init-charter/', 'ietf.group.edit.submit_initial_charter'),
+ (r'^(?P[a-zA-Z0-9-._]+)/edit/$', 'ietf.group.edit.edit', {'action': "edit"}, "group_edit"),
+ (r'^(?P[a-zA-Z0-9-._]+)/conclude/$', 'ietf.group.edit.conclude'),
+ (r'^(?P[a-zA-Z0-9-._]+)/milestones/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "current"}, "group_edit_milestones"),
+ (r'^(?P[a-zA-Z0-9-._]+)/milestones/charter/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "charter"}, "group_edit_charter_milestones"),
+ (r'^(?P[a-zA-Z0-9-._]+)/milestones/charter/reset/$', 'ietf.group.milestones.reset_charter_milestones', None, "group_reset_charter_milestones"),
+ (r'^(?P[a-zA-Z0-9-._]+)/ajax/searchdocs/$', 'ietf.group.milestones.ajax_search_docs', None, "group_ajax_search_docs"),
+ (r'^(?P[a-zA-Z0-9-._]+)/workflow/$', 'ietf.group.edit.customize_workflow'),
+
+ (r'^(?P[a-zA-Z0-9-._]+)/about/(?P.)?$', 'ietf.group.info.group_about', None, 'group_about'),
+ (r'^(?P[a-zA-Z0-9-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"),
+ (r'^(?P[a-zA-Z0-9-._]+)/materials/new/$', 'ietf.doc.views_material.choose_material_type'),
+ (r'^(?P[a-zA-Z0-9-._]+)/materials/new/(?P[\w-]+)/$', 'ietf.doc.views_material.edit_material', { 'action': "new" }, "group_new_material"),
)
diff --git a/ietf/group/urls_info.py b/ietf/group/urls_info.py
index 0039047b5..ba5d2ab1f 100644
--- a/ietf/group/urls_info.py
+++ b/ietf/group/urls_info.py
@@ -19,8 +19,9 @@ urlpatterns = patterns('',
(r'^bofs/$', info.bofs),
(r'^bofs/create/$', edit.edit, {'action': "create"}, "bof_create"),
(r'^(?P[a-zA-Z0-9-._]+)/documents/txt/$', info.group_documents_txt),
- (r'^(?P[a-zA-Z0-9-._]+)/$', info.group_documents, None, "group_docs"),
- (r'^(?P[a-zA-Z0-9-._]+)/charter/$', info.group_charter, None, 'group_charter'),
+ (r'^(?P[a-zA-Z0-9-._]+)/$', info.group_home, None, "group_home"),
+ (r'^(?P[a-zA-Z0-9-._]+)/documents/$', info.group_documents, None, "group_docs"),
+ (r'^(?P[a-zA-Z0-9-._]+)/charter/$', info.group_about, None, 'group_charter'),
(r'^(?P[a-zA-Z0-9-._]+)/history/$', info.history),
(r'^(?P[a-zA-Z0-9-._]+)/deps/dot/$', info.dependencies_dot),
(r'^(?P[a-zA-Z0-9-._]+)/deps/pdf/$', info.dependencies_pdf),
diff --git a/ietf/group/utils.py b/ietf/group/utils.py
index 8dda40488..d35df624f 100644
--- a/ietf/group/utils.py
+++ b/ietf/group/utils.py
@@ -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)
diff --git a/ietf/name/migrations/0020_sort_role_names.py b/ietf/name/migrations/0020_sort_role_names.py
new file mode 100644
index 000000000..29252a226
--- /dev/null
+++ b/ietf/name/migrations/0020_sort_role_names.py
@@ -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
diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py
index c39f19466..1d7f42c61 100644
--- a/ietf/nomcom/forms.py
+++ b/ietf/nomcom/forms.py
@@ -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
diff --git a/ietf/secr/groups/forms.py b/ietf/secr/groups/forms.py
index 1c5b671cd..075deedc4 100644
--- a/ietf/secr/groups/forms.py
+++ b/ietf/secr/groups/forms.py
@@ -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)
diff --git a/ietf/secr/templates/groups/view.html b/ietf/secr/templates/groups/view.html
index 575a473aa..7c581400c 100644
--- a/ietf/secr/templates/groups/view.html
+++ b/ietf/secr/templates/groups/view.html
@@ -45,7 +45,11 @@
Email Address: | {{ group.list_email }} |
Email Subscription: | {{ group.list_subscribe }} |
Email Archive: | {{ group.list_archive }} |
+ {% if group.features.has_chartering_process %}
Charter: | View Charter |
+ {% else %}
+ Description: | {{ group.description }} |
+ {% endif %}
Comments: | {{ group.comments }} |
Last Modified Date: | {{ group.time }} |
diff --git a/ietf/settings.py b/ietf/settings.py
index f835b5415..0a0eef879 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -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}",
}
diff --git a/ietf/templates/doc/document_charter.html b/ietf/templates/doc/document_charter.html
index a39f26226..52034cdb5 100644
--- a/ietf/templates/doc/document_charter.html
+++ b/ietf/templates/doc/document_charter.html
@@ -11,21 +11,14 @@
{% block content %}
{{ top|safe }}
-
- Snapshots:
-
- {% for rev in revisions %}
- {{ rev }}
- {% endfor %}
-
-
+{% include "doc/revisions_list.html" %}