diff --git a/ietf/secr/__init__.py b/ietf/secr/__init__.py index 64b3f0372..6dd3ce21e 100644 --- a/ietf/secr/__init__.py +++ b/ietf/secr/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.33" +__version__ = "1.41" __date__ = "$Date: 2011/07/26 14:29:17 $" diff --git a/ietf/secr/announcement/forms.py b/ietf/secr/announcement/forms.py index 64293c324..72dc4b937 100644 --- a/ietf/secr/announcement/forms.py +++ b/ietf/secr/announcement/forms.py @@ -13,7 +13,9 @@ from ietf.wgchairs.accounts import get_person_for_user # Globals # --------------------------------------------- -#ANNOUNCE_FROM_GROUPS = ['ietf','rsoc','iab',current_nomcom().acronym] +ANNOUNCE_FROM_GROUPS = ['ietf','rsoc','iab'] +if current_nomcom(): + ANNOUNCE_FROM_GROUPS += [ current_nomcom().acronym ] ANNOUNCE_TO_GROUPS= ['ietf'] # this list isn't currently available as a Role query so it's hardcoded @@ -32,8 +34,9 @@ FROM_LIST = ('IETF Secretariat ', 'The IETF Trust ', 'RSOC Chair ', 'ISOC Board of Trustees ', - 'RFC Series Editor ') - + 'RFC Series Editor ', + 'IAB Executive Director ') + TO_LIST = ('IETF Announcement List ', 'I-D Announcement List ', 'The IESG ', @@ -51,14 +54,14 @@ class MultiEmailField(forms.Field): # Return an empty list if no input was given. if not value: return [] - + import types if isinstance(value, types.StringTypes): values = value.split(',') return [ x.strip() for x in values ] else: return value - + def validate(self, value): "Check if value consists only of valid emails." @@ -67,7 +70,7 @@ class MultiEmailField(forms.Field): for email in value: validate_email(email) - + # --------------------------------------------- # Helper Functions # --------------------------------------------- @@ -87,13 +90,7 @@ def get_from_choices(user): f = (FROM_LIST[6],) elif has_role(user,'IAD'): f = (FROM_LIST[9],) - # NomCom, RSOC Chair, IAOC Chair aren't supported by has_role() - elif Role.objects.filter(name="chair", - group__acronym__startswith="nomcom", - group__state="active", - group__type="ietf", - person=person): - f = (FROM_LIST[7],) + #RSOC Chair, IAOC Chair aren't supported by has_role() elif Role.objects.filter(person=person, group__acronym='rsoc', name="chair"): @@ -106,17 +103,33 @@ def get_from_choices(user): group__acronym='rse', name="chair"): f = (FROM_LIST[15],) + elif Role.objects.filter(person=person, + group__acronym='iab', + name='execdir'): + f = (FROM_LIST[6],FROM_LIST[16]) + + # NomCom + nomcoms = Role.objects.filter(name="chair", + group__acronym__startswith="nomcom", + group__state="active", + group__type="ietf", + person=person) + if nomcoms: + year = nomcoms[0].group.acronym[-4:] + alias = 'NomCom Chair %s ' % (year,year) + f = (alias,) + return zip(f,f) - + def get_to_choices(): #groups = Group.objects.filter(acronym__in=ANNOUNCE_TO_GROUPS) #roles = Role.objects.filter(group__in=(groups),name="Announce") #choices = [ (r.email, r.person.name) for r in roles ] #choices.append(('Other...','Other...'),) return zip(TO_LIST,TO_LIST) - + # --------------------------------------------- -# Select Choices +# Select Choices # --------------------------------------------- #TO_CHOICES = tuple(AnnouncedTo.objects.values_list('announced_to_id','announced_to')) TO_CHOICES = get_to_choices() @@ -127,24 +140,33 @@ TO_CHOICES = get_to_choices() # --------------------------------------------- class AnnounceForm(forms.ModelForm): - nomcom = forms.BooleanField(required=False) + #nomcom = forms.BooleanField(required=False) + nomcom = forms.ModelChoiceField(queryset=Group.objects.filter(acronym__startswith='nomcom',type='ietf',state='active'),required=False) to_custom = MultiEmailField(required=False,label='') #cc = MultiEmailField(required=False) - + class Meta: model = Message fields = ('nomcom', 'to','to_custom','frm','cc','bcc','reply_to','subject','body') - + def __init__(self, *args, **kwargs): user = kwargs.pop('user') + person = user.get_profile() super(AnnounceForm, self).__init__(*args, **kwargs) self.fields['to'].widget = forms.Select(choices=TO_CHOICES) self.fields['to'].help_text = 'Select name OR select Other... and enter email below' self.fields['cc'].help_text = 'Use comma separated lists for emails (Cc, Bcc, Reply To)' self.fields['frm'].widget = forms.Select(choices=get_from_choices(user)) self.fields['frm'].label = 'From' - self.fields['nomcom'].label = 'NomCom message?' - + self.fields['nomcom'].label = 'NomCom message:' + nomcom_roles = person.role_set.filter(group__in=self.fields['nomcom'].queryset,name='chair') + secr_roles = person.role_set.filter(group__acronym='secretariat',name='secr') + if nomcom_roles: + self.initial['nomcom'] = nomcom_roles[0].group.pk + if not nomcom_roles and not secr_roles: + self.fields['nomcom'].widget = forms.HiddenInput() + self.initial['reply_to'] = 'ietf@ietf.org' + def clean(self): super(AnnounceForm, self).clean() data = self.cleaned_data @@ -152,9 +174,9 @@ class AnnounceForm(forms.ModelForm): return self.cleaned_data if data['to'] == 'Other...' and not data['to_custom']: raise forms.ValidationError('You must enter a "To" email address') - + return data - + def save(self, *args, **kwargs): user = kwargs.pop('user') message = super(AnnounceForm, self).save(commit=False) @@ -163,10 +185,10 @@ class AnnounceForm(forms.ModelForm): message.to = self.cleaned_data['to_custom'] if kwargs['commit']: message.save() - - # add nomcom to related groups if checked - if self.cleaned_data.get('nomcom', False): - nomcom = current_nomcom() + + # handle nomcom message + nomcom = self.cleaned_data.get('nomcom',False) + if nomcom: message.related_groups.add(nomcom) - + return message diff --git a/ietf/secr/announcement/views.py b/ietf/secr/announcement/views.py index 8d6ba084c..ca11a6e51 100644 --- a/ietf/secr/announcement/views.py +++ b/ietf/secr/announcement/views.py @@ -34,9 +34,13 @@ def check_access(user): group__type="ietf", person=person): return True - + if Role.objects.filter(person=person, + group__acronym='iab', + name='execdir'): + return True + return False - + # -------------------------------------------------- # STANDARD VIEW FUNCTIONS # -------------------------------------------------- @@ -50,12 +54,16 @@ def main(request): ''' if not check_access(request.user): return HttpResponseForbidden('Restricted to: Secretariat, IAD, or chair of IETF, IAB, RSOC, RSE, IAOC, NomCom.') - + form = AnnounceForm(request.POST or None,user=request.user) - + if form.is_valid(): - request.session['data'] = form.cleaned_data - + # nomcom is a ModelChoice, store pk, not Group object + data = form.cleaned_data + if data['nomcom']: + data['nomcom'] = data['nomcom'].pk + request.session['data'] = data + url = reverse('announcement_confirm') return HttpResponseRedirect(url) @@ -66,40 +74,39 @@ def main(request): @check_for_cancel('../') def confirm(request): - - # testing - #assert False, (request.session.get_expiry_age(),request.session.get_expiry_date()) - + if request.method == 'POST': form = AnnounceForm(request.session['data'],user=request.user) message = form.save(user=request.user,commit=True) - send_mail_text(None, + extra = {'Reply-To':message.reply_to} + send_mail_text(None, message.to, message.frm, message.subject, message.body, cc=message.cc, - bcc=message.bcc) + bcc=message.bcc, + extra=extra) # clear session request.session.clear() - + messages.success(request, 'The announcement was sent.') url = reverse('announcement') return HttpResponseRedirect(url) - + if request.session.get('data',None): data = request.session['data'] else: messages.error(request, 'No session data. Your session may have expired or cookies are disallowed.') redirect_url = reverse('announcement') return HttpResponseRedirect(redirect_url) - + if data['to'] == 'Other...': to = ','.join(data['to_custom']) else: to = data['to'] - + return render_to_response('announcement/confirm.html', { 'message': data, 'to': to}, diff --git a/ietf/secr/drafts/forms.py b/ietf/secr/drafts/forms.py index e2b764ac2..5d4d13846 100644 --- a/ietf/secr/drafts/forms.py +++ b/ietf/secr/drafts/forms.py @@ -13,16 +13,16 @@ import re from os.path import splitext # --------------------------------------------- -# Select Choices +# Select Choices # --------------------------------------------- WITHDRAW_CHOICES = (('ietf','Withdraw by IETF'),('author','Withdraw by Author')) # --------------------------------------------- -# Custom Fields +# Custom Fields # --------------------------------------------- class DocumentField(forms.FileField): '''A validating document upload field''' - + def __init__(self, unique=False, *args, **kwargs): self.extension = kwargs.pop('extension') self.filename = kwargs.pop('filename') @@ -36,7 +36,7 @@ class DocumentField(forms.FileField): m = re.search(r'.*-\d{2}\.(txt|pdf|ps|xml)', file.name) if not m: raise forms.ValidationError('File name must be in the form base-NN.[txt|pdf|ps|xml]') - + # ensure file extension is correct base,ext = os.path.splitext(file.name) if ext != self.extension: @@ -51,44 +51,44 @@ class DocumentField(forms.FileField): next_revision = str(int(self.rev)+1).zfill(2) if base[-2:] != next_revision: raise forms.ValidationError, "Expected revision # %s" % (next_revision) - + return file class GroupModelChoiceField(forms.ModelChoiceField): ''' - Custom ModelChoiceField sets queryset to include all active workgroups and the + Custom ModelChoiceField sets queryset to include all active workgroups and the individual submission group, none. Displays group acronyms as choices. Call it without the queryset argument, for example: - + group = GroupModelChoiceField(required=True) ''' def __init__(self, *args, **kwargs): kwargs['queryset'] = Group.objects.filter(type__in=('wg','individ'),state__in=('bof','proposed','active')).order_by('acronym') super(GroupModelChoiceField, self).__init__(*args, **kwargs) - + def label_from_instance(self, obj): return obj.acronym class AliasModelChoiceField(forms.ModelChoiceField): ''' - Custom ModelChoiceField, just uses Alias name in the select choices as opposed to the + Custom ModelChoiceField, just uses Alias name in the select choices as opposed to the more confusing alias -> doc format used by DocAlias.__unicode__ - ''' + ''' def label_from_instance(self, obj): return obj.name - + # --------------------------------------------- -# Forms +# Forms # --------------------------------------------- class AddModelForm(forms.ModelForm): start_date = forms.DateField() group = GroupModelChoiceField(required=True,help_text='Use group "none" for Individual Submissions') - + class Meta: model = Document fields = ('title','group','stream','start_date','pages','abstract','internal_comments') - - # use this method to set attrs which keeps other meta info from model. + + # use this method to set attrs which keeps other meta info from model. def __init__(self, *args, **kwargs): super(AddModelForm, self).__init__(*args, **kwargs) self.fields['title'].label='Document Name' @@ -104,17 +104,17 @@ class AuthorForm(forms.Form): ''' person = forms.CharField(max_length=50,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.") email = forms.CharField(widget=forms.Select(),help_text="Select an email") - - # check for id within parenthesis to ensure name was selected from the list + + # check for id within parenthesis to ensure name was selected from the list def clean_person(self): person = self.cleaned_data.get('person', '') m = re.search(r'(\d+)', person) if person and not m: - raise forms.ValidationError("You must select an entry from the list!") - + raise forms.ValidationError("You must select an entry from the list!") + # return person object return get_person(person) - + # check that email exists and return the Email object def clean_email(self): email = self.cleaned_data['email'] @@ -122,7 +122,7 @@ class AuthorForm(forms.Form): obj = Email.objects.get(address=email) except Email.ObjectDoesNoExist: raise forms.ValidationError("Email address not found!") - + # return email object return obj @@ -133,12 +133,12 @@ class EditModelForm(forms.ModelForm): group = GroupModelChoiceField(required=True) review_by_rfc_editor = forms.BooleanField(required=False) shepherd = forms.CharField(max_length=100,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.",required=False) - + class Meta: model = Document - fields = ('title','group','ad','shepherd','notify','stream','review_by_rfc_editor','name','rev','pages','intended_std_level','abstract','internal_comments') - - # use this method to set attrs which keeps other meta info from model. + fields = ('title','group','ad','shepherd','notify','stream','review_by_rfc_editor','name','rev','pages','intended_std_level','std_level','abstract','internal_comments') + + # use this method to set attrs which keeps other meta info from model. def __init__(self, *args, **kwargs): super(EditModelForm, self).__init__(*args, **kwargs) self.fields['ad'].queryset = Person.objects.filter(role__name='ad') @@ -146,40 +146,41 @@ class EditModelForm(forms.ModelForm): self.fields['title'].widget=forms.Textarea() self.fields['rev'].widget.attrs['size'] = 2 self.fields['abstract'].widget.attrs['cols'] = 72 - self.initial['state'] = self.instance.get_state() - self.initial['iesg_state'] = self.instance.get_state('draft-iesg') + self.initial['state'] = self.instance.get_state().pk + if self.instance.get_state('draft-iesg'): + self.initial['iesg_state'] = self.instance.get_state('draft-iesg').pk if self.instance.shepherd: self.initial['shepherd'] = "%s - (%s)" % (self.instance.shepherd.name, self.instance.shepherd.id) - + # setup special fields if self.instance: # setup replaced self.fields['review_by_rfc_editor'].initial = bool(self.instance.tags.filter(slug='rfc-rev')) - + def save(self, force_insert=False, force_update=False, commit=True): m = super(EditModelForm, self).save(commit=False) state = self.cleaned_data['state'] iesg_state = self.cleaned_data['iesg_state'] - + if 'state' in self.changed_data: m.set_state(state) - + # note we're not sending notices here, is this desired if 'iesg_state' in self.changed_data: if iesg_state == None: m.unset_state('draft-iesg') else: m.set_state(iesg_state) - + if 'review_by_rfc_editor' in self.changed_data: if self.cleaned_data.get('review_by_rfc_editor',''): m.tags.add('rfc-rev') else: m.tags.remove('rfc-rev') - + m.time = datetime.datetime.now() # handle replaced by - + if commit: m.save() return m @@ -188,19 +189,19 @@ class EditModelForm(forms.ModelForm): def clean_replaced_by(self): name = self.cleaned_data.get('replaced_by', '') if name and not InternetDraft.objects.filter(filename=name): - raise forms.ValidationError("ERROR: Draft does not exist") + raise forms.ValidationError("ERROR: Draft does not exist") return name - - # check for id within parenthesis to ensure name was selected from the list + + # check for id within parenthesis to ensure name was selected from the list def clean_shepherd(self): person = self.cleaned_data.get('shepherd', '') m = re.search(r'(\d+)', person) if person and not m: - raise forms.ValidationError("You must select an entry from the list!") - + raise forms.ValidationError("You must select an entry from the list!") + # return person object return get_person(person) - + def clean(self): super(EditModelForm, self).clean() cleaned_data = self.cleaned_data @@ -232,7 +233,7 @@ class EmailForm(forms.Form): class ExtendForm(forms.Form): expiration_date = forms.DateField() - + class ReplaceForm(forms.Form): replaced = AliasModelChoiceField(DocAlias.objects.none(),empty_label=None,help_text='This document may have more than one alias. Be sure to select the correct alias to replace.') replaced_by = forms.CharField(max_length=100,help_text='Enter the filename of the Draft which replaces this one.') @@ -241,7 +242,7 @@ class ReplaceForm(forms.Form): self.draft = kwargs.pop('draft') super(ReplaceForm, self).__init__(*args, **kwargs) self.fields['replaced'].queryset = DocAlias.objects.filter(document=self.draft) - + # field must contain filename of existing draft def clean_replaced_by(self): name = self.cleaned_data.get('replaced_by', '') @@ -262,49 +263,49 @@ class RevisionModelForm(forms.ModelForm): class Meta: model = Document fields = ('title','pages','abstract') - - # use this method to set attrs which keeps other meta info from model. + + # use this method to set attrs which keeps other meta info from model. def __init__(self, *args, **kwargs): super(RevisionModelForm, self).__init__(*args, **kwargs) self.fields['title'].label='Document Name' self.fields['title'].widget=forms.Textarea() self.fields['pages'].label='Number of Pages' - + class RfcModelForm(forms.ModelForm): rfc_number = forms.IntegerField() rfc_published_date = forms.DateField(initial=datetime.datetime.now) group = GroupModelChoiceField(required=True) - + class Meta: model = Document fields = ('title','group','pages','std_level','internal_comments') - - # use this method to set attrs which keeps other meta info from model. + + # use this method to set attrs which keeps other meta info from model. def __init__(self, *args, **kwargs): super(RfcModelForm, self).__init__(*args, **kwargs) self.fields['title'].widget = forms.Textarea() self.fields['std_level'].required = True - + def save(self, force_insert=False, force_update=False, commit=True): obj = super(RfcModelForm, self).save(commit=False) - + # create DocAlias DocAlias.objects.create(document=self.instance,name="rfc%d" % self.cleaned_data['rfc_number']) - + if commit: obj.save() return obj - + def clean_rfc_number(self): rfc_number = self.cleaned_data['rfc_number'] if DocAlias.objects.filter(name='rfc' + str(rfc_number)): raise forms.ValidationError("RFC %d already exists" % rfc_number) return rfc_number - + class RfcObsoletesForm(forms.Form): relation = forms.ModelChoiceField(queryset=DocRelationshipName.objects.filter(slug__in=('updates','obs')),required=False) rfc = forms.IntegerField(required=False) - + # ensure that RFC exists def clean_rfc(self): rfc = self.cleaned_data.get('rfc','') @@ -312,7 +313,7 @@ class RfcObsoletesForm(forms.Form): if not Document.objects.filter(docalias__name="rfc%s" % rfc): raise forms.ValidationError("RFC does not exist") return rfc - + def clean(self): super(RfcObsoletesForm, self).clean() cleaned_data = self.cleaned_data @@ -347,8 +348,8 @@ class UploadForm(forms.Form): for field in self.fields.itervalues(): field.filename = self.draft.name field.rev = self.draft.rev - - + + def clean(self): # Checks that all files have the same base if any(self.errors): @@ -358,7 +359,7 @@ class UploadForm(forms.Form): xml = self.cleaned_data['xml'] pdf = self.cleaned_data['pdf'] ps = self.cleaned_data['ps'] - + # we only need to do these validations for new drafts if not self.draft: names = [] @@ -367,19 +368,19 @@ class UploadForm(forms.Form): base = splitext(file.name)[0] if base not in names: names.append(base) - + if len(names) > 1: raise forms.ValidationError, "All files must have the same base name" - + # ensure that the basename is unique base = splitext(txt.name)[0] if Document.objects.filter(name=base[:-3]): raise forms.ValidationError, "This doucment filename already exists: %s" % base[:-3] - + # ensure that rev is 00 if base[-2:] != '00': raise forms.ValidationError, "New Drafts must start with 00 revision number." - + return self.cleaned_data class WithdrawForm(forms.Form): diff --git a/ietf/secr/drafts/views.py b/ietf/secr/drafts/views.py index ac68fa568..b1c096d2d 100644 --- a/ietf/secr/drafts/views.py +++ b/ietf/secr/drafts/views.py @@ -797,7 +797,8 @@ def edit(request, id): save_document_in_history(draft) DocEvent.objects.create(type='changed_document', by=request.user.get_profile(), - doc=draft) + doc=draft, + desc='Changed field(s): %s' % ','.join(form.changed_data)) # see EditModelForm.save() for detailed logic form.save() diff --git a/ietf/secr/groups/views.py b/ietf/secr/groups/views.py index 50745693b..5e9e739e5 100644 --- a/ietf/secr/groups/views.py +++ b/ietf/secr/groups/views.py @@ -37,7 +37,7 @@ def add_legacy_fields(group): query = GroupEvent.objects.filter(group=group, type="changed_state").order_by('time') proposed = query.filter(changestategroupevent__state="proposed") meeting = get_current_meeting() - + if proposed: group.proposed_date = proposed[0].time active = query.filter(changestategroupevent__state="active") @@ -46,27 +46,27 @@ def add_legacy_fields(group): concluded = query.filter(changestategroupevent__state="conclude") if concluded: group.concluded_date = concluded[0].time - + if group.session_set.filter(meeting__number=meeting.number): group.meeting_scheduled = 'YES' else: group.meeting_scheduled = 'NO' - + group.chairs = group.role_set.filter(name="chair") group.techadvisors = group.role_set.filter(name="techadv") group.editors = group.role_set.filter(name="editor") - group.secretaries = group.role_set.filter(name="secretaries") - + group.secretaries = group.role_set.filter(name="secr") + #fill_in_charter_info(group) - + #-------------------------------------------------- # AJAX Functions # ------------------------------------------------- ''' def get_ads(request): """ AJAX function which takes a URL parameter, "area" and returns the area directors - in the form of a list of dictionaries with "id" and "value" keys(in json format). - Used to populate select options. + in the form of a list of dictionaries with "id" and "value" keys(in json format). + Used to populate select options. """ results=[] @@ -83,7 +83,7 @@ def get_ads(request): # ------------------------------------------------- def add(request): - ''' + ''' Add a new IETF or IRTF Group **Templates:** @@ -119,11 +119,11 @@ def add(request): by=request.user.get_profile(), state=group.state, desc='Started group') - + messages.success(request, 'The Group was created successfully!') url = reverse('groups_view', kwargs={'acronym':group.acronym}) return HttpResponseRedirect(url) - + else: form = GroupModelForm(initial={'state':'active','type':'wg'}) awp_formset = AWPFormSet(prefix='awp') @@ -150,17 +150,17 @@ def blue_dot(request): entry = {'name':'%s, %s' % (parts[3], parts[1]), 'groups': ', '.join(groups)} chairs.append(entry) - + # sort the list sorted_chairs = sorted(chairs, key = lambda a: a['name']) - + return render_to_response('groups/blue_dot_report.txt', { 'chairs':sorted_chairs}, RequestContext(request, {}), mimetype="text/plain", ) - + def charter(request, acronym): - """ + """ View Group Charter **Templates:** @@ -179,7 +179,7 @@ def charter(request, acronym): charter_text = get_charter_text(group) else: charter_text = '' - + return render_to_response('groups/charter.html', { 'group': group, 'charter_text': charter_text}, @@ -187,7 +187,7 @@ def charter(request, acronym): ) def delete_role(request, acronym, id): - """ + """ Handle deleting roles for groups (chair, editor, advisor, secretary) **Templates:** @@ -199,18 +199,18 @@ def delete_role(request, acronym, id): """ group = get_object_or_404(Group, acronym=acronym) role = get_object_or_404(Role, id=id) - + # save group save_group_in_history(group) - + role.delete() - + messages.success(request, 'The entry was deleted successfully') url = reverse('groups_people', kwargs={'acronym':acronym}) return HttpResponseRedirect(url) def edit(request, acronym): - """ + """ Edit Group details **Templates:** @@ -235,16 +235,16 @@ def edit(request, acronym): form = GroupModelForm(request.POST, instance=group) awp_formset = AWPFormSet(request.POST, instance=group) if form.is_valid() and awp_formset.is_valid(): - + awp_formset.save() if form.changed_data: state = form.cleaned_data['state'] - + # save group save_group_in_history(group) - + form.save() - + # create appropriate GroupEvent if 'state' in form.changed_data: if state.name == 'Active': @@ -257,27 +257,29 @@ def edit(request, acronym): state=state, desc=desc) form.changed_data.remove('state') - + # if anything else was changed if form.changed_data: GroupEvent.objects.create(group=group, type='info_changed', by=request.user.get_profile(), desc='Info Changed') - + # if the acronym was changed we'll want to redirect using the new acronym below if 'acronym' in form.changed_data: acronym = form.cleaned_data['acronym'] - + messages.success(request, 'The Group was changed successfully') - + url = reverse('groups_view', kwargs={'acronym':acronym}) return HttpResponseRedirect(url) - + else: form = GroupModelForm(instance=group) awp_formset = AWPFormSet(instance=group) + messages.warning(request, "WARNING: don't use this tool to change group names. Use Datatracker when possible.") + return render_to_response('groups/edit.html', { 'group': group, 'awp_formset': awp_formset, @@ -286,7 +288,7 @@ def edit(request, acronym): ) def edit_gm(request, acronym): - """ + """ Edit IETF Group Goal and Milestone details **Templates:** @@ -295,7 +297,7 @@ def edit_gm(request, acronym): **Template Variables:** - * group, formset + * group, formset """ @@ -316,7 +318,7 @@ def edit_gm(request, acronym): return HttpResponseRedirect(url) else: formset = GMFormset(instance=group, prefix='goalmilestone') - + return render_to_response('groups/edit_gm.html', { 'group': group, 'formset': formset}, @@ -324,7 +326,7 @@ def edit_gm(request, acronym): ) def people(request, acronym): - """ + """ Edit Group Roles (Chairs, Secretary, etc) **Templates:** @@ -338,7 +340,7 @@ def people(request, acronym): """ group = get_object_or_404(Group, acronym=acronym) - + if request.method == 'POST': # we need to pass group for form validation form = RoleForm(request.POST,group=group) @@ -346,10 +348,10 @@ def people(request, acronym): name = form.cleaned_data['name'] person = form.cleaned_data['person'] email = form.cleaned_data['email'] - + # save group save_group_in_history(group) - + Role.objects.create(name=name, person=person, email=email, @@ -368,7 +370,7 @@ def people(request, acronym): ) def search(request): - """ + """ Search IETF Groups **Templates:** @@ -386,9 +388,9 @@ def search(request): if request.POST['submit'] == 'Add': url = reverse('groups_add') return HttpResponseRedirect(url) - + if form.is_valid(): - kwargs = {} + kwargs = {} group_acronym = form.cleaned_data['group_acronym'] group_name = form.cleaned_data['group_name'] primary_area = form.cleaned_data['primary_area'] @@ -396,7 +398,7 @@ def search(request): state = form.cleaned_data['state'] type = form.cleaned_data['type'] meeting = get_current_meeting() - + # construct seach query if group_acronym: kwargs['acronym__istartswith'] = group_acronym @@ -410,7 +412,7 @@ def search(request): kwargs['type'] = type #else: # kwargs['type__in'] = ('wg','rg','ietf','ag','sdo','team') - + if meeting_scheduled == 'YES': kwargs['session__meeting__number'] = meeting.number # perform query @@ -422,13 +424,13 @@ def search(request): else: qs = Group.objects.all() results = qs.order_by('acronym') - + # if there's just one result go straight to view if len(results) == 1: url = reverse('groups_view', kwargs={'acronym':results[0].acronym}) return HttpResponseRedirect(url) - - # process GET argument to support link from area app + + # process GET argument to support link from area app elif 'primary_area' in request.GET: area = request.GET.get('primary_area','') results = Group.objects.filter(parent__id=area,type='wg',state__in=('bof','active','proposed')).order_by('name') @@ -440,7 +442,7 @@ def search(request): # attribute of the meeting model for result in results: add_legacy_fields(result) - + return render_to_response('groups/search.html', { 'results': results, 'form': form}, @@ -448,7 +450,7 @@ def search(request): ) def view(request, acronym): - """ + """ View IETF Group details **Templates:** @@ -462,16 +464,16 @@ def view(request, acronym): """ group = get_object_or_404(Group, acronym=acronym) - + add_legacy_fields(group) - + return render_to_response('groups/view.html', { 'group': group}, RequestContext(request, {}), ) def view_gm(request, acronym): - """ + """ View IETF Group Goals and Milestones details **Templates:** diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index dfe14a2e3..1b1c37207 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -28,14 +28,15 @@ from forms import * import os import datetime +# prep for agenda changes # -------------------------------------------------- # Helper Functions # -------------------------------------------------- def build_timeslots(meeting,room=None): ''' - This function takes a Meeting object and an optional room argument. If room isn't passed we - pre-create the full set of timeslot records using the last meeting as a template. - If room is passed pre-create timeslots for the new room. Call this after saving new rooms + This function takes a Meeting object and an optional room argument. If room isn't passed we + pre-create the full set of timeslot records using the last meeting as a template. + If room is passed pre-create timeslots for the new room. Call this after saving new rooms or adding a room. ''' slots = meeting.timeslot_set.filter(type='session') @@ -50,7 +51,7 @@ def build_timeslots(meeting,room=None): source_meeting = meeting else: source_meeting = get_last_meeting(meeting) - + delta = meeting.date - source_meeting.date initial = [] timeslots = [] @@ -89,7 +90,7 @@ def build_nonsession(meeting): requested_by=system, status_id='sched') session.save() - + TimeSlot.objects.create(type=slot.type, meeting=meeting, session=session, @@ -97,11 +98,11 @@ def build_nonsession(meeting): time=new_time, duration=slot.duration, show_location=slot.show_location) - + def get_last_meeting(meeting): last_number = int(meeting.number) - 1 return Meeting.objects.get(number=last_number) - + def is_combined(session): ''' Check to see if this session is using two combined timeslots @@ -110,7 +111,7 @@ def is_combined(session): return True else: return False - + def make_directories(meeting): ''' This function takes a meeting object and creates the appropriate materials directories @@ -141,21 +142,21 @@ def send_notification(request, sessions): else: subject = '%s - Requested sessions have been scheduled for IETF %s' % (group.acronym, sessions[0].meeting.number) template = 'meetings/session_schedule_notification.txt' - + # easier to populate template from timeslot perspective. assuming one-to-one timeslot-session count = 0 session_info = '' data = [ (s,s.timeslot_set.all()[0]) for s in sessions ] for s,t in data: count += 1 - session_info += session_info_template.format(group.acronym, - count, + session_info += session_info_template.format(group.acronym, + count, s.requested_duration, t.time.strftime('%A'), t.name, '%s-%s' % (t.time.strftime('%H%M'),(t.time + t.duration).strftime('%H%M')), t.location) - + # send email context = {} context['to_name'] = sessions[0].requested_by @@ -191,9 +192,9 @@ def sort_groups(meeting): scheduled_groups.append(group) else: unscheduled_groups.append(group) - + return scheduled_groups, unscheduled_groups - + # ------------------------------------------------- # AJAX Functions # ------------------------------------------------- @@ -202,14 +203,14 @@ def ajax_get_times(request, meeting_id, day): Ajax function to get timeslot times for a given day. returns JSON format response: [{id:start_time, value:start_time-end_time},...] ''' - # TODO strip duplicates if there are any + # TODO strip duplicates if there are any results=[] room = Room.objects.filter(meeting__number=meeting_id)[0] slots = TimeSlot.objects.filter(meeting__number=meeting_id,time__week_day=day,location=room).order_by('time') for slot in slots: d = {'id': slot.time.strftime('%H%M'), 'value': '%s-%s' % (slot.time.strftime('%H%M'), slot.end_time().strftime('%H%M'))} results.append(d) - + return HttpResponse(simplejson.dumps(results), mimetype='application/javascript') # -------------------------------------------------- # STANDARD VIEW FUNCTIONS @@ -236,7 +237,7 @@ def add(request): form = MeetingModelForm(request.POST) if form.is_valid(): meeting = form.save() - + #Create Physical new meeting directory and subdirectories make_directories(meeting) @@ -258,9 +259,9 @@ def blue_sheet(request, meeting_id): Blue Sheet view. The user can generate blue sheets or upload scanned bluesheets ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + url = settings.SECR_BLUE_SHEET_URL - + if request.method == 'POST': form = UploadBlueSheetForm(request.POST,request.FILES) if form.is_valid(): @@ -269,26 +270,26 @@ def blue_sheet(request, meeting_id): messages.success(request, 'File Uploaded') url = reverse('meetings_blue_sheet', kwargs={'meeting_id':meeting.number}) return HttpResponseRedirect(url) - + else: form = UploadBlueSheetForm() - + return render_to_response('meetings/blue_sheet.html', { 'meeting': meeting, 'url': url, 'form': form}, RequestContext(request, {}), ) - + def blue_sheet_generate(request, meeting_id): ''' Generate bluesheets ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + groups = Group.objects.filter(session__meeting=meeting).order_by('acronym') create_blue_sheets(meeting, groups) - + messages.success(request, 'Blue Sheets generated') url = reverse('meetings_blue_sheet', kwargs={'meeting_id':meeting.number}) return HttpResponseRedirect(url) @@ -349,14 +350,14 @@ def main(request): In this view the user can choose a meeting to manage or elect to create a new meeting. ''' meetings = Meeting.objects.filter(type='ietf').order_by('-number') - + if request.method == 'POST': redirect_url = reverse('meetings_view', kwargs={'meeting_id':request.POST['group']}) return HttpResponseRedirect(redirect_url) - + choices = [ (str(x.number),str(x.number)) for x in meetings ] form = GroupSelectForm(choices=choices) - + return render_to_response('meetings/main.html', { 'form': form, 'meetings': meetings}, @@ -368,13 +369,13 @@ def non_session(request, meeting_id): Display and add "non-session" time slots, ie. registration, beverage and snack breaks ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + # if the Break/Registration records don't exist yet (new meeting) create them if not TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other')): build_nonsession(meeting) - + slots = TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other','plenary')).order_by('-type__name','time') - + if request.method == 'POST': form = NonSessionForm(request.POST) if form.is_valid(): @@ -387,9 +388,9 @@ def non_session(request, meeting_id): duration = form.cleaned_data['duration'] t = meeting.date + datetime.timedelta(days=int(day)) new_time = datetime.datetime(t.year,t.month,t.day,time.hour,time.minute) - + # create a dummy Session object to hold materials - # NOTE: we're setting group to none here, but the set_room page will force user + # NOTE: we're setting group to none here, but the set_room page will force user # to pick a legitimate group session = None if type.slug in ('other','plenary'): @@ -400,7 +401,7 @@ def non_session(request, meeting_id): requested_by=Person.objects.get(name='(system)'), status_id='sched') session.save() - + # create TimeSlot object TimeSlot.objects.create(type=form.cleaned_data['type'], meeting=meeting, @@ -409,16 +410,16 @@ def non_session(request, meeting_id): time=new_time, duration=duration, show_location=form.cleaned_data['show_location']) - + messages.success(request, 'Non-Sessions updated successfully') url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - else: + else: form = NonSessionForm(initial={'show_location':True}) - + if TimeSlot.objects.filter(meeting=meeting,type='other',location__isnull=True): messages.warning(request, 'There are non-session items which do not have a room assigned') - + return render_to_response('meetings/non_session.html', { 'slots': slots, 'form': form, @@ -429,7 +430,7 @@ def non_session(request, meeting_id): def non_session_delete(request, meeting_id, slot_id): ''' This function deletes the non-session TimeSlot. For "other" and "plenary" timeslot types - we need to delete the corresponding Session object as well. Check for uploaded material + we need to delete the corresponding Session object as well. Check for uploaded material first. ''' slot = get_object_or_404(TimeSlot, id=slot_id) @@ -438,11 +439,11 @@ def non_session_delete(request, meeting_id, slot_id): messages.error(request, 'Materials have already been uploaded for "%s". You must delete those before deleting the timeslot.' % slot.name) url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + else: slot.session.delete() slot.delete() - + messages.success(request, 'Non-Session timeslot deleted successfully') url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) @@ -459,7 +460,7 @@ def non_session_edit(request, meeting_id, slot_id): if button_text == 'Cancel': url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + form = NonSessionEditForm(request.POST,meeting=meeting, session=slot.session) if form.is_valid(): location = form.cleaned_data['location'] @@ -475,11 +476,11 @@ def non_session_edit(request, meeting_id, slot_id): session.name = name session.short = short session.save() - + messages.success(request, 'Location saved') url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + else: # we need to pass the session to the form in order to disallow changing # of group after materials have been uploaded @@ -488,14 +489,14 @@ def non_session_edit(request, meeting_id, slot_id): 'name':slot.session.name, 'short':slot.session.short} form = NonSessionEditForm(meeting=meeting,session=slot.session,initial=initial) - + return render_to_response('meetings/non_session_edit.html', { 'meeting': meeting, 'form': form, 'slot': slot}, RequestContext(request, {}), ) - + def remove_session(request, meeting_id, acronym): ''' Remove session from agenda. Disassociate session from timeslot and set status. @@ -506,16 +507,16 @@ def remove_session(request, meeting_id, acronym): group = get_object_or_404(Group, acronym=acronym) sessions = Session.objects.filter(meeting=meeting,group=group) now = datetime.datetime.now() - + for session in sessions: - timeslot = session.timeslot_set.all()[0] - timeslot.session = None - timeslot.modified = now - timeslot.save() + for timeslot in session.timeslot_set.all(): + timeslot.session = None + timeslot.modified = now + timeslot.save() session.status_id = 'canceled' session.modified = now session.save() - + messages.success(request, '%s Session removed from agenda' % (group.acronym)) url = reverse('meetings_select_group', kwargs={'meeting_id':meeting.number}) return HttpResponseRedirect(url) @@ -525,7 +526,7 @@ def rooms(request, meeting_id): Display and edit MeetingRoom records for the specified meeting ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + # if no rooms exist yet (new meeting) formset extra=10 first_time = not bool(meeting.room_set.all()) extra = 10 if first_time else 0 @@ -540,18 +541,18 @@ def rooms(request, meeting_id): formset = RoomFormset(request.POST, instance=meeting, prefix='room') if formset.is_valid(): formset.save() - + # if we are creating rooms for the first time create full set of timeslots if first_time: build_timeslots(meeting) - + # otherwise if we're modifying rooms else: # add timeslots for new rooms, deleting rooms automatically deletes timeslots for form in formset.forms[formset.initial_form_count():]: if form.instance.pk: build_timeslots(meeting,room=form.instance) - + messages.success(request, 'Meeting Rooms changed successfully') url = reverse('meetings_rooms', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) @@ -574,7 +575,7 @@ def schedule(request, meeting_id, acronym): legacy_session = get_initial_session(sessions) session_conflicts = session_conflicts_as_string(group, meeting) now = datetime.datetime.now() - + # build initial initial = [] for s in sessions: @@ -590,11 +591,11 @@ def schedule(request, meeting_id, acronym): if is_combined(s): d['combine'] = True initial.append(d) - + # need to use curry here to pass custom variable to form init NewSessionFormset = formset_factory(NewSessionForm, extra=0) NewSessionFormset.form = staticmethod(curry(NewSessionForm, meeting=meeting)) - + if request.method == 'POST': button_text = request.POST.get('submit', '') if button_text == 'Cancel': @@ -602,8 +603,8 @@ def schedule(request, meeting_id, acronym): return HttpResponseRedirect(url) formset = NewSessionFormset(request.POST,initial=initial) - extra_form = ExtraSessionForm(request.POST) - + extra_form = ExtraSessionForm(request.POST) + if formset.is_valid() and extra_form.is_valid(): # TODO formsets don't have has_changed until Django 1.3 has_changed = False @@ -617,11 +618,13 @@ def schedule(request, meeting_id, acronym): day = form.cleaned_data['day'] combine = form.cleaned_data.get('combine',None) session = Session.objects.get(id=id) - if session.timeslot_set.all(): - initial_timeslot = session.timeslot_set.all()[0] + was_combined = is_combined(session) + initial_timeslots = session.timeslot_set.all() + if initial_timeslots: + initial_timeslot = initial_timeslots[0] else: initial_timeslot = None - + # find new timeslot new_day = meeting.date + datetime.timedelta(days=int(day)-1) hour = datetime.time(int(time[:2]),int(time[2:])) @@ -640,68 +643,82 @@ def schedule(request, meeting_id, acronym): show_location=qs[0].show_location, modified=now) messages.warning(request, 'WARNING: There are now two sessions scheduled for the timeslot: %s' % timeslot) - + + # COMBINE SECTION - BEFORE -------------- + if 'combine' in form.changed_data and not combine: + next_slot = get_next_slot(initial_timeslot) + next_slot.session = None + next_slot.modified = now + next_slot.save() + # --------------------------------------- + if any(x in form.changed_data for x in ('day','time','room')): - # clear the old timeslot - if initial_timeslot: + # clear the old timeslot(s) + for ts in initial_timeslots: # if the initial timeslot is one of multiple we should delete it tqs = TimeSlot.objects.filter(meeting=meeting, type='session', - time=initial_timeslot.time, - location=initial_timeslot.location) + time=ts.time, + location=ts.location) if tqs.count() > 1: - initial_timeslot.delete() + ts.delete() else: - initial_timeslot.session = None - initial_timeslot.modified = now - initial_timeslot.save() + ts.session = None + ts.modified = now + ts.save() + # assign new timeslot(s) + new_slots = [] if timeslot: + new_slots.append(timeslot) + if was_combined: + new_slots.append(get_next_slot(timeslot)) + for ts in new_slots: timeslot.session = session timeslot.modified = now timeslot.save() + + if new_slots: session.status_id = 'sched' else: session.status_id = 'schedw' - + session.modified = now session.save() - + + if 'note' in form.changed_data: session.agenda_note = note session.modified = now session.save() - - # COMBINE SECTION ---------------------- - if 'combine' in form.changed_data: + + # COMBINE SECTION - AFTER --------------- + if 'combine' in form.changed_data and combine: next_slot = get_next_slot(timeslot) - if combine: - next_slot.session = session - else: - next_slot.session = None + next_slot.session = session next_slot.modified = now next_slot.save() # --------------------------------------- - + # notify. dont send if Tutorial, BOF or indicated on form notification_message = "No notification has been sent to anyone for this session." - if (has_changed + if (has_changed and not extra_form.cleaned_data.get('no_notify',False) and group.state.slug != 'bof' and session.timeslot_set.all()): # and the session is scheduled, else skip - + send_notification(request, sessions) notification_message = "Notification sent." - + if has_changed: messages.success(request, 'Session(s) Scheduled for %s. %s' % (group.acronym, notification_message)) - + url = reverse('meetings_select_group', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) else: formset = NewSessionFormset(initial=initial) extra_form = ExtraSessionForm() - + return render_to_response('meetings/schedule.html', { 'extra_form': extra_form, 'group': group, @@ -711,16 +728,16 @@ def schedule(request, meeting_id, acronym): 'formset': formset}, RequestContext(request, {}), ) - + def select_group(request, meeting_id): ''' In this view the user can select the group to schedule. Only those groups that have submitted session requests appear in the dropdowns. - + NOTE: BOF list includes Proposed Working Group type, per Wanda ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + if request.method == 'POST': group = request.POST.get('group',None) if group: @@ -728,24 +745,24 @@ def select_group(request, meeting_id): else: redirect_url = reverse('meetings_select_group',kwargs={'meeting_id':meeting_id}) messages.error(request, 'No group selected') - + return HttpResponseRedirect(redirect_url) - + # split groups into scheduled / unscheduled scheduled_groups, unscheduled_groups = sort_groups(meeting) - + # prep group form wgs = filter(lambda a: a.type_id in ('wg','ag') and a.state_id=='active', unscheduled_groups) group_form = GroupSelectForm(choices=build_choices(wgs)) - + # prep BOFs form bofs = filter(lambda a: a.type_id=='wg' and a.state_id in ('bof','proposed'), unscheduled_groups) bof_form = GroupSelectForm(choices=build_choices(bofs)) - + # prep IRTF form irtfs = filter(lambda a: a.type_id=='rg' and a.state_id in ('active','proposed'), unscheduled_groups) irtf_form = GroupSelectForm(choices=build_choices(irtfs)) - + return render_to_response('meetings/select_group.html', { 'group_form': group_form, 'bof_form': bof_form, @@ -754,17 +771,17 @@ def select_group(request, meeting_id): 'meeting': meeting}, RequestContext(request, {}), ) - + def times(request, meeting_id): ''' Display and edit time slots (TimeSlots). It doesn't display every TimeSlot - object for the meeting because there is one timeslot per time per room, + object for the meeting because there is one timeslot per time per room, rather it displays all the unique times. The first time this view is called for a meeting it creates a form with times prepopulated from the last meeting ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + # build list of timeslots slots = [] timeslots = [] @@ -778,7 +795,7 @@ def times(request, meeting_id): 'time':t.time, 'end_time':t.end_time()}) times = sorted(slots, key=lambda a: a['time']) - + if request.method == 'POST': form = TimeSlotForm(request.POST) if form.is_valid(): @@ -786,17 +803,17 @@ def times(request, meeting_id): time = form.cleaned_data['time'] duration = form.cleaned_data['duration'] name = form.cleaned_data['name'] - + t = meeting.date + datetime.timedelta(days=int(day)) new_time = datetime.datetime(t.year,t.month,t.day,time.hour,time.minute) - + # don't allow creation of timeslots with same start time as existing timeslots # assert False, (new_time, time_seen) if new_time in time_seen: messages.error(request, 'There is already a timeslot for %s. To change you must delete the old one first.' % new_time.strftime('%a %H:%M')) url = reverse('meetings_times', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + for room in meeting.room_set.all(): TimeSlot.objects.create(type_id='session', meeting=meeting, @@ -804,11 +821,11 @@ def times(request, meeting_id): time=new_time, location=room, duration=duration) - + messages.success(request, 'Timeslots created') url = reverse('meetings_times', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + else: form = TimeSlotForm() @@ -818,28 +835,28 @@ def times(request, meeting_id): 'times': times}, RequestContext(request, {}), ) - + def times_delete(request, meeting_id, time): ''' This view handles bulk delete of all timeslots matching time (datetime) for the given meeting. There is one timeslot for each room. ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + parts = [ int(x) for x in time.split(':') ] dtime = datetime.datetime(*parts) - + if Session.objects.filter(timeslot__time=dtime,timeslot__meeting=meeting): messages.error(request, 'ERROR deleting timeslot. There is one or more sessions scheduled for this timeslot.') url = reverse('meetings_times', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + TimeSlot.objects.filter(meeting=meeting,time=dtime).delete() - + messages.success(request, 'Timeslot deleted') url = reverse('meetings_times', kwargs={'meeting_id':meeting_id}) return HttpResponseRedirect(url) - + def view(request, meeting_id): ''' View Meeting information. @@ -854,7 +871,7 @@ def view(request, meeting_id): ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + return render_to_response('meetings/view.html', { 'meeting': meeting}, RequestContext(request, {}), diff --git a/ietf/secr/proceedings/forms.py b/ietf/secr/proceedings/forms.py index 385e6df66..2e4dd7cbc 100644 --- a/ietf/secr/proceedings/forms.py +++ b/ietf/secr/proceedings/forms.py @@ -13,7 +13,7 @@ import re # Globals # --------------------------------------------- -VALID_SLIDE_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt') +VALID_SLIDE_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt','.zip') VALID_MINUTES_EXTENSIONS = ('.txt','.html','.htm','.pdf') VALID_AGENDA_EXTENSIONS = ('.txt','.html','.htm') @@ -101,4 +101,4 @@ class UnifiedUploadForm(forms.Form): return cleaned_data - \ No newline at end of file + diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 191fe0bce..bc3f599af 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -37,7 +37,7 @@ def mycomp(timeslot): except AttributeError: key = None return key - + def get_progress_stats(sdate,edate): ''' This function takes a date range and produces a dictionary of statistics / objects for use @@ -46,7 +46,7 @@ def get_progress_stats(sdate,edate): data = {} data['sdate'] = sdate data['edate'] = edate - + # Activty Report Section new_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision', docevent__newrevisiondocevent__rev='00', @@ -61,20 +61,20 @@ def get_progress_stats(sdate,edate): data['updated'] += 1 if updates > 2: data['updated_more'] +=1 - + # calculate total documents updated, not counting new, rev=00 result = set() events = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lte=edate) for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'): result.add(e.doc) data['total_updated'] = len(result) - + # calculate sent last call data['last_call'] = events.filter(type='sent_last_call').count() - + # calculate approved data['approved'] = events.filter(type='iesg_approved').count() - + # get 4 weeks monday = Meeting.get_ietf_monday() cutoff = monday + datetime.timedelta(days=3) @@ -82,14 +82,14 @@ def get_progress_stats(sdate,edate): ff2_date = cutoff - datetime.timedelta(days=21) ff3_date = cutoff - datetime.timedelta(days=14) ff4_date = cutoff - datetime.timedelta(days=7) - + ff_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision', docevent__newrevisiondocevent__rev='00', docevent__time__gte=ff1_date, docevent__time__lte=cutoff) ff_new_count = ff_docs.count() ff_new_percent = format(ff_new_count / float(data['new']),'.0%') - + # calculate total documents updated in final four weeks, not counting new, rev=00 result = set() events = DocEvent.objects.filter(doc__type='draft',time__gte=ff1_date,time__lte=cutoff) @@ -97,48 +97,48 @@ def get_progress_stats(sdate,edate): result.add(e.doc) ff_update_count = len(result) ff_update_percent = format(ff_update_count / float(data['total_updated']),'.0%') - + data['ff_new_count'] = ff_new_count data['ff_new_percent'] = ff_new_percent data['ff_update_count'] = ff_update_count data['ff_update_percent'] = ff_update_percent - + # Progress Report Section data['docevents'] = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lte=edate) data['action_events'] = data['docevents'].filter(type='iesg_approved') data['lc_events'] = data['docevents'].filter(type='sent_last_call') - + data['new_groups'] = Group.objects.filter(type='wg', groupevent__changestategroupevent__state='active', groupevent__time__gte=sdate, groupevent__time__lte=edate) - + data['concluded_groups'] = Group.objects.filter(type='wg', groupevent__changestategroupevent__state='conclude', groupevent__time__gte=sdate, groupevent__time__lte=edate) - + data['new_docs'] = Document.objects.filter(type='draft').filter(docevent__type='new_revision', docevent__time__gte=sdate, docevent__time__lte=edate).distinct() - + data['rfcs'] = DocEvent.objects.filter(type='published_rfc', doc__type='draft', time__gte=sdate, time__lte=edate) - + # attach the ftp URL for use in the template for event in data['rfcs']: num = get_rfc_num(event.doc) event.ftp_url = 'ftp://ftp.ietf.org/rfc/rfc%s.txt' % num - + data['counts'] = {'std':data['rfcs'].filter(doc__intended_std_level__in=('ps','ds','std')).count(), 'bcp':data['rfcs'].filter(doc__intended_std_level='bcp').count(), 'exp':data['rfcs'].filter(doc__intended_std_level='exp').count(), 'inf':data['rfcs'].filter(doc__intended_std_level='inf').count()} - + return data - + def write_html(path,content): f = open(path,'w') f.write(content) @@ -156,7 +156,7 @@ def create_interim_directory(): Create static Interim Meeting directory pages that will live in a different URL space than the secretariat Django project ''' - + # produce date sorted output page = 'proceedings.html' meetings = InterimMeeting.objects.order_by('-date') @@ -165,7 +165,7 @@ def create_interim_directory(): f = open(path,'w') f.write(response.content) f.close() - + # produce group sorted output page = 'proceedings-bygroup.html' qs = InterimMeeting.objects.all() @@ -175,7 +175,7 @@ def create_interim_directory(): f = open(path,'w') f.write(response.content) f.close() - + def create_proceedings(meeting, group, is_final=False): ''' This function creates the proceedings html document. It gets called anytime there is an @@ -185,7 +185,7 @@ def create_proceedings(meeting, group, is_final=False): # abort, proceedings from meetings before 79 have a different format, don't overwrite if meeting.type_id == 'ietf' and int(meeting.number) < 79: return - + sessions = Session.objects.filter(meeting=meeting,group=group) if sessions: session = sessions[0] @@ -194,7 +194,7 @@ def create_proceedings(meeting, group, is_final=False): agenda = None minutes = None slides = None - + chairs = group.role_set.filter(name='chair') secretaries = group.role_set.filter(name='secr') if group.parent: # Certain groups like Tools Team do no have a parent @@ -202,7 +202,7 @@ def create_proceedings(meeting, group, is_final=False): else: ads = None tas = group.role_set.filter(name='techadv') - + docs = Document.objects.filter(group=group,type='draft').order_by('time') meeting_root = get_upload_root(meeting) @@ -213,15 +213,15 @@ def create_proceedings(meeting, group, is_final=False): settings.MEDIA_URL, meeting.date.strftime('%Y/%m/%d'), group.acronym) - + # Only do these tasks if we are running official proceedings generation, - # otherwise skip them for expediency. This procedure is called any time meeting + # otherwise skip them for expediency. This procedure is called any time meeting # materials are uploaded/deleted, and we don't want to do all this work each time. - + if is_final: # ---------------------------------------------------------------------- # Find active Drafts and RFCs, copy them to id and rfc directories - + drafts = docs.filter(states__slug='active') for draft in drafts: source = os.path.join(draft.get_file_path(),draft.filename_with_rev()) @@ -234,7 +234,7 @@ def create_proceedings(meeting, group, is_final=False): else: draft.bytes = 0 draft.url = url_root + "id/%s" % draft.filename_with_rev() - + rfcs = docs.filter(states__slug='rfc') for rfc in rfcs: # TODO should use get_file_path() here but is incorrect for rfcs @@ -245,7 +245,7 @@ def create_proceedings(meeting, group, is_final=False): target = os.path.join(meeting_root,'rfc') rfc.rmsg = '' rfc.msg = '' - + if not os.path.exists(target): os.makedirs(target) shutil.copy(source,target) @@ -282,7 +282,7 @@ def create_proceedings(meeting, group, is_final=False): # ---------------------------------------------------------------------- else: drafts = rfcs = bluesheets = None - + # the simplest way to display the charter is to place it in a
 block
     # however, because this forces a fixed-width font, different than the rest of
     # the document we modify the charter by adding replacing linefeeds with 
