From 0144ef366b6ea4a4bf9a110ae45d802bdb77e25a Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 20 May 2014 10:19:26 +0000 Subject: [PATCH 01/26] Add description field on group, enable most group info page views to work without a group type to pave the way for /group// URLs, make charter page more generic, enable it to handle descriptions and walk over all personnel instead of picking out specific types of roles, add a redirect view on /group// to lead to either documents/ or charter/, add simple group.features() class to be able to condition on group features rather than specific group types, adjust group pages menu accordingly - Legacy-Id: 7758 --- ietf/community/display.py | 2 +- ietf/doc/templatetags/ietf_filters.py | 4 - ietf/doc/views_doc.py | 2 +- ietf/group/features.py | 19 ++ ietf/group/info.py | 184 +++++++--- ...ion__add_field_grouphistory_description.py | 317 ++++++++++++++++++ ietf/group/models.py | 5 + ietf/group/urls.py | 14 + ietf/name/migrations/0020_sort_role_names.py | 207 ++++++++++++ ietf/templates/group/active_rgs.html | 2 +- ietf/templates/group/active_wgs.html | 2 +- ietf/templates/group/group_base.html | 13 +- ietf/templates/group/group_charter.html | 87 +---- .../group/group_entry_with_charter.txt | 2 +- 14 files changed, 721 insertions(+), 139 deletions(-) create mode 100644 ietf/group/features.py create mode 100644 ietf/group/migrations/0007_auto__add_field_group_description__add_field_grouphistory_description.py create mode 100644 ietf/name/migrations/0020_sort_role_names.py diff --git a/ietf/community/display.py b/ietf/community/display.py index d42769597..75d2ba421 100644 --- a/ietf/community/display.py +++ b/ietf/community/display.py @@ -88,7 +88,7 @@ class WGField(DisplayField): if raw: 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/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/views_doc.py b/ietf/doc/views_doc.py index 4c5ad4c88..133ab5ea0 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -243,7 +243,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 diff --git a/ietf/group/features.py b/ietf/group/features.py new file mode 100644 index 000000000..532dfc06a --- /dev/null +++ b/ietf/group/features.py @@ -0,0 +1,19 @@ +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 + customize_workflow = False + default_tab = "group_charter" + + 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_documents = True diff --git a/ietf/group/info.py b/ietf/group/info.py index 4068873f4..22a17a2d6 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -38,11 +38,12 @@ from tempfile import mkstemp from django.shortcuts import get_object_or_404, 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 @@ -58,10 +59,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 +92,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 +123,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 +157,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 +175,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 +256,52 @@ 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 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 + + # entries + entries = [] + if group.features().has_documents: + entries.append(("Documents", urlreverse("ietf.group.info.group_documents", kwargs=kwargs))) + entries.append(("Charter", urlreverse("ietf.group.info.group_charter", 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)))) + actions.append((u"Edit group", urlreverse("group_edit", 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.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 +341,37 @@ 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 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) + +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) 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) docs, meta, docs_related, meta_related = search_for_group_documents(group) @@ -316,39 +394,45 @@ def group_documents_txt(request, group_type, acronym): return HttpResponse(u"\n".join(rows), content_type='text/plain; charset=UTF-8') -def group_charter(request, group_type, acronym): - group = get_object_or_404(Group, type=group_type, acronym=acronym) +def group_charter(request, acronym, group_type=None): + group = get_group_or_404(acronym, group_type) - 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", - ) + verbose_group_types = { + "wg": "Working Group", + "rg": "Research Group", + "team": "Team", + } can_manage = can_manage_group_type(request.user, group.type_id) + if group.features().has_chartering_process: + description = group.charter_text + else: + description = group.description or "No description entered yet." + return render(request, 'group/group_charter.html', - construct_group_menu_context(request, group, "charter", { + construct_group_menu_context(request, group, "charter", 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"), + "verbose_group_type": verbose_group_types.get(group.type_id, "Group"), "can_manage": can_manage, + "description": description, })) -def history(request, group_type, acronym): - group = get_object_or_404(Group, 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", { + construct_group_menu_context(request, group, "history", group_type, { "events": events, })) @@ -470,19 +554,17 @@ def make_dot(group): dict( nodes=nodes, edges=edges ) ) -def dependencies_dot(request, group_type, acronym): - - group = get_object_or_404(Group, acronym=acronym) +def dependencies_dot(request, acronym, group_type=None): + group = get_group_or_404(acronym, group_type) 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) - group = get_object_or_404(Group, acronym=acronym) - dothandle,dotname = mkstemp() os.close(dothandle) dotfile = open(dotname,"w") 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..e9266e459 --- /dev/null +++ b/ietf/group/migrations/0007_auto__add_field_group_description__add_field_grouphistory_description.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +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'] \ No newline at end of file diff --git a/ietf/group/models.py b/ietf/group/models.py index 7ea66a8d1..e66572b37 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) @@ -79,6 +80,10 @@ class Group(GroupInfo): def bg_color(self): return bg_group_colors[self.upcase_acronym] + def features(self): + from ietf.group.features import GroupFeatures + return GroupFeatures(self) + def json_url(self): return "/group/%s.json" % (self.acronym,) diff --git a/ietf/group/urls.py b/ietf/group/urls.py index ea049c7e5..f3cf567b3 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -7,6 +7,20 @@ 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'), + (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_charter', 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'), ) 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..e15a11004 --- /dev/null +++ b/ietf/name/migrations/0020_sort_role_names.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +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/templates/group/active_rgs.html b/ietf/templates/group/active_rgs.html index 58b8ca7b4..b6f3c6aeb 100644 --- a/ietf/templates/group/active_rgs.html +++ b/ietf/templates/group/active_rgs.html @@ -22,7 +22,7 @@ {% for group in groups %} - + diff --git a/ietf/templates/group/active_wgs.html b/ietf/templates/group/active_wgs.html index d631a2405..a9ce6e530 100644 --- a/ietf/templates/group/active_wgs.html +++ b/ietf/templates/group/active_wgs.html @@ -80,7 +80,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
{{ group.acronym }}{{ group.acronym }} {{ group.name }} {% for chair in group.chairs %}{{ chair.person.plain_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% for group in area.groups %} - + diff --git a/ietf/templates/group/group_base.html b/ietf/templates/group/group_base.html index d605d803a..ec1e0c381 100644 --- a/ietf/templates/group/group_base.html +++ b/ietf/templates/group/group_base.html @@ -66,16 +66,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- Documents | - Charter | - History - | Dependency Graph - {% if group.list_archive|startswith:"http:" or group.list_archive|startswith:"https:" or group.list_archive|startswith:"ftp:" %} - | List Archive » - {% endif %} - {% if group.has_tools_page %} - | Tools {{ group.type.name }} Page » - {% endif %} + {% for name, url in menu_entries %} + {{ name }} {% if not forloop.last %} | {% endif %} + {% endfor %}
{% if menu_actions %} diff --git a/ietf/templates/group/group_charter.html b/ietf/templates/group/group_charter.html index 1481229e3..9fb5ca5ba 100644 --- a/ietf/templates/group/group_charter.html +++ b/ietf/templates/group/group_charter.html @@ -3,11 +3,6 @@ {% load ietf_filters %} {% block group_subtitle %}Charter{% endblock %} -{% block morecss %} -{{ block.super }} -h2 a.button { margin-left: 0.5em; font-size: 13px; } -{% endblock %} - {% block group_content %}
@@ -39,6 +34,7 @@ is occasionally incorrect.
+ {% if group.features.has_chartering_process %} + {% endif %} + {% for slug, label, roles in group.personnel %} - + + {% endfor %} - {% if group.parent.type_id == "area" %} - - - - {% endif %} - - {% if group.techadvisors %} - - - - - {% endif %} - - {% if group.editors %} - - - - - {% endif %} - - {% if group.secretaries %} - - - - - {% endif %} - - {% if group.delegates %} - - - - - {% endif %} - + {% if group.list_email %} + {% endif %} {% if group.state_id != "conclude" %} @@ -152,17 +99,19 @@ is occasionally incorrect. {% endif %} {% endwith %} -

Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ long_group_type }}

+

Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ verbose_group_type }}

-

{{ group.charter_text|escape|format_charter|safe }}

+

{{ description|linebreaks }}

-

{% if group.state_id == "proposed" %}Proposed{% endif %} Milestones

+{% if group.features.has_milestones %} +

{% if group.state_id == "proposed" %}Proposed{% endif %} Milestones

-{% include "group/milestones.html" with milestones=group.milestones %} + {% include "group/milestones.html" with milestones=group.milestones %} -{% if milestones_in_review %} -

+ {{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }} -currently in {{ milestone_reviewer }} review.

+ {% if milestones_in_review %} +

+ {{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }} + currently in {{ milestone_reviewer }} review.

+ {% endif %} {% endif %} {% endblock %} diff --git a/ietf/templates/group/group_entry_with_charter.txt b/ietf/templates/group/group_entry_with_charter.txt index abde2531a..cfce907fb 100644 --- a/ietf/templates/group/group_entry_with_charter.txt +++ b/ietf/templates/group/group_entry_with_charter.txt @@ -13,7 +13,7 @@ {% for ad in group.area.ads %} {{ ad.person.plain_name }} <{{ ad.email.address }}> {% endfor %} {% if group.areadirector %} {{ group.area.name }} Advisor: - {{ group.areadirector.person.plain_name }} <{{ group.areadirector.address }}> + {{ group.areadirector.person.plain_name }} <{{ group.areadirector.email.address }}> {% endif %}{% if group.techadvisors %} Tech Advisor{{ group.techadvisors|pluralize }}: {% for techadvisor in group.techadvisors %} {{ techadvisor.person.plain_name }} <{{ techadvisor.email.address }}> From d16bdb37237ba8dafdeb0b5937e96d1e049109f7 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 22 May 2014 15:08:37 +0000 Subject: [PATCH 02/26] Add support for editing Group.description in Secretariat tools - Legacy-Id: 7767 --- ietf/secr/groups/forms.py | 2 +- ietf/secr/templates/groups/view.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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..fd2acb26b 100644 --- a/ietf/secr/templates/groups/view.html +++ b/ietf/secr/templates/groups/view.html @@ -46,6 +46,7 @@ +
{{ group.acronym }}{{ group.acronym }} {% for ad in area.ads %}{% if ad.person_id == group.ad_id %}{% endif %}{% endfor %} {{ group.name }} {% for chair in group.chairs %}{{ chair.person.plain_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
Charter: @@ -52,77 +48,28 @@ is occasionally incorrect. {% endif %}
Personnel
Chair{{ group.chairs|pluralize }}:{{ label }}: - {% for chair in group.chairs %} - {{ chair.person.plain_name }} <{{ chair.email.address }}>
+ {% for r in roles %} + {{ r.person.plain_name }} <{{ r.email.address }}>
{% endfor %}
Area Director: - {% if group.areadirector %} - {{ group.areadirector.person.plain_name }} <{{ group.areadirector.address }}> - {% else %}?{% endif %} -
Tech Advisor{{ group.techadvisors|pluralize }}: - {% for techadvisor in group.techadvisors %} - {{ techadvisor.person.plain_name }} <{{ techadvisor.email.address }}>
- {% endfor %} -
Editor{{ group.editors|pluralize }}: - {% for editor in group.editors %} - {{ editor.person.plain_name }} <{{ editor.email.address }}>
- {% endfor %} -
Secretar{{ group.secretaries|pluralize:"y,ies" }}: - {% for secretary in group.secretaries %} - {{ secretary.person.plain_name }} <{{ secretary.email.address }}>
- {% endfor %} -
Delegate{{ group.delegates|pluralize }}: - {% for delegate in group.delegates %} - {{ delegate.person.plain_name }} <{{ delegate.email.address }}>
- {% endfor %} -
Mailing List
Address:{{ group.list_email|urlize }}
To Subscribe:{{ group.list_subscribe|urlize }}
Archive:{{ group.list_archive|urlize }}
Jabber Chat
Email Subscription:{{ group.list_subscribe }}
Email Archive:{{ group.list_archive }}
Charter:View Charter
Description:{{ group.description }}
Comments:{{ group.comments }}
Last Modified Date:{{ group.time }}
From 9579525ca468df56b50b92b04ba18eddf9316d02 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 22 May 2014 15:53:20 +0000 Subject: [PATCH 03/26] Add simple materials page, tie up some of the loose ends in group generalization work - Legacy-Id: 7768 --- ietf/group/edit.py | 34 ++++++++++++++-- ietf/group/features.py | 5 ++- ietf/group/info.py | 61 +++++++++++++++++++---------- ietf/group/milestones.py | 17 +++++--- ietf/group/models.py | 7 +++- ietf/group/urls.py | 3 ++ ietf/group/urls_info.py | 3 +- ietf/group/utils.py | 12 ++++++ ietf/templates/group/materials.html | 27 +++++++++++++ 9 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 ietf/templates/group/materials.html diff --git a/ietf/group/edit.py b/ietf/group/edit.py index b77eafd62..9d5477698 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -7,7 +7,7 @@ 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 HttpResponseForbidden, Http404 from django.utils.html import mark_safe from django.http import Http404, HttpResponse from django.contrib.auth.decorators import login_required @@ -18,7 +18,8 @@ from ietf.doc.models import DocAlias, DocTagName, Document, State, save_document from ietf.doc.utils import get_tags_for_stream_id 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 (save_group_in_history, can_manage_group_type, can_manage_materials, + get_group_or_404) from ietf.ietfauth.utils import has_role from ietf.person.forms import EmailsField from ietf.person.models import Person, Email @@ -166,6 +167,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 +192,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(): @@ -330,7 +337,7 @@ class ConcludeForm(forms.Form): @login_required def conclude(request, group_type, acronym): """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): return HttpResponseForbidden("You don't have permission to access this view") @@ -357,7 +364,10 @@ def conclude(request, group_type, acronym): @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") @@ -445,3 +455,19 @@ def customize_workflow(request, group_type, acronym): 'states': states, 'tags': tags, }) + +@login_required +def upload_materials(request, acronym, group_type=None): + group = get_group_or_404(acronym, group_type) + if not group.features.has_materials: + raise Http404 + + if not can_manage_materials(request.user, group): + return HttpResponseForbidden("You don't have permission to access this view") + + # FIXME: fill in + + return render(request, 'group/materials.html', + construct_group_menu_context(request, group, "materials", group_type, { + "materials": materials, + })) diff --git a/ietf/group/features.py b/ietf/group/features.py index 532dfc06a..e97d06529 100644 --- a/ietf/group/features.py +++ b/ietf/group/features.py @@ -4,7 +4,8 @@ class GroupFeatures(object): has_milestones = False has_chartering_process = False - has_documents = False + has_documents = False # i.e. drafts/RFCs + has_materials = False customize_workflow = False default_tab = "group_charter" @@ -16,4 +17,4 @@ class GroupFeatures(object): self.customize_workflow = True self.default_tab = "group_docs" elif group.type_id in ("team",): - self.has_documents = True + self.has_materials = True diff --git a/ietf/group/info.py b/ietf/group/info.py index 22a17a2d6..90a9c633d 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -46,12 +46,13 @@ 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): @@ -256,6 +257,9 @@ def concluded_groups(request): return render(request, 'group/concluded_groups.html', dict(group_types=group_types)) +def get_group_materials(group): + return Document.objects.filter(group=group).exclude(states__slug="deleted")#.exclude(session=None).exclude(type="charter") + 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) @@ -264,11 +268,13 @@ def construct_group_menu_context(request, group, selected, group_type, others): # entries entries = [] - if group.features().has_documents: + if group.features.has_documents: entries.append(("Documents", urlreverse("ietf.group.info.group_documents", kwargs=kwargs))) entries.append(("Charter", urlreverse("ietf.group.info.group_charter", 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: + 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:"): @@ -283,14 +289,17 @@ def construct_group_menu_context(request, group, selected, group_type, others): is_chair = group.has_role(request.user, "chair") can_manage = can_manage_group_type(request.user, group.type_id) - if group.features().has_milestones: + 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.features.has_materials and can_manage_materials(request.user, group): + actions.append((u"Upload materials", urlreverse("group_upload_materials", kwargs=kwargs))) + if 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): + 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: @@ -341,23 +350,17 @@ def search_for_group_documents(group): return docs, meta, docs_related, meta_related -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) - 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)) + 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) @@ -372,6 +375,8 @@ def group_documents(request, acronym, group_type=None): def group_documents_txt(request, acronym, group_type=None): """Return tabulator-separated rows with documents for group.""" 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) @@ -393,7 +398,6 @@ def group_documents_txt(request, acronym, group_type=None): return HttpResponse(u"\n".join(rows), content_type='text/plain; charset=UTF-8') - def group_charter(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) @@ -410,10 +414,10 @@ def group_charter(request, acronym, group_type=None): can_manage = can_manage_group_type(request.user, group.type_id) - if group.features().has_chartering_process: + if group.features.has_chartering_process: description = group.charter_text else: - description = group.description or "No description entered yet." + description = group.description or "No description yet." return render(request, 'group/group_charter.html', construct_group_menu_context(request, group, "charter", group_type, { @@ -433,10 +437,21 @@ def history(request, acronym, group_type=None): return render(request, 'group/history.html', construct_group_menu_context(request, group, "history", group_type, { - "events": events, - })) - - + "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 + + materials = get_group_materials(group).order_by("-time") + + return render(request, 'group/materials.html', + construct_group_menu_context(request, group, "materials", group_type, { + "materials": materials, + })) + def nodename(name): return name.replace('-','_') @@ -556,6 +571,8 @@ def make_dot(group): 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' @@ -564,6 +581,8 @@ def dependencies_dot(request, acronym, group_type=None): @cache_page ( 60 * 60 ) def dependencies_pdf(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) + if not group.features.has_documents: + raise Http404 dothandle,dotname = mkstemp() os.close(dothandle) diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py index 6b4a0083e..44ac4423d 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -12,7 +12,8 @@ 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.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 @@ -108,17 +109,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 @@ -332,8 +335,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 e66572b37..64009012e 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -80,9 +80,12 @@ class Group(GroupInfo): def bg_color(self): return bg_group_colors[self.upcase_acronym] + @property def features(self): - from ietf.group.features import GroupFeatures - return GroupFeatures(self) + if not hasattr(self, "features_cache"): + from ietf.group.features import GroupFeatures + self.features_cache = GroupFeatures(self) + return self.features_cache def json_url(self): return "/group/%s.json" % (self.acronym,) diff --git a/ietf/group/urls.py b/ietf/group/urls.py index f3cf567b3..f68272aad 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -7,10 +7,13 @@ 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 remainder here is currently duplicated in urls_info.py, 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_charter', None, 'group_charter'), (r'^(?P[a-zA-Z0-9-._]+)/history/$', 'ietf.group.info.history'), + (r'^(?P[a-zA-Z0-9-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/upload/$', 'ietf.group.edit.upload_materials', None, "group_upload_materials"), (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'), diff --git a/ietf/group/urls_info.py b/ietf/group/urls_info.py index 0039047b5..0d838a1b3 100644 --- a/ietf/group/urls_info.py +++ b/ietf/group/urls_info.py @@ -19,7 +19,8 @@ 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-._]+)/$', 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_charter, None, 'group_charter'), (r'^(?P[a-zA-Z0-9-._]+)/history/$', info.history), (r'^(?P[a-zA-Z0-9-._]+)/deps/dot/$', info.dependencies_dot), diff --git a/ietf/group/utils.py b/ietf/group/utils.py index 89e8cd120..849802411 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -1,6 +1,7 @@ import os from django.conf import settings +from django.shortcuts import get_object_or_404 from ietf.group.models import Group, RoleHistory from ietf.person.models import Email @@ -107,3 +108,14 @@ def milestone_reviewer_for_group_type(group_type): else: return "Area Director" +def can_manage_materials(user, group): + return group.has_role(user, ("chair", "delegate", "secr")) or has_role(user, 'Secretariat') + +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/templates/group/materials.html b/ietf/templates/group/materials.html new file mode 100644 index 000000000..d56b623e3 --- /dev/null +++ b/ietf/templates/group/materials.html @@ -0,0 +1,27 @@ +{% extends "group/group_base.html" %} +{% load ietf_filters %} + +{% block group_subtitle %}Materials{% endblock %} + +{% block morecss %} + {{ block.super }} + .material { margin-top: 0.5em; } +{% endblock %} + +{% block group_content %} +{% load ietf_filters %} + +

Materials

+ + {% if materials %} + {% for d in materials %} +
+ {{ d.title }} + ({{ d.time|date:"Y-m-d" }}) +
+ {% endfor %} + {% else %} +

No materials uploaded.

+ {% endif %} + +{% endblock %} From 0bd82434c8b671ae5137b31c35e66755153e3d6f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 22 May 2014 15:55:15 +0000 Subject: [PATCH 04/26] Fix problem with milestones editing claiming that all resolved milestones have been changed when you click the review changes button (by removing work-around for bug in previous version of Django) - Legacy-Id: 7769 --- ietf/group/milestones.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py index 44ac4423d..7747b9154 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -54,7 +54,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, From 17cf1095c79f6484ad15e6af0950f69ef83504f8 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 22 May 2014 16:10:37 +0000 Subject: [PATCH 05/26] Fix cosmetic problem with label_suffix and the delete labels on the milestones page - Legacy-Id: 7770 --- ietf/group/milestones.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py index 7747b9154..fdfc0eaa5 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -26,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") @@ -40,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) From f9407c57b80a3ec0bb35ce8c13109eceeab4ffd9 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 23 May 2014 16:24:40 +0000 Subject: [PATCH 06/26] It looks like Django 1.6 finally added a span around form field help texts so take advantage of that and apply our help text style to it - Legacy-Id: 7775 --- static/css/base2.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/base2.css b/static/css/base2.css index 80e850c1a..8d5820e56 100644 --- a/static/css/base2.css +++ b/static/css/base2.css @@ -177,7 +177,7 @@ form table th { vertical-align: top; } -form table .help { +form table .help, form table .helptext { font-style: italic; font-size: 11px; } From 3bfb6dcd4320a403925c395f61297fa368430bf0 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 23 May 2014 16:26:56 +0000 Subject: [PATCH 07/26] Added support data for a tentative new material document type, add a upload/edit/revise page for uploading and revising group materials, still missing a couple of details and tests - Legacy-Id: 7776 --- .../migrations/0019_create_material_states.py | 373 ++++++++++++++++++ ietf/doc/models.py | 2 + ietf/group/edit.py | 92 ++++- ietf/group/info.py | 15 +- ietf/group/urls.py | 4 +- .../0021_create_material_doc_type.py | 190 +++++++++ ietf/settings.py | 2 + ietf/templates/group/materials.html | 33 +- 8 files changed, 693 insertions(+), 18 deletions(-) create mode 100644 ietf/doc/migrations/0019_create_material_states.py create mode 100644 ietf/name/migrations/0021_create_material_doc_type.py diff --git a/ietf/doc/migrations/0019_create_material_states.py b/ietf/doc/migrations/0019_create_material_states.py new file mode 100644 index 000000000..29bc729bf --- /dev/null +++ b/ietf/doc/migrations/0019_create_material_states.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + depends_on = ( + ("name", "0021_create_material_doc_type"), + ) + + def forwards(self, orm): + state_type, _ = orm.StateType.objects.get_or_create(slug="material", label="State") + orm.State.objects.get_or_create(slug="active", name="Active", order=1, type=state_type) + orm.State.objects.get_or_create(slug="inactive", name="Inactive", order=2, type=state_type) + + def backwards(self, orm): + orm.State.objects.filter(slug="inactive", type="material").delete() + orm.State.objects.filter(slug="active", type="material").delete() + orm.StateType.objects.filter(slug="material").delete() + + 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.ballotdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': [u'doc.DocEvent']}, + 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.BallotType']"}), + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + u'doc.ballotpositiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': [u'doc.DocEvent']}, + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), + 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['doc.BallotDocEvent']", 'null': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': u"orm['name.BallotPositionName']"}) + }, + u'doc.ballottype': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, + 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'doc.consensusdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': [u'doc.DocEvent']}, + 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + u'doc.deletedevent': { + 'Meta': {'object_name': 'DeletedEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'json': ('django.db.models.fields.TextField', [], {}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}) + }, + u'doc.docalias': { + 'Meta': {'object_name': 'DocAlias'}, + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + u'doc.docevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'DocEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'doc.dochistory': { + 'Meta': {'object_name': 'DocHistory'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_dochistory_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.DocHistoryAuthor']", 'blank': 'True'}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': u"orm['doc.Document']"}), + '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'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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'}), + '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'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.DocAlias']", 'symmetrical': 'False', 'through': u"orm['doc.RelatedDocHistory']", '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_dochistory_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.dochistoryauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocHistoryAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocHistory']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + u'doc.docreminder': { + 'Meta': {'object_name': 'DocReminder'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'due': ('django.db.models.fields.DateTimeField', [], {}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocEvent']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocReminderTypeName']"}) + }, + 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.initialreviewdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'InitialReviewDocEvent', '_ormbases': [u'doc.DocEvent']}, + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'doc.lastcalldocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'LastCallDocEvent', '_ormbases': [u'doc.DocEvent']}, + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'doc.newrevisiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'NewRevisionDocEvent', '_ormbases': [u'doc.DocEvent']}, + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16'}) + }, + u'doc.relateddochistory': { + 'Meta': {'object_name': 'RelatedDocHistory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocHistory']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reversely_related_document_history_set'", 'to': u"orm['doc.DocAlias']"}) + }, + u'doc.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocAlias']"}) + }, + 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.statedocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': [u'doc.DocEvent']}, + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.State']", 'null': 'True', 'blank': 'True'}), + 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.StateType']"}) + }, + 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'doc.telechatdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': [u'doc.DocEvent']}, + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) + }, + u'doc.writeupdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': [u'doc.DocEvent']}, + u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 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'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.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.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.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 = ['doc'] + symmetrical = True diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 5b4d5d65d..fb2e1f287 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -85,6 +85,8 @@ class DocumentInfo(models.Model): return settings.CONFLICT_REVIEW_PATH elif self.type_id == "statchg": return settings.STATUS_CHANGE_PATH + elif self.type_id == "material": + return settings.MATERIALS_PATH else: raise NotImplemented diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 9d5477698..e58793e72 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -9,12 +9,13 @@ 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.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, DocTypeName, DocEvent, State, save_document_in_history from ietf.doc.utils import get_tags_for_stream_id from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName, GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent ) @@ -456,8 +457,30 @@ def customize_workflow(request, group_type, acronym): 'tags': tags, }) +class UploadMaterialForm(forms.Form): + title = forms.CharField(max_length=Document._meta.get_field("title").max_length) + state = forms.ModelChoiceField(State.objects.filter(type="material"), empty_label=None) + material = forms.FileField(label='File', help_text="PDF or text file (ASCII/UTF-8)") + + def __init__(self, *args, **kwargs): + action = kwargs.pop("action") + doc = kwargs.pop("doc", None) + + super(UploadMaterialForm, self).__init__(*args, **kwargs) + + if action == "new": + self.fields["state"].widget = forms.HiddenInput() + self.fields["state"].queryset = self.fields["state"].queryset.filter(slug="active") + else: + self.fields["title"].initial = doc.title + self.fields["state"].initial = doc.get_state().pk if doc.get_state() else None + + if action == "edit": + del self.fields["material"] + + @login_required -def upload_materials(request, acronym, group_type=None): +def edit_material(request, acronym, action="new", name=None, group_type=None): group = get_group_or_404(acronym, group_type) if not group.features.has_materials: raise Http404 @@ -465,9 +488,64 @@ def upload_materials(request, acronym, group_type=None): if not can_manage_materials(request.user, group): return HttpResponseForbidden("You don't have permission to access this view") - # FIXME: fill in + existing = None + if name and action != "new": + existing = get_object_or_404(Document, type="material", name=name) - return render(request, 'group/materials.html', - construct_group_menu_context(request, group, "materials", group_type, { - "materials": materials, - })) + if request.method == 'POST': + form = UploadMaterialForm(request.POST, request.FILES, action=action, doc=existing) + + if form.is_valid(): + if action == "new": + d = Document() + d.type = DocTypeName.objects.get(slug="material") + d.group = group + d.rev = "01" + else: + d = existing + + d.title = form.cleaned_data["title"] + d.time = datetime.datetime.now() + + if not d.name: + d.name = "material-%s-%s" % (d.group.acronym, slugify(d.title)) + i = 2 + while True: + if not Document.objects.filter(name=d.name).exists(): + break + d.name = "material-%s-%s-%s" % (d.group.acronym, slugify(d.title), i) + i += 1 + + if "material" in form.fields: + if action != "new": + d.rev = "%02d" % (int(d.rev) + 1) + + f = form.cleaned_data["material"] + file_ext = os.path.splitext(f.name)[1] + + with open(os.path.join(d.get_file_path(), d.name + "-" + d.rev + file_ext), 'wb+') as dest: + for chunk in f.chunks(): + dest.write(chunk) + + d.save() + + # FIXME: missing edit title event + + # FIXME: missing changed state event + d.set_state(form.cleaned_data["state"]) + + # FIXME: wrong, should be new revision event + DocEvent.objects.create(doc=d, + by=request.user.person, + type='uploaded', + desc="Uploaded material") + + return redirect("group_materials", acronym=group.acronym) + else: + form = UploadMaterialForm(action=action, doc=existing) + + return render(request, 'group/edit_material.html', { + 'group': group, + 'form': form, + 'action': action, + }) diff --git a/ietf/group/info.py b/ietf/group/info.py index 90a9c633d..44fe2b3cd 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -35,6 +35,7 @@ import os import itertools from tempfile import mkstemp +import glob from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string @@ -258,7 +259,7 @@ def concluded_groups(request): dict(group_types=group_types)) def get_group_materials(group): - return Document.objects.filter(group=group).exclude(states__slug="deleted")#.exclude(session=None).exclude(type="charter") + return Document.objects.filter(group=group, type="material", session=None).exclude(states__slug="inactive") def construct_group_menu_context(request, group, selected, group_type, others): """Return context with info for the group menu filled in.""" @@ -294,9 +295,9 @@ def construct_group_menu_context(request, group, selected, group_type, others): actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs))) if group.features.has_materials and can_manage_materials(request.user, group): - actions.append((u"Upload materials", urlreverse("group_upload_materials", kwargs=kwargs))) + actions.append((u"Upload material", urlreverse("group_new_material", kwargs=kwargs))) - if group.state_id != "conclude" and can_manage: + 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): @@ -446,10 +447,18 @@ def materials(request, acronym, group_type=None): raise Http404 materials = get_group_materials(group).order_by("-time") + for d in materials: + extension = "" + globs = glob.glob(d.get_file_path() + d.name + "-" + d.rev + ".*") + if globs: + extension = os.path.splitext(globs[0])[1] + + d.full_url = d.href() + extension return render(request, 'group/materials.html', construct_group_menu_context(request, group, "materials", group_type, { "materials": materials, + "can_manage_materials": can_manage_materials(request.user, group) })) def nodename(name): diff --git a/ietf/group/urls.py b/ietf/group/urls.py index f68272aad..800dc4d3c 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -13,7 +13,9 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-._]+)/charter/$', 'ietf.group.info.group_charter', None, 'group_charter'), (r'^(?P[a-zA-Z0-9-._]+)/history/$', 'ietf.group.info.history'), (r'^(?P[a-zA-Z0-9-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/upload/$', 'ietf.group.edit.upload_materials', None, "group_upload_materials"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/new/$', 'ietf.group.edit.edit_material', { 'action': "new" }, "group_new_material"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/(?Pmaterial-[^/]+)/edit/$', 'ietf.group.edit.edit_material', { 'action': "edit" }, "group_edit_material"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/(?Pmaterial-[^/]+)/revise/$', 'ietf.group.edit.edit_material', { 'action': "revise" }, "group_revise_material"), (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'), diff --git a/ietf/name/migrations/0021_create_material_doc_type.py b/ietf/name/migrations/0021_create_material_doc_type.py new file mode 100644 index 000000000..06db0ea5b --- /dev/null +++ b/ietf/name/migrations/0021_create_material_doc_type.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + def forwards(self, orm): + orm.DocTypeName.objects.get_or_create(slug="material", name="Material") + + def backwards(self, orm): + orm.DocTypeName.objects.filter(slug="material").delete() + + 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/settings.py b/ietf/settings.py index 837f0f34e..00c8d6a7b 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -265,6 +265,7 @@ AGENDA_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/agenda/%(wg)s.%(ext) MINUTES_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/minutes/%(wg)s.%(ext)s' SLIDES_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/slides/%(wg)s-*' IPR_DOCUMENT_PATH = '/a/www/ietf-ftp/ietf/IPR/' +MATERIALS_PATH = '/a/www/ietf-ftp/materials/' IESG_TASK_FILE = '/a/www/www6/iesg/internal/task.txt' IESG_ROLL_CALL_FILE = '/a/www/www6/iesg/internal/rollcall.txt' IESG_MINUTES_FILE = '/a/www/www6/iesg/internal/minutes.txt' @@ -285,6 +286,7 @@ DOC_HREFS = { #"liaison": None "minutes": "http://www.ietf.org/proceedings/{meeting}/minutes/{doc.external_url}", "slides": "http://www.ietf.org/proceedings/{meeting}/slides/{doc.external_url}", + "material": "http://www.ietf.org/material/{doc.name}-{doc.rev}", } # Override this in settings_local.py if needed diff --git a/ietf/templates/group/materials.html b/ietf/templates/group/materials.html index d56b623e3..d80251781 100644 --- a/ietf/templates/group/materials.html +++ b/ietf/templates/group/materials.html @@ -5,7 +5,7 @@ {% block morecss %} {{ block.super }} - .material { margin-top: 0.5em; } + .materials .edit-options { float: right; margin-left: 2em; font-style: italic; } {% endblock %} {% block group_content %} @@ -14,12 +14,31 @@

Materials

{% if materials %} - {% for d in materials %} -
- {{ d.title }} - ({{ d.time|date:"Y-m-d" }}) -
- {% endfor %} + + + + + + + + + {% for d in materials %} + + + + + + {% endfor %} +
TitleRev.Date
+ {% if can_manage_materials %} + + edit | + revise + + {% endif %} + + {{ d.title }} + {{ d.rev }}{{ d.time|date:"Y-m-d" }}
{% else %}

No materials uploaded.

{% endif %} From 3c88dc127b3709863b32fd640350a18e3219da23 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 23 May 2014 16:27:24 +0000 Subject: [PATCH 08/26] Add the template for the material upload page - Legacy-Id: 7777 --- ietf/templates/group/edit_material.html | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 ietf/templates/group/edit_material.html diff --git a/ietf/templates/group/edit_material.html b/ietf/templates/group/edit_material.html new file mode 100644 index 000000000..29e7e768c --- /dev/null +++ b/ietf/templates/group/edit_material.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}Upload Material for Group {{ group.acronym }}{% endblock %} + +{% block morecss %} +{{ block.super }} +form.upload-material td { padding-bottom: 0.6em; } +form.upload-material #id_title { width: 30em; } +form.upload-material .submit-row td { padding-top: 1em; text-align: right; } +{% endblock %} + +{% block content %} +{% load ietf_filters %} + +
+ +

Upload Material for Group {{ group.acronym }}

+ +{% if action == "new" %} +

+ Below you can upload a file for the group + {{ group.acronym }}. + The file will appear under the materials tab in the group pages. +

+{% elif action == "edit" %} +

+ Below you can edit the details of the material for the group + {{ group.acronym }}. +

+{% elif action == "revise" %} +

+ Below you can upload a new revision of the material for the group + {{ group.acronym }}. +

+{% endif %} + +

Upload

+ +
{% csrf_token %} + + {{ form.as_table }} + + + + +
+ Cancel + +
+
+ +{% endblock content %} From 47c9ccf0e116325753ce448be9213fd2d9d1ab8b Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 26 May 2014 15:40:07 +0000 Subject: [PATCH 09/26] Handle the case of asking for get_state() on an unsaved document - Legacy-Id: 7779 --- ietf/doc/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index fb2e1f287..443ea3bf0 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -122,6 +122,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 From 252db0dffbd153ef2892a963d5c23e893ee2f9e7 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 26 May 2014 15:40:51 +0000 Subject: [PATCH 10/26] Fix some bugs, add correct document events in materials upload - Legacy-Id: 7780 --- ietf/group/edit.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/ietf/group/edit.py b/ietf/group/edit.py index e58793e72..33b9f3a3e 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -15,8 +15,9 @@ from django.contrib.auth.decorators import login_required import debug # pyflakes:ignore -from ietf.doc.models import Document, DocAlias, DocTagName, DocTypeName, DocEvent, State, save_document_in_history -from ietf.doc.utils import get_tags_for_stream_id +from ietf.doc.models import Document, DocAlias, DocTagName, DocTypeName, DocEvent, State +from ietf.doc.models import NewRevisionDocEvent, save_document_in_history +from ietf.doc.utils import get_tags_for_stream_id, add_state_change_event 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, can_manage_materials, @@ -471,6 +472,7 @@ class UploadMaterialForm(forms.Form): 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 else: self.fields["title"].initial = doc.title self.fields["state"].initial = doc.get_state().pk if doc.get_state() else None @@ -504,6 +506,10 @@ def edit_material(request, acronym, action="new", name=None, group_type=None): else: d = existing + prev_rev = d.rev + prev_title = d.title + prev_state = d.get_state() + d.title = form.cleaned_data["title"] d.time = datetime.datetime.now() @@ -529,16 +535,23 @@ def edit_material(request, acronym, action="new", name=None, group_type=None): d.save() - # FIXME: missing edit title event + if not existing or prev_rev != d.rev: + e = NewRevisionDocEvent(type="new_revision", doc=d, rev=d.rev) + e.time = d.time + e.by = request.user.person + e.desc = "New version available: %s-%s" % (d.name, d.rev) + e.save() + + if prev_title != d.title: + e = DocEvent(doc=d, by=request.user.person, type='changed_document') + e.desc = u"Changed title to %s" % d.title + if prev_title: + e.desc += u" from %s" % prev_title + e.time = d.time + e.save() - # FIXME: missing changed state event d.set_state(form.cleaned_data["state"]) - - # FIXME: wrong, should be new revision event - DocEvent.objects.create(doc=d, - by=request.user.person, - type='uploaded', - desc="Uploaded material") + add_state_change_event(d, request.user.person, prev_state, form.cleaned_data["state"]) return redirect("group_materials", acronym=group.acronym) else: From 97e6cebe911d9202734eca60ae8a3791e1ef9617 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 28 May 2014 15:58:03 +0000 Subject: [PATCH 11/26] Don't show charter for group types without chartering processes - Legacy-Id: 7795 --- ietf/secr/templates/groups/view.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ietf/secr/templates/groups/view.html b/ietf/secr/templates/groups/view.html index fd2acb26b..7c581400c 100644 --- a/ietf/secr/templates/groups/view.html +++ b/ietf/secr/templates/groups/view.html @@ -45,8 +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 }} From 544cd702bba57b64473a3351eaa014ff1385151d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 30 May 2014 14:07:16 +0000 Subject: [PATCH 12/26] Drop the material doc type, instead present a choice of doc types for uploading materials (currently only slides) and use that, introduce a DOCUMENT_PATH_PATTERN setting for storing the files and only look in proceedings/ for documents that are actually meeting related - Legacy-Id: 7802 --- .../migrations/0019_create_material_states.py | 373 ------------------ ietf/doc/models.py | 24 +- ietf/group/edit.py | 41 +- ietf/group/features.py | 1 + ietf/group/info.py | 18 +- ietf/group/urls.py | 12 +- ietf/group/utils.py | 1 - .../0021_create_material_doc_type.py | 190 --------- ietf/settings.py | 9 +- .../templates/group/choose_material_type.html | 23 ++ ietf/templates/group/edit_material.html | 2 - ietf/templates/group/materials.html | 56 +-- 12 files changed, 124 insertions(+), 626 deletions(-) delete mode 100644 ietf/doc/migrations/0019_create_material_states.py delete mode 100644 ietf/name/migrations/0021_create_material_doc_type.py create mode 100644 ietf/templates/group/choose_material_type.html diff --git a/ietf/doc/migrations/0019_create_material_states.py b/ietf/doc/migrations/0019_create_material_states.py deleted file mode 100644 index 29bc729bf..000000000 --- a/ietf/doc/migrations/0019_create_material_states.py +++ /dev/null @@ -1,373 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import DataMigration -from django.db import models - -class Migration(DataMigration): - depends_on = ( - ("name", "0021_create_material_doc_type"), - ) - - def forwards(self, orm): - state_type, _ = orm.StateType.objects.get_or_create(slug="material", label="State") - orm.State.objects.get_or_create(slug="active", name="Active", order=1, type=state_type) - orm.State.objects.get_or_create(slug="inactive", name="Inactive", order=2, type=state_type) - - def backwards(self, orm): - orm.State.objects.filter(slug="inactive", type="material").delete() - orm.State.objects.filter(slug="active", type="material").delete() - orm.StateType.objects.filter(slug="material").delete() - - 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.ballotdocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': [u'doc.DocEvent']}, - 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.BallotType']"}), - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'doc.ballotpositiondocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': [u'doc.DocEvent']}, - 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), - 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['doc.BallotDocEvent']", 'null': 'True'}), - 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': u"orm['name.BallotPositionName']"}) - }, - u'doc.ballottype': { - 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, - 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), - 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), - 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) - }, - u'doc.consensusdocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': [u'doc.DocEvent']}, - 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'doc.deletedevent': { - 'Meta': {'object_name': 'DeletedEvent'}, - 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'json': ('django.db.models.fields.TextField', [], {}), - 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}) - }, - u'doc.docalias': { - 'Meta': {'object_name': 'DocAlias'}, - 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) - }, - u'doc.docevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'DocEvent'}, - 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), - 'desc': ('django.db.models.fields.TextField', [], {}), - 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), - 'type': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'doc.dochistory': { - 'Meta': {'object_name': 'DocHistory'}, - 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_dochistory_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.DocHistoryAuthor']", 'blank': 'True'}), - 'doc': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': u"orm['doc.Document']"}), - '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'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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'}), - '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'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.DocAlias']", 'symmetrical': 'False', 'through': u"orm['doc.RelatedDocHistory']", '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_dochistory_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.dochistoryauthor': { - 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocHistoryAuthor'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}), - 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocHistory']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'order': ('django.db.models.fields.IntegerField', [], {}) - }, - u'doc.docreminder': { - 'Meta': {'object_name': 'DocReminder'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'due': ('django.db.models.fields.DateTimeField', [], {}), - 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocEvent']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocReminderTypeName']"}) - }, - 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.initialreviewdocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'InitialReviewDocEvent', '_ormbases': [u'doc.DocEvent']}, - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'doc.lastcalldocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'LastCallDocEvent', '_ormbases': [u'doc.DocEvent']}, - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'doc.newrevisiondocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'NewRevisionDocEvent', '_ormbases': [u'doc.DocEvent']}, - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16'}) - }, - u'doc.relateddochistory': { - 'Meta': {'object_name': 'RelatedDocHistory'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocRelationshipName']"}), - 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocHistory']"}), - 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reversely_related_document_history_set'", 'to': u"orm['doc.DocAlias']"}) - }, - u'doc.relateddocument': { - 'Meta': {'object_name': 'RelatedDocument'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocRelationshipName']"}), - 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), - 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.DocAlias']"}) - }, - 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.statedocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': [u'doc.DocEvent']}, - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.State']", 'null': 'True', 'blank': 'True'}), - 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.StateType']"}) - }, - 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'doc.telechatdocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': [u'doc.DocEvent']}, - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) - }, - u'doc.writeupdocevent': { - 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': [u'doc.DocEvent']}, - u'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), - 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) - }, - 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'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.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.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.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 = ['doc'] - symmetrical = True diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 443ea3bf0..e70af5db4 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": @@ -85,21 +86,27 @@ class DocumentInfo(models.Model): return settings.CONFLICT_REVIEW_PATH elif self.type_id == "statchg": return settings.STATUS_CHANGE_PATH - elif self.type_id == "material": - return settings.MATERIALS_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): @@ -163,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())) + class Meta: abstract = True diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 33b9f3a3e..620aab91c 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -20,8 +20,8 @@ from ietf.doc.models import NewRevisionDocEvent, save_document_in_history from ietf.doc.utils import get_tags_for_stream_id, add_state_change_event 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, can_manage_materials, - get_group_or_404) +from ietf.group.utils import save_group_in_history, can_manage_group_type, can_manage_materials +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 @@ -458,17 +458,28 @@ def customize_workflow(request, group_type, acronym): 'tags': tags, }) +@login_required +def choose_material_type(request, acronym, group_type=None): + group = get_group_or_404(acronym, group_type) + if not group.features.has_materials: + raise Http404 + + return render(request, 'group/choose_material_type.html', { + 'group': group, + 'material_types': DocTypeName.objects.filter(slug__in=group.features.material_types), + }) + + class UploadMaterialForm(forms.Form): title = forms.CharField(max_length=Document._meta.get_field("title").max_length) - state = forms.ModelChoiceField(State.objects.filter(type="material"), empty_label=None) + 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, *args, **kwargs): - action = kwargs.pop("action") - doc = kwargs.pop("doc", None) - + def __init__(self, doc_type, action, doc, *args, **kwargs): super(UploadMaterialForm, self).__init__(*args, **kwargs) + self.fields["state"].queryset = self.fields["state"].queryset.filter(type=doc_type) + if action == "new": self.fields["state"].widget = forms.HiddenInput() self.fields["state"].queryset = self.fields["state"].queryset.filter(slug="active") @@ -482,7 +493,7 @@ class UploadMaterialForm(forms.Form): @login_required -def edit_material(request, acronym, action="new", name=None, group_type=None): +def edit_material(request, acronym, action="new", name=None, doc_type=None, group_type=None): group = get_group_or_404(acronym, group_type) if not group.features.has_materials: raise Http404 @@ -490,19 +501,21 @@ def edit_material(request, acronym, action="new", name=None, group_type=None): if not can_manage_materials(request.user, group): return HttpResponseForbidden("You don't have permission to access this view") + document_type = get_object_or_404(DocTypeName, slug=doc_type) + existing = None if name and action != "new": existing = get_object_or_404(Document, type="material", name=name) if request.method == 'POST': - form = UploadMaterialForm(request.POST, request.FILES, action=action, doc=existing) + form = UploadMaterialForm(document_type, action, existing, request.POST, request.FILES) if form.is_valid(): if action == "new": d = Document() - d.type = DocTypeName.objects.get(slug="material") + d.type = document_type d.group = group - d.rev = "01" + d.rev = "00" else: d = existing @@ -514,12 +527,12 @@ def edit_material(request, acronym, action="new", name=None, group_type=None): d.time = datetime.datetime.now() if not d.name: - d.name = "material-%s-%s" % (d.group.acronym, slugify(d.title)) + d.name = "%s-%s-%s" % (d.type_id, d.group.acronym, slugify(d.title)) i = 2 while True: if not Document.objects.filter(name=d.name).exists(): break - d.name = "material-%s-%s-%s" % (d.group.acronym, slugify(d.title), i) + d.name = "%s-%s-%s-%s" % (d.type_id, d.group.acronym, slugify(d.title), i) i += 1 if "material" in form.fields: @@ -555,7 +568,7 @@ def edit_material(request, acronym, action="new", name=None, group_type=None): return redirect("group_materials", acronym=group.acronym) else: - form = UploadMaterialForm(action=action, doc=existing) + form = UploadMaterialForm(document_type, action, existing) return render(request, 'group/edit_material.html', { 'group': group, diff --git a/ietf/group/features.py b/ietf/group/features.py index e97d06529..3ab67a5f9 100644 --- a/ietf/group/features.py +++ b/ietf/group/features.py @@ -8,6 +8,7 @@ class GroupFeatures(object): has_materials = False customize_workflow = False default_tab = "group_charter" + material_types = ["slides"] def __init__(self, group): if group.type_id in ("wg", "rg"): diff --git a/ietf/group/info.py b/ietf/group/info.py index 44fe2b3cd..4c237cb3f 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -36,6 +36,7 @@ import os import itertools from tempfile import mkstemp import glob +from collections import OrderedDict from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string @@ -259,7 +260,7 @@ def concluded_groups(request): dict(group_types=group_types)) def get_group_materials(group): - return Document.objects.filter(group=group, type="material", session=None).exclude(states__slug="inactive") + return Document.objects.filter(group=group, type__in=group.features.material_types, session=None).exclude(states__slug="inactive") def construct_group_menu_context(request, group, selected, group_type, others): """Return context with info for the group menu filled in.""" @@ -267,7 +268,7 @@ def construct_group_menu_context(request, group, selected, group_type, others): if group_type: kwargs["group_type"] = group_type - # entries + # menu entries entries = [] if group.features.has_documents: entries.append(("Documents", urlreverse("ietf.group.info.group_documents", kwargs=kwargs))) @@ -295,7 +296,7 @@ def construct_group_menu_context(request, group, selected, group_type, others): actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs))) if group.features.has_materials and can_manage_materials(request.user, group): - actions.append((u"Upload material", urlreverse("group_new_material", kwargs=kwargs))) + actions.append((u"Upload material", urlreverse("group_choose_material_type", kwargs=kwargs))) 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))) @@ -446,8 +447,9 @@ def materials(request, acronym, group_type=None): if not group.features.has_materials: raise Http404 - materials = get_group_materials(group).order_by("-time") - for d in materials: + docs = get_group_materials(group).order_by("type", "-time").select_related("type") + doc_types = OrderedDict() + for d in docs: extension = "" globs = glob.glob(d.get_file_path() + d.name + "-" + d.rev + ".*") if globs: @@ -455,9 +457,13 @@ def materials(request, acronym, group_type=None): d.full_url = d.href() + extension + 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, { - "materials": materials, + "doc_types": doc_types.items(), "can_manage_materials": can_manage_materials(request.user, group) })) diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 800dc4d3c..7ea9150a8 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -7,15 +7,11 @@ 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 remainder here is currently duplicated in urls_info.py, need to unify these at some point + # FIXME: the things below are duplicated in urls_info.py, 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_charter', None, 'group_charter'), (r'^(?P[a-zA-Z0-9-._]+)/history/$', 'ietf.group.info.history'), - (r'^(?P[a-zA-Z0-9-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/new/$', 'ietf.group.edit.edit_material', { 'action': "new" }, "group_new_material"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/(?Pmaterial-[^/]+)/edit/$', 'ietf.group.edit.edit_material', { 'action': "edit" }, "group_edit_material"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/(?Pmaterial-[^/]+)/revise/$', 'ietf.group.edit.edit_material', { 'action': "revise" }, "group_revise_material"), (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'), @@ -26,6 +22,12 @@ urlpatterns = patterns('', (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-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/new/$', 'ietf.group.edit.choose_material_type', None, "group_choose_material_type"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/new/(?P[\w-]+)/$', 'ietf.group.edit.edit_material', { 'action': "new" }, "group_new_material"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/(?P[^/]+)/edit/$', 'ietf.group.edit.edit_material', { 'action': "edit" }, "group_edit_material"), + (r'^(?P[a-zA-Z0-9-._]+)/materials/(?P[^/]+)/revise/$', 'ietf.group.edit.edit_material', { 'action': "revise" }, "group_revise_material"), ) diff --git a/ietf/group/utils.py b/ietf/group/utils.py index 849802411..c6a96a03d 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -118,4 +118,3 @@ def get_group_or_404(acronym, 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/0021_create_material_doc_type.py b/ietf/name/migrations/0021_create_material_doc_type.py deleted file mode 100644 index 06db0ea5b..000000000 --- a/ietf/name/migrations/0021_create_material_doc_type.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import DataMigration -from django.db import models - -class Migration(DataMigration): - def forwards(self, orm): - orm.DocTypeName.objects.get_or_create(slug="material", name="Material") - - def backwards(self, orm): - orm.DocTypeName.objects.filter(slug="material").delete() - - 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/settings.py b/ietf/settings.py index 00c8d6a7b..3321622ee 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -251,6 +251,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/' @@ -265,7 +266,6 @@ AGENDA_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/agenda/%(wg)s.%(ext) MINUTES_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/minutes/%(wg)s.%(ext)s' SLIDES_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/slides/%(wg)s-*' IPR_DOCUMENT_PATH = '/a/www/ietf-ftp/ietf/IPR/' -MATERIALS_PATH = '/a/www/ietf-ftp/materials/' IESG_TASK_FILE = '/a/www/www6/iesg/internal/task.txt' IESG_ROLL_CALL_FILE = '/a/www/www6/iesg/internal/rollcall.txt' IESG_MINUTES_FILE = '/a/www/www6/iesg/internal/minutes.txt' @@ -275,7 +275,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}/", @@ -284,9 +283,13 @@ DOC_HREFS = { # who understands this better can take care of it. #"liai-att": None #"liaison": None + "slides": 'http://www.ietf.org/slides/', +} + +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}", - "material": "http://www.ietf.org/material/{doc.name}-{doc.rev}", } # Override this in settings_local.py if needed diff --git a/ietf/templates/group/choose_material_type.html b/ietf/templates/group/choose_material_type.html new file mode 100644 index 000000000..e99604630 --- /dev/null +++ b/ietf/templates/group/choose_material_type.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}Upload Material for Group {{ group.acronym }}{% endblock %} + +{% block morecss %} +{{ block.super }} +.material-types li { margin-bottom: 0.5em; } +{% endblock %} + +{% block content %} +{% load ietf_filters %} + +

Upload Material for Group {{ group.acronym }}

+ +

Select what kind of material you wish to upload:

+ +
    + {% for t in material_types %} +
  • {{ t.name }}
  • + {% endfor %} +
+ +{% endblock content %} diff --git a/ietf/templates/group/edit_material.html b/ietf/templates/group/edit_material.html index 29e7e768c..fed4e6f51 100644 --- a/ietf/templates/group/edit_material.html +++ b/ietf/templates/group/edit_material.html @@ -12,8 +12,6 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; } {% block content %} {% load ietf_filters %} -
-

Upload Material for Group {{ group.acronym }}

{% if action == "new" %} diff --git a/ietf/templates/group/materials.html b/ietf/templates/group/materials.html index d80251781..4287e1ff0 100644 --- a/ietf/templates/group/materials.html +++ b/ietf/templates/group/materials.html @@ -9,37 +9,41 @@ {% endblock %} {% block group_content %} -{% load ietf_filters %} + {% load ietf_filters %} -

Materials

+ {% if doc_types %} + {% for doc_type, docs in doc_types %} +

{{ doc_type.name }}

- {% if materials %} - +
- - - - - - - {% for d in materials %} - - - - + + + + - {% endfor %} -
TitleRev.Date
- {% if can_manage_materials %} - - edit | - revise - - {% endif %} - - {{ d.title }} - {{ d.rev }}{{ d.time|date:"Y-m-d" }}
TitleRev.Date
+ + {% for d in docs %} + + + {% if can_manage_materials %} + + edit | + revise + + {% endif %} + + {{ d.title }} + + {{ d.rev }} + {{ d.time|date:"Y-m-d" }} + + {% endfor %} + + {% endfor %} {% else %} +

Materials

+

No materials uploaded.

{% endif %} From 976d0b9d685d5fe34a406db4113646fd1a9c5de8 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 30 May 2014 15:59:02 +0000 Subject: [PATCH 13/26] Turn charter page for non-chartering groups into about page, fix a bunch of potentially broken links due to the split between group-type prefixed URLs and /group/ ones - Legacy-Id: 7805 --- ietf/group/edit.py | 19 ++++++++------- ietf/group/features.py | 6 ++++- ietf/group/feeds.py | 4 ++-- ietf/group/info.py | 24 ++++++------------- ietf/group/mails.py | 8 +++---- ietf/group/milestones.py | 4 ++-- ietf/group/models.py | 8 +++++++ ietf/group/tests_info.py | 2 +- ietf/group/urls.py | 3 ++- ietf/group/urls_info.py | 2 +- ietf/templates/doc/document_charter.html | 2 +- ietf/templates/group/bofs.html | 2 +- ietf/templates/group/chartering_groups.html | 2 +- ietf/templates/group/conclude.html | 2 +- ietf/templates/group/concluded_groups.html | 2 +- ietf/templates/group/customize_workflow.html | 2 +- ietf/templates/group/edit.html | 2 +- ietf/templates/group/edit_milestones.html | 4 ++-- .../{group_charter.html => group_about.html} | 11 +++++++-- 19 files changed, 60 insertions(+), 49 deletions(-) rename ietf/templates/group/{group_charter.html => group_about.html} (93%) diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 620aab91c..5cdcdb08a 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -7,10 +7,9 @@ import shutil from django import forms from django.shortcuts import render, get_object_or_404, redirect -from django.http import HttpResponseForbidden, Http404 +from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect from django.utils.html import mark_safe from django.utils.text import slugify -from django.http import Http404, HttpResponse from django.contrib.auth.decorators import login_required import debug # pyflakes:ignore @@ -304,7 +303,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, @@ -337,11 +336,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_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': @@ -356,13 +355,15 @@ 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): diff --git a/ietf/group/features.py b/ietf/group/features.py index 3ab67a5f9..f2e0b2293 100644 --- a/ietf/group/features.py +++ b/ietf/group/features.py @@ -7,7 +7,8 @@ class GroupFeatures(object): has_documents = False # i.e. drafts/RFCs has_materials = False customize_workflow = False - default_tab = "group_charter" + about_page = "group_about" + default_tab = about_page material_types = ["slides"] def __init__(self, group): @@ -19,3 +20,6 @@ class GroupFeatures(object): self.default_tab = "group_docs" elif group.type_id in ("team",): self.has_materials = True + + if self.has_chartering_process: + about_page = "group_charter" diff --git a/ietf/group/feeds.py b/ietf/group/feeds.py index e54c6a2bf..2a09314e4 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 group.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 group.about_url() def item_pubdate(self, obj): return obj.time diff --git a/ietf/group/info.py b/ietf/group/info.py index 4c237cb3f..d194a20e6 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -272,7 +272,10 @@ def construct_group_menu_context(request, group, selected, group_type, others): entries = [] if group.features.has_documents: entries.append(("Documents", urlreverse("ietf.group.info.group_documents", kwargs=kwargs))) - entries.append(("Charter", urlreverse("ietf.group.info.group_charter", 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))) @@ -400,7 +403,7 @@ def group_documents_txt(request, acronym, group_type=None): return HttpResponse(u"\n".join(rows), content_type='text/plain; charset=UTF-8') -def group_charter(request, acronym, group_type=None): +def group_about(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) fill_in_charter_info(group) @@ -408,27 +411,14 @@ def group_charter(request, acronym, group_type=None): e = group.latest_event(type__in=("changed_state", "requested_close",)) requested_close = group.state_id != "conclude" and e and e.type == "requested_close" - verbose_group_types = { - "wg": "Working Group", - "rg": "Research Group", - "team": "Team", - } - can_manage = can_manage_group_type(request.user, group.type_id) - if group.features.has_chartering_process: - description = group.charter_text - else: - description = group.description or "No description yet." - - return render(request, 'group/group_charter.html', - construct_group_menu_context(request, group, "charter", group_type, { + 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, - "verbose_group_type": verbose_group_types.get(group.type_id, "Group"), "can_manage": can_manage, - "description": description, })) diff --git a/ietf/group/mails.py b/ietf/group/mails.py index 4a2cbdd26..1fc7e8b84 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -22,7 +22,7 @@ def email_secretariat(request, group, subject, text): "group/email_secretariat.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)), ) ) @@ -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/milestones.py b/ietf/group/milestones.py index fdfc0eaa5..a82e1a637 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -5,7 +5,7 @@ import calendar import json from django import forms -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest +from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required @@ -311,7 +311,7 @@ def edit_milestones(request, acronym, group_type=None, 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)) diff --git a/ietf/group/models.py b/ietf/group/models.py index 64009012e..c69a1d458 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -36,6 +36,14 @@ class GroupInfo(models.Model): res += " %s (%s)" % (self.type, self.acronym) return res + 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..e04433ffc 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) diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 7ea9150a8..4afb28fc9 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -10,7 +10,7 @@ urlpatterns = patterns('', # FIXME: the things below are duplicated in urls_info.py, 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_charter', None, 'group_charter'), + (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'), @@ -23,6 +23,7 @@ urlpatterns = patterns('', (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.group.edit.choose_material_type', None, "group_choose_material_type"), (r'^(?P[a-zA-Z0-9-._]+)/materials/new/(?P[\w-]+)/$', 'ietf.group.edit.edit_material', { 'action': "new" }, "group_new_material"), diff --git a/ietf/group/urls_info.py b/ietf/group/urls_info.py index 0d838a1b3..ba5d2ab1f 100644 --- a/ietf/group/urls_info.py +++ b/ietf/group/urls_info.py @@ -21,7 +21,7 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-._]+)/documents/txt/$', info.group_documents_txt), (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_charter, None, 'group_charter'), + (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/templates/doc/document_charter.html b/ietf/templates/doc/document_charter.html index a39f26226..ed9f119f3 100644 --- a/ietf/templates/doc/document_charter.html +++ b/ietf/templates/doc/document_charter.html @@ -25,7 +25,7 @@ {% if snapshot %}Snapshot of{% endif %} {% if doc.get_state_slug != "approved" %}Proposed{% endif %} Charter for "{{ group.name }}" - ({{ group.acronym }}) {{ group.type.name }} + ({{ group.acronym }}) {{ group.type.name }}
diff --git a/ietf/templates/group/bofs.html b/ietf/templates/group/bofs.html index 04fafa646..4ead7bb54 100644 --- a/ietf/templates/group/bofs.html +++ b/ietf/templates/group/bofs.html @@ -25,7 +25,7 @@ {% for g in groups %} diff --git a/ietf/templates/group/concluded_groups.html b/ietf/templates/group/concluded_groups.html index e309a388c..f5d7f1d0b 100644 --- a/ietf/templates/group/concluded_groups.html +++ b/ietf/templates/group/concluded_groups.html @@ -25,7 +25,7 @@ {% for g in t.concluded_groups %}
- {{ g.acronym }} + {{ g.acronym }} {{ g.name }} diff --git a/ietf/templates/group/chartering_groups.html b/ietf/templates/group/chartering_groups.html index 6218c1073..a7ebb298b 100644 --- a/ietf/templates/group/chartering_groups.html +++ b/ietf/templates/group/chartering_groups.html @@ -33,7 +33,7 @@ {% for g in t.chartering_groups %}
- {{ g.acronym }} + {{ g.acronym }} {{ g.name }} diff --git a/ietf/templates/group/conclude.html b/ietf/templates/group/conclude.html index 705dd4989..872e6bea3 100644 --- a/ietf/templates/group/conclude.html +++ b/ietf/templates/group/conclude.html @@ -29,7 +29,7 @@ form.conclude .actions { {{ form.as_table }}
- Cancel + Cancel
- {{ g.acronym }} + {{ g.acronym }} {{ g.name }} ({% if g.start_date %}{{ g.start_date|date:"M. Y" }}{% else %}?{% endif %} diff --git a/ietf/templates/group/customize_workflow.html b/ietf/templates/group/customize_workflow.html index 533690b09..56471d3dd 100644 --- a/ietf/templates/group/customize_workflow.html +++ b/ietf/templates/group/customize_workflow.html @@ -34,7 +34,7 @@

Customize Workflow for {{ group.acronym }} {{ group.type.name }}

Below you can customize the draft states and tags used in the -{{ group.acronym }} {{ group.type.name }}. Note that some states are +{{ group.acronym }} {{ group.type.name }}. Note that some states are mandatory for group operation and cannot be deactivated.

{% if group.type_id == "wg" %} diff --git a/ietf/templates/group/edit.html b/ietf/templates/group/edit.html index 2cadc8910..1445382d2 100644 --- a/ietf/templates/group/edit.html +++ b/ietf/templates/group/edit.html @@ -69,7 +69,7 @@ so. New accounts can be created here.
{% if action == "edit" %} - Cancel + Cancel {% else %} {% if action == "charter" %} diff --git a/ietf/templates/group/edit_milestones.html b/ietf/templates/group/edit_milestones.html index 045821194..f129e9bb5 100644 --- a/ietf/templates/group/edit_milestones.html +++ b/ietf/templates/group/edit_milestones.html @@ -42,7 +42,7 @@ tr.milestone.add { font-style: italic; }

Links: - {{ group.acronym }} {{ group.type.name }} + {{ group.acronym }} {{ group.type.name }} - {{ group.charter.canonical_name }}

@@ -90,7 +90,7 @@ this list to the milestones currently in use for the {{ group.acronym }} {{
diff --git a/ietf/templates/group/group_charter.html b/ietf/templates/group/group_about.html similarity index 93% rename from ietf/templates/group/group_charter.html rename to ietf/templates/group/group_about.html index 9fb5ca5ba..4e1ef4bb6 100644 --- a/ietf/templates/group/group_charter.html +++ b/ietf/templates/group/group_about.html @@ -99,9 +99,16 @@ is occasionally incorrect. {% endif %} {% endwith %} -

Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ verbose_group_type }}

+{% if group.features.has_chartering_process %} +

Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ group.type.desc.title }}

+ +

{{ group.charter_text|linebreaks }}

+{% else %} +

About

+ +

{{ group.description|default:"No description yet."|linebreaks }}

+{% endif %} -

{{ description|linebreaks }}

{% if group.features.has_milestones %}

{% if group.state_id == "proposed" %}Proposed{% endif %} Milestones

From 63dabff77902bbfc99f1a93f2803137e9c4ff5db Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 30 May 2014 16:17:21 +0000 Subject: [PATCH 14/26] Fix some bugs in previous commit - Legacy-Id: 7806 --- ietf/group/features.py | 2 +- ietf/group/models.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ietf/group/features.py b/ietf/group/features.py index f2e0b2293..98b5ba2da 100644 --- a/ietf/group/features.py +++ b/ietf/group/features.py @@ -22,4 +22,4 @@ class GroupFeatures(object): self.has_materials = True if self.has_chartering_process: - about_page = "group_charter" + self.about_page = "group_charter" diff --git a/ietf/group/models.py b/ietf/group/models.py index c69a1d458..efa4d871a 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -36,6 +36,13 @@ 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 @@ -88,13 +95,6 @@ class Group(GroupInfo): def bg_color(self): return bg_group_colors[self.upcase_acronym] - @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 json_url(self): return "/group/%s.json" % (self.acronym,) From 8aede5ebcd38dc1c6f123258a4098570e51f40ad Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Sat, 31 May 2014 14:14:47 +0000 Subject: [PATCH 15/26] Move the additional group URLs to info box - Legacy-Id: 7807 --- ietf/templates/group/group_about.html | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ietf/templates/group/group_about.html b/ietf/templates/group/group_about.html index 4e1ef4bb6..6f8265b37 100644 --- a/ietf/templates/group/group_about.html +++ b/ietf/templates/group/group_about.html @@ -50,6 +50,19 @@ is occasionally incorrect. {% endif %} + {% with group.groupurl_set.all as urls %} + {% if urls %} + + More info: + + {% for url in urls %} + {{ url.name }}{% if not forloop.last %}
{% endif %} + {% endfor %} + + + {% endif %} + {% endwith %} + Personnel {% for slug, label, roles in group.personnel %} @@ -89,16 +102,6 @@ is occasionally incorrect.
-{% with group.groupurl_set.all as urls %} -{% if urls %} -

In addition to the charter, there is additional information about this group on the Web at: - {% for url in urls %} - {{ url.name }}{% if not forloop.last %}, {% endif %} - {% endfor %} -

-{% endif %} -{% endwith %} - {% if group.features.has_chartering_process %}

Charter for {% if group.state_id == "proposed" %}Proposed{% endif %} {{ group.type.desc.title }}

From 7df4de78e3198ff2fe3cfa0d1ada49a1b0a70759 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Sat, 31 May 2014 14:16:59 +0000 Subject: [PATCH 16/26] Throw a validation error when uploading a material with an already used name, asking uploader to either choose a new title or revise the previous material - Legacy-Id: 7808 --- ietf/group/edit.py | 45 ++++++++++++++++--------- ietf/group/info.py | 2 +- ietf/templates/group/edit_material.html | 2 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 5cdcdb08a..67fcbf95b 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -11,6 +11,7 @@ from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpRespon 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 @@ -469,18 +470,24 @@ def choose_material_type(request, acronym, group_type=None): '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) 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, doc, *args, **kwargs): + 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") @@ -488,10 +495,23 @@ class UploadMaterialForm(forms.Form): else: 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 == "edit": del self.fields["material"] + def clean_title(self): + title = self.cleaned_data["title"] + if self.action == "new": + name = name_for_material(self.doc_type, self.group, title) + existing = Document.objects.filter(type=self.doc_type, name=name) + if existing: + url = urlreverse("group_revise_material", kwargs={ 'acronym': self.group.acronym, 'name': existing[0].name }) + raise forms.ValidationError(mark_safe("Can't upload: %s with name %s already exists. The name is derived from the title so you must either choose another title for what you're uploading or revise the existing %s." % (self.doc_type.name, name, url, name))) + + return title + @login_required def edit_material(request, acronym, action="new", name=None, doc_type=None, group_type=None): @@ -502,14 +522,15 @@ def edit_material(request, acronym, action="new", name=None, doc_type=None, grou if not can_manage_materials(request.user, group): return HttpResponseForbidden("You don't have permission to access this view") - document_type = get_object_or_404(DocTypeName, slug=doc_type) - existing = None if name and action != "new": - existing = get_object_or_404(Document, type="material", name=name) + existing = get_object_or_404(Document, name=name) + document_type = existing.type + else: + document_type = get_object_or_404(DocTypeName, slug=doc_type) if request.method == 'POST': - form = UploadMaterialForm(document_type, action, existing, request.POST, request.FILES) + form = UploadMaterialForm(document_type, action, group, existing, request.POST, request.FILES) if form.is_valid(): if action == "new": @@ -517,6 +538,7 @@ def edit_material(request, acronym, action="new", name=None, doc_type=None, grou d.type = document_type d.group = group d.rev = "00" + d.name = name_for_material(d.type, d.group, form.cleaned_data["title"]) else: d = existing @@ -527,15 +549,6 @@ def edit_material(request, acronym, action="new", name=None, doc_type=None, grou d.title = form.cleaned_data["title"] d.time = datetime.datetime.now() - if not d.name: - d.name = "%s-%s-%s" % (d.type_id, d.group.acronym, slugify(d.title)) - i = 2 - while True: - if not Document.objects.filter(name=d.name).exists(): - break - d.name = "%s-%s-%s-%s" % (d.type_id, d.group.acronym, slugify(d.title), i) - i += 1 - if "material" in form.fields: if action != "new": d.rev = "%02d" % (int(d.rev) + 1) @@ -569,7 +582,7 @@ def edit_material(request, acronym, action="new", name=None, doc_type=None, grou return redirect("group_materials", acronym=group.acronym) else: - form = UploadMaterialForm(document_type, action, existing) + form = UploadMaterialForm(document_type, action, group, existing) return render(request, 'group/edit_material.html', { 'group': group, diff --git a/ietf/group/info.py b/ietf/group/info.py index d194a20e6..39881fc40 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -260,7 +260,7 @@ def concluded_groups(request): dict(group_types=group_types)) def get_group_materials(group): - return Document.objects.filter(group=group, type__in=group.features.material_types, session=None).exclude(states__slug="inactive") + 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.""" diff --git a/ietf/templates/group/edit_material.html b/ietf/templates/group/edit_material.html index fed4e6f51..934d56abb 100644 --- a/ietf/templates/group/edit_material.html +++ b/ietf/templates/group/edit_material.html @@ -41,7 +41,7 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; } Cancel - + From 779763e58e5f9182873ab1600d7b2dd677236c9f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 2 Jun 2014 11:09:05 +0000 Subject: [PATCH 17/26] Move common revision list in document templates to a separate included file - Legacy-Id: 7824 --- ietf/templates/doc/document_charter.html | 9 +-------- ietf/templates/doc/document_conflict_review.html | 9 +-------- ietf/templates/doc/document_status_change.html | 9 +-------- ietf/templates/doc/revisions_list.html | 8 ++++++++ 4 files changed, 11 insertions(+), 24 deletions(-) create mode 100644 ietf/templates/doc/revisions_list.html diff --git a/ietf/templates/doc/document_charter.html b/ietf/templates/doc/document_charter.html index ed9f119f3..52034cdb5 100644 --- a/ietf/templates/doc/document_charter.html +++ b/ietf/templates/doc/document_charter.html @@ -11,14 +11,7 @@ {% block content %} {{ top|safe }} -
- Snapshots: - - {% for rev in revisions %} - {{ rev }} - {% endfor %} - -
+{% include "doc/revisions_list.html" %}
diff --git a/ietf/templates/doc/document_conflict_review.html b/ietf/templates/doc/document_conflict_review.html index fe17f2810..8588517ab 100644 --- a/ietf/templates/doc/document_conflict_review.html +++ b/ietf/templates/doc/document_conflict_review.html @@ -11,14 +11,7 @@ {% block content %} {{ top|safe }} -
- Versions: - - {% for rev in revisions %} - {{ rev }} - {% endfor %} - -
+{% include "doc/revisions_list.html" %}
diff --git a/ietf/templates/doc/document_status_change.html b/ietf/templates/doc/document_status_change.html index 244da99ec..8553a09e1 100644 --- a/ietf/templates/doc/document_status_change.html +++ b/ietf/templates/doc/document_status_change.html @@ -11,14 +11,7 @@ {% block content %} {{ top|safe }} -
- Versions: - - {% for rev in revisions %} - {{ rev }} - {% endfor %} - -
+{% include "doc/revisions_list.html" %}
diff --git a/ietf/templates/doc/revisions_list.html b/ietf/templates/doc/revisions_list.html new file mode 100644 index 000000000..0c23b878a --- /dev/null +++ b/ietf/templates/doc/revisions_list.html @@ -0,0 +1,8 @@ +
+ Versions: + + {% for rev in revisions %} + {{ rev }} + {% endfor %} + +
From c94757405d68caa05abded5586389f75bfe75c16 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 2 Jun 2014 12:18:42 +0000 Subject: [PATCH 18/26] Add a /doc/ page for materials (slides, agendas, minutes), link to that from the group materials page - Legacy-Id: 7825 --- ietf/doc/views_doc.py | 39 +++++++++++- ietf/group/edit.py | 3 + ietf/group/info.py | 9 +-- ietf/group/utils.py | 2 +- ietf/settings.py | 2 +- ietf/templates/doc/document_material.html | 78 +++++++++++++++++++++++ ietf/templates/group/materials.html | 11 +--- static/css/doc.css | 2 + 8 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 ietf/templates/doc/document_material.html diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 133ab5ea0..5bc56768e 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 @@ -53,7 +53,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 @@ -481,6 +481,41 @@ 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 = "%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() + extension + + if extension == ".txt": + content = get_document_content(basename, pathname, 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/group/edit.py b/ietf/group/edit.py index 67fcbf95b..6e8200193 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -562,6 +562,9 @@ def edit_material(request, acronym, action="new", name=None, doc_type=None, grou d.save() + if action == "new": + DocAlias.objects.get_or_create(name=d.name, document=d) + if not existing or prev_rev != d.rev: e = NewRevisionDocEvent(type="new_revision", doc=d, rev=d.rev) e.time = d.time diff --git a/ietf/group/info.py b/ietf/group/info.py index 39881fc40..727cc5d44 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -437,16 +437,9 @@ def materials(request, acronym, group_type=None): if not group.features.has_materials: raise Http404 - docs = get_group_materials(group).order_by("type", "-time").select_related("type") + docs = get_group_materials(group).order_by("type__order", "-time").select_related("type") doc_types = OrderedDict() for d in docs: - extension = "" - globs = glob.glob(d.get_file_path() + d.name + "-" + d.rev + ".*") - if globs: - extension = os.path.splitext(globs[0])[1] - - d.full_url = d.href() + extension - if d.type not in doc_types: doc_types[d.type] = [] doc_types[d.type].append(d) diff --git a/ietf/group/utils.py b/ietf/group/utils.py index c6a96a03d..c4de6a8a7 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -109,7 +109,7 @@ def milestone_reviewer_for_group_type(group_type): return "Area Director" def can_manage_materials(user, group): - return group.has_role(user, ("chair", "delegate", "secr")) or has_role(user, 'Secretariat') + 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.""" diff --git a/ietf/settings.py b/ietf/settings.py index 3321622ee..3856ded0f 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -283,7 +283,7 @@ DOC_HREFS = { # who understands this better can take care of it. #"liai-att": None #"liaison": None - "slides": 'http://www.ietf.org/slides/', + "slides": 'http://www.ietf.org/slides/{doc.name}-{doc.rev}', } MEETING_DOC_HREFS = { diff --git a/ietf/templates/doc/document_material.html b/ietf/templates/doc/document_material.html new file mode 100644 index 000000000..4c4902126 --- /dev/null +++ b/ietf/templates/doc/document_material.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% load ietf_filters %} + +{% block title %}{{ doc.canonical_name }}-{{ doc.rev }}{% endblock %} + +{% block pagehead %} + +{% endblock %} + +{% block content %} +{{ top|safe }} + +{% include "doc/revisions_list.html" %} + +
+
+ {% if snapshot %}Snapshot of{% endif %} {% if doc.meeting_related %}Meeting{% endif %} {{ doc.type.name }} for {{ doc.group.acronym }} group +
+ + + + + + + + + + + + + {% if other_types %} + + + + + {% endif %} + + + + + + + {% if not snapshot and can_manage_material %} + + {% endif %} + +
Title: + {{ doc.title }} +
State: + {{ doc.get_state.name }} +
Other versions: + {% for t, url in other_types %} + {{ t }}{% if not forloop.last %},{% endif %} + {% endfor %} +
Last updated:{{ doc.time|date:"Y-m-d" }}
+ Change title/state + + Revise content +
+
+ +{% if doc.rev and content != None %} +

{{ doc.title }}

+ +
+ {{ content|fill:"80"|safe|linebreaksbr|keep_spacing|sanitize_html|safe }} +
+{% else %} +

Not available as plain text.

+ + {% if other_types %} +

Download as {{ other_types.0.0.upper }}

+ {% endif %} +{% endif %} + +{% endblock %} + diff --git a/ietf/templates/group/materials.html b/ietf/templates/group/materials.html index 4287e1ff0..221e00e1f 100644 --- a/ietf/templates/group/materials.html +++ b/ietf/templates/group/materials.html @@ -25,16 +25,7 @@ {% for d in docs %} - - {% if can_manage_materials %} - - edit | - revise - - {% endif %} - - {{ d.title }} - + {{ d.title }} {{ d.rev }} {{ d.time|date:"Y-m-d" }} diff --git a/static/css/doc.css b/static/css/doc.css index deaa737d9..1482408dc 100644 --- a/static/css/doc.css +++ b/static/css/doc.css @@ -46,3 +46,5 @@ h3 a.edit { font-weight: normal; font-size: 13px; display: inline-block; margin- h4 { margin-bottom: 0; } h4 + p { margin-top: 0; max-width: 400px; } + +p.download-instead a { font-size: 20px; font-weight: bold; color: #2647a0; } From 88cf68d43f84a6e943d2481f4591ad5e1d2a94fc Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 3 Jun 2014 09:42:16 +0000 Subject: [PATCH 19/26] Move group material handling code to doc/material/ now that the view of the material is integrated into /doc/, fix a bunch of bugs - Legacy-Id: 7834 --- ietf/doc/models.py | 2 +- ietf/doc/urls.py | 1 + ietf/doc/urls_material.py | 6 + ietf/doc/views_material.py | 168 ++++++++++++++++++ ietf/group/edit.py | 133 -------------- ietf/group/info.py | 2 +- ietf/group/urls.py | 10 +- ietf/templates/doc/document_material.html | 8 +- .../material}/choose_material_type.html | 0 .../material}/edit_material.html | 21 +-- 10 files changed, 194 insertions(+), 157 deletions(-) create mode 100644 ietf/doc/urls_material.py create mode 100644 ietf/doc/views_material.py rename ietf/templates/{group => doc/material}/choose_material_type.html (100%) rename ietf/templates/{group => doc/material}/edit_material.html (59%) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index e70af5db4..10fd3a00d 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -173,7 +173,7 @@ class DocumentInfo(models.Model): def meeting_related(self): return(self.type_id in ("agenda", "minutes", "slides") and ( self.name.split("-")[1] == "interim" - or self.session_set.exists())) + or (self.session_set.exists() if isinstance(self, Document) else self.doc.session_set.exists()))) class Meta: abstract = True diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index aba6daa89..9d2ea1086 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/views_material.py b/ietf/doc/views_material.py new file mode 100644 index 000000000..4e462fb6e --- /dev/null +++ b/ietf/doc/views_material.py @@ -0,0 +1,168 @@ +# views for managing group materials (slides, ...) +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 HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect +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 +from ietf.group.models import Group +from ietf.group.utils import can_manage_materials +from ietf.ietfauth.utils import has_role + +@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) + 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 + else: + 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_title(self): + title = self.cleaned_data["title"] + if self.action == "new": + name = name_for_material(self.doc_type, self.group, title) + 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. The name is derived from the title so you must either choose another title for what you're uploading or revise the existing %s." % (self.doc_type.name, name, url, name))) + + return title + +@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 6e8200193..7e3739acb 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -459,136 +459,3 @@ def customize_workflow(request, group_type, acronym): 'states': states, 'tags': tags, }) - -@login_required -def choose_material_type(request, acronym, group_type=None): - group = get_group_or_404(acronym, group_type) - if not group.features.has_materials: - raise Http404 - - return render(request, 'group/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) - 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 - else: - 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 == "edit": - del self.fields["material"] - - def clean_title(self): - title = self.cleaned_data["title"] - if self.action == "new": - name = name_for_material(self.doc_type, self.group, title) - existing = Document.objects.filter(type=self.doc_type, name=name) - if existing: - url = urlreverse("group_revise_material", kwargs={ 'acronym': self.group.acronym, 'name': existing[0].name }) - raise forms.ValidationError(mark_safe("Can't upload: %s with name %s already exists. The name is derived from the title so you must either choose another title for what you're uploading or revise the existing %s." % (self.doc_type.name, name, url, name))) - - return title - - -@login_required -def edit_material(request, acronym, action="new", name=None, doc_type=None, group_type=None): - group = get_group_or_404(acronym, group_type) - if not group.features.has_materials: - raise Http404 - - if not can_manage_materials(request.user, group): - return HttpResponseForbidden("You don't have permission to access this view") - - existing = None - if name and action != "new": - existing = get_object_or_404(Document, name=name) - document_type = existing.type - else: - document_type = get_object_or_404(DocTypeName, slug=doc_type) - - if request.method == 'POST': - form = UploadMaterialForm(document_type, action, group, existing, request.POST, request.FILES) - - if form.is_valid(): - if action == "new": - d = Document() - d.type = document_type - d.group = group - d.rev = "00" - d.name = name_for_material(d.type, d.group, form.cleaned_data["title"]) - else: - d = existing - - prev_rev = d.rev - prev_title = d.title - prev_state = d.get_state() - - d.title = form.cleaned_data["title"] - d.time = datetime.datetime.now() - - if "material" in form.fields: - if action != "new": - d.rev = "%02d" % (int(d.rev) + 1) - - f = form.cleaned_data["material"] - file_ext = os.path.splitext(f.name)[1] - - with open(os.path.join(d.get_file_path(), d.name + "-" + d.rev + file_ext), 'wb+') as dest: - for chunk in f.chunks(): - dest.write(chunk) - - d.save() - - if action == "new": - DocAlias.objects.get_or_create(name=d.name, document=d) - - if not existing or prev_rev != d.rev: - e = NewRevisionDocEvent(type="new_revision", doc=d, rev=d.rev) - e.time = d.time - e.by = request.user.person - e.desc = "New version available: %s-%s" % (d.name, d.rev) - e.save() - - if prev_title != d.title: - e = DocEvent(doc=d, by=request.user.person, type='changed_document') - e.desc = u"Changed title to %s" % d.title - if prev_title: - e.desc += u" from %s" % prev_title - e.time = d.time - e.save() - - d.set_state(form.cleaned_data["state"]) - add_state_change_event(d, request.user.person, prev_state, form.cleaned_data["state"]) - - return redirect("group_materials", acronym=group.acronym) - else: - form = UploadMaterialForm(document_type, action, group, existing) - - return render(request, 'group/edit_material.html', { - 'group': group, - 'form': form, - 'action': action, - }) diff --git a/ietf/group/info.py b/ietf/group/info.py index 727cc5d44..332a2c5d6 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -299,7 +299,7 @@ def construct_group_menu_context(request, group, selected, group_type, others): actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs))) if group.features.has_materials and can_manage_materials(request.user, group): - actions.append((u"Upload material", urlreverse("group_choose_material_type", kwargs=kwargs))) + actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs))) 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))) diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 4afb28fc9..283eb38cd 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -7,7 +7,9 @@ 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, need to unify these at some point + # 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'), @@ -25,10 +27,8 @@ urlpatterns = patterns('', (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.group.edit.choose_material_type', None, "group_choose_material_type"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/new/(?P[\w-]+)/$', 'ietf.group.edit.edit_material', { 'action': "new" }, "group_new_material"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/(?P[^/]+)/edit/$', 'ietf.group.edit.edit_material', { 'action': "edit" }, "group_edit_material"), - (r'^(?P[a-zA-Z0-9-._]+)/materials/(?P[^/]+)/revise/$', 'ietf.group.edit.edit_material', { 'action': "revise" }, "group_revise_material"), + (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/templates/doc/document_material.html b/ietf/templates/doc/document_material.html index 4c4902126..e1d13b40c 100644 --- a/ietf/templates/doc/document_material.html +++ b/ietf/templates/doc/document_material.html @@ -22,14 +22,14 @@ Title: - {{ doc.title }} + {{ doc.title }} State: - {{ doc.get_state.name }} + {{ doc.get_state.name }} @@ -51,9 +51,7 @@ {% if not snapshot and can_manage_material %} - Change title/state - - Revise content + Upload New Revision {% endif %} diff --git a/ietf/templates/group/choose_material_type.html b/ietf/templates/doc/material/choose_material_type.html similarity index 100% rename from ietf/templates/group/choose_material_type.html rename to ietf/templates/doc/material/choose_material_type.html diff --git a/ietf/templates/group/edit_material.html b/ietf/templates/doc/material/edit_material.html similarity index 59% rename from ietf/templates/group/edit_material.html rename to ietf/templates/doc/material/edit_material.html index 934d56abb..b6cf6ab35 100644 --- a/ietf/templates/group/edit_material.html +++ b/ietf/templates/doc/material/edit_material.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Upload Material for Group {{ group.acronym }}{% endblock %} +{% block title %}{% if action == "new" or action == "revise" %}Upload{% else %}Edit{% endif %} {{ document_type.name }} for Group {{ group.acronym }}{% endblock %} {% block morecss %} {{ block.super }} @@ -12,7 +12,7 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; } {% block content %} {% load ietf_filters %} -

Upload Material for Group {{ group.acronym }}

+

{% if action == "new" or action == "revise" %}Upload{% else %}Edit{% endif %} {{ document_type.name }} for Group {{ group.acronym }}

{% if action == "new" %}

@@ -20,19 +20,16 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; } {{ group.acronym }}. The file will appear under the materials tab in the group pages.

-{% elif action == "edit" %} -

- Below you can edit the details of the material for the group - {{ group.acronym }}. -

+ +

Upload

{% elif action == "revise" %}

- Below you can upload a new revision of the material for the group + Below you can upload a new revision of {{ doc_name }} for the group {{ group.acronym }}.

-{% endif %} -

Upload

+

Upload New Revision

+{% endif %}
{% csrf_token %} @@ -40,8 +37,8 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; }
- Cancel - + Cancel +
From 2a1f902a6716cb6a883be87dd6afb5543cb5c171 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 3 Jun 2014 10:42:33 +0000 Subject: [PATCH 20/26] Rewrite error handling in get_document_content to use a with statement instead of finally - Legacy-Id: 7837 --- ietf/doc/utils.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index bcab4d70f..ddbcf59d2 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -211,16 +211,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() - except IOError: + with open(filename, 'rb') as f: + raw_content = f.read() + except IOError as e: error = "Error; cannot read ("+key+")" return error - finally: - if f: - f.close() + if markup: return markup_txt.markup(raw_content, split) else: From b56b8deff94c7a01b74a66aadb21ea9f890a534e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 3 Jun 2014 10:43:15 +0000 Subject: [PATCH 21/26] Fix some bugs in displaying meeting-related materials in the new material support under /doc/ - Legacy-Id: 7838 --- ietf/doc/views_doc.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 5bc56768e..09de0c302 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -487,8 +487,13 @@ def document_main(request, name, rev=None): # 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) - basename = "%s-%s" % (doc.canonical_name(), doc.rev) pathname = os.path.join(doc.get_file_path(), basename) content = None @@ -497,10 +502,12 @@ def document_main(request, name, rev=None): for g in globs: extension = os.path.splitext(g)[1] t = os.path.splitext(g)[1].lstrip(".") - url = doc.href() + extension + url = doc.href() + if not url.endswith("/") and not url.endswith(extension): + url += extension if extension == ".txt": - content = get_document_content(basename, pathname, split=False) + content = get_document_content(basename, pathname + extension, split=False) t = "plain text" other_types.append((t, url)) From 6e83335f0f61d80d74be31ede3c81b5f393307f7 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 3 Jun 2014 11:28:27 +0000 Subject: [PATCH 22/26] Fix a bunch of mistakes found by PyFlakes in the new materials code - Legacy-Id: 7839 --- ietf/doc/utils.py | 2 +- ietf/doc/views_material.py | 5 +---- ietf/group/edit.py | 9 +++------ ietf/group/feeds.py | 4 ++-- ietf/group/info.py | 3 +-- ...up_description__add_field_grouphistory_description.py | 4 +--- ietf/group/milestones.py | 6 +++--- ietf/name/migrations/0020_sort_role_names.py | 3 --- 8 files changed, 12 insertions(+), 24 deletions(-) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index ddbcf59d2..ed1d0a247 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -214,7 +214,7 @@ def get_document_content(key, filename, split=True, markup=True): try: with open(filename, 'rb') as f: raw_content = f.read() - except IOError as e: + except IOError: error = "Error; cannot read ("+key+")" return error diff --git a/ietf/doc/views_material.py b/ietf/doc/views_material.py index 4e462fb6e..235fcbb08 100644 --- a/ietf/doc/views_material.py +++ b/ietf/doc/views_material.py @@ -1,12 +1,10 @@ # views for managing group materials (slides, ...) -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 HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect +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 @@ -19,7 +17,6 @@ from ietf.doc.models import NewRevisionDocEvent, save_document_in_history from ietf.doc.utils import add_state_change_event from ietf.group.models import Group from ietf.group.utils import can_manage_materials -from ietf.ietfauth.utils import has_role @login_required def choose_material_type(request, acronym): diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 7e3739acb..a346d27c1 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -9,18 +9,15 @@ from django import forms from django.shortcuts import render, get_object_or_404, redirect from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect 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, DocTagName, DocTypeName, DocEvent, State -from ietf.doc.models import NewRevisionDocEvent, save_document_in_history -from ietf.doc.utils import get_tags_for_stream_id, add_state_change_event +from ietf.doc.models import Document, DocAlias, DocTagName, State, save_document_in_history +from ietf.doc.utils import get_tags_for_stream_id 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, can_manage_materials +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 diff --git a/ietf/group/feeds.py b/ietf/group/feeds.py index 2a09314e4..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 group.about_url() + 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 group.about_url() + 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 332a2c5d6..9c0a2e262 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -35,10 +35,9 @@ import os import itertools from tempfile import mkstemp -import glob 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, HttpResponseRedirect from django.conf import settings 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 index e9266e459..3291e98ec 100644 --- 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 @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime from south.db import db from south.v2 import SchemaMigration -from django.db import models class Migration(SchemaMigration): @@ -314,4 +312,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['group'] \ No newline at end of file + complete_apps = ['group'] diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py index a82e1a637..1778e316c 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -5,13 +5,13 @@ import calendar import json from django import forms -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect -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.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 diff --git a/ietf/name/migrations/0020_sort_role_names.py b/ietf/name/migrations/0020_sort_role_names.py index e15a11004..29252a226 100644 --- a/ietf/name/migrations/0020_sort_role_names.py +++ b/ietf/name/migrations/0020_sort_role_names.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db from south.v2 import DataMigration -from django.db import models class Migration(DataMigration): From 602feff7c9fd4bfe82638661a40bbf82039b9ac2 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 4 Jun 2014 13:10:55 +0000 Subject: [PATCH 23/26] Add tests for new material code - Legacy-Id: 7850 --- ietf/doc/tests.py | 16 +++++ ietf/doc/tests_material.py | 137 +++++++++++++++++++++++++++++++++++++ ietf/group/tests_info.py | 44 ++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 ietf/doc/tests_material.py diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index a9b3ccbad..888b5c773 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..297e6091e --- /dev/null +++ b/ietf/doc/tests_material.py @@ -0,0 +1,137 @@ +# Copyright The IETF Trust 2011, All Rights Reserved + +import os, shutil, datetime +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 +from ietf.utils.test_data import make_test_data + +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="", 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", + 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 title is prevented + test_file.seek(0) + + r = self.client.post(url, dict(title="Test File", + 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/group/tests_info.py b/ietf/group/tests_info.py index e04433ffc..daa838e59 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -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): + draft = 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): + draft = 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 From f9ee750afd99a43a91e68e1cfa6c76fba137a43f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 13 Jun 2014 12:30:42 +0000 Subject: [PATCH 24/26] Make it possible to adjust the name of new materials upon upload, also fix a couple of warnings from PyFlakes - Legacy-Id: 7914 --- ietf/doc/tests_material.py | 9 +++--- ietf/doc/utils.py | 16 ++++++++++ ietf/doc/views_material.py | 29 ++++++++++++------- ietf/group/tests_info.py | 4 +-- ietf/group/utils.py | 1 - ietf/templates/base.html | 2 +- .../templates/doc/material/edit_material.html | 15 ++++++++-- ietf/utils/tests.py | 1 + 8 files changed, 57 insertions(+), 20 deletions(-) diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py index 297e6091e..4c175415e 100644 --- a/ietf/doc/tests_material.py +++ b/ietf/doc/tests_material.py @@ -1,6 +1,6 @@ # Copyright The IETF Trust 2011, All Rights Reserved -import os, shutil, datetime +import os, shutil from StringIO import StringIO from pyquery import PyQuery @@ -10,7 +10,6 @@ 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 -from ietf.utils.test_data import make_test_data class GroupMaterialTests(TestCase): def setUp(self): @@ -57,7 +56,7 @@ class GroupMaterialTests(TestCase): test_file.name = "unnamed.pdf" # faulty post - r = self.client.post(url, dict(title="", state="", material=test_file)) + r = self.client.post(url, dict(title="", name="", state="", material=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) @@ -67,6 +66,7 @@ class GroupMaterialTests(TestCase): # 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) @@ -79,10 +79,11 @@ class GroupMaterialTests(TestCase): 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 title is prevented + # 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) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index ed1d0a247..91079a289 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -4,6 +4,7 @@ import urllib import math from django.conf import settings +from django.forms import ValidationError from ietf.utils import markup_txt from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder @@ -377,3 +378,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/views_material.py b/ietf/doc/views_material.py index 235fcbb08..ae8efa1c7 100644 --- a/ietf/doc/views_material.py +++ b/ietf/doc/views_material.py @@ -1,6 +1,7 @@ # 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 @@ -14,7 +15,7 @@ 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 +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 @@ -34,6 +35,7 @@ def name_for_material(doc_type, group, 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)") @@ -50,7 +52,10 @@ class UploadMaterialForm(forms.Form): 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": @@ -63,16 +68,20 @@ class UploadMaterialForm(forms.Form): del self.fields["title"] del self.fields["material"] - def clean_title(self): - title = self.cleaned_data["title"] - if self.action == "new": - name = name_for_material(self.doc_type, self.group, title) - 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. The name is derived from the title so you must either choose another title for what you're uploading or revise the existing %s." % (self.doc_type.name, name, url, name))) + def clean_name(self): + name = self.cleaned_data["name"].strip().rstrip("-") - return title + 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): diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index daa838e59..802eb6442 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -193,7 +193,7 @@ class GroupPagesTests(TestCase): self.assertTrue(milestone.docs.all()[0].name in r.content) def test_group_about(self): - draft = make_test_data() + make_test_data() group = Group.objects.create( type_id="team", acronym="testteam", @@ -210,7 +210,7 @@ class GroupPagesTests(TestCase): self.assertTrue(group.description in r.content) def test_materials(self): - draft = make_test_data() + make_test_data() group = Group.objects.create(type_id="team", acronym="testteam", name="Test Team", state_id="active") doc = Document.objects.create( diff --git a/ietf/group/utils.py b/ietf/group/utils.py index c4de6a8a7..d35df624f 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -1,6 +1,5 @@ import os -from django.conf import settings from django.shortcuts import get_object_or_404 from ietf.group.models import Group, RoleHistory diff --git a/ietf/templates/base.html b/ietf/templates/base.html index f9025f31f..02e765690 100644 --- a/ietf/templates/base.html +++ b/ietf/templates/base.html @@ -61,7 +61,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
{% if user.is_authenticated %} {{ user }} {% else %} -Sign In +Sign In {% endif %}
{% endif %} diff --git a/ietf/templates/doc/material/edit_material.html b/ietf/templates/doc/material/edit_material.html index b6cf6ab35..735761e50 100644 --- a/ietf/templates/doc/material/edit_material.html +++ b/ietf/templates/doc/material/edit_material.html @@ -5,7 +5,7 @@ {% block morecss %} {{ block.super }} form.upload-material td { padding-bottom: 0.6em; } -form.upload-material #id_title { width: 30em; } +form.upload-material #id_title, form.upload-material #id_name { width: 30em; } form.upload-material .submit-row td { padding-top: 1em; text-align: right; } {% endblock %} @@ -31,7 +31,7 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; }

Upload New Revision

{% endif %} -{% csrf_token %} +{% csrf_token %} {{ form.as_table }} @@ -45,3 +45,14 @@ form.upload-material .submit-row td { padding-top: 1em; text-align: right; } {% endblock content %} + +{% block scripts %} + jQuery(document).ready(function () { + jQuery("form.upload-material input#id_title").on("change keyup", function () { + var v = jQuery(this).val(); + var slug = jQuery(this).parents("form").data("nameprefix"); + slug += v.toLowerCase().replace(/ /g,'-').replace(/[-]+/g, '-').replace(/[^a-z-]+/g,''); + jQuery(this).parents("form").find("input#id_name").val(slug); + }); + }); +{% endblock %} diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py index c6e087128..8f58e2c79 100644 --- a/ietf/utils/tests.py +++ b/ietf/utils/tests.py @@ -11,6 +11,7 @@ from ietf.utils.test_utils import TestCase class PyFlakesTestCase(TestCase): def test_pyflakes(self): + self.maxDiff = None path = os.path.join(settings.BASE_DIR) warnings = [] warnings = pyflakes.checkPaths([path], verbosity=0) From e92e57acd2f61320ae7cbbe092315517569d803a Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 26 Jun 2014 16:24:06 +0000 Subject: [PATCH 25/26] Fix an instance of charter-ietf-%s that should really be charter-%s-%s to support RG charters properly, also remove the dead code for handling group acronym renames (it contained some instances of charter-ietf-%s too) - Legacy-Id: 7936 --- ietf/doc/models.py | 3 ++- ietf/doc/utils_charter.py | 7 +++++++ ietf/group/edit.py | 25 +++---------------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 10fd3a00d..1e7bb152c 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -288,7 +288,8 @@ class Document(DocumentInfo): if a: name = a[0].name elif self.type_id == "charter": - return "charter-ietf-%s" % self.chartered_group.acronym + from ietf.doc.utils_charter import charter_name_for_group + return charter_name_for_group(self.chartered_group) return name def canonical_docalias(self): 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/group/edit.py b/ietf/group/edit.py index a346d27c1..262269751 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -1,9 +1,7 @@ # 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 @@ -13,8 +11,9 @@ from django.contrib.auth.decorators import login_required import debug # pyflakes:ignore -from ietf.doc.models import Document, DocAlias, DocTagName, 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 @@ -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) @@ -239,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") @@ -251,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] From 20fb8ff31541d2a34e4b0abf1dcad94ff0009767 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 26 Jun 2014 16:31:17 +0000 Subject: [PATCH 26/26] Fix the now wrong comment about WG charter canonical names being able to change - Legacy-Id: 7937 --- ietf/doc/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 1e7bb152c..71777be58 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -482,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):