's @@ -292,8 +292,8 @@ def create_proceedings(meeting, group, is_final=False): else: charter = None ctime = None - - + + # rather than return the response as in a typical view function we save it as the snapshot # proceedings.html response = render_to_response('proceedings/proceedings.html',{ @@ -312,10 +312,10 @@ def create_proceedings(meeting, group, is_final=False): 'minutes': minutes, 'agenda': agenda} ) - + # save proceedings proceedings_path = get_proceedings_path(meeting,group) - + f = open(proceedings_path,'w') f.write(response.content) f.close() @@ -323,7 +323,7 @@ def create_proceedings(meeting, group, is_final=False): os.chmod(proceedings_path, 0664) except OSError: pass - + # rebuild the directory if meeting.type.slug == 'interim': create_interim_directory() @@ -335,20 +335,20 @@ def create_proceedings(meeting, group, is_final=False): def gen_areas(context): meeting = context['meeting'] gmet, gnot = groups_by_session(None,meeting) - + # append proceedings URL for group in gmet + gnot: group.proceedings_url = "%s/proceedings/%s/%s.html" % (settings.MEDIA_URL,meeting.number,group.acronym) - - for (counter,area) in enumerate(context['areas'], start=1): - groups_met = {'wg':filter(lambda a: a.parent==area and a.state.slug!='bof' and a.type_id=='wg',gmet), - 'bof':filter(lambda a: a.parent==area and a.state.slug=='bof' and a.type_id=='wg',gmet), + + for (counter,area) in enumerate(context['areas'], start=1): + groups_met = {'wg':filter(lambda a: a.parent==area and a.state.slug not in ('bof','bof-conc') and a.type_id=='wg',gmet), + 'bof':filter(lambda a: a.parent==area and a.state.slug in ('bof','bof-conc') and a.type_id=='wg',gmet), 'ag':filter(lambda a: a.parent==area and a.type_id=='ag',gmet)} - - groups_not = {'wg':filter(lambda a: a.parent==area and a.state.slug!='bof' and a.type_id=='wg',gnot), + + groups_not = {'wg':filter(lambda a: a.parent==area and a.state.slug not in ('bof','bof-conc') and a.type_id=='wg',gnot), 'bof':filter(lambda a: a.parent==area and a.state.slug=='bof' and a.type_id=='wg',gnot), 'ag':filter(lambda a: a.parent==area and a.type_id=='ag',gnot)} - + html = render_to_response('proceedings/area.html',{ 'area': area, 'meeting': meeting, @@ -356,63 +356,63 @@ def gen_areas(context): 'groups_not': groups_not, 'index': counter} ) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'%s.html' % area.acronym) write_html(path,html.content) def gen_acknowledgement(context): meeting = context['meeting'] - + html = render_to_response('proceedings/acknowledgement.html',{ 'meeting': meeting} ) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'acknowledgement.html') write_html(path,html.content) - + def gen_agenda(context): meeting = context['meeting'] - + #timeslots, update, meeting, venue, ads, plenaryw_agenda, plenaryt_agenda = agenda_info(meeting.number) timeslots = TimeSlot.objects.filter(meeting=meeting) - + # sort by area:group then time sort1 = sorted(timeslots, key = mycomp) sort2 = sorted(sort1, key = lambda a: a.time) - + html = render_to_response('proceedings/agenda.html',{ 'meeting': meeting, 'timeslots': sort2} ) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'agenda.html') write_html(path,html.content) - + # get the text agenda from datatracker url = 'https://datatracker.ietf.org/meeting/%s/agenda.txt' % meeting.number text = urlopen(url).read() path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'agenda.txt') write_html(path,text) - + def gen_attendees(context): meeting = context['meeting'] - + attendees = Registration.objects.using('ietf' + meeting.number).all().order_by('lname') - + html = render_to_response('proceedings/attendee.html',{ 'meeting': meeting, 'attendees': attendees} ) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'attendee.html') write_html(path,html.content) - + def gen_group_pages(context): meeting = context['meeting'] - + for group in Group.objects.filter(type__in=('wg','ag','rg'), state__in=('bof','proposed','active')): create_proceedings(meeting,group,is_final=True) - + def gen_index(context): index = render_to_response('proceedings/index.html',context) path = os.path.join(settings.SECR_PROCEEDINGS_DIR,context['meeting'].number,'index.html') @@ -421,36 +421,36 @@ def gen_index(context): def gen_irtf(context): meeting = context['meeting'] irtf_chair = Role.objects.filter(group__acronym='irtf',name='chair')[0] - + html = render_to_response('proceedings/irtf.html',{ 'irtf_chair':irtf_chair} ) path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'irtf.html') write_html(path,html.content) - + def gen_overview(context): meeting = context['meeting'] - + ietf_chair = Role.objects.get(group__acronym='ietf',name='chair') ads = Role.objects.filter(group__type='area',group__state='active',name='ad') sorted_ads = sorted(ads, key = lambda a: a.person.name_parts()[3]) - + html = render_to_response('proceedings/overview.html',{ 'meeting': meeting, 'ietf_chair': ietf_chair, 'ads': sorted_ads} ) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'overview.html') write_html(path,html.content) - + def gen_plenaries(context): ''' - This function generates pages for the Plenaries. At meeting 85 the Plenary sessions + This function generates pages for the Plenaries. At meeting 85 the Plenary sessions were combined into one, so we need to handle not finding one of the sessions. ''' meeting = context['meeting'] - + # Administration Plenary try: admin_session = Session.objects.get(meeting=meeting,name__contains='Administration Plenary') @@ -466,7 +466,7 @@ def gen_plenaries(context): write_html(path,admin.content) except Session.DoesNotExist: pass - + # Technical Plenary try: tech_session = Session.objects.get(meeting=meeting,name__contains='Technical Plenary') @@ -482,16 +482,16 @@ def gen_plenaries(context): write_html(path,tech.content) except Session.DoesNotExist: pass - + def gen_progress(context, final=True): ''' This function generates the Progress Report. This report is actually produced twice. First for inclusion in the Admin Plenary, then for the final proceedings. When produced the first - time we want to exclude the headers because they are broken links until all the proceedings + time we want to exclude the headers because they are broken links until all the proceedings are generated. ''' meeting = context['meeting'] - + # proceedings are run sometime after the meeting, so end date = the previous meeting # date and start date = the date of the meeting before that now = datetime.date.today() @@ -501,30 +501,30 @@ def gen_progress(context, final=True): data = get_progress_stats(start_date,end_date) data['meeting'] = meeting data['final'] = final - + html = render_to_response('proceedings/progress.html',data) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'progress-report.html') write_html(path,html.content) - + def gen_research(context): meeting = context['meeting'] gmet, gnot = groups_by_session(None,meeting) - + groups = filter(lambda a: a.type_id=='rg', gmet) - + # append proceedings URL for group in groups: group.proceedings_url = "%s/proceedings/%s/%s.html" % (settings.MEDIA_URL,meeting.number,group.acronym) - + html = render_to_response('proceedings/rg_irtf.html',{ 'meeting': meeting, 'groups': groups} ) - + path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'rg_irtf.html') write_html(path,html.content) - + def gen_training(context): meeting = context['meeting'] timeslots = context['others'] @@ -540,4 +540,4 @@ def gen_training(context): ) path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'train-%s.html' % counter ) write_html(path,html.content) - + diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index de172c99c..dddea1a13 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -48,7 +48,7 @@ import zipfile def build_choices(queryset): ''' - This function takes a queryset (or list) of Groups and builds a list of tuples for use + This function takes a queryset (or list) of Groups and builds a list of tuples for use as choices in a select widget. Using acronym for both value and label. ''' choices = [ (g.acronym,g.acronym) for g in queryset ] @@ -79,6 +79,17 @@ def get_doc_filename(doc): # TODO we might want to choose from among multiple files using some logic return files[0] +def get_extras(meeting): + ''' + Gather "extras" which are one off groups. ie iab-wcit(86) + ''' + groups = [] + sessions = Session.objects.filter(meeting=meeting).exclude(group__parent__acronym__in=('app','gen','int','ops','rai','rtg','sec','tsv','irtf')).filter(timeslot__type='session') + for session in sessions: + if session.materials.all(): + groups.append(session.group) + return groups + def get_next_interim_num(acronym,date): ''' This function takes a group acronym and date object and returns the next number to use for an @@ -92,13 +103,13 @@ def get_next_interim_num(acronym,date): return base + str(int(parts[-1]) + 1) else: return base + '1' - + def get_next_slide_num(session): ''' This function takes a session object and returns the next slide number to use for a newly added slide as a string. ''' - + """ slides = session.materials.filter(type='slides').order_by('-name') if slides: @@ -127,7 +138,7 @@ def get_next_order_num(session): next slide order number to use for a newly added slide as an integer. ''' max_order = session.materials.aggregate(Max('order'))['order__max'] - + return max_order + 1 if max_order else 1 # --- These could be properties/methods on meeting @@ -143,15 +154,15 @@ def get_proceedings_url(meeting,group=None): url = "%s/proceedings/%s/" % (settings.MEDIA_URL,meeting.number) if group: url = url + "%s.html" % group.acronym - + elif meeting.type_id == 'interim': url = "%s/proceedings/interim/%s/%s/proceedings.html" % ( settings.MEDIA_URL, meeting.date.strftime('%Y/%m/%d'), group.acronym) return url - -def handle_upload_file(file,filename,meeting,subdir): + +def handle_upload_file(file,filename,meeting,subdir): ''' This function takes a file object, a filename and a meeting object and subdir as string. It saves the file to the appropriate directory, get_upload_root() + subdir. @@ -159,21 +170,21 @@ def handle_upload_file(file,filename,meeting,subdir): zip file and unzips the file in the new directory. ''' base, extension = os.path.splitext(filename) - + if extension == '.zip': path = os.path.join(get_upload_root(meeting),subdir,base) if not os.path.exists(path): os.mkdir(path) else: path = os.path.join(get_upload_root(meeting),subdir) - + # agendas and minutes can only have one file instance so delete file if it already exists if subdir in ('agenda','minutes'): old_files = glob.glob(os.path.join(path,base) + '.*') for f in old_files: os.remove(f) - - destination = open(os.path.join(path,filename), 'wb+') + + destination = open(os.path.join(path,filename), 'wb+') for chunk in file.chunks(): destination.write(chunk) destination.close() @@ -193,13 +204,13 @@ def make_directories(meeting): target = os.path.join(path,leaf) if not os.path.exists(target): os.makedirs(target) - + def parsedate(d): ''' This function takes a date object and returns a tuple of year,month,day ''' return (d.strftime('%Y'),d.strftime('%m'),d.strftime('%d')) - + # ------------------------------------------------- # AJAX Functions # ------------------------------------------------- @@ -207,17 +218,19 @@ def parsedate(d): def ajax_generate_proceedings(request, meeting_num): ''' Ajax function which takes a meeting number and generates the proceedings - pages for the meeting. It returns a snippet of HTML that gets placed in the + pages for the meeting. It returns a snippet of HTML that gets placed in the Secretariat Only section of the select page. ''' meeting = get_object_or_404(Meeting, number=meeting_num) areas = Group.objects.filter(type='area',state='active').order_by('name') others = TimeSlot.objects.filter(meeting=meeting,type='other').order_by('time') + extras = get_extras(meeting) context = {'meeting':meeting, 'areas':areas, - 'others':others} + 'others':others, + 'extras':extras} proceedings_url = get_proceedings_url(meeting) - + # the acknowledgement page can be edited manually so only produce if it doesn't already exist path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'acknowledgement.html') if not os.path.exists(path): @@ -233,16 +246,16 @@ def ajax_generate_proceedings(request, meeting_num): gen_irtf(context) gen_research(context) gen_group_pages(context) - + # get the time proceedings were generated path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html') last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path)) - + return render_to_response('includes/proceedings_functions.html',{ 'meeting':meeting, 'last_run':last_run, 'proceedings_url':proceedings_url}, - RequestContext(request,{}), + RequestContext(request,{}), ) @jsonapi @@ -258,13 +271,13 @@ def ajax_order_slide(request): slide_name = request.POST.get('slide_name',None) order = request.POST.get('order',None) slide = get_object_or_404(Document, name=slide_name) - + # get all the slides for this session session = slide.session_set.all()[0] meeting = session.meeting group = session.group qs = session.materials.exclude(states__slug='deleted').filter(type='slides').order_by('order') - + # move slide and reorder list slides = list(qs) index = slides.index(slide) @@ -274,9 +287,9 @@ def ajax_order_slide(request): if item.order != ord: item.order = ord item.save() - + return {'success':True,'order':order,'slide':slide_name} - + # -------------------------------------------------- # STANDARD VIEW FUNCTIONS # -------------------------------------------------- @@ -288,13 +301,13 @@ def build(request,meeting_num,acronym): ''' meeting = Meeting.objects.get(number=meeting_num) group = get_object_or_404(Group,acronym=acronym) - + create_proceedings(meeting,group) - + messages.success(request,'proceedings.html was rebuilt') url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting_num,'acronym':acronym}) return HttpResponseRedirect(url) - + @check_permissions def delete_material(request,slide_id): ''' @@ -306,30 +319,30 @@ def delete_material(request,slide_id): session = doc.session_set.all()[0] meeting = session.meeting group = session.group - + path = get_full_path(doc) if path and os.path.exists(path): os.remove(path) - + # leave it related #session.materials.remove(doc) - + state = State.objects.get(type=doc.type,slug='deleted') doc.set_state(state) - + # create deleted_document DocEvent.objects.create(doc=doc, by=request.user.get_profile(), type='deleted') - + create_proceedings(meeting,group) - + messages.success(request,'The material was deleted successfully') if group.type.slug in ('wg','rg'): url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym}) else: url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id}) - + return HttpResponseRedirect(url) @sec_only @@ -342,17 +355,17 @@ def delete_interim_meeting(request, meeting_num): meeting = get_object_or_404(Meeting, number=meeting_num) sessions = Session.objects.filter(meeting=meeting) group = sessions[0].group - + # delete directories path = get_upload_root(meeting) - + # do a quick sanity check on this path before we go and delete it parts = path.split('/') assert parts[-1] == group.acronym - + if os.path.exists(path): shutil.rmtree(path) - + meeting.delete() sessions.delete() @@ -369,27 +382,27 @@ def edit_slide(request, slide_id): session = slide.session_set.all()[0] meeting = session.meeting group = session.group - + if group.type.slug in ('wg','rg'): url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym}) else: url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id}) - + if request.method == 'POST': # If the form has been submitted... button_text = request.POST.get('submit', '') if button_text == 'Cancel': return HttpResponseRedirect(url) - + form = EditSlideForm(request.POST, instance=slide) # A form bound to the POST data - if form.is_valid(): + if form.is_valid(): form.save() - + # rebuild proceedings.html create_proceedings(meeting,group) return HttpResponseRedirect(url) else: form = EditSlideForm(instance=slide) - + return render_to_response('proceedings/edit_slide.html',{ 'group': group, 'meeting':meeting, @@ -410,7 +423,7 @@ def interim(request, acronym): if button_text == 'Back': url = reverse('proceedings_select_interim') return HttpResponseRedirect(url) - + form = InterimMeetingForm(request.POST) # A form bound to the POST data if form.is_valid(): date = form.cleaned_data['date'] @@ -418,13 +431,13 @@ def interim(request, acronym): meeting=Meeting.objects.create(type_id='interim', date=date, number=number) - + # create session to associate this meeting with a group and hold material Session.objects.create(meeting=meeting, group=group, requested_by=request.user.get_profile(), status_id='sched') - + create_interim_directory() make_directories(meeting) @@ -433,9 +446,9 @@ def interim(request, acronym): return HttpResponseRedirect(url) else: form = InterimMeetingForm(initial={'group_acronym_id':acronym}) # An unbound form - + meetings = Meeting.objects.filter(type='interim',session__group__acronym=acronym).order_by('date') - + return render_to_response('proceedings/interim_meeting.html',{ 'group': group, 'meetings':meetings, @@ -444,7 +457,7 @@ def interim(request, acronym): ) def interim_directory(request, sortby=None): - + if sortby == 'group': qs = InterimMeeting.objects.all() meetings = sorted(qs, key=lambda a: a.group.acronym) @@ -474,27 +487,27 @@ def main(request): person = request.user.get_profile() except Person.DoesNotExist: return HttpResponseForbidden('ACCESS DENIED: user=%s' % request.META['REMOTE_USER']) - + if has_role(request.user,'Secretariat'): meetings = Meeting.objects.filter(type='ietf').order_by('-number') else: # select meetings still within the cutoff period meetings = Meeting.objects.filter(type='ietf',date__gt=datetime.datetime.today() - datetime.timedelta(days=settings.SUBMISSION_CORRECTION_DAYS)).order_by('number') - + groups = get_my_groups(request.user) interim_meetings = Meeting.objects.filter(type='interim',session__group__in=groups).order_by('-date') # tac on group for use in templates for m in interim_meetings: m.group = m.session_set.all()[0].group - + # we today's date to see if we're past the submissio cutoff today = datetime.date.today() - + return render_to_response('proceedings/main.html',{ 'meetings': meetings, 'interim_meetings': interim_meetings, 'today': today}, - RequestContext(request,{}), + RequestContext(request,{}), ) @check_permissions @@ -504,13 +517,13 @@ def move_slide(request, slide_id, direction): a direction argument which is a string [up|down]. ''' slide = get_object_or_404(Document, name=slide_id) - + # derive other objects session = slide.session_set.all()[0] meeting = session.meeting group = session.group qs = session.materials.exclude(states__slug='deleted').filter(type='slides').order_by('order') - + # if direction is up and we aren't already the first slide if direction == 'up' and slide_id != str(qs[0].pk): index = find_index(slide_id, qs) @@ -526,7 +539,7 @@ def move_slide(request, slide_id, direction): slide_after.order, slide.order = slide.order, slide_after.order slide.save() slide_after.save() - + if group.type.slug in ('wg','rg'): url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym}) else: @@ -539,7 +552,7 @@ def process_pdfs(request, meeting_num): This function is used to update the database once meeting materials in PPT format are converted to PDF format and uploaded to the server. It basically finds every PowerPoint slide document for the given meeting and checks to see if there is a PDF version. If there - is external_url is changed. Then when proceedings are generated the URL will refer to the + is external_url is changed. Then when proceedings are generated the URL will refer to the PDF document. ''' warn_count = 0 @@ -557,14 +570,14 @@ def process_pdfs(request, meeting_num): count += 1 else: warn_count += 1 - + if warn_count: messages.warning(request, '%s PDF files processed. %s PowerPoint files still not converted.' % (count, warn_count)) else: messages.success(request, '%s PDF files processed' % count) url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num}) return HttpResponseRedirect(url) - + @sec_only def progress_report(request, meeting_num): ''' @@ -572,10 +585,10 @@ def progress_report(request, meeting_num): ''' meeting = get_object_or_404(Meeting, number=meeting_num) gen_progress({'meeting':meeting},final=False) - + url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num}) return HttpResponseRedirect(url) - + @check_permissions def replace_slide(request, slide_id): ''' @@ -591,37 +604,37 @@ def replace_slide(request, slide_id): url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym}) else: url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id}) - + if request.method == 'POST': # If the form has been submitted... button_text = request.POST.get('submit', '') if button_text == 'Cancel': return HttpResponseRedirect(url) - + form = ReplaceSlideForm(request.POST,request.FILES,instance=slide) # A form bound to the POST data - if form.is_valid(): + if form.is_valid(): new_slide = form.save(commit=False) new_slide.time = datetime.datetime.now() - + file = request.FILES[request.FILES.keys()[0]] file_ext = os.path.splitext(file.name)[1] disk_filename = new_slide.name + file_ext handle_upload_file(file,disk_filename,meeting,'slides') - + new_slide.external_url = disk_filename new_slide.save() - + # create DocEvent uploaded DocEvent.objects.create(doc=slide, by=request.user.get_profile(), type='uploaded') - + # rebuild proceedings.html create_proceedings(meeting,group) - + return HttpResponseRedirect(url) else: form = ReplaceSlideForm(instance=slide) - + return render_to_response('proceedings/replace_slide.html',{ 'group': group, 'meeting':meeting, @@ -645,31 +658,31 @@ def select(request, meeting_num): else: messages.error(request, 'No Group selected') - + meeting = get_object_or_404(Meeting, number=meeting_num) user = request.user person = user.get_profile() groups_session, groups_no_session = groups_by_session(user, meeting) proceedings_url = get_proceedings_url(meeting) - + # get the time proceedings were generated path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html') if os.path.exists(path): last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path)) else: last_run = None - + # initialize group form wgs = filter(lambda x: x.type_id in ('wg','ag','team'),groups_session) group_form = GroupSelectForm(choices=build_choices(wgs)) - + # intialize IRTF form, only show if user is sec or irtf chair if has_role(user,'Secretariat') or person.role_set.filter(name__slug='chair',group__type__slug__in=('irtf','rg')): rgs = filter(lambda x: x.type_id == 'rg',groups_session) irtf_form = GroupSelectForm(choices=build_choices(rgs)) else: irtf_form = None - + # initialize Training form, this select widget needs to have a session id, because it's # utilmately the session that we associate material with # NOTE: there are two ways to query for the groups we want, the later seems more specific @@ -681,7 +694,7 @@ def select(request, meeting_num): training_form = GroupSelectForm(choices=choices) else: training_form = None - + # iniialize plenary form if has_role(user,['Secretariat','IETF Chair','IAB Chair']): choices = [] @@ -691,7 +704,7 @@ def select(request, meeting_num): plenary_form = GroupSelectForm(choices=choices) else: plenary_form = None - + # count PowerPoint files waiting to be converted if has_role(user,'Secretariat'): ppt = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.ppt').exclude(states__slug='deleted') @@ -699,7 +712,7 @@ def select(request, meeting_num): ppt_count = ppt.count() + pptx.count() else: ppt_count = 0 - + return render_to_response('proceedings/select.html', { 'group_form': group_form, 'irtf_form': irtf_form, @@ -709,7 +722,7 @@ def select(request, meeting_num): 'last_run': last_run, 'proceedings_url': proceedings_url, 'ppt_count': ppt_count}, - RequestContext(request,{}), + RequestContext(request,{}), ) def select_interim(request): @@ -720,17 +733,17 @@ def select_interim(request): if request.method == 'POST': redirect_url = reverse('proceedings_interim', kwargs={'acronym':request.POST['group']}) return HttpResponseRedirect(redirect_url) - + if request.user_is_secretariat: # initialize working groups form choices = build_choices(Group.objects.active_wgs()) group_form = GroupSelectForm(choices=choices) - + # per Alexa, not supporting Interim IRTF meetings at this time # intialize IRTF form #choices = build_choices(Group.objects.filter(type='wg', state='active') #irtf_form = GroupSelectForm(choices=choices) - + else: # these forms aren't used for non-secretariat groups = get_my_groups(request.user) @@ -738,11 +751,11 @@ def select_interim(request): group_form = GroupSelectForm(choices=choices) irtf_form = None training_form = None - + return render_to_response('proceedings/interim_select.html', { 'group_form': group_form}, #'irtf_form': irtf_form, - RequestContext(request,{}), + RequestContext(request,{}), ) @check_permissions @@ -750,9 +763,9 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): ''' This view is the main view for uploading / re-ordering material for regular and interim meetings. There are two urls.py entries which map to this view. The acronym_id option is used - most often for groups of regular and interim meetings. session_id is used for uploading + most often for groups of regular and interim meetings. session_id is used for uploading material for Training sessions (where group is not a unique identifier). We could have used - session_id all the time but this makes for an ugly URL which most of the time would be + session_id all the time but this makes for an ugly URL which most of the time would be avoided by using acronym. ''' meeting = get_object_or_404(Meeting, number=meeting_num) @@ -767,7 +780,7 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): session = get_object_or_404(Session, id=int(session_id)) group = session.group session_name = session.name - + if request.method == 'POST': button_text = request.POST.get('submit','') if button_text == 'Back': @@ -776,12 +789,12 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): else: url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num}) return HttpResponseRedirect(url) - + form = UnifiedUploadForm(request.POST,request.FILES) if form.is_valid(): material_type = form.cleaned_data['material_type'] slide_name = form.cleaned_data['slide_name'] - + file = request.FILES[request.FILES.keys()[0]] file_ext = os.path.splitext(file.name)[1] @@ -790,21 +803,21 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): filename = '%s-%s-%s' % (material_type.slug,meeting.number,group.acronym) elif meeting.type.slug == 'interim': filename = '%s-%s' % (material_type.slug,meeting.number) - + # NonSession material, use short name for shorter URLs if session.short: filename += "-%s" % session.short elif session_name: filename += "-%s" % slugify(session_name) # -------------------------------- - + if material_type.slug == 'slides': order_num = get_next_order_num(session) slide_num = get_next_slide_num(session) filename += "-%s" % slide_num - + disk_filename = filename + file_ext - + # create the Document object, in the case of slides the name will always be unique # so you'll get a new object, agenda and minutes will reuse doc object if it exists doc, created = Document.objects.get_or_create(type=material_type, @@ -827,13 +840,13 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): doc.save() DocAlias.objects.get_or_create(name=doc.name, document=doc) - + handle_upload_file(file,disk_filename,meeting,material_type.slug) - + # set Doc state state = State.objects.get(type=doc.type,slug='active') doc.set_state(state) - + # create session relationship, per Henrik we should associate documents to all sessions # for the current meeting (until tools support different materials for diff sessions) if sessions: @@ -841,7 +854,7 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): s.materials.add(doc) else: session.materials.add(doc) - + # create NewRevisionDocEvent instead of uploaded, per Ole NewRevisionDocEvent.objects.create(type='new_revision', by=request.user.get_profile(), @@ -849,26 +862,26 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None): rev=doc.rev, desc='New revision available', time=now) - + create_proceedings(meeting,group) messages.success(request,'File uploaded sucessfully') - + else: form = UnifiedUploadForm(initial={'meeting_id':meeting.id,'acronym':group.acronym,'material_type':'slides'}) - + agenda,minutes,slides = get_material(session) - + # gather DocEvents # include deleted material to catch deleted doc events docs = session.materials.all() docevents = DocEvent.objects.filter(doc__in=docs) - + path = get_proceedings_path(meeting,group) if os.path.exists(path): proceedings_url = get_proceedings_url(meeting,group) - else: + else: proceedings_url = '' - + return render_to_response('proceedings/upload_unified.html', { 'docevents': docevents, 'meeting': meeting, diff --git a/ietf/secr/rolodex/views.py b/ietf/secr/rolodex/views.py index 86b669755..076a51286 100644 --- a/ietf/secr/rolodex/views.py +++ b/ietf/secr/rolodex/views.py @@ -1,5 +1,6 @@ from django.contrib import messages from django.core.urlresolvers import reverse +from django.db import IntegrityError from django.db.models import Q from django.forms.formsets import formset_factory from django.forms.models import inlineformset_factory, modelformset_factory diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 4cb6fae20..cd28a622e 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -15,7 +15,7 @@ from ietf.ietfauth.decorators import has_role from ietf.utils.mail import send_mail from ietf.meeting.models import Meeting, Session, Constraint -from ietf.group.models import Group, Role +from ietf.group.models import Group, Role from ietf.name.models import SessionStatusName, ConstraintName from forms import * @@ -50,7 +50,7 @@ def get_initial_session(sessions): initial = {} # even if there are three sessions requested, the old form has 2 in this field initial['num_session'] = sessions.count() if sessions.count() <= 2 else 2 - + # accessing these foreign key fields throw errors if they are unset so we # need to catch these initial['length_session1'] = str(sessions[0].requested_duration.seconds) @@ -65,7 +65,7 @@ def get_initial_session(sessions): initial['conflict3'] = ' '.join([ c.target.acronym for c in conflicts.filter(name__slug='conflic3') ]) initial['comments'] = sessions[0].comments return initial - + def get_lock_message(): ''' Returns the message to display to non-secretariat users when the tool is locked. @@ -84,6 +84,19 @@ def get_meeting(): ''' return Meeting.objects.filter(type='ietf').order_by('-date')[0] +def get_requester_text(person,group): + ''' + This function takes a Person object and a Group object and returns the text to use in the + session request notification email, ie. Joe Smith, a Chair of the ancp working group + ''' + roles = group.role_set.filter(name__in=('chair','secr'),person=person) + if roles: + return '%s, a %s of the %s working group' % (person, roles[0].name, group.acronym) + if group.parent.role_set.filter(name='ad',person=person): + return '%s, a %s Area Director' % (person, group.parent.acronym.upper()) + if person.role_set.filter(name='secr',group__acronym='secretariat'): + return '%s, on behalf of the %s working group' % (person, group.acronym) + def save_conflicts(group, meeting, conflicts, name): ''' This function takes a Group, Meeting a string which is a list of Groups acronyms (conflicts), @@ -93,7 +106,7 @@ def save_conflicts(group, meeting, conflicts, name): acronyms = conflicts.replace(',',' ').split() for acronym in acronyms: target = Group.objects.get(acronym=acronym) - + constraint = Constraint(source=group, target=target, meeting=meeting, @@ -111,7 +124,7 @@ def send_notification(group,meeting,login,session,action): from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org') subject = '%s - New Meeting Session Request for IETF %s' % (group.acronym, meeting.number) template = 'sreq/session_request_notification.txt' - + # send email context = {} context['session'] = session @@ -119,12 +132,13 @@ def send_notification(group,meeting,login,session,action): context['meeting'] = meeting context['login'] = login context['header'] = 'A new' - + context['requester'] = get_requester_text(login,group) + # update overrides if action == 'update': subject = '%s - Update to a Meeting Session Request for IETF %s' % (group.acronym, meeting.number) context['header'] = 'An update to a' - + # if third session requested approval is required # change headers TO=ADs, CC=session-request, submitter and cochairs if session.get('length_session3',None): @@ -164,11 +178,11 @@ def approve(request, acronym): meeting = get_meeting() group = get_object_or_404(Group, acronym=acronym) session = Session.objects.get(meeting=meeting,group=group,status='apprw') - + if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.get_profile()): session.status = SessionStatusName.objects.get(slug='appr') session.save() - + messages.success(request, 'Third session approved') url = reverse('sessions_view', kwargs={'acronym':acronym}) return HttpResponseRedirect(url) @@ -184,33 +198,33 @@ def cancel(request, acronym): This view cancels a session request and sends a notification. To cancel, or withdraw the request set status = deleted. "canceled" status is used by the secretariat. - + NOTE: this function can also be called after a session has been - scheduled during the period when the session request tool is + scheduled during the period when the session request tool is reopened. In this case be sure to clear the timeslot assignment as well. ''' meeting = get_meeting() group = get_object_or_404(Group, acronym=acronym) sessions = Session.objects.filter(meeting=meeting,group=group).order_by('id') login = request.user.get_profile() - + # delete conflicts Constraint.objects.filter(meeting=meeting,source=group).delete() - + # mark sessions as deleted for session in sessions: session.status_id = 'deleted' session.save() - + # clear timeslot assignment if already scheduled if session.timeslot_set.all(): timeslot = session.timeslot_set.all()[0] timeslot.session = None timeslot.save() - + # log activity #add_session_activity(group,'Session was cancelled',meeting,user) - + # send notifitcation to_email = SESSION_REQUEST_EMAIL cc_list = get_cc_list(group, login) @@ -220,7 +234,7 @@ def cancel(request, acronym): {'login':login, 'group':group, 'meeting':meeting}, cc=cc_list) - + messages.success(request, 'The %s Session Request has been canceled' % group.acronym) url = reverse('sessions') return HttpResponseRedirect(url) @@ -237,23 +251,23 @@ def confirm(request, acronym): meeting = get_meeting() group = get_object_or_404(Group,acronym=acronym) login = request.user.get_profile() - + if request.method == 'POST': # clear http session data del request.session['session_form'] - + button_text = request.POST.get('submit', '') if button_text == 'Cancel': messages.success(request, 'Session Request has been canceled') url = reverse('sessions') return HttpResponseRedirect(url) - + # delete any existing session records with status = canceled or notmeet Session.objects.filter(group=group,meeting=meeting,status__in=('canceled','notmeet')).delete() - + # create new session records count = 0 - # lenth_session2 and length_session3 fields might be disabled by javascript and so + # lenth_session2 and length_session3 fields might be disabled by javascript and so # wouldn't appear in form data for duration in (form.get('length_session1',None),form.get('length_session2',None),form.get('length_session3',None)): count += 1 @@ -268,30 +282,30 @@ def confirm(request, acronym): comments=form['comments'], status=SessionStatusName.objects.get(slug=slug)) new_session.save() - + # write constraint records save_conflicts(group,meeting,form['conflict1'],'conflict') save_conflicts(group,meeting,form['conflict2'],'conflic2') save_conflicts(group,meeting,form['conflict3'],'conflic3') - + # deprecated in new schema # log activity #add_session_activity(group,'New session was requested',meeting,user) - + # clear not meeting Session.objects.filter(group=group,meeting=meeting,status='notmeet').delete() - + # send notification send_notification(group,meeting,login,form,'new') - + status_text = 'IETF Agenda to be scheduled' messages.success(request, 'Your request has been sent to %s' % status_text) url = reverse('sessions') return HttpResponseRedirect(url) - + # GET logic session_conflicts = session_conflicts_as_string(group, meeting) - + return render_to_response('sreq/confirm.html', { 'session': form, 'group': group, @@ -300,7 +314,7 @@ def confirm(request, acronym): ) @check_permissions -def edit(request, acronym): +def edit(request, acronym): ''' This view allows the user to edit details of the session request ''' @@ -311,13 +325,13 @@ def edit(request, acronym): initial = get_initial_session(sessions) session_conflicts = session_conflicts_as_string(group, meeting) login = request.user.get_profile() - + if request.method == 'POST': button_text = request.POST.get('submit', '') if button_text == 'Cancel': url = reverse('sessions_view', kwargs={'acronym':acronym}) return HttpResponseRedirect(url) - + form = SessionForm(request.POST,initial=initial) if form.is_valid(): if form.has_changed(): @@ -328,7 +342,7 @@ def edit(request, acronym): session = sessions[0] session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1'])) session.save() - + # session 2 if 'length_session2' in form.changed_data: length_session2 = form.cleaned_data['length_session2'] @@ -350,7 +364,7 @@ def edit(request, acronym): session = sessions[1] session.requested_duration = duration session.save() - + # session 3 if 'length_session3' in form.changed_data: length_session3 = form.cleaned_data['length_session3'] @@ -372,8 +386,8 @@ def edit(request, acronym): session = sessions[2] session.requested_duration = duration session.save() - - + + if 'attendees' in form.changed_data: sessions.update(attendees=form.cleaned_data['attendees']) if 'comments' in form.changed_data: @@ -387,21 +401,21 @@ def edit(request, acronym): if 'conflict3' in form.changed_data: Constraint.objects.filter(meeting=meeting,source=group,name='conflic3').delete() save_conflicts(group,meeting,form.cleaned_data['conflict3'],'conflic3') - + # deprecated # log activity #add_session_activity(group,'Session Request was updated',meeting,user) - + # send notification send_notification(group,meeting,login,form.cleaned_data,'update') - + messages.success(request, 'Session Request updated') url = reverse('sessions_view', kwargs={'acronym':acronym}) return HttpResponseRedirect(url) - + else: form = SessionForm(initial=initial) - + return render_to_response('sreq/edit.html', { 'meeting': meeting, 'form': form, @@ -413,22 +427,22 @@ def edit(request, acronym): def main(request): ''' Display list of groups the user has access to. - + Template variables form: a select box populated with unscheduled groups meeting: the current meeting - scheduled_sessions: + scheduled_sessions: ''' # check for locked flag is_locked = check_app_locked() - + if is_locked and not has_role(request.user,'Secretariat'): message = get_lock_message() return render_to_response('sreq/locked.html', { 'message': message}, RequestContext(request, {}), ) - + # TODO this is not currently used in the main template if request.method == 'POST': button_text = request.POST.get('submit', '') @@ -438,15 +452,15 @@ def main(request): else: redirect_url = reverse('sessions_new', kwargs={'acronym':request.POST['group']}) return HttpResponseRedirect(redirect_url) - + meeting = get_meeting() scheduled_groups,unscheduled_groups = groups_by_session(request.user, meeting) - + # load form select with unscheduled groups choices = zip([ g.pk for g in unscheduled_groups ], [ str(g) for g in unscheduled_groups ]) form = GroupSelectForm(choices=choices) - + # add session status messages for use in template for group in scheduled_groups: sessions = group.session_set.filter(meeting=meeting) @@ -454,12 +468,12 @@ def main(request): group.status_message = sessions[0].status else: group.status_message = 'First two sessions: %s, Third session: %s' % (sessions[0].status,sessions[2].status) - + # add not meeting indicators for use in template for group in unscheduled_groups: if group.session_set.filter(meeting=meeting,status='notmeet'): group.not_meeting = True - + return render_to_response('sreq/main.html', { 'is_locked': is_locked, 'form': form, @@ -475,18 +489,18 @@ def new(request, acronym): This view gathers details for a new session request. The user proceeds to confirm() to create the request. ''' - + group = get_object_or_404(Group, acronym=acronym) meeting = get_meeting() session_conflicts = session_conflicts_as_string(group, meeting) user = request.user - + if request.method == 'POST': button_text = request.POST.get('submit', '') if button_text == 'Cancel': url = reverse('sessions') return HttpResponseRedirect(url) - + form = SessionForm(request.POST) if form.is_valid(): # check if request already exists for this group @@ -494,13 +508,13 @@ def new(request, acronym): messages.warning(request, 'Sessions for working group %s have already been requested once.' % group.acronym) url = reverse('sessions') return HttpResponseRedirect(url) - + # save in user session request.session['session_form'] = form.data - + url = reverse('sessions_confirm',kwargs={'acronym':acronym}) return HttpResponseRedirect(url) - + # the "previous" querystring causes the form to be returned # pre-populated with data from last meeeting's session request elif request.method == 'GET' and request.GET.has_key('previous'): @@ -513,10 +527,10 @@ def new(request, acronym): initial = get_initial_session(previous_sessions) form = SessionForm(initial=initial) - + else: form = SessionForm() - + return render_to_response('sreq/new.html', { 'meeting': meeting, 'form': form, @@ -536,7 +550,7 @@ def no_session(request, acronym): meeting = get_meeting() group = get_object_or_404(Group, acronym=acronym) login = request.user.get_profile() - + # delete canceled record if there is one Session.objects.filter(group=group,meeting=meeting,status='canceled').delete() @@ -545,7 +559,7 @@ def no_session(request, acronym): messages.info(request, 'The group %s is already marked as not meeting' % group.acronym) url = reverse('sessions') return HttpResponseRedirect(url) - + session = Session(group=group, meeting=meeting, requested=datetime.datetime.now(), @@ -553,7 +567,7 @@ def no_session(request, acronym): requested_duration=0, status=SessionStatusName.objects.get(slug='notmeet')) session.save() - + # send notification to_email = SESSION_REQUEST_EMAIL cc_list = get_cc_list(group, login) @@ -563,12 +577,12 @@ def no_session(request, acronym): {'login':login, 'group':group, 'meeting':meeting}, cc=cc_list) - + # deprecated? # log activity #text = 'A message was sent to notify not having a session at IETF %d' % meeting.meeting_num #add_session_activity(group,text,meeting,request.person) - + # redirect messages.success(request, 'A message was sent to notify not having a session at IETF %s' % meeting.number) url = reverse('sessions') @@ -580,32 +594,32 @@ def tool_status(request): This view handles locking and unlocking of the tool to the public. ''' is_locked = check_app_locked() - + if request.method == 'POST': button_text = request.POST.get('submit', '') if button_text == 'Done': url = reverse('sessions') return HttpResponseRedirect(url) - + form = ToolStatusForm(request.POST) - + if button_text == 'Lock': if form.is_valid(): f = open(LOCKFILE,'w') f.write(form.cleaned_data['message']) f.close() - + messages.success(request, 'Session Request Tool is now Locked') url = reverse('sessions') return HttpResponseRedirect(url) - + elif button_text == 'Unlock': os.remove(LOCKFILE) - + messages.success(request, 'Session Request Tool is now Unlocked') url = reverse('sessions') return HttpResponseRedirect(url) - + else: if is_locked: message = get_lock_message() @@ -613,7 +627,7 @@ def tool_status(request): form = ToolStatusForm(initial=initial) else: form = ToolStatusForm() - + return render_to_response('sreq/tool_status.html', { 'is_locked': is_locked, 'form': form}, @@ -627,12 +641,12 @@ def view(request, acronym): meeting = get_meeting() group = get_object_or_404(Group, acronym=acronym) sessions = Session.objects.filter(~Q(status__in=('canceled','notmeet','deleted')),meeting=meeting,group=group).order_by('id') - + # if there are no session requests yet, redirect to new session request page if not sessions: redirect_url = reverse('sessions_new', kwargs={'acronym':acronym}) return HttpResponseRedirect(redirect_url) - + # TODO simulate activity records activities = [{'act_date':sessions[0].requested.strftime('%b %d, %Y'), 'act_time':sessions[0].requested.strftime('%H:%M:%S'), @@ -643,20 +657,20 @@ def view(request, acronym): 'act_time':sessions[0].scheduled.strftime('%H:%M:%S'), 'activity':'Session was scheduled', 'act_by':'Secretariat'}) - + # other groups that list this group in their conflicts session_conflicts = session_conflicts_as_string(group, meeting) show_approve_button = False - + # if sessions include a 3rd session waiting approval and the user is a secretariat or AD of the group # display approve button if sessions.filter(status='apprw'): if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.get_profile()): show_approve_button = True - + # build session dictionary (like querydict from new session request form) for use in template session = get_initial_session(sessions) - + return render_to_response('sreq/view.html', { 'session': session, 'activities': activities, diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index 3f4930d94..05d4e79bc 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -54,9 +54,9 @@ def get_doc_list(agenda): docs = [] for key in sorted(agenda['docs']): docs.extend(agenda['docs'][key]) - + return [x['obj'] for x in docs] - + def get_doc_writeup(doc): ''' This function takes a Document object and returns the ballot writeup for display @@ -72,7 +72,7 @@ def get_doc_writeup(doc): path = os.path.join(doc.get_file_path(),doc.filename_with_rev()) writeup = get_document_content(doc.name,path,split=False,markup=False) return writeup - + def get_last_telechat_date(): ''' This function returns the date of the last telechat @@ -80,37 +80,37 @@ def get_last_telechat_date(): ''' return TelechatDate.objects.filter(date__lt=datetime.date.today()).order_by('-date')[0].date #return '2011-11-01' # uncomment for testing - + def get_next_telechat_date(): ''' This function returns the date of the next telechat ''' return TelechatDate.objects.filter(date__gte=datetime.date.today()).order_by('date')[0].date - + def get_section_header(file,agenda): ''' - This function takes a filename and an agenda dictionary and returns the + This function takes a filename and an agenda dictionary and returns the agenda section header as a string for use in the doc template ''' h1 = {'2':'Protocol Actions','3':'Document Actions','4':'Working Group Actions'} - h2a = {'1':'WG Submissions','2':'Individual Submissions'} - h2b = {'1':'WG Submissions','2':'Individual Submissions via AD','3':'IRTF and Independent Submission Stream Documents'} + h2a = {'1':'WG Submissions','2':'Individual Submissions','3':'Status Changes'} + h2b = {'1':'WG Submissions','2':'Individual Submissions via AD','3':'Status Changes','4':'IRTF and Independent Submission Stream Documents'} h2c = {'1':'WG Creation','2':'WG Chartering'} h3a = {'1':'New Item','2':'Returning Item','3':'For Action'} h3b = {'1':'Proposed for IETF Review','2':'Proposed for Approval'} h3c = {'1':'Under Evaluation for IETF Review','2':'Proposed for Approval'} - + # Robert updated _agenda_data to return Document objects instead of the ID wrapper #doc = InternetDraft.objects.get(filename=file) doc = Document.objects.get(name=file) - + test = {'obj':doc} for k,v in agenda['docs'].iteritems(): if test in v: section = k count = '%s of %s' % (v.index(test) + 1, len(v)) break - + header = [ '%s %s' % (section[1], h1[section[1]]) ] if section[1] == '2': header.append('%s.%s %s' % (section[1], section[2], h2a[section[2]])) @@ -126,7 +126,7 @@ def get_section_header(file,agenda): else: header.append('%s.%s.%s %s' % (section[1], section[2], section[3], h3a[section[3]])) header.append(count) - + return header def get_first_doc(agenda): @@ -136,28 +136,28 @@ def get_first_doc(agenda): for k,v in sorted(agenda['docs'].iteritems()): if v: return v[0]['obj'] - + return None - + # ------------------------------------------------- # View Functions # ------------------------------------------------- def bash(request, date): - + agenda = _agenda_data(request, date=date) - + return render_to_response('telechat/bash.html', { 'agenda': agenda, 'date': date}, RequestContext(request, {}), ) - + def doc(request, date): ''' This view redirects to doc_detail using the first document in the agenda or displays the message "No Documents" ''' - + agenda = _agenda_data(request, date=date) doc = get_first_doc(agenda) if doc: @@ -170,32 +170,29 @@ def doc(request, date): 'document': None}, RequestContext(request, {}), ) - + def doc_detail(request, date, name): ''' This view displays the ballot information for the document, and lets the user make changes to ballot positions and document state. ''' doc = get_object_or_404(Document, docalias__name=name) - - # As of Datatracker v4.32, Conflict Review (conflrev) Document Types can - # be added to the Telechat agenda. We need to check the document type here - # and set the state_type for use later in the view + + # As of Datatracker v4.32, Conflict Review (conflrev) Document Types can + # be added to the Telechat agenda. If Document.type_id == draft use draft-iesg + # for state type + state_type = doc.type_id if doc.type_id == 'draft': state_type = 'draft-iesg' - elif doc.type_id == 'conflrev': - state_type = 'conflrev' - elif doc.type_id == 'charter': - state_type = 'charter' - + started_process = doc.latest_event(type="started_iesg_process") login = request.user.get_profile() - + if doc.active_ballot(): ballots = doc.active_ballot().active_ad_positions() # returns dict of ad:ballotpositiondocevent else: ballots = [] - + # setup form initials initial_ballot = [] open_positions = 0 @@ -205,19 +202,19 @@ def doc_detail(request, date, name): open_positions += 1 elif not ballots[key]: open_positions += 1 - + tags = doc.tags.filter(slug__in=TELECHAT_TAGS) tag = tags[0].pk if tags else None - + writeup = get_doc_writeup(doc) - + initial_state = {'state':doc.get_state(state_type).pk, 'substate':tag} - + BallotFormset = formset_factory(BallotForm, extra=0) agenda = _agenda_data(request, date=date) header = get_section_header(name,agenda) if name else '' - + # nav button logic doc_list = get_doc_list(agenda) nav_start = nav_end = False @@ -225,10 +222,10 @@ def doc_detail(request, date, name): nav_start = True if doc == doc_list[-1]: nav_end = True - + if request.method == 'POST': button_text = request.POST.get('submit', '') - + # logic from doc/views_ballot.py EditPositionRedesign if button_text == 'update_ballot': formset = BallotFormset(request.POST, initial=initial_ballot) @@ -250,12 +247,12 @@ def doc_detail(request, date, name): pos.desc = '[Ballot Position Update] Position for %s has been changed to %s by %s' % (ad.name, pos.pos.name, login.name) pos.save() has_changed = True - + if has_changed: messages.success(request,'Ballot position changed.') url = reverse('telechat_doc_detail', kwargs={'date':date,'name':name}) return HttpResponseRedirect(url) - + # logic from doc/views_draft.py change_state elif button_text == 'update_state': formset = BallotFormset(initial=initial_ballot) @@ -269,45 +266,45 @@ def doc_detail(request, date, name): # as if IESG tags are a substate prev_tag = doc.tags.filter(slug__in=(TELECHAT_TAGS)) prev_tag = prev_tag[0] if prev_tag else None - + #if state != prev or tag != prev_tag: if state_form.changed_data: save_document_in_history(doc) old_description = doc.friendly_state() - + if 'state' in state_form.changed_data: doc.set_state(state) - + if 'substate' in state_form.changed_data: if prev_tag: doc.tags.remove(prev_tag) if tag: doc.tags.add(tag) - + new_description = doc.friendly_state() e = log_state_changed(request, doc, login, new_description, old_description) doc.time = e.time doc.save() - + email_state_changed(request, doc, e.desc) email_ad(request, doc, doc.ad, login, e.desc) if state.slug == "lc-req": request_last_call(request, doc) - + messages.success(request,'Document state updated') url = reverse('telechat_doc_detail', kwargs={'date':date,'name':name}) - return HttpResponseRedirect(url) + return HttpResponseRedirect(url) else: formset = BallotFormset(initial=initial_ballot) state_form = ChangeStateForm(initial=initial_state) - + # if this is a conflict review document add referenced document if doc.type_id == 'conflrev': - conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document + conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document else: conflictdoc = None - + return render_to_response('telechat/doc.html', { 'date': date, 'document': doc, @@ -322,10 +319,10 @@ def doc_detail(request, date, name): 'nav_end': nav_end}, RequestContext(request, {}), ) - + def doc_navigate(request, date, name, nav): ''' - This view takes three arguments: + This view takes three arguments: date - the date of the Telechat name - the name of the current document being displayed nav - [next|previous] which direction the user wants to navigate in the list of docs @@ -334,22 +331,22 @@ def doc_navigate(request, date, name, nav): doc = get_object_or_404(Document, docalias__name=name) agenda = _agenda_data(request, date=date) target = name - + docs = get_doc_list(agenda) index = docs.index(doc) - + if nav == 'next' and index < len(docs) - 1: target = docs[index + 1].name elif nav == 'previous' and index != 0: target = docs[index - 1].name - + url = reverse('telechat_doc_detail', kwargs={'date':date,'name':target}) return HttpResponseRedirect(url) def main(request): ''' The is the main view where the user selects an existing telechat or creates a new one. - + NOTES ON EXTERNAL HELPER FUNCTIONS: _agenda_data(): returns dictionary of agenda sections get_ballot(name): returns a BallotWrapper and RfcWrapper or IdWrapper @@ -358,7 +355,7 @@ def main(request): date=request.POST['date'] url = reverse('telechat_doc', kwargs={'date':date}) return HttpResponseRedirect(url) - + choices = [ (d.date.strftime('%Y-%m-%d'), d.date.strftime('%Y-%m-%d')) for d in TelechatDate.objects.all() ] next_telechat = get_next_telechat_date().strftime('%Y-%m-%d') @@ -368,22 +365,22 @@ def main(request): 'form': form}, RequestContext(request, {}), ) - + def management(request, date): ''' This view displays management issues and lets the user update the status ''' - + agenda = _agenda_data(request, date=date) issues = TelechatAgendaItem.objects.filter(type=3).order_by('id') - + return render_to_response('telechat/management.html', { 'agenda': agenda, 'date': date, 'issues': issues}, RequestContext(request, {}), ) - + def minutes(request, date): ''' This view shows a list of documents that were approved since the last telechat @@ -398,9 +395,9 @@ def minutes(request, date): docs = [ e.doc for e in events ] pa_docs = [ d for d in docs if d.intended_std_level.slug not in ('inf','exp','hist') ] da_docs = [ d for d in docs if d.intended_std_level.slug in ('inf','exp','hist') ] - + agenda = _agenda_data(request, date=date) - + return render_to_response('telechat/minutes.html', { 'agenda': agenda, 'date': date, @@ -409,7 +406,7 @@ def minutes(request, date): 'da_docs': da_docs}, RequestContext(request, {}), ) - + def new(request): ''' This view creates a new telechat agenda and redirects to the default view @@ -418,17 +415,17 @@ def new(request): date = request.POST['date'] # create legacy telechat record Telechat.objects.create(telechat_date=date) - + messages.success(request,'New Telechat Agenda created') url = reverse('telechat_doc', kwargs={'date':date,'name':name}) - return HttpResponseRedirect(url) - + return HttpResponseRedirect(url) + def roll_call(request, date): - + agenda = _agenda_data(request, date=date) ads = Person.objects.filter(role__name='ad') sorted_ads = sorted(ads, key = lambda a: a.name_parts()[3]) - + return render_to_response('telechat/roll_call.html', { 'agenda': agenda, 'date': date, diff --git a/ietf/secr/templates/announcement/confirm.html b/ietf/secr/templates/announcement/confirm.html index 2b74516a8..69441ccbf 100644 --- a/ietf/secr/templates/announcement/confirm.html +++ b/ietf/secr/templates/announcement/confirm.html @@ -6,7 +6,7 @@ {% endblock %} -{% block breadcrumbs %}{{ block.super }} +{% block breadcrumbs %}{{ block.super }} » Announcement {% endblock %} @@ -14,9 +14,9 @@

Announcement

- +
- +
 To: {{ to }}
 From: {{ message.frm }}
@@ -27,10 +27,11 @@ Subject: {{ message.subject }}
 
 {{ message.body }}
 
- +
  • +
diff --git a/ietf/secr/templates/drafts/edit.html b/ietf/secr/templates/drafts/edit.html index 066c22137..eeb4ed7ee 100644 --- a/ietf/secr/templates/drafts/edit.html +++ b/ietf/secr/templates/drafts/edit.html @@ -9,10 +9,10 @@ {% endblock %} -{% block breadcrumbs %}{{ block.super }} +{% block breadcrumbs %}{{ block.super }} » Drafts » {{ draft.name }} - » Edit + » Edit {% endblock %} {% block content %} @@ -36,12 +36,13 @@ Abstract:{{ form.abstract.errors }}{{ form.abstract }} Expiration Date:{{ draft.expires|date:"M. d, Y" }} Intended Std Level:{{ form.intended_std_level.errors }}{{ form.intended_std_level }} + Standardization Level:{{ form.std_level.errors }}{{ form.std_level }} RFC Number:{{ draft.rfc_number }} Comments:{{ form.internal_comments.errors }}{{ form.internal_comments }} Replaced by:{{ form.replaced_by.errors }}{{ form.replaced_by }} Last Modified Date:{{ draft.time }} - +
  • diff --git a/ietf/secr/templates/drafts/view.html b/ietf/secr/templates/drafts/view.html index c2f54bfd4..4cbb87851 100644 --- a/ietf/secr/templates/drafts/view.html +++ b/ietf/secr/templates/drafts/view.html @@ -6,7 +6,7 @@ {% endblock %} -{% block breadcrumbs %}{{ block.super }} +{% block breadcrumbs %}{{ block.super }} » Drafts » {{ draft.name }} {% endblock %} @@ -51,6 +51,7 @@ Abstract:Click here to view the abstract Expiration Date:{{ draft.expires|date:"M. d, Y" }} Intended Status:{{ draft.intended_std_level|default_if_none:"" }} + Standardization Level:{{ draft.std_level|default_if_none:"" }} RFC Number:{{ draft.rfc_number|default_if_none:"" }} Comments:{{ draft.internal_comments|default_if_none:"" }} Last Modified Date:{{ draft.time }} diff --git a/ietf/secr/templates/includes/sessions_footer.html b/ietf/secr/templates/includes/sessions_footer.html index 9f10c7caa..5f0bbd0d3 100755 --- a/ietf/secr/templates/includes/sessions_footer.html +++ b/ietf/secr/templates/includes/sessions_footer.html @@ -1,4 +1,4 @@
  • Instructions.
  • -
  • IETF Meeting Materials Management Tool.
  • +
  • IETF Meeting Materials Management Tool.
  • If you require assistance in using this tool, or wish to report a bug, then please send a message to ietf-action@ietf.org.
  • To submit your request via email, please send your request to agenda@ietf.org.
  • diff --git a/ietf/secr/templates/ipradmin/notify.html b/ietf/secr/templates/ipradmin/notify.html index 3f1213f99..5c0c8807d 100644 --- a/ietf/secr/templates/ipradmin/notify.html +++ b/ietf/secr/templates/ipradmin/notify.html @@ -3,7 +3,7 @@ {% block title %} IPR Admin Notify Page {% endblock %} - + {% block extrahead %} {% endblock %} @@ -33,26 +33,28 @@ IPR Admin Notify Page {% ifequal page_id 'detail_notify' %} - +

    Notification to the submitter of IPR that's being updated



    - +

    {% endifequal %} @@ -87,7 +89,7 @@ IETF Secretariat

    Notification to the submitter of IPR that's being updated

    Please change the DATE and UPDATE NAME

    - + diff --git a/ietf/secr/templates/proceedings/index.html b/ietf/secr/templates/proceedings/index.html index 8ef774f7a..da7b487a7 100644 --- a/ietf/secr/templates/proceedings/index.html +++ b/ietf/secr/templates/proceedings/index.html @@ -10,11 +10,11 @@

    1. Introduction


    2. Area, Working Group and BoF Reports

    @@ -26,8 +26,8 @@

    3. Plenaries


    4. Training

    @@ -39,9 +39,18 @@

    5. Internet Research Task Force


    +{% if extras %} +

    6. Other

    + +{% endif %} {% endblock %} diff --git a/ietf/secr/templates/proceedings/proceedings.html b/ietf/secr/templates/proceedings/proceedings.html index ec32cf9ea..21e2d2f61 100644 --- a/ietf/secr/templates/proceedings/proceedings.html +++ b/ietf/secr/templates/proceedings/proceedings.html @@ -42,7 +42,9 @@ and end with {% endif %} +{% if group.type.slug == "wg" %}

    Additional information is available at tools.ietf.org/wg/{{ group.acronym }} +{% endif %}
    diff --git a/ietf/secr/templates/proceedings/upload_unified.html b/ietf/secr/templates/proceedings/upload_unified.html index d2f144b05..72fb2f6b3 100755 --- a/ietf/secr/templates/proceedings/upload_unified.html +++ b/ietf/secr/templates/proceedings/upload_unified.html @@ -83,7 +83,7 @@