Merge in changes from community-list-cleanup

- Legacy-Id: 10968
This commit is contained in:
Ole Laursen 2016-03-23 13:31:24 +00:00
commit c061caaf04
61 changed files with 2003 additions and 1924 deletions

View file

@ -1,23 +0,0 @@
SIGNIFICANT_STATES = [
'Adopted by a WG',
'In WG Last Call',
'WG Consensus: Waiting for Write-Up',
'Parked WG Document',
'Dead WG Document',
'Active IAB Document',
'Community Review',
'Sent to the RFC Editor',
'Active RG Document',
'In RG Last Call',
'Awaiting IRSG Reviews',
'In IESG Review',
'Document on Hold Based On IESG Request',
'Submission Received',
'In ISE Review',
'In IESG Review',
'RFC Published',
'Dead',
'IESG Evaluation',
'Publication Requested',
'In Last Call',
]

View file

@ -1,203 +0,0 @@
import datetime
from django.core.urlresolvers import reverse as urlreverse
from ietf.doc.models import DocAlias
class DisplayField(object):
codename = ''
description = ''
rfcDescription = ''
def get_value(self, document, raw=False):
return None
class FilenameField(DisplayField):
codename = 'filename'
description = 'I-D filename'
rfcDescription = 'RFC Number'
def get_value(self, document, raw=False):
if not raw:
return '<a href="%s">%s</a>' % (document.get_absolute_url(), document.canonical_name())
else:
return document.canonical_name()
class TitleField(DisplayField):
codename = 'title'
description = 'I-D title'
rfcDescription = 'RFC title'
def get_value(self, document, raw=False):
return document.title
class DateField(DisplayField):
codename = 'date'
description = 'Last revision'
rfcDescription = 'Published'
def get_value(self, document, raw=False):
date = document.latest_event(type='new_revision')
if date:
return date.time.strftime('%Y-%m-%d')
return document.time.strftime('%Y-%m-%d')
class StatusField(DisplayField):
codename = 'status'
description = 'Status in the IETF process'
rfcDescription = description
def get_value(self, document, raw=False):
draft_state = document.get_state('draft')
stream_state = document.get_state('draft-stream-%s' % (document.stream.slug)) if document.stream else None
iesg_state = document.get_state('draft-iesg') or ''
rfceditor_state = document.get_state('draft-rfceditor')
if draft_state.slug == 'rfc':
state = draft_state.name
else:
state = ""
if stream_state:
state = state + ("%s<br/>" % stream_state.name)
if iesg_state:
state = state + ("%s<br/>" % iesg_state.name)
if rfceditor_state:
state = state + ("%s<br/>" % rfceditor_state.name)
#
if draft_state.slug == 'rfc':
tags = ""
else:
tags = [ tag.name for tag in document.tags.all() ]
if tags:
tags = '[%s]' % ",".join(tags)
else:
tags = ''
return '%s<br/>%s' % (state, tags)
class WGField(DisplayField):
codename = 'wg_rg'
description = 'Associated WG or RG'
rfcDescription = description
def get_value(self, document, raw=False):
if raw or not document.group.type_id in ['wg','rg']:
return document.group.acronym
else:
return '<a href="%s">%s</a>' % (urlreverse('group_home', kwargs=dict(group_type=document.group.type_id, acronym=document.group.acronym)), document.group.acronym) if (document.group and document.group.acronym != 'none') else ''
class ADField(DisplayField):
codename = 'ad'
description = 'Associated AD, if any'
rfcDescription = description
def get_value(self, document, raw=False):
return document.ad or ''
class OneDayField(DisplayField):
codename = '1_day'
description = 'Changed within the last 1 day'
rfcDescription = description
def get_value(self, document, raw=False):
now = datetime.datetime.now()
last = now - datetime.timedelta(days=1)
if document.docevent_set.filter(time__gte=last):
return raw and 'YES' or '&#10004;'
return ''
class TwoDaysField(DisplayField):
codename = '2_days'
description = 'Changed within the last 2 days'
rfcDescription = description
def get_value(self, document, raw=False):
now = datetime.datetime.now()
last = now - datetime.timedelta(days=2)
if document.docevent_set.filter(time__gte=last):
return raw and 'YES' or '&#10004;'
return ''
class SevenDaysField(DisplayField):
codename = '7_days'
description = 'Changed within the last 7 days'
rfcDescription = description
def get_value(self, document, raw=False):
now = datetime.datetime.now()
last = now - datetime.timedelta(days=7)
if document.docevent_set.filter(time__gte=last):
return raw and 'YES' or '&#10004;'
return ''
TYPES_OF_DISPLAY_FIELDS = [(i.codename, i.description) for i in DisplayField.__subclasses__()]
class SortMethod(object):
codename = ''
description = ''
def get_sort_field(self):
return 'pk'
class FilenameSort(SortMethod):
codename = 'by_filename'
description = 'Alphabetical by I-D filename and RFC number'
def get_sort_field(self):
return 'name'
def get_full_rfc_sort(self, documents):
return [i.document for i in DocAlias.objects.filter(document__in=documents, name__startswith='rfc').order_by('name')]
class TitleSort(SortMethod):
codename = 'by_title'
description = 'Alphabetical by document title'
def get_sort_field(self):
return 'title'
class WGSort(SortMethod):
codename = 'by_wg'
description = 'Alphabetical by associated WG'
def get_sort_field(self):
return 'group__name'
class PublicationSort(SortMethod):
codename = 'date_publication'
description = 'Date of publication of current version of the document'
def get_sort_field(self):
return '-documentchangedates__new_version_date'
class ChangeSort(SortMethod):
codename = 'recent_change'
description = 'Date of most recent change of status of any type'
def get_sort_field(self):
return '-documentchangedates__normal_change_date'
class SignificantSort(SortMethod):
codename = 'recent_significant'
description = 'Date of most recent significant change of status'
def get_sort_field(self):
return '-documentchangedates__significant_change_date'
TYPES_OF_SORT = [(i.codename, i.description) for i in SortMethod.__subclasses__()]

View file

@ -1,101 +1,114 @@
import hashlib
import datetime
from django import forms
from django.conf import settings
from django.contrib.sites.models import Site
from django.db.models import Q
from ietf.utils.mail import send_mail
from ietf.community.models import Rule, DisplayConfiguration, RuleManager
from ietf.community.display import DisplayField
from ietf.community.models import SearchRule, EmailSubscription
from ietf.doc.fields import SearchableDocumentsField
from ietf.person.models import Person
from ietf.person.fields import SearchablePersonField
class AddDocumentsForm(forms.Form):
documents = SearchableDocumentsField(label="Add documents to track", doc_type="draft")
class RuleForm(forms.ModelForm):
class SearchRuleTypeForm(forms.Form):
rule_type = forms.ChoiceField(choices=[('', '--------------')] + SearchRule.RULE_TYPES)
class SearchRuleForm(forms.ModelForm):
person = SearchablePersonField()
class Meta:
model = Rule
fields = ('rule_type', 'value')
model = SearchRule
fields = ('state', 'group', 'person', 'text')
def __init__(self, *args, **kwargs):
self.clist = kwargs.pop('clist', None)
super(RuleForm, self).__init__(*args, **kwargs)
def __init__(self, clist, rule_type, *args, **kwargs):
kwargs["prefix"] = rule_type # add prefix to avoid mixups in the Javascript
super(SearchRuleForm, self).__init__(*args, **kwargs)
def save(self):
self.instance.community_list = self.clist
super(RuleForm, self).save()
def restrict_state(state_type, slug=None):
f = self.fields['state']
f.queryset = f.queryset.filter(used=True).filter(type=state_type)
if slug:
f.queryset = f.queryset.filter(slug=slug)
if len(f.queryset) == 1:
f.initial = f.queryset[0].pk
f.widget = forms.HiddenInput()
def get_all_options(self):
result = []
for i in RuleManager.__subclasses__():
options = i(None).options()
if options:
result.append({'type': i.codename,
'options': options})
return result
if rule_type in ['group', 'group_rfc', 'area', 'area_rfc']:
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
class DisplayForm(forms.ModelForm):
if rule_type.startswith("area"):
self.fields["group"].label = "Area"
self.fields["group"].queryset = self.fields["group"].queryset.filter(Q(type="area") | Q(acronym="irtf")).order_by("acronym")
else:
self.fields["group"].queryset = self.fields["group"].queryset.filter(type__in=("wg", "rg")).order_by("acronym")
del self.fields["person"]
del self.fields["text"]
elif rule_type.startswith("state_"):
mapping = {
"state_iab": "draft-stream-iab",
"state_iana": "draft-iana-review",
"state_iesg": "draft-iesg",
"state_irtf": "draft-stream-irtf",
"state_ise": "draft-stream-ise",
"state_rfceditor": "draft-rfceditor",
"state_ietf": "draft-stream-ietf",
}
restrict_state(mapping[rule_type])
del self.fields["group"]
del self.fields["person"]
del self.fields["text"]
elif rule_type in ["author", "author_rfc", "shepherd", "ad"]:
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
if rule_type.startswith("author"):
self.fields["person"].label = "Author"
elif rule_type.startswith("shepherd"):
self.fields["person"].label = "Shepherd"
elif rule_type.startswith("ad"):
self.fields["person"].label = "Area Director"
self.fields["person"] = forms.ModelChoiceField(queryset=Person.objects.filter(role__name__in=("ad", "pre-ad"), role__group__state="active").distinct().order_by("name"))
del self.fields["group"]
del self.fields["text"]
elif rule_type == "name_contains":
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
del self.fields["person"]
del self.fields["group"]
if 'group' in self.fields:
self.fields['group'].queryset = self.fields['group'].queryset.filter(state="active").order_by("acronym")
self.fields['group'].choices = [(g.pk, u"%s - %s" % (g.acronym, g.name)) for g in self.fields['group'].queryset]
for name, f in self.fields.iteritems():
f.required = True
def clean_text(self):
return self.cleaned_data["text"].strip().lower() # names are always lower case
class SubscriptionForm(forms.ModelForm):
def __init__(self, user, clist, *args, **kwargs):
self.clist = clist
self.user = user
super(SubscriptionForm, self).__init__(*args, **kwargs)
self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices)
self.fields["email"].queryset = self.fields["email"].queryset.filter(person__user=user, active=True).order_by("-primary")
self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]])
if self.fields["email"].queryset:
self.fields["email"].initial = self.fields["email"].queryset[0]
def clean(self):
if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], notify_on=self.cleaned_data["notify_on"]).exists():
raise forms.ValidationError("You already have a subscription like this.")
class Meta:
model = DisplayConfiguration
fields = ('sort_method', )
def save(self):
data = self.data
fields = []
for i in DisplayField.__subclasses__():
if data.get(i.codename, None):
fields.append(i.codename)
self.instance.display_fields = ','.join(fields)
super(DisplayForm, self).save()
class SubscribeForm(forms.Form):
email = forms.EmailField(label="Your email")
def __init__(self, *args, **kwargs):
self.clist = kwargs.pop('clist')
self.significant = kwargs.pop('significant')
super(SubscribeForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
self.send_email()
return True
def send_email(self):
domain = Site.objects.get_current().domain
today = datetime.date.today().strftime('%Y%m%d')
subject = 'Confirm list subscription: %s' % self.clist
from_email = settings.DEFAULT_FROM_EMAIL
to_email = self.cleaned_data['email']
auth = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, to_email, 'subscribe', self.significant)).hexdigest()
context = {
'domain': domain,
'clist': self.clist,
'today': today,
'auth': auth,
'to_email': to_email,
'significant': self.significant,
}
send_mail(None, to_email, from_email, subject, 'community/public/subscribe_email.txt', context)
class UnSubscribeForm(SubscribeForm):
def send_email(self):
domain = Site.objects.get_current().domain
today = datetime.date.today().strftime('%Y%m%d')
subject = 'Confirm list subscription cancelation: %s' % self.clist
from_email = settings.DEFAULT_FROM_EMAIL
to_email = self.cleaned_data['email']
auth = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, to_email, 'unsubscribe', self.significant)).hexdigest()
context = {
'domain': domain,
'clist': self.clist,
'today': today,
'auth': auth,
'to_email': to_email,
'significant': self.significant,
}
send_mail(None, to_email, from_email, subject, 'community/public/unsubscribe_email.txt', context)
model = EmailSubscription
fields = ("notify_on", "email")

View file

@ -1,36 +0,0 @@
import sys
import datetime
from django.core.management.base import BaseCommand
from ietf.community.models import Rule, CommunityList
class Command(BaseCommand):
help = (u"Update drafts in community lists by reviewing their rules")
def handle(self, *args, **options):
now = datetime.datetime.now()
rules = Rule.objects.filter(last_updated__lt=now - datetime.timedelta(hours=1))
count = rules.count()
index = 1
for rule in rules:
sys.stdout.write('Updating rule [%s/%s]\r' % (index, count))
sys.stdout.flush()
rule.save()
index += 1
if index > 1:
print
cls = CommunityList.objects.filter(cached__isnull=False)
count = cls.count()
index = 1
for cl in cls:
sys.stdout.write('Clearing community list cache [%s/%s]\r' % (index, count))
sys.stdout.flush()
cl.cached = None
cl.save()
index += 1
if index > 1:
print

View file

@ -1,38 +0,0 @@
import sys
from django.core.management.base import BaseCommand
from ietf.community.constants import SIGNIFICANT_STATES
from ietf.community.models import DocumentChangeDates
from ietf.doc.models import Document
class Command(BaseCommand):
help = (u"Update drafts in community lists by reviewing their rules")
def handle(self, *args, **options):
documents = Document.objects.filter(type='draft')
index = 1
total = documents.count()
for doc in documents.iterator():
(changes, created) = DocumentChangeDates.objects.get_or_create(document=doc)
new_version = doc.latest_event(type='new_revision')
normal_change = doc.latest_event()
significant_change = None
for event in doc.docevent_set.filter(type='changed_document'):
for state in SIGNIFICANT_STATES:
if ('<b>%s</b>' % state) in event.desc:
significant_change = event
break
changes.new_version_date = new_version and new_version.time.date()
changes.normal_change_date = normal_change and normal_change.time.date()
changes.significant_change_date = significant_change and significant_change.time.date()
changes.save()
sys.stdout.write('Document %s/%s\r' % (index, total))
sys.stdout.flush()
index += 1
print

View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('person', '0004_auto_20150308_0440'),
('doc', '0010_auto_20150930_0251'),
('group', '0006_auto_20150718_0509'),
('community', '0002_auto_20141222_1749'),
]
operations = [
migrations.RenameModel(
old_name='Rule',
new_name='SearchRule',
),
migrations.RemoveField(
model_name='displayconfiguration',
name='community_list',
),
migrations.DeleteModel(
name='DisplayConfiguration',
),
migrations.RemoveField(
model_name='documentchangedates',
name='document',
),
migrations.DeleteModel(
name='DocumentChangeDates',
),
migrations.RemoveField(
model_name='expectedchange',
name='community_list',
),
migrations.RemoveField(
model_name='expectedchange',
name='document',
),
migrations.DeleteModel(
name='ExpectedChange',
),
migrations.RemoveField(
model_name='listnotification',
name='event',
),
migrations.DeleteModel(
name='ListNotification',
),
migrations.RemoveField(
model_name='searchrule',
name='cached_ids',
),
migrations.RenameField(
model_name='communitylist',
old_name='added_ids',
new_name='added_docs',
),
migrations.RemoveField(
model_name='communitylist',
name='cached',
),
migrations.RemoveField(
model_name='communitylist',
name='secret',
),
migrations.AddField(
model_name='searchrule',
name='group',
field=models.ForeignKey(blank=True, to='group.Group', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='searchrule',
name='person',
field=models.ForeignKey(blank=True, to='person.Person', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='searchrule',
name='state',
field=models.ForeignKey(blank=True, to='doc.State', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='searchrule',
name='text',
field=models.CharField(default=b'', max_length=255, verbose_name=b'Text/RegExp', blank=True),
preserve_default=True,
),
migrations.RemoveField(
model_name='searchrule',
name='last_updated',
),
migrations.AlterField(
model_name='searchrule',
name='rule_type',
field=models.CharField(max_length=30, choices=[(b'group', b'All I-Ds associated with a particular group'), (b'area', b'All I-Ds associated with all groups in a particular Area'), (b'group_rfc', b'All RFCs associated with a particular group'), (b'area_rfc', b'All RFCs associated with all groups in a particular Area'), (b'state_iab', b'All I-Ds that are in a particular IAB state'), (b'state_iana', b'All I-Ds that are in a particular IANA state'), (b'state_iesg', b'All I-Ds that are in a particular IESG state'), (b'state_irtf', b'All I-Ds that are in a particular IRTF state'), (b'state_ise', b'All I-Ds that are in a particular ISE state'), (b'state_rfceditor', b'All I-Ds that are in a particular RFC Editor state'), (b'state_ietf', b'All I-Ds that are in a particular Working Group state'), (b'author', b'All I-Ds with a particular author'), (b'author_rfc', b'All RFCs with a particular author'), (b'ad', b'All I-Ds with a particular responsible AD'), (b'shepherd', b'All I-Ds with a particular document shepherd'), (b'name_contains', b'All I-Ds with particular text/regular expression in the name')]),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='searchrule',
unique_together=set([]),
),
migrations.AddField(
model_name='emailsubscription',
name='notify_on',
field=models.CharField(default=b'all', max_length=30, choices=[(b'all', b'All changes'), (b'significant', b'Only significant state changes')]),
preserve_default=True,
),
migrations.AddField(
model_name='searchrule',
name='name_contains_index',
field=models.ManyToManyField(to='doc.Document'),
preserve_default=True,
),
]

View file

@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def port_rules_to_typed_system(apps, schema_editor):
SearchRule = apps.get_model("community", "SearchRule")
State = apps.get_model("doc", "State")
Group = apps.get_model("group", "Group")
Person = apps.get_model("person", "Person")
draft_active = State.objects.get(type="draft", slug="active")
draft_rfc = State.objects.get(type="draft", slug="rfc")
def try_to_uniquify_person(rule, person_qs):
if rule.community_list.user and len(person_qs) > 1:
user_specific_qs = person_qs.filter(user=rule.community_list.user)
if len(user_specific_qs) > 0 and len(user_specific_qs) < len(person_qs):
return user_specific_qs
return person_qs
for rule in SearchRule.objects.all().iterator():
handled = False
if rule.rule_type in ['wg_asociated', 'area_asociated', 'wg_asociated_rfc', 'area_asociated_rfc']:
try:
rule.group = Group.objects.get(acronym=rule.value)
if rule.rule_type in ['wg_asociated_rfc', 'area_asociated_rfc']:
rule.state = draft_rfc
else:
rule.state = draft_active
handled = True
except Group.DoesNotExist:
pass
elif rule.rule_type in ['in_iab_state', 'in_iana_state', 'in_iesg_state', 'in_irtf_state', 'in_ise_state', 'in_rfcEdit_state', 'in_wg_state']:
state_types = {
'in_iab_state': 'draft-stream-iab',
'in_iana_state': 'draft-iana-review',
'in_iesg_state': 'draft-iesg',
'in_irtf_state': 'draft-stream-irtf',
'in_ise_state': 'draft-stream-ise',
'in_rfcEdit_state': 'draft-rfceditor',
'in_wg_state': 'draft-stream-ietf',
}
try:
rule.state = State.objects.get(type=state_types[rule.rule_type], slug=rule.value)
handled = True
except State.DoesNotExist:
pass
elif rule.rule_type in ["author", "author_rfc"]:
found_persons = list(try_to_uniquify_person(rule, Person.objects.filter(email__documentauthor__id__gte=1).filter(name__icontains=rule.value).distinct()))
if found_persons:
rule.person = found_persons[0]
rule.state = draft_active
for p in found_persons[1:]:
SearchRule.objects.create(
community_list=rule.community_list,
rule_type=rule.rule_type,
state=rule.state,
person=p,
)
#print "created", rule.rule_type, p.name
handled = True
elif rule.rule_type == "ad_responsible":
try:
rule.person = Person.objects.get(id=rule.value)
rule.state = draft_active
handled = True
except Person.DoesNotExist:
pass
elif rule.rule_type == "shepherd":
found_persons = list(try_to_uniquify_person(rule, Person.objects.filter(email__shepherd_document_set__type="draft").filter(name__icontains=rule.value).distinct()))
if found_persons:
rule.person = found_persons[0]
rule.state = draft_active
for p in found_persons[1:]:
SearchRule.objects.create(
community_list=rule.community_list,
rule_type=rule.rule_type,
state=rule.state,
person=p,
)
#print "created", rule.rule_type, p.name
handled = True
elif rule.rule_type == "with_text":
rule.state = draft_active
if rule.value:
rule.text = rule.value
handled = True
if handled:
rule.save()
else:
rule.delete()
#print "NOT HANDLED", rule.pk, rule.rule_type, rule.value
def delete_extra_person_rules(apps, schema_editor):
SearchRule = apps.get_model("community", "SearchRule")
SearchRule.objects.exclude(person=None).filter(value="").delete()
RENAMED_RULES = [
('wg_asociated', 'group'),
('area_asociated', 'area'),
('wg_asociated_rfc', 'group_rfc'),
('area_asociated_rfc', 'area_rfc'),
('in_iab_state', 'state_iab'),
('in_iana_state', 'state_iana'),
('in_iesg_state', 'state_iesg'),
('in_irtf_state', 'state_irtf'),
('in_ise_state', 'state_ise'),
('in_rfcEdit_state', 'state_rfceditor'),
('in_wg_state', 'state_ietf'),
('ad_responsible', 'ad'),
('with_text', 'name_contains'),
]
def rename_rule_type_forwards(apps, schema_editor):
SearchRule = apps.get_model("community", "SearchRule")
renamings = dict(RENAMED_RULES)
for r in SearchRule.objects.all():
if r.rule_type in renamings:
r.rule_type = renamings[r.rule_type]
r.save()
def rename_rule_type_backwards(apps, schema_editor):
SearchRule = apps.get_model("community", "SearchRule")
renamings = dict((to, fro) for fro, to in RENAMED_RULES)
for r in SearchRule.objects.all():
if r.rule_type in renamings:
r.rule_type = renamings[r.rule_type]
r.save()
def get_rid_of_empty_lists(apps, schema_editor):
CommunityList = apps.get_model("community", "CommunityList")
for cl in CommunityList.objects.all():
if not cl.added_docs.exists() and not cl.searchrule_set.exists() and not cl.emailsubscription_set.exists():
cl.delete()
def move_email_subscriptions_to_preregistered_email(apps, schema_editor):
EmailSubscription = apps.get_model("community", "EmailSubscription")
Email = apps.get_model("person", "Email")
Person = apps.get_model("person", "Person")
for e in EmailSubscription.objects.all():
email_obj = None
try:
email_obj = Email.objects.get(address=e.email)
except Email.DoesNotExist:
if e.community_list.user:
person = Person.objects.filter(user=e.community_list.user).first()
#print "creating", e.email, person.ascii
# we'll register it on the user, on the assumption
# that the user and the subscriber is the same person
email_obj = Email.objects.create(
address=e.email,
person=person,
)
if not email_obj:
print "deleting", e.email
e.delete()
def fill_in_notify_on(apps, schema_editor):
EmailSubscription = apps.get_model("community", "EmailSubscription")
EmailSubscription.objects.filter(significant=False, notify_on="all")
EmailSubscription.objects.filter(significant=True, notify_on="significant")
def add_group_community_lists(apps, schema_editor):
Group = apps.get_model("group", "Group")
Document = apps.get_model("doc", "Document")
State = apps.get_model("doc", "State")
CommunityList = apps.get_model("community", "CommunityList")
SearchRule = apps.get_model("community", "SearchRule")
active_state = State.objects.get(slug="active", type="draft")
rfc_state = State.objects.get(slug="rfc", type="draft")
for g in Group.objects.filter(type__in=("rg", "wg")):
clist = CommunityList.objects.filter(group=g).first()
if clist:
SearchRule.objects.get_or_create(community_list=clist, rule_type="group", group=g, state=active_state)
SearchRule.objects.get_or_create(community_list=clist, rule_type="group_rfc", group=g, state=rfc_state)
r, _ = SearchRule.objects.get_or_create(community_list=clist, rule_type="name_contains", text=r"^draft-[^-]+-%s-" % g.acronym, state=active_state)
r.name_contains_index = Document.objects.filter(docalias__name__regex=r.text)
else:
clist = CommunityList.objects.create(group=g)
SearchRule.objects.create(community_list=clist, rule_type="group", group=g, state=active_state)
SearchRule.objects.create(community_list=clist, rule_type="group_rfc", group=g, state=rfc_state)
r = SearchRule.objects.create(community_list=clist, rule_type="name_contains", text=r"^draft-[^-]+-%s-" % g.acronym, state=active_state)
r.name_contains_index = Document.objects.filter(docalias__name__regex=r.text)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('community', '0003_cleanup'),
]
operations = [
migrations.RunPython(port_rules_to_typed_system, delete_extra_person_rules),
migrations.RunPython(rename_rule_type_forwards, rename_rule_type_backwards),
migrations.RunPython(move_email_subscriptions_to_preregistered_email, noop),
migrations.RunPython(get_rid_of_empty_lists, noop),
migrations.RunPython(fill_in_notify_on, noop),
migrations.RunPython(add_group_community_lists, noop),
migrations.RemoveField(
model_name='searchrule',
name='value',
),
migrations.AlterField(
model_name='emailsubscription',
name='email',
field=models.ForeignKey(to='person.Email'),
preserve_default=True,
),
migrations.RemoveField(
model_name='emailsubscription',
name='significant',
),
]

View file

@ -1,263 +1,102 @@
import hashlib
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals, Q
from ietf.utils.mail import send_mail
from ietf.doc.models import Document, DocEvent
from ietf.group.models import Group, Role
from ietf.community.rules import TYPES_OF_RULES, RuleManager
from ietf.community.display import (TYPES_OF_SORT, DisplayField,
SortMethod)
from ietf.community.constants import SIGNIFICANT_STATES
from django.db.models import signals
from django.core.urlresolvers import reverse as urlreverse
from ietf.doc.models import Document, DocEvent, State
from ietf.group.models import Group
from ietf.person.models import Person, Email
class CommunityList(models.Model):
user = models.ForeignKey(User, blank=True, null=True)
group = models.ForeignKey(Group, blank=True, null=True)
added_ids = models.ManyToManyField(Document)
secret = models.CharField(max_length=255, null=True, blank=True)
cached = models.TextField(null=True, blank=True)
def check_manager(self, user):
if user == self.user:
return True
if not self.group or self.group.type.slug not in ('area', 'wg'):
return False
try:
person = user.person
except:
return False
if self.group.type.slug == 'area':
return bool(Role.objects.filter(name__slug='ad', email__in=person.email_set.all(), group=self.group).count())
elif self.group.type.slug == 'wg':
return bool(Role.objects.filter(name__slug='chair', email__in=person.email_set.all(), group=self.group).count())
return False
def short_name(self):
if self.user:
return 'mine'
else:
return '%s' % self.group.acronym
added_docs = models.ManyToManyField(Document)
def long_name(self):
if self.user:
return 'Personal ID list of %s' % self.user.username
else:
elif self.group:
return 'ID list for %s' % self.group.name
else:
return 'ID list'
def __unicode__(self):
return self.long_name()
def get_public_url(self):
def get_absolute_url(self):
if self.user:
return reverse('view_personal_list', None, args=(self.user.username, ))
else:
return reverse('view_group_list', None, args=(self.group.acronym, ))
def get_manage_url(self):
if self.user:
return reverse('manage_personal_list', None, args=())
else:
return reverse('manage_group_list', None, args=(self.group.acronym, ))
def get_display_config(self):
dconfig = getattr(self, '_cached_dconfig', None)
if not dconfig:
try:
self._cached_dconfig = DisplayConfiguration.objects.get(community_list=self)
except DisplayConfiguration.DoesNotExist:
self._cached_dconfig = DisplayConfiguration(community_list=self)
return self._cached_dconfig
return self._cached_dconfig
def get_documents(self):
if hasattr(self, '_cached_documents'):
return self._cached_documents
docs = self.added_ids.all().distinct().select_related('type', 'group', 'ad')
for rule in self.rule_set.all():
docs = docs | rule.cached_ids.all().distinct()
sort_field = self.get_display_config().get_sort_method().get_sort_field()
docs = docs.distinct().order_by(sort_field)
self._cached_documents = docs
return self._cached_documents
def get_rfcs_and_drafts(self):
if hasattr(self, '_cached_rfcs_and_drafts'):
return self._cached_rfcs_and_drafts
docs = self.get_documents()
sort_method = self.get_display_config().get_sort_method()
sort_field = sort_method.get_sort_field()
if hasattr(sort_method, 'get_full_rfc_sort'):
rfcs = sort_method.get_full_rfc_sort(docs.filter(states__name='rfc').distinct())
else:
rfcs = docs.filter(states__name='rfc').distinct().order_by(sort_field)
if hasattr(sort_method, 'get_full_draft_sort'):
drafts = sort_method.get_full_draft_sort(docs.exclude(pk__in=rfcs).distinct())
else:
drafts = docs.exclude(pk__in=rfcs).distinct().order_by(sort_field)
self._cached_rfcs_and_drafts = (rfcs, drafts)
return self._cached_rfcs_and_drafts
def add_subscriptor(self, email, significant):
self.emailsubscription_set.get_or_create(email=email, significant=significant)
def save(self, *args, **kwargs):
super(CommunityList, self).save(*args, **kwargs)
if not self.secret:
self.secret = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, self.id, self.user and self.user.id or '', self.group and self.group.id or '')).hexdigest()
self.save()
def update(self):
self.cached=None
self.save()
return urlreverse("community_personal_view_list", kwargs={ 'username': self.user.username })
elif self.group:
return urlreverse("group_docs", kwargs={ 'acronym': self.group.acronym })
return ""
class Rule(models.Model):
class SearchRule(models.Model):
# these types define the UI for setting up the rule, and also
# helps when interpreting the rule and matching documents
RULE_TYPES = [
('group', 'All I-Ds associated with a particular group'),
('area', 'All I-Ds associated with all groups in a particular Area'),
('group_rfc', 'All RFCs associated with a particular group'),
('area_rfc', 'All RFCs associated with all groups in a particular Area'),
('state_iab', 'All I-Ds that are in a particular IAB state'),
('state_iana', 'All I-Ds that are in a particular IANA state'),
('state_iesg', 'All I-Ds that are in a particular IESG state'),
('state_irtf', 'All I-Ds that are in a particular IRTF state'),
('state_ise', 'All I-Ds that are in a particular ISE state'),
('state_rfceditor', 'All I-Ds that are in a particular RFC Editor state'),
('state_ietf', 'All I-Ds that are in a particular Working Group state'),
('author', 'All I-Ds with a particular author'),
('author_rfc', 'All RFCs with a particular author'),
('ad', 'All I-Ds with a particular responsible AD'),
('shepherd', 'All I-Ds with a particular document shepherd'),
('name_contains', 'All I-Ds with particular text/regular expression in the name'),
]
community_list = models.ForeignKey(CommunityList)
cached_ids = models.ManyToManyField(Document)
rule_type = models.CharField(max_length=30, choices=TYPES_OF_RULES)
value = models.CharField(max_length=255)
rule_type = models.CharField(max_length=30, choices=RULE_TYPES)
class Meta:
unique_together= ("community_list", "rule_type", "value")
# these are filled in depending on the type
state = models.ForeignKey(State, blank=True, null=True)
group = models.ForeignKey(Group, blank=True, null=True)
person = models.ForeignKey(Person, blank=True, null=True)
text = models.CharField(verbose_name="Text/RegExp", max_length=255, blank=True, default="")
last_updated = models.DateTimeField(
auto_now=True)
def get_callable_rule(self):
for i in RuleManager.__subclasses__():
if i.codename == self.rule_type:
return i(self.value)
return RuleManager(self.value)
def save(self, *args, **kwargs):
super(Rule, self).save(*args, **kwargs)
rule = self.get_callable_rule()
self.cached_ids = rule.get_documents()
self.community_list.update()
def delete(self):
self.community_list.update()
super(Rule, self).delete()
class DisplayConfiguration(models.Model):
community_list = models.ForeignKey(CommunityList)
sort_method = models.CharField(
max_length=100,
choices=TYPES_OF_SORT,
default='by_filename',
blank=False,
null=False)
display_fields = models.TextField(
default='filename,title,date')
def get_display_fields_config(self):
fields = self.display_fields and self.display_fields.split(',') or []
config = []
for i in DisplayField.__subclasses__():
config.append({
'codename': i.codename,
'description': i.description,
'active': i.codename in fields,
})
return config
def get_active_fields(self):
fields = self.display_fields and self.display_fields.split(',') or ''
active_fields = [i for i in DisplayField.__subclasses__() if i.codename in fields]
return active_fields
def get_all_fields(self):
all_fields = [i for i in DisplayField.__subclasses__()]
return all_fields
def get_sort_method(self):
for i in SortMethod.__subclasses__():
if i.codename == self.sort_method:
return i()
return SortMethod()
def save(self, *args, **kwargs):
super(DisplayConfiguration, self).save(*args, **kwargs)
self.community_list.update()
def delete(self):
self.community_list.update()
super(DisplayConfiguration, self).delete()
class ExpectedChange(models.Model):
community_list = models.ForeignKey(CommunityList)
document = models.ForeignKey(Document)
expected_date = models.DateField(
verbose_name='Expected date'
)
# store a materialized view/index over which documents are matched
# by the name_contains rule to avoid having to scan the whole
# database - we update this manually when the rule is changed and
# when new documents are submitted
name_contains_index = models.ManyToManyField(Document)
class EmailSubscription(models.Model):
community_list = models.ForeignKey(CommunityList)
email = models.CharField(max_length=200)
significant = models.BooleanField(default=False)
email = models.ForeignKey(Email)
NOTIFICATION_CHOICES = [
("all", "All changes"),
("significant", "Only significant state changes")
]
notify_on = models.CharField(max_length=30, choices=NOTIFICATION_CHOICES, default="all")
class ListNotification(models.Model):
event = models.ForeignKey(DocEvent)
significant = models.BooleanField(default=False)
def notify_by_email(self):
clists = CommunityList.objects.filter(
Q(added_ids=self.event.doc) | Q(rule__cached_ids=self.event.doc)).distinct()
from_email = settings.DEFAULT_FROM_EMAIL
for l in clists:
subject = '%s notification: Changes on %s' % (l.long_name(), self.event.doc.name)
context = {'notification': self.event,
'clist': l}
filter_subscription = {'community_list': l}
if not self.significant:
filter_subscription['significant'] = False
for to_email in list(set([i.email for i in EmailSubscription.objects.filter(**filter_subscription)])):
send_mail(None, to_email, from_email, subject, 'community/public/notification_email.txt', context)
def __unicode__(self):
return u"%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on)
def notify_events(sender, instance, **kwargs):
if not isinstance(instance, DocEvent):
return
if instance.doc.type.slug != 'draft' or instance.type == 'added_comment':
if instance.doc.type_id != 'draft':
return
(changes, created) = DocumentChangeDates.objects.get_or_create(document=instance.doc)
changes.normal_change_date = instance.time
significant = False
if instance.type == 'changed_document' and 'tate changed' in instance.desc:
for i in SIGNIFICANT_STATES:
if ('<b>%s</b>' % i) in instance.desc:
significant = True
changes.significant_change_date = instance.time
break
elif instance.type == 'new_revision':
changes.new_version_date = instance.time
changes.save()
notification = ListNotification.objects.create(
event=instance,
significant=significant,
)
notification.notify_by_email()
from ietf.community.utils import notify_event_to_subscribers
notify_event_to_subscribers(instance)
signals.post_save.connect(notify_events)
class DocumentChangeDates(models.Model):
document = models.ForeignKey(Document)
new_version_date = models.DateTimeField(blank=True, null=True)
normal_change_date = models.DateTimeField(blank=True, null=True)
significant_change_date = models.DateTimeField(blank=True, null=True)

View file

@ -1,14 +1,13 @@
# Autogenerated by the mkresources management command 2014-11-13 23:53
from tastypie.resources import ModelResource
from ietf.api import ToOneField
from tastypie.fields import ToManyField
from tastypie.fields import ToOneField, ToManyField
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.cache import SimpleCache
from ietf import api
from ietf.community.models import ( CommunityList, ExpectedChange, DisplayConfiguration,
ListNotification, Rule, EmailSubscription, DocumentChangeDates )
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
from ietf.doc.resources import DocumentResource
from ietf.group.resources import GroupResource
@ -16,7 +15,7 @@ from ietf.utils.resources import UserResource
class CommunityListResource(ModelResource):
user = ToOneField(UserResource, 'user', null=True)
group = ToOneField(GroupResource, 'group', null=True)
added_ids = ToManyField(DocumentResource, 'added_ids', null=True)
added_docs = ToManyField(DocumentResource, 'added_docs', null=True)
class Meta:
cache = SimpleCache()
queryset = CommunityList.objects.all()
@ -28,75 +27,24 @@ class CommunityListResource(ModelResource):
"cached": ALL,
"user": ALL_WITH_RELATIONS,
"group": ALL_WITH_RELATIONS,
"added_ids": ALL_WITH_RELATIONS,
"added_docs": ALL_WITH_RELATIONS,
}
api.community.register(CommunityListResource())
from ietf.doc.resources import DocumentResource
class ExpectedChangeResource(ModelResource):
community_list = ToOneField(CommunityListResource, 'community_list')
document = ToOneField(DocumentResource, 'document')
class Meta:
cache = SimpleCache()
queryset = ExpectedChange.objects.all()
serializer = api.Serializer()
#resource_name = 'expectedchange'
filtering = {
"id": ALL,
"expected_date": ALL,
"community_list": ALL_WITH_RELATIONS,
"document": ALL_WITH_RELATIONS,
}
api.community.register(ExpectedChangeResource())
class DisplayConfigurationResource(ModelResource):
class SearchRuleResource(ModelResource):
community_list = ToOneField(CommunityListResource, 'community_list')
class Meta:
cache = SimpleCache()
queryset = DisplayConfiguration.objects.all()
serializer = api.Serializer()
#resource_name = 'displayconfiguration'
filtering = {
"id": ALL,
"sort_method": ALL,
"display_fields": ALL,
"community_list": ALL_WITH_RELATIONS,
}
api.community.register(DisplayConfigurationResource())
from ietf.doc.resources import DocEventResource
class ListNotificationResource(ModelResource):
event = ToOneField(DocEventResource, 'event')
class Meta:
cache = SimpleCache()
queryset = ListNotification.objects.all()
serializer = api.Serializer()
#resource_name = 'listnotification'
filtering = {
"id": ALL,
"significant": ALL,
"event": ALL_WITH_RELATIONS,
}
api.community.register(ListNotificationResource())
from ietf.doc.resources import DocumentResource
class RuleResource(ModelResource):
community_list = ToOneField(CommunityListResource, 'community_list')
cached_ids = ToManyField(DocumentResource, 'cached_ids', null=True)
class Meta:
cache = SimpleCache()
queryset = Rule.objects.all()
queryset = SearchRule.objects.all()
serializer = api.Serializer()
#resource_name = 'rule'
filtering = {
"id": ALL,
"rule_type": ALL,
"value": ALL,
"last_updated": ALL,
"community_list": ALL_WITH_RELATIONS,
"cached_ids": ALL_WITH_RELATIONS,
}
api.community.register(RuleResource())
api.community.register(SearchRuleResource())
class EmailSubscriptionResource(ModelResource):
community_list = ToOneField(CommunityListResource, 'community_list')
@ -107,26 +55,8 @@ class EmailSubscriptionResource(ModelResource):
#resource_name = 'emailsubscription'
filtering = {
"id": ALL,
"email": ALL,
"significant": ALL,
"email": ALL_WITH_RELATIONS,
"notify_on": ALL,
"community_list": ALL_WITH_RELATIONS,
}
api.community.register(EmailSubscriptionResource())
from ietf.doc.resources import DocumentResource
class DocumentChangeDatesResource(ModelResource):
document = ToOneField(DocumentResource, 'document')
class Meta:
cache = SimpleCache()
queryset = DocumentChangeDates.objects.all()
serializer = api.Serializer()
#resource_name = 'documentchangedates'
filtering = {
"id": ALL,
"new_version_date": ALL,
"normal_change_date": ALL,
"significant_change_date": ALL,
"document": ALL_WITH_RELATIONS,
}
api.community.register(DocumentChangeDatesResource())

View file

@ -1,292 +0,0 @@
from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.person.models import Person
from ietf.doc.models import State
class RuleManager(object):
codename = ''
description = ''
def __init__(self, value):
self.value = self.get_value(value)
def get_value(self, value):
return value
def get_documents(self):
return Document.objects.none()
def options(self):
return None
def show_value(self):
return self.value
class WgAsociatedRule(RuleManager):
codename = 'wg_asociated'
description = 'All I-Ds associated with a particular WG'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='active').filter(group__acronym=self.value).distinct()
def options(self):
return [(i.acronym, "%s &mdash; %s"%(i.acronym, i.name)) for i in Group.objects.filter(type='wg', state='active').distinct().order_by('acronym')]
def show_value(self):
try:
return Group.objects.get(acronym=self.value).name
except Group.DoesNotExist:
return self.value
class AreaAsociatedRule(RuleManager):
codename = 'area_asociated'
description = 'All I-Ds associated with all WGs in a particular Area'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='active').filter(group__parent__acronym=self.value, group__parent__type='area').distinct()
def options(self):
return [(i.acronym, "%s &mdash; %s"%(i.acronym, i.name)) for i in Group.objects.filter(type='area', state='active').distinct().order_by('name')]
def show_value(self):
try:
return Group.objects.get(acronym=self.value).name
except Group.DoesNotExist:
return self.value
class AdResponsibleRule(RuleManager):
codename = 'ad_responsible'
description = 'All I-Ds with a particular responsible AD'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='active').filter(ad=self.value).distinct()
def options(self):
return [(i.pk, i.name) for i in Person.objects.filter(role__name='ad',role__group__state='active').distinct().order_by('name')]
def show_value(self):
try:
return Person.objects.get(pk=self.value).name
except Person.DoesNotExist:
return self.value
class AuthorRule(RuleManager):
codename = 'author'
description = 'All I-Ds with a particular author'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='active').filter(authors__person__name__icontains=self.value).distinct()
class ShepherdRule(RuleManager):
codename = 'shepherd'
description = 'All I-Ds with a particular document shepherd'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='active').filter(shepherd__person__name__icontains=self.value).distinct()
# class ReferenceToRFCRule(RuleManager):
# codename = 'reference_to_rfc'
# description = 'All I-Ds that have a reference to a particular RFC'
#
# def get_documents(self):
# return Document.objects.filter(type='draft', states__slug='active').filter(relateddocument__target__document__states__slug='rfc', relateddocument__target__name__icontains=self.value).distinct()
#
#
# class ReferenceToIDRule(RuleManager):
# codename = 'reference_to_id'
# description = 'All I-Ds that have a reference to a particular I-D'
#
# def get_documents(self):
# return Document.objects.filter(type='draft', states__slug='active').filter(relateddocument__target__document__type='draft', relateddocument__target__name__icontains=self.value).distinct()
#
#
# class ReferenceFromRFCRule(RuleManager):
# codename = 'reference_from_rfc'
# description = 'All I-Ds that are referenced by a particular RFC'
#
# def get_documents(self):
# return Document.objects.filter(type='draft', states__slug='active').filter(relateddocument__source__states__slug='rfc', relateddocument__source__name__icontains=self.value).distinct()
#
#
#
# class ReferenceFromIDRule(RuleManager):
# codename = 'reference_from_id'
# description = 'All I-Ds that are referenced by a particular I-D'
#
# def get_documents(self):
# return Document.objects.filter(type='draft', states__slug='active').filter(relateddocument__source__type='draft', relateddocument__source__name__icontains=self.value).distinct()
class WithTextRule(RuleManager):
codename = 'with_text'
description = 'All I-Ds that contain a particular text string in the name'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='active').filter(name__icontains=self.value).distinct()
class IABInState(RuleManager):
codename = 'in_iab_state'
description = 'All I-Ds that are in a particular IAB state'
def get_documents(self):
return Document.objects.filter(states__type='draft-stream-iab', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.name) for i in State.objects.filter(type='draft-stream-iab').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-stream-iab', slug=self.value).name
except State.DoesNotExist:
return self.value
class IANAInState(RuleManager):
codename = 'in_iana_state'
description = 'All I-Ds that are in a particular IANA state'
def get_documents(self):
return Document.objects.filter(states__type='draft-iana-review', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.name) for i in State.objects.filter(type='draft-iana-review').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-iana-review', slug=self.value).name
except State.DoesNotExist:
return self.value
class IESGInState(RuleManager):
codename = 'in_iesg_state'
description = 'All I-Ds that are in a particular IESG state'
def get_documents(self):
return Document.objects.filter(states__type='draft-iesg', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.name) for i in State.objects.filter(type='draft-iesg').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-iesg', slug=self.value).name
except State.DoesNotExist:
return self.value
class IRTFInState(RuleManager):
codename = 'in_irtf_state'
description = 'All I-Ds that are in a particular IRTF state'
def get_documents(self):
return Document.objects.filter(states__type='draft-stream-irtf', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.name) for i in State.objects.filter(type='draft-stream-irtf').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-stream-irtf', slug=self.value).name
except State.DoesNotExist:
return self.value
class ISEInState(RuleManager):
codename = 'in_ise_state'
description = 'All I-Ds that are in a particular ISE state'
def get_documents(self):
return Document.objects.filter(states__type='draft-stream-ise', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.name) for i in State.objects.filter(type='draft-stream-ise').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-stream-ise', slug=self.value).name
except State.DoesNotExist:
return self.value
class RfcEditorInState(RuleManager):
codename = 'in_rfcEdit_state'
description = 'All I-Ds that are in a particular RFC Editor state'
def get_documents(self):
return Document.objects.filter(states__type='draft-rfceditor', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.type_id + ": " + i.name) for i in State.objects.filter(type='draft-rfceditor').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-rfceditor', slug=self.value).name
except State.DoesNotExist:
return self.value
class WGInState(RuleManager):
codename = 'in_wg_state'
description = 'All I-Ds that are in a particular Working Group state'
def get_documents(self):
return Document.objects.filter(states__type='draft-stream-ietf', states__slug=self.value).distinct()
def options(self):
return [(i.slug, i.type_id + ": " + i.name) for i in State.objects.filter(type='draft-stream-ietf').order_by('name')]
def show_value(self):
try:
return State.objects.get(type='draft-stream-ietf', slug=self.value).name
except State.DoesNotExist:
return self.value
class RfcWgAsociatedRule(RuleManager):
codename = 'wg_asociated_rfc'
description = 'All RFCs associated with a particular WG'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='rfc').filter(group__acronym=self.value).distinct()
def options(self):
return [(i.acronym, "%s &mdash; %s"%(i.acronym, i.name)) for i in Group.objects.filter(type='wg').distinct().order_by('acronym')]
def show_value(self):
try:
return Group.objects.get(type='draft', acronym=self.value).name
except Group.DoesNotExist:
return self.value
class RfcAreaAsociatedRule(RuleManager):
codename = 'area_asociated_rfc'
description = 'All RFCs associated with all WGs in a particular Area'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='rfc').filter(group__parent__acronym=self.value, group__parent__type='area').distinct()
def options(self):
return [(i.acronym, "%s &mdash; %s"%(i.acronym, i.name)) for i in Group.objects.filter(type='area').distinct().order_by('name')]
def show_value(self):
try:
return Group.objects.get(type='draft', acronym=self.value).name
except Group.DoesNotExist:
return self.value
class RfcAuthorRule(RuleManager):
codename = 'author_rfc'
description = 'All RFCs with a particular author'
def get_documents(self):
return Document.objects.filter(type='draft', states__slug='rfc').filter(authors__person__name__icontains=self.value).distinct()
TYPES_OF_RULES = [(i.codename, i.description) for i in RuleManager.__subclasses__()]

View file

@ -1,45 +0,0 @@
from django import template
from django.template.loader import render_to_string
from django.conf import settings
from ietf.community.models import CommunityList
from ietf.group.models import Role
register = template.Library()
@register.assignment_tag
def get_user_managed_lists(user):
if not (user and hasattr(user, "is_authenticated") and user.is_authenticated()):
return ''
lists = {'personal': CommunityList.objects.get_or_create(user=user)[0]}
try:
person = user.person
groups = []
managed_areas = [i.group for i in Role.objects.filter(name__slug='ad', group__type__slug='area', group__state__slug='active', email__in=person.email_set.all())]
for area in managed_areas:
groups.append(CommunityList.objects.get_or_create(group=area)[0])
managed_wg = [i.group for i in Role.objects.filter(name__slug='chair', group__type__slug='wg', group__state__slug__in=('active','bof'), email__in=person.email_set.all())]
for wg in managed_wg:
groups.append(CommunityList.objects.get_or_create(group=wg)[0])
lists['group'] = groups
except:
pass
return lists
@register.inclusion_tag('community/display_field.html', takes_context=False)
def show_field(field, doc):
return {'field': field,
'value': field.get_value(doc),
}
@register.simple_tag
def get_clist_view(clist):
if settings.DEBUG or not clist.cached:
clist.cached = render_to_string('community/raw_view.html', {
'cl': clist,
'dc': clist.get_display_config()
})
clist.save()
return clist.cached

354
ietf/community/tests.py Normal file
View file

@ -0,0 +1,354 @@
import json
from pyquery import PyQuery
from django.core.urlresolvers import reverse as urlreverse
from django.contrib.auth.models import User
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
from ietf.community.utils import reset_name_contains_index_for_rule
from ietf.group.utils import setup_default_community_list_for_group
from ietf.doc.models import State
from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person, Email
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
from ietf.utils.mail import outbox
class CommunityListTests(TestCase):
def test_rule_matching(self):
draft = make_test_data()
iesg_state = State.objects.get(type="draft-iesg", slug="lc")
draft.set_state(iesg_state)
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
rule_group = SearchRule.objects.create(rule_type="group", group=draft.group, state=State.objects.get(type="draft", slug="active"), community_list=clist)
rule_group_rfc = SearchRule.objects.create(rule_type="group_rfc", group=draft.group, state=State.objects.get(type="draft", slug="rfc"), community_list=clist)
rule_area = SearchRule.objects.create(rule_type="area", group=draft.group.parent, state=State.objects.get(type="draft", slug="active"), community_list=clist)
rule_state_iesg = SearchRule.objects.create(rule_type="state_iesg", state=State.objects.get(type="draft-iesg", slug="lc"), community_list=clist)
rule_author = SearchRule.objects.create(rule_type="author", state=State.objects.get(type="draft", slug="active"), person=Person.objects.filter(email__documentauthor__document=draft).first(), community_list=clist)
rule_ad = SearchRule.objects.create(rule_type="ad", state=State.objects.get(type="draft", slug="active"), person=draft.ad, community_list=clist)
rule_shepherd = SearchRule.objects.create(rule_type="shepherd", state=State.objects.get(type="draft", slug="active"), person=draft.shepherd.person, community_list=clist)
rule_name_contains = SearchRule.objects.create(rule_type="name_contains", state=State.objects.get(type="draft", slug="active"), text="draft-.*" + "-".join(draft.name.split("-")[2:]), community_list=clist)
reset_name_contains_index_for_rule(rule_name_contains)
# doc -> rules
matching_rules = list(community_list_rules_matching_doc(draft))
self.assertTrue(rule_group in matching_rules)
self.assertTrue(rule_group_rfc not in matching_rules)
self.assertTrue(rule_area in matching_rules)
self.assertTrue(rule_state_iesg in matching_rules)
self.assertTrue(rule_author in matching_rules)
self.assertTrue(rule_ad in matching_rules)
self.assertTrue(rule_shepherd in matching_rules)
self.assertTrue(rule_name_contains in matching_rules)
# rule -> docs
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_group)))
self.assertTrue(draft not in list(docs_matching_community_list_rule(rule_group_rfc)))
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_area)))
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_state_iesg)))
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_author)))
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_ad)))
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_shepherd)))
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_name_contains)))
def test_view_list(self):
draft = make_test_data()
url = urlreverse("community_personal_view_list", kwargs={ "username": "plain" })
# without list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
clist.added_docs.add(draft)
SearchRule.objects.create(
community_list=clist,
rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"),
text="test",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.name in r.content)
def test_manage_personal_list(self):
draft = make_test_data()
url = urlreverse("community_personal_manage_list", kwargs={ "username": "plain" })
login_testing_unauthorized(self, "plain", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# add document
r = self.client.post(url, { "action": "add_documents", "documents": draft.pk })
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(user__username="plain")
self.assertTrue(clist.added_docs.filter(pk=draft.pk))
# document shows up on GET
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.name in r.content)
# remove document
r = self.client.post(url, { "action": "remove_document", "document": draft.pk })
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(user__username="plain")
self.assertTrue(not clist.added_docs.filter(pk=draft.pk))
# add rule
r = self.client.post(url, {
"action": "add_rule",
"rule_type": "author_rfc",
"author_rfc-person": Person.objects.filter(email__documentauthor__document=draft).first().pk,
"author_rfc-state": State.objects.get(type="draft", slug="rfc").pk,
})
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(user__username="plain")
self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc"))
# add name_contains rule
r = self.client.post(url, {
"action": "add_rule",
"rule_type": "name_contains",
"name_contains-text": "draft.*mars",
"name_contains-state": State.objects.get(type="draft", slug="active").pk,
})
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(user__username="plain")
self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains"))
# rule shows up on GET
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
rule = clist.searchrule_set.filter(rule_type="author_rfc").first()
q = PyQuery(r.content)
self.assertEqual(len(q('#r%s' % rule.pk)), 1)
# remove rule
r = self.client.post(url, {
"action": "remove_rule",
"rule": rule.pk,
})
clist = CommunityList.objects.get(user__username="plain")
self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc"))
def test_manage_group_list(self):
draft = make_test_data()
url = urlreverse("community_group_manage_list", kwargs={ "acronym": draft.group.acronym })
setup_default_community_list_for_group(draft.group)
login_testing_unauthorized(self, "marschairman", url)
# test GET, rest is tested with personal list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_track_untrack_document(self):
draft = make_test_data()
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
login_testing_unauthorized(self, "plain", url)
# track
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url)
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [draft])
# untrack
url = urlreverse("community_personal_untrack_document", kwargs={ "username": "plain", "name": draft.name })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url)
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [])
def test_track_untrack_document_through_ajax(self):
draft = make_test_data()
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
login_testing_unauthorized(self, "plain", url)
# track
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(r.status_code, 200)
self.assertEqual(json.loads(r.content)["success"], True)
clist = CommunityList.objects.get(user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [draft])
# untrack
url = urlreverse("community_personal_untrack_document", kwargs={ "username": "plain", "name": draft.name })
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(r.status_code, 200)
self.assertEqual(json.loads(r.content)["success"], True)
clist = CommunityList.objects.get(user__username="plain")
self.assertEqual(list(clist.added_docs.all()), [])
def test_csv(self):
draft = make_test_data()
url = urlreverse("community_personal_csv", kwargs={ "username": "plain" })
# without list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
clist.added_docs.add(draft)
SearchRule.objects.create(
community_list=clist,
rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"),
text="test",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# this is a simple-minded test, we don't actually check the fields
self.assertTrue(draft.name in r.content)
def test_csv_for_group(self):
draft = make_test_data()
url = urlreverse("community_group_csv", kwargs={ "acronym": draft.group.acronym })
setup_default_community_list_for_group(draft.group)
# test GET, rest is tested with personal list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_feed(self):
draft = make_test_data()
url = urlreverse("community_personal_feed", kwargs={ "username": "plain" })
# without list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
clist.added_docs.add(draft)
SearchRule.objects.create(
community_list=clist,
rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"),
text="test",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.name in r.content)
# only significant
r = self.client.get(url + "?significant=1")
self.assertEqual(r.status_code, 200)
self.assertTrue('<entry>' not in r.content)
def test_feed_for_group(self):
draft = make_test_data()
url = urlreverse("community_group_feed", kwargs={ "acronym": draft.group.acronym })
setup_default_community_list_for_group(draft.group)
# test GET, rest is tested with personal list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_subscription(self):
draft = make_test_data()
url = urlreverse("community_personal_subscription", kwargs={ "username": "plain" })
login_testing_unauthorized(self, "plain", url)
# subscription without list
r = self.client.get(url)
self.assertEqual(r.status_code, 404)
# subscription with list
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
clist.added_docs.add(draft)
SearchRule.objects.create(
community_list=clist,
rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"),
text="test",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# subscribe
email = Email.objects.filter(person__user__username="plain").first()
r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" })
self.assertEqual(r.status_code, 302)
subscription = EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").first()
self.assertTrue(subscription)
# delete subscription
r = self.client.post(url, { "subscription_id": subscription.pk, "action": "unsubscribe" })
self.assertEqual(r.status_code, 302)
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0)
def test_subscription_for_group(self):
draft = make_test_data()
url = urlreverse("community_group_subscription", kwargs={ "acronym": draft.group.acronym })
setup_default_community_list_for_group(draft.group)
login_testing_unauthorized(self, "marschairman", url)
# test GET, rest is tested with personal list
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_notification(self):
draft = make_test_data()
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
clist.added_docs.add(draft)
SearchRule.objects.create(
community_list=clist,
rule_type="name_contains",
state=State.objects.get(type="draft", slug="active"),
text="test",
)
EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant")
mailbox_before = len(outbox)
active_state = State.objects.get(type="draft", slug="active")
system = Person.objects.get(name="(System)")
add_state_change_event(draft, system, None, active_state)
self.assertEqual(len(outbox), mailbox_before)
mailbox_before = len(outbox)
rfc_state = State.objects.get(type="draft", slug="rfc")
add_state_change_event(draft, system, active_state, rfc_state)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue(draft.name in outbox[-1]["Subject"])

View file

@ -1,34 +1,13 @@
from django.conf.urls import patterns, url
urlpatterns = patterns('ietf.community.views',
url(r'^personal/$', 'manage_personal_list', name='manage_personal_list'),
url(r'^personal/csv/$', 'csv_personal_list', name='csv_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/view/$', 'view_personal_list', name='view_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/csv/$', 'view_csv_personal_list', name='view_csv_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/changes/feed/$', 'changes_personal_list', name='changes_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/changes/significant/feed/$', 'significant_personal_list', name='significant_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/$', 'subscribe_personal_list', {'significant': False}, name='subscribe_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/significant/$', 'subscribe_personal_list', {'significant': True}, name='subscribe_significant_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/$', 'unsubscribe_personal_list', {'significant': False}, name='unsubscribe_personal_list'),
url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/significant/$', 'unsubscribe_personal_list', {'significant': True}, name='unsubscribe_significant_personal_list'),
urlpatterns = patterns('',
url(r'^personal/(?P<username>[^/]+)/$', 'ietf.community.views.view_list', name='community_personal_view_list'),
url(r'^personal/(?P<username>[^/]+)/manage/$', 'ietf.community.views.manage_list', name='community_personal_manage_list'),
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.track_document', name='community_personal_track_document'),
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.untrack_document', name='community_personal_untrack_document'),
url(r'^personal/(?P<username>[^/]+)/csv/$', 'ietf.community.views.export_to_csv', name='community_personal_csv'),
url(r'^personal/(?P<username>[^/]+)/feed/$', 'ietf.community.views.feed', name='community_personal_feed'),
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'ietf.community.views.subscription', name='community_personal_subscription'),
url(r'^group/(?P<acronym>[\w.@+-]+)/$', 'manage_group_list', name='manage_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/view/$', 'view_group_list', name='view_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/changes/feed/$', 'changes_group_list', name='changes_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/changes/significant/feed/$', 'significant_group_list', name='significant_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'csv_group_list', name='csv_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/subscribe/$', 'subscribe_group_list', {'significant': False}, name='subscribe_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/subscribe/significant/$', 'subscribe_group_list', {'significant': True}, name='subscribe_significant_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/unsubscribe/$', 'unsubscribe_group_list', {'significant': False}, name='unsubscribe_group_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/unsubscribe/significant/$', 'unsubscribe_group_list', {'significant': True}, name='unsubscribe_significant_group_list'),
url(r'^add_track_document/(?P<document_name>[^/]+)/$', 'add_track_document', name='community_add_track_document'),
url(r'^remove_track_document/(?P<document_name>[^/]+)/$', 'remove_track_document', name='community_remove_track_document'),
url(r'^(?P<list_id>[\d]+)/remove_document/(?P<document_name>[^/]+)/$', 'remove_document', name='community_remove_document'),
url(r'^(?P<list_id>[\d]+)/remove_rule/(?P<rule_id>[^/]+)/$', 'remove_rule', name='community_remove_rule'),
url(r'^(?P<list_id>[\d]+)/subscribe/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_subscription', name='confirm_subscription'),
url(r'^(?P<list_id>[\d]+)/subscribe/significant/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_significant_subscription', name='confirm_significant_subscription'),
url(r'^(?P<list_id>[\d]+)/unsubscribe/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_unsubscription', name='confirm_unsubscription'),
url(r'^(?P<list_id>[\d]+)/unsubscribe/significant/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_significant_unsubscription', name='confirm_significant_unsubscription'),
)

183
ietf/community/utils.py Normal file
View file

@ -0,0 +1,183 @@
import re
from django.db.models import Q
from django.conf import settings
from ietf.community.models import CommunityList, EmailSubscription, SearchRule
from ietf.doc.models import Document, State
from ietf.group.models import Role, Group
from ietf.person.models import Person
from ietf.ietfauth.utils import has_role
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from ietf.utils.mail import send_mail
def states_of_significant_change():
return State.objects.filter(used=True).filter(
Q(type="draft-stream-ietf", slug__in=['adopt-wg', 'wg-lc', 'writeupw', 'parked', 'dead']) |
Q(type="draft-iesg", slug__in=['pub-req', 'lc', 'iesg-eva', 'rfcqueue']) |
Q(type="draft-stream-iab", slug__in=['active', 'review-c', 'rfc-edit']) |
Q(type="draft-stream-irtf", slug__in=['active', 'rg-lc', 'irsg-w', 'iesg-rev', 'rfc-edit', 'iesghold']) |
Q(type="draft-stream-ise", slug__in=['receive', 'ise-rev', 'iesg-rev', 'rfc-edit', 'iesghold']) |
Q(type="draft", slug__in=['rfc', 'dead'])
)
def lookup_community_list(username=None, acronym=None):
assert username or acronym
if acronym:
group = get_object_or_404(Group, acronym=acronym)
clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group)
else:
user = get_object_or_404(User, username=username)
clist = CommunityList.objects.filter(user=user).first() or CommunityList(user=user)
return clist
def can_manage_community_list(user, clist):
if not user or not user.is_authenticated():
return False
if clist.user:
return user == clist.user
elif clist.group:
if has_role(user, 'Secretariat'):
return True
if clist.group.type_id == 'area':
return Role.objects.filter(name__slug='ad', person__user=user, group=clist.group).exists()
elif clist.group.type_id in ('wg', 'rg'):
return Role.objects.filter(name__slug='chair', person__user=user, group=clist.group).exists()
return False
def augment_docs_with_tracking_info(docs, user):
"""Add attribute to each document with whether the document is tracked
by the user or not."""
tracked = set()
if user and user.is_authenticated():
clist = CommunityList.objects.filter(user=user).first()
if clist:
tracked.update(docs_tracked_by_community_list(clist).filter(pk__in=docs).values_list("pk", flat=True))
for d in docs:
d.tracked_in_personal_community_list = d.pk in tracked
def reset_name_contains_index_for_rule(rule):
if not rule.rule_type == "name_contains":
return
rule.name_contains_index = Document.objects.filter(docalias__name__regex=rule.text)
def update_name_contains_indexes_with_new_doc(doc):
for r in SearchRule.objects.filter(rule_type="name_contains"):
# in theory we could use the database to do this query, but
# Django doesn't support a reversed regex operator, and regexp
# support needs backend-specific code so custom SQL is a bit
# cumbersome too
if re.search(r.text, doc.name):
r.name_contains_index.add(doc)
def docs_matching_community_list_rule(rule):
docs = Document.objects.all()
if rule.rule_type in ['group', 'area', 'group_rfc', 'area_rfc']:
return docs.filter(Q(group=rule.group_id) | Q(group__parent=rule.group_id), states=rule.state)
elif rule.rule_type.startswith("state_"):
return docs.filter(states=rule.state)
elif rule.rule_type in ["author", "author_rfc"]:
return docs.filter(states=rule.state, documentauthor__author__person=rule.person)
elif rule.rule_type == "ad":
return docs.filter(states=rule.state, ad=rule.person)
elif rule.rule_type == "shepherd":
return docs.filter(states=rule.state, shepherd__person=rule.person)
elif rule.rule_type == "name_contains":
return docs.filter(states=rule.state, searchrule=rule)
raise NotImplementedError
def community_list_rules_matching_doc(doc):
states = list(doc.states.values_list("pk", flat=True))
rules = SearchRule.objects.none()
if doc.group_id:
groups = [doc.group_id]
if doc.group.parent_id:
groups.append(doc.group.parent_id)
rules |= SearchRule.objects.filter(
rule_type__in=['group', 'area', 'group_rfc', 'area_rfc'],
state__in=states,
group__in=groups
)
rules |= SearchRule.objects.filter(
rule_type__in=['state_iab', 'state_iana', 'state_iesg', 'state_irtf', 'state_ise', 'state_rfceditor', 'state_ietf'],
state__in=states,
)
rules |= SearchRule.objects.filter(
rule_type__in=["author", "author_rfc"],
state__in=states,
person__in=list(Person.objects.filter(email__documentauthor__document=doc)),
)
if doc.ad_id:
rules |= SearchRule.objects.filter(
rule_type="ad",
state__in=states,
person=doc.ad_id,
)
if doc.shepherd_id:
rules |= SearchRule.objects.filter(
rule_type="shepherd",
state__in=states,
person__email=doc.shepherd_id,
)
rules |= SearchRule.objects.filter(
rule_type="name_contains",
state__in=states,
name_contains_index=doc, # search our materialized index to avoid full scan
)
return rules
def docs_tracked_by_community_list(clist):
if clist.pk is None:
return Document.objects.none()
# in theory, we could use an OR query, but databases seem to have
# trouble with OR queries and complicated joins so do the OR'ing
# manually
doc_ids = set(clist.added_docs.values_list("pk", flat=True))
for rule in clist.searchrule_set.all():
doc_ids = doc_ids | set(docs_matching_community_list_rule(rule).values_list("pk", flat=True))
return Document.objects.filter(pk__in=doc_ids)
def community_lists_tracking_doc(doc):
return CommunityList.objects.filter(Q(added_docs=doc) | Q(searchrule__in=community_list_rules_matching_doc(doc)))
def notify_event_to_subscribers(event):
significant = event.type == "changed_state" and event.state_id in [s.pk for s in states_of_significant_change()]
subscriptions = EmailSubscription.objects.filter(community_list__in=community_lists_tracking_doc(event.doc)).distinct()
if not significant:
subscriptions = subscriptions.filter(notify_on="all")
for sub in subscriptions.select_related("community_list", "email"):
clist = sub.community_list
subject = '%s notification: Changes to %s' % (clist.long_name(), event.doc.name)
send_mail(None, sub.email.address, settings.DEFAULT_FROM_EMAIL, subject, 'community/notification_email.txt',
context = {
'event': event,
'clist': clist,
})

View file

@ -1,352 +1,268 @@
import csv
import uuid
import datetime
import hashlib
import json
from django.db import IntegrityError
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.template import RequestContext
from django.utils.http import urlquote
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required
from django.utils.html import strip_tags
from ietf.community.models import CommunityList, Rule, EmailSubscription
from ietf.community.forms import RuleForm, DisplayForm, SubscribeForm, UnSubscribeForm
from ietf.group.models import Group
from ietf.doc.models import DocEvent, DocAlias
from ietf.community.models import SearchRule, EmailSubscription
from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
from ietf.community.utils import lookup_community_list, can_manage_community_list
from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule
from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule
from ietf.doc.models import DocEvent, Document
from ietf.doc.utils_search import prepare_document_table
def view_list(request, username=None):
clist = lookup_community_list(username)
def _manage_list(request, clist):
display_config = clist.get_display_config()
if request.method == 'POST' and request.POST.get('save_rule', None):
rule_form = RuleForm(request.POST, clist=clist)
display_form = DisplayForm(instance=display_config)
if rule_form.is_valid():
try:
rule_form.save()
except IntegrityError:
pass;
rule_form = RuleForm(clist=clist)
display_form = DisplayForm(instance=display_config)
elif request.method == 'POST' and request.POST.get('save_display', None):
display_form = DisplayForm(request.POST, instance=display_config)
rule_form = RuleForm(clist=clist)
if display_form.is_valid():
display_form.save()
rule_form = RuleForm(clist=clist)
display_form = DisplayForm(instance=display_config)
docs = docs_tracked_by_community_list(clist)
docs, meta = prepare_document_table(request, docs, request.GET)
subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
return render(request, 'community/view_list.html', {
'clist': clist,
'docs': docs,
'meta': meta,
'can_manage_list': can_manage_community_list(request.user, clist),
'subscribed': subscribed,
})
@login_required
def manage_list(request, username=None, acronym=None, group_type=None):
# we need to be a bit careful because clist may not exist in the
# database so we can't call related stuff on it yet
clist = lookup_community_list(username, acronym)
if not can_manage_community_list(request.user, clist):
return HttpResponseForbidden("You do not have permission to access this view")
action = request.POST.get('action')
if request.method == 'POST' and action == 'add_documents':
add_doc_form = AddDocumentsForm(request.POST)
if add_doc_form.is_valid():
if clist.pk is None:
clist.save()
for d in add_doc_form.cleaned_data['documents']:
clist.added_docs.add(d)
return HttpResponseRedirect("")
else:
rule_form = RuleForm(clist=clist)
display_form = DisplayForm(instance=display_config)
clist = CommunityList.objects.get(id=clist.id)
return render_to_response('community/manage_clist.html',
{'cl': clist,
'dc': display_config,
'display_form': display_form,
'rule_form': rule_form},
context_instance=RequestContext(request))
add_doc_form = AddDocumentsForm()
if request.method == 'POST' and action == 'remove_document':
document_pk = request.POST.get('document')
if clist.pk is not None and document_pk:
document = get_object_or_404(clist.added_docs, pk=document_pk)
clist.added_docs.remove(document)
return HttpResponseRedirect("")
if request.method == 'POST' and action == 'add_rule':
rule_type_form = SearchRuleTypeForm(request.POST)
if rule_type_form.is_valid():
rule_type = rule_type_form.cleaned_data['rule_type']
if rule_type:
rule_form = SearchRuleForm(clist, rule_type, request.POST)
if rule_form.is_valid():
if clist.pk is None:
clist.save()
rule = rule_form.save(commit=False)
rule.community_list = clist
rule.rule_type = rule_type
rule.save()
if rule.rule_type == "name_contains":
reset_name_contains_index_for_rule(rule)
return HttpResponseRedirect("")
else:
rule_type_form = SearchRuleTypeForm()
rule_form = None
if request.method == 'POST' and action == 'remove_rule':
rule_pk = request.POST.get('rule')
if clist.pk is not None and rule_pk:
rule = get_object_or_404(SearchRule, pk=rule_pk, community_list=clist)
rule.delete()
return HttpResponseRedirect("")
rules = clist.searchrule_set.all() if clist.pk is not None else []
for r in rules:
r.matching_documents_count = docs_matching_community_list_rule(r).count()
empty_rule_forms = { rule_type: SearchRuleForm(clist, rule_type) for rule_type, _ in SearchRule.RULE_TYPES }
total_count = docs_tracked_by_community_list(clist).count()
return render(request, 'community/manage_list.html', {
'clist': clist,
'rules': rules,
'individually_added': clist.added_docs.all() if clist.pk is not None else [],
'rule_type_form': rule_type_form,
'rule_form': rule_form,
'empty_rule_forms': empty_rule_forms,
'total_count': total_count,
'add_doc_form': add_doc_form,
})
def manage_personal_list(request):
user = request.user
if not user.is_authenticated():
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
clist = CommunityList.objects.get_or_create(user=request.user)[0]
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
return _manage_list(request, clist)
@login_required
def track_document(request, name, username=None, acronym=None):
doc = get_object_or_404(Document, docalias__name=name)
if request.method == "POST":
clist = lookup_community_list(username, acronym)
if not can_manage_community_list(request.user, clist):
return HttpResponseForbidden("You do not have permission to access this view")
if clist.pk is None:
clist.save()
clist.added_docs.add(doc)
if request.is_ajax():
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
else:
return HttpResponseRedirect(clist.get_absolute_url())
return render(request, "community/track_document.html", {
"name": doc.name,
})
@login_required
def untrack_document(request, name, username=None, acronym=None):
doc = get_object_or_404(Document, docalias__name=name)
clist = lookup_community_list(username, acronym)
if not can_manage_community_list(request.user, clist):
return HttpResponseForbidden("You do not have permission to access this view")
if request.method == "POST":
if clist.pk is not None:
clist.added_docs.remove(doc)
if request.is_ajax():
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
else:
return HttpResponseRedirect(clist.get_absolute_url())
return render(request, "community/untrack_document.html", {
"name": doc.name,
})
def manage_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if group.type.slug not in ('area', 'wg'):
raise Http404
clist = CommunityList.objects.get_or_create(group=group)[0]
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
return _manage_list(request, clist)
def export_to_csv(request, username=None, acronym=None, group_type=None):
clist = lookup_community_list(username, acronym)
response = HttpResponse(content_type='text/csv')
def add_track_document(request, document_name):
"""supports the "Track this document" functionality
This is exposed in the document view and in document search results."""
if not request.user.is_authenticated():
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
doc = get_object_or_404(DocAlias, name=document_name).document
clist = CommunityList.objects.get_or_create(user=request.user)[0]
clist.update()
return add_document_to_list(request, clist, doc)
if clist.group:
filename = "%s-draft-list.csv" % clist.group.acronym
else:
filename = "draft-list.csv"
def remove_track_document(request, document_name):
"""supports the "Untrack this document" functionality
This is exposed in the document view and in document search results."""
clist = CommunityList.objects.get_or_create(user=request.user)[0]
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
doc = get_object_or_404(DocAlias, name=document_name).document
clist.added_ids.remove(doc)
clist.update()
return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
response['Content-Disposition'] = 'attachment; filename=%s' % filename
def remove_document(request, list_id, document_name):
clist = get_object_or_404(CommunityList, pk=list_id)
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
doc = get_object_or_404(DocAlias, name=document_name).document
clist.added_ids.remove(doc)
clist.update()
return HttpResponseRedirect(clist.get_manage_url())
writer = csv.writer(response, dialect=csv.excel, delimiter=',')
def add_document_to_list(request, clist, doc):
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
clist.added_ids.add(doc)
return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
header = [
"Name",
"Title",
"Date of latest revision",
"Status in the IETF process",
"Associated group",
"Associated AD",
"Date of latest change",
]
writer.writerow(header)
docs = docs_tracked_by_community_list(clist).select_related('type', 'group', 'ad')
for doc in docs.prefetch_related("states", "tags"):
row = []
row.append(doc.name)
row.append(doc.title)
e = doc.latest_event(type='new_revision')
row.append(e.time.strftime("%Y-%m-%d") if e else "")
row.append(strip_tags(doc.friendly_state()))
row.append(doc.group.acronym if doc.group else "")
row.append(unicode(doc.ad) if doc.ad else "")
e = doc.latest_event()
row.append(e.time.strftime("%Y-%m-%d") if e else "")
writer.writerow([v.encode("utf-8") for v in row])
def remove_rule(request, list_id, rule_id):
clist = get_object_or_404(CommunityList, pk=list_id)
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
rule = get_object_or_404(Rule, pk=rule_id)
rule.delete()
return HttpResponseRedirect(clist.get_manage_url())
return response
def feed(request, username=None, acronym=None, group_type=None):
clist = lookup_community_list(username, acronym)
def _view_list(request, clist):
display_config = clist.get_display_config()
return render_to_response('community/public/view_list.html',
{'cl': clist,
'dc': display_config,
},
context_instance=RequestContext(request))
significant = request.GET.get('significant', '') == '1'
documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True)
since = datetime.datetime.now() - datetime.timedelta(days=14)
def view_personal_list(request, secret):
clist = get_object_or_404(CommunityList, secret=secret)
return _view_list(request, clist)
events = DocEvent.objects.filter(
doc__in=documents,
time__gte=since,
).distinct().order_by('-time', '-id').select_related("doc")
def view_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
clist = get_object_or_404(CommunityList, group=group)
return _view_list(request, clist)
def _atom_view(request, clist, significant=False):
documents = [i['pk'] for i in clist.get_documents().values('pk')]
startDate = datetime.datetime.now() - datetime.timedelta(days=14)
notifications = DocEvent.objects.filter(doc__pk__in=documents, time__gte=startDate)\
.distinct()\
.order_by('-time', '-id')
if significant:
notifications = notifications.filter(listnotification__significant=True)
events = events.filter(type="changed_state", statedocevent__state__in=list(states_of_significant_change()))
host = request.get_host()
feed_url = 'https://%s%s' % (host, request.get_full_path())
feed_id = uuid.uuid5(uuid.NAMESPACE_URL, feed_url.encode('utf-8'))
title = '%s RSS Feed' % clist.long_name()
title = u'%s RSS Feed' % clist.long_name()
if significant:
subtitle = 'Document significant changes'
subtitle = 'Significant document changes'
else:
subtitle = 'Document changes'
return render_to_response('community/public/atom.xml',
{'cl': clist,
'entries': notifications,
'title': title,
'subtitle': subtitle,
'id': feed_id.get_urn(),
'updated': datetime.datetime.today(),
},
content_type='text/xml',
context_instance=RequestContext(request))
return render(request, 'community/atom.xml', {
'clist': clist,
'entries': events[:50],
'title': title,
'subtitle': subtitle,
'id': feed_id.get_urn(),
'updated': datetime.datetime.now(),
}, content_type='text/xml')
def changes_personal_list(request, secret):
clist = get_object_or_404(CommunityList, secret=secret)
return _atom_view(request, clist)
def changes_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
clist = get_object_or_404(CommunityList, group=group)
return _atom_view(request, clist)
def significant_personal_list(request, secret):
clist = get_object_or_404(CommunityList, secret=secret)
return _atom_view(request, clist, significant=True)
def significant_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
clist = get_object_or_404(CommunityList, group=group)
return _atom_view(request, clist, significant=True)
def _csv_list(request, clist):
display_config = clist.get_display_config()
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=draft-list.csv'
writer = csv.writer(response, dialect=csv.excel, delimiter=',')
header = []
fields = display_config.get_all_fields()
for field in fields:
header.append(field.description)
writer.writerow(header)
for doc in clist.get_documents():
row = []
for field in fields:
row.append(field().get_value(doc, raw=True))
writer.writerow(row)
return response
def csv_personal_list(request):
user = request.user
if not user.is_authenticated():
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
clist = CommunityList.objects.get_or_create(user=user)[0]
if not clist.check_manager(user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
return _csv_list(request, clist)
def csv_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if group.type.slug not in ('area', 'wg'):
@login_required
def subscription(request, username=None, acronym=None, group_type=None):
clist = lookup_community_list(username, acronym)
if clist.pk is None:
raise Http404
clist = CommunityList.objects.get_or_create(group=group)[0]
if not clist.check_manager(request.user):
path = urlquote(request.get_full_path())
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
return HttpResponseRedirect('%s?%s=%s' % tup)
return _csv_list(request, clist)
def view_csv_personal_list(request, secret):
clist = get_object_or_404(CommunityList, secret=secret)
return _csv_list(request, clist)
existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
def _subscribe_list(request, clist, significant):
success = False
if request.method == 'POST':
form = SubscribeForm(data=request.POST, clist=clist, significant=significant)
if form.is_valid():
form.save()
success = True
action = request.POST.get("action")
if action == "subscribe":
form = SubscriptionForm(request.user, clist, request.POST)
if form.is_valid():
subscription = form.save(commit=False)
subscription.community_list = clist
subscription.save()
return HttpResponseRedirect("")
elif action == "unsubscribe":
existing_subscriptions.filter(pk=request.POST.get("subscription_id")).delete()
return HttpResponseRedirect("")
else:
form = SubscribeForm(clist=clist, significant=significant)
return render_to_response('community/public/subscribe.html',
{'cl': clist,
'form': form,
'success': success,
},
context_instance=RequestContext(request))
form = SubscriptionForm(request.user, clist)
def _unsubscribe_list(request, clist, significant):
success = False
if request.method == 'POST':
form = UnSubscribeForm(data=request.POST, clist=clist, significant=significant)
if form.is_valid():
form.save()
success = True
else:
form = UnSubscribeForm(clist=clist, significant=significant)
return render_to_response('community/public/unsubscribe.html',
{'cl': clist,
'form': form,
'success': success,
'significant': significant,
},
context_instance=RequestContext(request))
def subscribe_personal_list(request, secret, significant=False):
clist = get_object_or_404(CommunityList, secret=secret)
return _subscribe_list(request, clist, significant=significant)
def subscribe_group_list(request, acronym, significant=False):
group = get_object_or_404(Group, acronym=acronym)
clist = get_object_or_404(CommunityList, group=group)
return _subscribe_list(request, clist, significant=significant)
def unsubscribe_personal_list(request, secret, significant=False):
clist = get_object_or_404(CommunityList, secret=secret)
return _unsubscribe_list(request, clist, significant=significant)
def unsubscribe_group_list(request, acronym, significant=False):
group = get_object_or_404(Group, acronym=acronym)
clist = get_object_or_404(CommunityList, group=group)
return _unsubscribe_list(request, clist, significant=significant)
def confirm_subscription(request, list_id, email, date, confirm_hash, significant=False):
clist = get_object_or_404(CommunityList, pk=list_id)
valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, email, 'subscribe', significant)).hexdigest() == confirm_hash
if not valid:
raise Http404
(subscription, created) = EmailSubscription.objects.get_or_create(
community_list=clist,
email=email,
significant=significant)
return render_to_response('community/public/subscription_confirm.html',
{'cl': clist,
'significant': significant,
},
context_instance=RequestContext(request))
def confirm_significant_subscription(request, list_id, email, date, confirm_hash):
return confirm_subscription(request, list_id, email, date, confirm_hash, significant=True)
def confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=False):
clist = get_object_or_404(CommunityList, pk=list_id)
valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, email, 'unsubscribe', significant)).hexdigest() == confirm_hash
if not valid:
raise Http404
EmailSubscription.objects.filter(
community_list=clist,
email=email,
significant=significant).delete()
return render_to_response('community/public/unsubscription_confirm.html',
{'cl': clist,
'significant': significant,
},
context_instance=RequestContext(request))
def confirm_significant_unsubscription(request, list_id, email, date, confirm_hash):
return confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=True)
return render(request, 'community/subscription.html', {
'clist': clist,
'form': form,
'existing_subscriptions': existing_subscriptions,
})

View file

@ -0,0 +1,26 @@
from django import template
from ietf.group.models import Group
register = template.Library()
@register.filter
def managed_groups(user):
if not (user and hasattr(user, "is_authenticated") and user.is_authenticated()):
return []
groups = []
# groups.extend(Group.objects.filter(
# role__name__slug='ad',
# role__person__user=user,
# type__slug='area',
# state__slug='active').select_related("type"))
groups.extend(Group.objects.filter(
role__name__slug='chair',
role__person__user=user,
type__slug__in=('rg', 'wg'),
state__slug__in=('active', 'bof')).select_related("type"))
return groups

189
ietf/doc/utils_search.py Normal file
View file

@ -0,0 +1,189 @@
from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent
from ietf.doc.expire import expirable_draft
from ietf.community.utils import augment_docs_with_tracking_info
def wrap_value(v):
return lambda: v
def fill_in_document_table_attributes(docs):
# fill in some attributes for the document table results to save
# some hairy template code and avoid repeated SQL queries
docs_dict = dict((d.pk, d) for d in docs)
doc_ids = docs_dict.keys()
rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=doc_ids).values_list("document_id", "name"))
# latest event cache
event_types = ("published_rfc",
"changed_ballot_position",
"started_iesg_process",
"new_revision")
for d in docs:
d.latest_event_cache = dict()
for e in event_types:
d.latest_event_cache[e] = None
for e in DocEvent.objects.filter(doc__in=doc_ids, type__in=event_types).order_by('time'):
docs_dict[e.doc_id].latest_event_cache[e.type] = e
# telechat date, can't do this with above query as we need to get TelechatDocEvents out
seen = set()
for e in TelechatDocEvent.objects.filter(doc__in=doc_ids, type="scheduled_for_telechat").order_by('-time'):
if e.doc_id not in seen:
d = docs_dict[e.doc_id]
d.telechat_date = wrap_value(d.telechat_date(e))
seen.add(e.doc_id)
# misc
for d in docs:
# emulate canonical name which is used by a lot of the utils
d.canonical_name = wrap_value(rfc_aliases[d.pk] if d.pk in rfc_aliases else d.name)
if d.rfc_number() != None and d.latest_event_cache["published_rfc"]:
d.latest_revision_date = d.latest_event_cache["published_rfc"].time
elif d.latest_event_cache["new_revision"]:
d.latest_revision_date = d.latest_event_cache["new_revision"].time
else:
d.latest_revision_date = d.time
if d.type_id == "draft":
if d.get_state_slug() == "rfc":
d.search_heading = "RFC"
elif d.get_state_slug() in ("ietf-rm", "auth-rm"):
d.search_heading = "Withdrawn Internet-Draft"
else:
d.search_heading = "%s Internet-Draft" % d.get_state()
else:
d.search_heading = "%s" % (d.type,);
d.expirable = expirable_draft(d)
if d.get_state_slug() != "rfc":
d.milestones = d.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
# RFCs
# errata
erratas = set(Document.objects.filter(tags="errata", name__in=rfc_aliases.keys()).distinct().values_list("name", flat=True))
for d in docs:
d.has_errata = d.name in erratas
# obsoleted/updated by
for a in rfc_aliases:
d = docs_dict[a]
d.obsoleted_by_list = []
d.updated_by_list = []
xed_by = RelatedDocument.objects.filter(target__name__in=rfc_aliases.values(),
relationship__in=("obs", "updates")).select_related('target__document_id')
rel_rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc",
document__in=[rel.source_id for rel in xed_by]).values_list('document_id', 'name'))
for rel in xed_by:
d = docs_dict[rel.target.document_id]
if rel.relationship_id == "obs":
l = d.obsoleted_by_list
elif rel.relationship_id == "updates":
l = d.updated_by_list
l.append(rel_rfc_aliases[rel.source_id].upper())
l.sort()
def prepare_document_table(request, docs, query=None, max_results=500):
"""Take a queryset of documents and a QueryDict with sorting info
and return list of documents with attributes filled in for
displaying a full table of information about the documents, plus
dict with information about the columns."""
if not isinstance(docs, list):
# evaluate and fill in attribute results immediately to decrease
# the number of queries
docs = docs.select_related("ad", "ad__person", "std_level", "intended_std_level", "group", "stream")
docs = docs.prefetch_related("states__type", "tags")
docs = list(docs[:max_results])
fill_in_document_table_attributes(docs)
augment_docs_with_tracking_info(docs, request.user)
meta = {}
sort_key = query and query.get('sort') or ""
sort_reversed = sort_key.startswith("-")
sort_key = sort_key.lstrip("-")
# sort
def generate_sort_key(d):
res = []
rfc_num = d.rfc_number()
if d.type_id == "draft":
res.append(["Active", "Expired", "Replaced", "Withdrawn", "RFC"].index(d.search_heading.split()[0]))
else:
res.append(d.type_id);
res.append("-");
res.append(d.get_state_slug());
res.append("-");
if sort_key == "title":
res.append(d.title)
elif sort_key == "date":
res.append(str(d.latest_revision_date))
elif sort_key == "status":
if rfc_num != None:
res.append(int(rfc_num))
else:
res.append(d.get_state().order if d.get_state() else None)
elif sort_key == "ipr":
res.append(len(d.ipr()))
elif sort_key == "ad":
if rfc_num != None:
res.append(int(rfc_num))
elif d.get_state_slug() == "active":
if d.get_state("draft-iesg"):
res.append(d.get_state("draft-iesg").order)
else:
res.append(0)
else:
if rfc_num != None:
res.append(int(rfc_num))
else:
res.append(d.canonical_name())
return res
docs.sort(key=generate_sort_key, reverse=sort_reversed)
# fill in a meta dict with some information for rendering the table
if len(docs) == max_results:
meta['max'] = max_results
meta['headers'] = [{'title': 'Document', 'key':'document'},
{'title': 'Title', 'key':'title'},
{'title': 'Date', 'key':'date'},
{'title': 'Status', 'key':'status'},
{'title': 'IPR', 'key':'ipr'},
{'title': 'AD / Shepherd', 'key':'ad'}]
if query and hasattr(query, "urlencode"): # fed a Django QueryDict
d = query.copy()
for h in meta['headers']:
h["sort_url"] = "?" + d.urlencode()
if h['key'] == sort_key:
h['sorted'] = True
if sort_reversed:
h['direction'] = 'desc'
d["sort"] = h["key"]
else:
h['direction'] = 'asc'
d["sort"] = "-" + h["key"]
else:
d["sort"] = h["key"]
return (docs, meta)

View file

@ -36,7 +36,6 @@ from django.http import HttpResponse, Http404 , HttpResponseForbidden
from django.shortcuts import render, render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse as urlreverse
from django.conf import settings
from django import forms
@ -50,7 +49,7 @@ from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_wi
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, crawl_history)
from ietf.community.models import CommunityList
from ietf.community.utils import augment_docs_with_tracking_info
from ietf.group.models import Role
from ietf.group.utils import can_manage_group_type, can_manage_materials
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
@ -345,15 +344,7 @@ def document_main(request, name, rev=None):
elif can_edit_stream_info and (not iesg_state or iesg_state.slug == 'watching'):
actions.append(("Submit to IESG for Publication", urlreverse('doc_to_iesg', kwargs=dict(name=doc.name))))
tracking_document = False
if request.user.is_authenticated():
try:
clist = CommunityList.objects.get(user=request.user)
clist.update()
if clist.get_documents().filter(name=doc.name).count() > 0:
tracking_document = True
except ObjectDoesNotExist:
pass
augment_docs_with_tracking_info([doc], request.user)
replaces = [d.name for d in doc.related_that_doc("replaces")]
replaced_by = [d.name for d in doc.related_that("replaces")]
@ -420,7 +411,6 @@ def document_main(request, name, rev=None):
shepherd_writeup=shepherd_writeup,
search_archive=search_archive,
actions=actions,
tracking_document=tracking_document,
presentations=presentations,
),
context_instance=RequestContext(request))

View file

@ -881,6 +881,7 @@ class ShepherdWriteupUploadForm(forms.Form):
def clean_txt(self):
return get_cleaned_text_file_content(self.cleaned_data["txt"])
@login_required
def edit_shepherd_writeup(request, name):
"""Change this document's shepherd writeup"""
doc = get_object_or_404(Document, type="draft", name=name)

View file

@ -35,7 +35,6 @@ import datetime, re
from django import forms
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse as urlreverse
from django.db.models import Q
from django.http import Http404, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect
@ -44,16 +43,15 @@ from django.utils.cache import _generate_cache_key
import debug # pyflakes:ignore
from ietf.community.models import CommunityList
from ietf.doc.models import ( Document, DocHistory, DocAlias, State, RelatedDocument,
DocEvent, LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
from ietf.doc.expire import expirable_draft
from ietf.doc.models import ( Document, DocHistory, DocAlias, State,
LastCallDocEvent, IESG_SUBSTATE_TAGS )
from ietf.doc.fields import select2_id_doc_name_json
from ietf.group.models import Group
from ietf.idindex.index import active_drafts_index_by_group
from ietf.name.models import DocTagName, DocTypeName, StreamName
from ietf.person.models import Person
from ietf.utils.draft_search import normalize_draftname
from ietf.doc.utils_search import prepare_document_table
class SearchForm(forms.Form):
@ -62,7 +60,7 @@ class SearchForm(forms.Form):
activedrafts = forms.BooleanField(required=False, initial=True)
olddrafts = forms.BooleanField(required=False, initial=False)
by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state','stream')], required=False, initial='wg')
by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state','stream')], required=False, initial='group')
author = forms.CharField(required=False)
group = forms.CharField(required=False)
stream = forms.ModelChoiceField(StreamName.objects.all().order_by('name'), empty_label="any stream", required=False)
@ -81,7 +79,7 @@ class SearchForm(forms.Form):
("ad", "AD"), ("-ad", "AD (desc)"), ),
required=False, widget=forms.HiddenInput)
doctypes = DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name');
doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name'), required=False)
def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs)
@ -123,126 +121,27 @@ class SearchForm(forms.Form):
q['state'] = q['substate'] = None
return q
def wrap_value(v):
return lambda: v
def fill_in_search_attributes(docs):
# fill in some attributes for the search results to save some
# hairy template code and avoid repeated SQL queries
docs_dict = dict((d.pk, d) for d in docs)
doc_ids = docs_dict.keys()
rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=doc_ids).values_list("document_id", "name"))
# latest event cache
event_types = ("published_rfc",
"changed_ballot_position",
"started_iesg_process",
"new_revision")
for d in docs:
d.latest_event_cache = dict()
for e in event_types:
d.latest_event_cache[e] = None
for e in DocEvent.objects.filter(doc__in=doc_ids, type__in=event_types).order_by('time'):
docs_dict[e.doc_id].latest_event_cache[e.type] = e
# telechat date, can't do this with above query as we need to get TelechatDocEvents out
seen = set()
for e in TelechatDocEvent.objects.filter(doc__in=doc_ids, type="scheduled_for_telechat").order_by('-time'):
if e.doc_id not in seen:
d = docs_dict[e.doc_id]
d.telechat_date = wrap_value(d.telechat_date(e))
seen.add(e.doc_id)
# misc
for d in docs:
# emulate canonical name which is used by a lot of the utils
d.canonical_name = wrap_value(rfc_aliases[d.pk] if d.pk in rfc_aliases else d.name)
if d.rfc_number() != None and d.latest_event_cache["published_rfc"]:
d.latest_revision_date = d.latest_event_cache["published_rfc"].time
elif d.latest_event_cache["new_revision"]:
d.latest_revision_date = d.latest_event_cache["new_revision"].time
else:
d.latest_revision_date = d.time
if d.type_id == "draft":
if d.get_state_slug() == "rfc":
d.search_heading = "RFC"
elif d.get_state_slug() in ("ietf-rm", "auth-rm"):
d.search_heading = "Withdrawn Internet-Draft"
else:
d.search_heading = "%s Internet-Draft" % d.get_state()
else:
d.search_heading = "%s" % (d.type,);
d.expirable = expirable_draft(d)
if d.get_state_slug() != "rfc":
d.milestones = d.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
# RFCs
# errata
erratas = set(Document.objects.filter(tags="errata", name__in=rfc_aliases.keys()).distinct().values_list("name", flat=True))
for d in docs:
d.has_errata = d.name in erratas
# obsoleted/updated by
for a in rfc_aliases:
d = docs_dict[a]
d.obsoleted_by_list = []
d.updated_by_list = []
xed_by = RelatedDocument.objects.filter(target__name__in=rfc_aliases.values(),
relationship__in=("obs", "updates")).select_related('target__document_id')
rel_rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc",
document__in=[rel.source_id for rel in xed_by]).values_list('document_id', 'name'))
for rel in xed_by:
d = docs_dict[rel.target.document_id]
if rel.relationship_id == "obs":
l = d.obsoleted_by_list
elif rel.relationship_id == "updates":
l = d.updated_by_list
l.append(rel_rfc_aliases[rel.source_id].upper())
l.sort()
def retrieve_search_results(form, all_types=False):
"""Takes a validated SearchForm and return the results."""
if not form.is_valid():
raise ValueError("SearchForm doesn't validate: %s" % form.errors)
query = form.cleaned_data
types=[];
meta = {}
if (query['activedrafts'] or query['olddrafts'] or query['rfcs']):
types.append('draft')
# Advanced document types are data-driven, so we need to read them from the
# raw form.data field (and track their checked/unchecked state ourselves)
meta['checked'] = {}
alltypes = DocTypeName.objects.exclude(slug='draft').order_by('name');
for doctype in alltypes:
if form.data.__contains__('include-' + doctype.slug):
types.append(doctype.slug)
meta['checked'][doctype.slug] = True
if len(types) == 0 and not all_types:
return ([], {})
MAX = 500
if all_types:
docs = Document.objects.all()
else:
types = []
if query['activedrafts'] or query['olddrafts'] or query['rfcs']:
types.append('draft')
types.extend(query["doctypes"])
if not types:
return []
docs = Document.objects.filter(type__in=types)
# name
@ -281,104 +180,7 @@ def retrieve_search_results(form, all_types=False):
elif by == "stream":
docs = docs.filter(stream=query["stream"])
# evaluate and fill in attribute results immediately to cut down
# the number of queries
docs = docs.select_related("ad", "ad__person", "std_level", "intended_std_level", "group", "stream")
docs = docs.prefetch_related("states__type", "tags")
results = list(docs[:MAX])
fill_in_search_attributes(results)
# sort
def sort_key(d):
res = []
rfc_num = d.rfc_number()
if d.type_id == "draft":
res.append(["Active", "Expired", "Replaced", "Withdrawn", "RFC"].index(d.search_heading.split()[0] ))
else:
res.append(d.type_id);
res.append("-");
res.append(d.get_state_slug());
res.append("-");
if query["sort"] in ["title", "-title"]:
res.append(d.title)
elif query["sort"] in ["date", "-date" ]:
res.append(str(d.latest_revision_date))
elif query["sort"] in ["status", "-status"]:
if rfc_num != None:
res.append(int(rfc_num))
else:
res.append(d.get_state().order if d.get_state() else None)
elif query["sort"] in ["ipr", "-ipr"]:
res.append(len(d.ipr()))
elif query["sort"] in ["ad", "-ad"]:
if rfc_num != None:
res.append(int(rfc_num))
elif d.get_state_slug() == "active":
if d.get_state("draft-iesg"):
res.append(d.get_state("draft-iesg").order)
else:
res.append(0)
else:
if rfc_num != None:
res.append(int(rfc_num))
else:
res.append(d.canonical_name())
return res
results.sort(key=sort_key, reverse=query["sort"].startswith("-"))
# fill in a meta dict with some information for rendering the result table
if len(results) == MAX:
meta['max'] = MAX
meta['by'] = query['by']
meta['advanced'] = bool(query['by'] or len(meta['checked']))
meta['headers'] = [{'title': 'Document', 'key':'document'},
{'title': 'Title', 'key':'title'},
{'title': 'Date', 'key':'date'},
{'title': 'Status', 'key':'status'},
{'title': 'IPR', 'key':'ipr'},
{'title': 'AD / Shepherd', 'key':'ad'}]
if hasattr(form.data, "urlencode"): # form was fed a Django QueryDict, not local plain dict
d = form.data.copy()
for h in meta['headers']:
sort = query.get('sort')
if sort.endswith(h['key']):
h['sorted'] = True
if sort.startswith('-'):
h['direction'] = 'desc'
d["sort"] = h["key"]
else:
h['direction'] = 'asc'
d["sort"] = "-" + h["key"]
else:
d["sort"] = h["key"]
h["sort_url"] = "?" + d.urlencode()
return (results, meta)
def get_doc_is_tracked(request, results):
# Determine whether each document is being tracked or not, and remember
# that so we can display the proper track/untrack option.
doc_is_tracked = { }
if request.user.is_authenticated():
try:
clist = CommunityList.objects.get(user=request.user)
clist.update()
except ObjectDoesNotExist:
return doc_is_tracked
for doc in results:
if clist.get_documents().filter(name=doc.name).count() > 0:
doc_is_tracked[doc.name] = True
return doc_is_tracked
return docs
def search(request):
if request.GET:
@ -395,17 +197,16 @@ def search(request):
if not form.is_valid():
return HttpResponseBadRequest("form not valid: %s" % form.errors)
results, meta = retrieve_search_results(form)
results = retrieve_search_results(form)
results, meta = prepare_document_table(request, results, get_params)
meta['searching'] = True
else:
form = SearchForm()
results = []
meta = { 'by': None, 'advanced': False, 'searching': False }
doc_is_tracked = get_doc_is_tracked(request, results)
meta = { 'by': None, 'searching': False }
return render(request, 'doc/search/search.html', {
'form':form, 'docs':results, 'doc_is_tracked':doc_is_tracked, 'meta':meta, },
'form':form, 'docs':results, 'meta':meta, },
)
def frontpage(request):
@ -472,7 +273,7 @@ def search_for_name(request, name):
else:
for t in doctypenames:
if n.startswith(t.prefix):
search_args += "&include-%s=on" % t.slug
search_args += "&doctypes=%s" % t.slug
break
else:
search_args += "&rfcs=on&activedrafts=on&olddrafts=on"
@ -579,8 +380,9 @@ def docs_for_ad(request, name):
raise Http404
form = SearchForm({'by':'ad','ad': ad.id,
'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
'sort': 'status'})
results, meta = retrieve_search_results(form, all_types=True)
'sort': 'status',
'doctypes': list(DocTypeName.objects.filter(used=True).exclude(slug='draft').values_list("pk", flat=True))})
results, meta = prepare_document_table(request, retrieve_search_results(form), form.data)
results.sort(key=ad_dashboard_sort_key)
del meta["headers"][-1]
#
@ -594,7 +396,7 @@ def docs_for_ad(request, name):
def drafts_in_last_call(request):
lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
form = SearchForm({'by':'state','state': lc_state, 'rfcs':'on', 'activedrafts':'on'})
results, meta = retrieve_search_results(form)
results, meta = prepare_document_table(request, retrieve_search_results(form), form.data)
return render(request, 'doc/drafts_in_last_call.html', {
'form':form, 'docs':results, 'meta':meta

View file

@ -17,7 +17,7 @@ from ietf.doc.utils_charter import charter_name_for_group
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
from ietf.group.utils import save_group_in_history, can_manage_group_type
from ietf.group.utils import get_group_or_404
from ietf.group.utils import get_group_or_404, setup_default_community_list_for_group
from ietf.ietfauth.utils import has_role
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Person, Email
@ -231,6 +231,9 @@ def edit(request, group_type=None, acronym=None, action="edit"):
state=clean["state"]
)
if group.features.has_documents:
setup_default_community_list_for_group(group)
e = ChangeStateGroupEvent(group=group, type="changed_state")
e.time = group.time
e.by = request.user.person

View file

@ -39,7 +39,7 @@ from tempfile import mkstemp
import datetime
from collections import OrderedDict
from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404
from django.template.loader import render_to_string
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.conf import settings
@ -48,14 +48,16 @@ from django.views.decorators.cache import cache_page
from django.db.models import Q
from django.utils.safestring import mark_safe
from ietf.doc.views_search import SearchForm, retrieve_search_results, get_doc_is_tracked
from ietf.doc.models import Document, State, DocAlias, RelatedDocument
from ietf.doc.utils import get_chartering_type
from ietf.doc.templatetags.ietf_filters import clean_whitespace
from ietf.doc.utils_search import prepare_document_table
from ietf.group.models import Group, Role, ChangeStateGroupEvent
from ietf.name.models import GroupTypeName
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
from ietf.group.utils import can_manage_materials, get_group_or_404
from ietf.community.utils import docs_tracked_by_community_list, can_manage_community_list
from ietf.community.models import CommunityList, EmailSubscription
from ietf.utils.pipe import pipe
from ietf.settings import MAILING_LIST_INFO_URL
from ietf.mailtrigger.utils import gather_relevant_expansions
@ -369,6 +371,11 @@ def construct_group_menu_context(request, group, selected, group_type, others):
if group.state_id != "proposed" and (is_chair or can_manage):
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
if group.features.has_documents:
clist = CommunityList.objects.filter(group=group).first()
if clist and can_manage_community_list(request.user, clist):
actions.append((u'Manage document list', urlreverse('community_group_manage_list', kwargs=kwargs)))
if group.features.has_materials and can_manage_materials(request.user, group):
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
@ -393,36 +400,23 @@ def construct_group_menu_context(request, group, selected, group_type, others):
return d
def search_for_group_documents(group):
form = SearchForm({ 'by':'group', 'group': group.acronym or "", 'rfcs':'on', 'activedrafts': 'on' })
docs, meta = retrieve_search_results(form)
# get the related docs
form_related = SearchForm({ 'by':'group', 'name': u'-%s-' % group.acronym, 'activedrafts': 'on' })
raw_docs_related, meta_related = retrieve_search_results(form_related)
def prepare_group_documents(request, group, clist):
found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET)
docs = []
docs_related = []
for d in raw_docs_related:
parts = d.name.split("-", 2);
# canonical form draft-<name|ietf|irtf>-wg-etc
if len(parts) >= 3 and parts[1] not in ("ietf", "irtf") and parts[2].startswith(group.acronym + "-") and d not in docs:
# split results
for d in found_docs:
# non-WG drafts and call for WG adoption are considered related
if (d.group != group
or (d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"))):
d.search_heading = "Related Internet-Draft"
docs_related.append(d)
# move call for WG adoption to related
cleaned_docs = []
docs_related_names = set(d.name for d in docs_related)
for d in docs:
if d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"):
if d.name not in docs_related_names:
d.search_heading = "Related Internet-Draft"
docs_related.append(d)
else:
cleaned_docs.append(d)
docs.append(d)
docs = cleaned_docs
docs_related.sort(key=lambda d: d.name)
meta_related = meta.copy()
return docs, meta, docs_related, meta_related
@ -438,17 +432,19 @@ def group_documents(request, acronym, group_type=None):
if not group.features.has_documents:
raise Http404
docs, meta, docs_related, meta_related = search_for_group_documents(group)
clist = get_object_or_404(CommunityList, group=group)
doc_is_tracked = get_doc_is_tracked(request, docs)
doc_is_tracked.update(get_doc_is_tracked(request, docs_related))
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
context = construct_group_menu_context(request, group, "documents", group_type, {
'docs': docs,
'meta': meta,
'docs_related': docs_related,
'meta_related': meta_related,
'doc_is_tracked': doc_is_tracked,
'subscribed': subscribed,
'clist': clist,
})
return render(request, 'group/group_documents.html', context)
@ -459,7 +455,9 @@ def group_documents_txt(request, acronym, group_type=None):
if not group.features.has_documents:
raise Http404
docs, meta, docs_related, meta_related = search_for_group_documents(group)
clist = get_object_or_404(CommunityList, group=group)
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
for d in docs:
d.prefix = d.get_state().name

View file

@ -15,7 +15,7 @@ from django.core.urlresolvers import NoReverseMatch
from ietf.doc.models import Document, DocAlias, DocEvent, State
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
from ietf.group.utils import save_group_in_history
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
from ietf.name.models import DocTagName, GroupStateName, GroupTypeName
from ietf.person.models import Person, Email
from ietf.utils.test_utils import TestCase, unicontent
@ -186,20 +186,21 @@ class GroupPagesTests(TestCase):
name=draft2.name,
)
setup_default_community_list_for_group(group)
url = urlreverse('ietf.group.info.group_documents', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.name in unicontent(r))
self.assertTrue(group.name in unicontent(r))
self.assertTrue(group.acronym in unicontent(r))
self.assertTrue(draft2.name in unicontent(r))
# Make sure that a logged in user is presented with an opportunity to add results to their community list
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.community-list-add-remove-doc')]))
self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.track-untrack-doc')]))
# test the txt version too while we're at it
url = urlreverse('ietf.group.info.group_documents_txt', kwargs=dict(group_type=group.type_id, acronym=group.acronym))

View file

@ -5,6 +5,10 @@ urlpatterns = patterns('',
(r'^$', 'ietf.group.info.group_home', None, "group_home"),
(r'^documents/txt/$', 'ietf.group.info.group_documents_txt'),
(r'^documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
(r'^documents/manage/$', 'ietf.community.views.manage_list', None, "community_group_manage_list"),
(r'^documents/csv/$', 'ietf.community.views.export_to_csv', None, 'community_group_csv'),
(r'^documents/feed/$', 'ietf.community.views.feed', None, 'community_group_feed'),
(r'^documents/subscription/$', 'ietf.community.views.subscription', None, 'community_group_subscription'),
(r'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
(r'^history/$','ietf.group.info.history'),

View file

@ -8,6 +8,9 @@ from ietf.group.models import Group, RoleHistory
from ietf.person.models import Email
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
from ietf.ietfauth.utils import has_role
from ietf.community.models import CommunityList, SearchRule
from ietf.community.utils import reset_name_contains_index_for_rule
from ietf.doc.models import State
def save_group_in_history(group):
@ -107,3 +110,25 @@ def get_group_or_404(acronym, group_type):
possible_groups = possible_groups.filter(type=group_type)
return get_object_or_404(possible_groups, acronym=acronym)
def setup_default_community_list_for_group(group):
clist = CommunityList.objects.create(group=group)
SearchRule.objects.create(
community_list=clist,
rule_type="group",
group=group,
state=State.objects.get(slug="active", type="draft"),
)
SearchRule.objects.create(
community_list=clist,
rule_type="group_rfc",
group=group,
state=State.objects.get(slug="rfc", type="draft"),
)
related_docs_rule = SearchRule.objects.create(
community_list=clist,
rule_type="name_contains",
text=r"^draft-[^-]+-%s-" % group.acronym,
state=State.objects.get(slug="active", type="draft"),
)
reset_name_contains_index_for_rule(related_docs_rule)

View file

@ -5,7 +5,8 @@ from django.template import RequestContext
from django.http import Http404, HttpResponseForbidden
from django import forms
from ietf.doc.views_search import SearchForm, retrieve_search_results
from ietf.doc.models import Document
from ietf.doc.utils_search import prepare_document_table
from ietf.group.models import Group, GroupEvent, Role
from ietf.group.utils import save_group_in_history
from ietf.ietfauth.utils import has_role
@ -27,9 +28,9 @@ def stream_documents(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
stream = StreamName.objects.get(slug=acronym)
form = SearchForm({'by':'stream', 'stream':acronym,
'rfcs':'on', 'activedrafts':'on'})
docs, meta = retrieve_search_results(form)
qs = Document.objects.filter(states__type="draft", states__slug__in=["active", "rfc"], stream=acronym)
docs, meta = prepare_document_table(request, qs)
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable }, context_instance=RequestContext(request))
class StreamEditForm(forms.Form):

View file

@ -60,7 +60,7 @@ from ietf.iesg.models import TelechatDate
from ietf.iesg.utils import telechat_page_count
from ietf.ietfauth.utils import has_role, role_required, user_is_person
from ietf.person.models import Person
from ietf.doc.views_search import fill_in_search_attributes
from ietf.doc.utils_search import fill_in_document_table_attributes
def review_decisions(request, year=None):
events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
@ -370,7 +370,7 @@ def agenda_documents(request):
sections = agenda_sections()
# augment the docs with the search attributes, since we're using
# the search_result_row view to display them (which expects them)
fill_in_search_attributes(docs_by_date[date])
fill_in_document_table_attributes(docs_by_date[date])
fill_in_agenda_docs(date, sections, docs_by_date[date])
telechats.append({

View file

@ -10,7 +10,7 @@ urlpatterns = patterns('ietf.ietfauth.views',
url(r'^logout/$', logout),
# url(r'^loggedin/$', 'ietf_loggedin'),
# url(r'^loggedout/$', 'logged_out'),
url(r'^profile/$', 'profile'),
url(r'^profile/$', 'profile', name="account_profile"),
# (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'url_login'),
url(r'^testemail/$', 'test_email'),
url(r'^create/$', 'create_account', name='create_account'),

View file

@ -5,7 +5,7 @@ from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from ietf.group.models import Group, GroupMilestone, ChangeStateGroupEvent, GroupEvent, GroupURL, Role
from ietf.group.utils import save_group_in_history, get_charter_text
from ietf.group.utils import save_group_in_history, get_charter_text, setup_default_community_list_for_group
from ietf.ietfauth.utils import role_required
from ietf.person.models import Person
from ietf.secr.groups.forms import GroupModelForm, GroupMilestoneForm, RoleForm, SearchForm
@ -102,6 +102,9 @@ def add(request):
awp.group = group
awp.save()
if group.features.has_documents:
setup_default_community_list_for_group(group)
# create GroupEvent(s)
# always create started event
ChangeStateGroupEvent.objects.create(group=group,

View file

@ -448,3 +448,14 @@ form.navbar-form input.form-control.input-sm { width: 141px; }
padding: inherit;
outline: inherit;
}
/* Community lists */
label#list-feeds {
display: inline-block;
font-weight: normal;
}
.email-subscription button[type=submit] {
margin-left: 3em;
}

View file

@ -100,24 +100,20 @@ $(document).ready(function () {
}
// search results
$('.community-list-add-remove-doc').click(function(e) {
$('.track-untrack-doc').click(function(e) {
e.preventDefault();
var trigger = $(this);
$.ajax({
var trigger = $(this);
$.ajax({
url: trigger.attr('href'),
type: 'GET',
type: 'POST',
cache: false,
dataType: 'json',
success: function(response){
if (response.success) {
trigger.parent().find(".tooltip").remove();
trigger.find("span.fa").toggleClass("fa-bookmark fa-bookmark-o");
if (trigger.hasClass('btn')) {
trigger.attr('disabled', true).blur();
} else {
trigger.contents().unwrap().blur();
trigger.parent().find(".tooltip").remove();
trigger.addClass("hide");
trigger.parent().find(".track-untrack-doc").not(trigger).removeClass("hide");
}
}
}
});
});

View file

@ -0,0 +1,27 @@
$(document).ready(function () {
$("[name=rule_type]").on("click change keypress", function () {
var form = $(this).closest("form");
var ruleType = $(this).val();
var emptyForms = $(".empty-forms");
var currentFormContent = form.find(".form-content-placeholder .rule-type");
if (!ruleType || !currentFormContent.hasClass(ruleType)) {
// move previous back into the collection
if (currentFormContent.length > 0)
emptyForms.append(currentFormContent);
else
currentFormContent.html(""); // make sure it's empty
// insert new
if (ruleType)
form.find(".form-content-placeholder").append(emptyForms.find("." + ruleType));
}
});
$("[name=rule_type]").each(function () {
// don't trigger the handler if we have a form with errors
var placeholderContent = $(this).closest("form").find(".form-content-placeholder >");
if (placeholderContent.length == 0 || placeholderContent.hasClass("rule-type"))
$(this).trigger("change");
});
});

View file

@ -20,6 +20,7 @@ from ietf.person.models import Person
from ietf.group.models import Group
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
from ietf.submit.models import Submission, Preapproval
from ietf.group.utils import setup_default_community_list_for_group
class SubmitTests(TestCase):
def setUp(self):
@ -143,7 +144,8 @@ class SubmitTests(TestCase):
def submit_new_wg(self, formats):
# submit new -> supply submitter info -> approve
draft = make_test_data()
setup_default_community_list_for_group(draft.group)
# prepare draft to suggest replace
sug_replaced_draft = Document.objects.create(
name="draft-ietf-ames-sug-replaced",

View file

@ -13,6 +13,7 @@ from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.name.models import StreamName
from ietf.person.models import Person, Email
from ietf.community.utils import update_name_contains_indexes_with_new_doc
from ietf.submit.mail import announce_to_lists, announce_new_version, announce_to_authors
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
from ietf.utils import unaccent
@ -126,7 +127,7 @@ def post_submission(request, submission):
if not (group.type_id == "individ" and draft.group and draft.group.type_id == "area"):
# don't overwrite an assigned area if it's still an individual
# submission
draft.group_id = group.pk
draft.group = group
draft.rev = submission.rev
draft.pages = submission.pages
draft.abstract = submission.abstract
@ -205,6 +206,8 @@ def post_submission(request, submission):
new_replaces, new_possibly_replaces = update_replaces_from_submission(request, submission, draft)
update_name_contains_indexes_with_new_doc(draft)
announce_to_lists(request, submission)
announce_new_version(request, submission, draft, state_change_msg)
announce_to_authors(request, submission)

View file

@ -1,5 +1,5 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
{% load ietf_filters community_tags wg_menu streams_menu active_groups_menu %}
{% load ietf_filters managed_groups wg_menu streams_menu active_groups_menu %}
{% if flavor != "top" %}
{% include "base/menu_user.html" %}
@ -51,14 +51,14 @@
<li><a href="{% url "submit_approvals" %}">Approve a draft</a></li>
{% endif %}
{% get_user_managed_lists user as community_lists %}
{% if community_lists %}
<li><a href="{{ community_lists.personal.get_manage_url }}">My tracked docs</a></li>
{% for cl in community_lists.group %}
<li><a href="{{ cl.get_manage_url }}">{{ cl.short_name }} {{cl.group.type.slug}} docs</a></li>
{% if user and user.is_authenticated %}
<li><a href="{% url "community_personal_view_list" user.username %}">My tracked docs</a></li>
{% for g in user|managed_groups %}
<li><a href="{% url "group_docs" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
{% endfor %}
{% else %}
<li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in to track docs</a></li>
<li><a rel="nofollow" href="/accounts/login/?next={{ request.get_full_path|urlencode }}">Sign in to track docs</a></li>
{% endif %}
{% if user|has_role:"Area Director,Secretariat" %}

View file

@ -1,24 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load bootstrap3 %}
<form method="post" action="#custom">
{% csrf_token %}
{% bootstrap_form display_form %}
<div class="form-group">
<label>Show fields</label>
{% for field in dc.get_display_fields_config %}
<div class="checkbox">
<label for="id_{{ field.codename }}">
<input id="id_{{ field.codename }}" type="checkbox" name="{{ field.codename }}" {% if field.active %}checked{% endif %}>
{{ field.description }}
</label>
</div>
{% endfor %}
</div>
{% buttons %}
<input type="submit" class="btn btn-primary" name="save_display" value="Save configuration">
{% endbuttons %}
</form>

View file

@ -1,2 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
<span class="displayField displayField-{{ field.codename }}">{{ value|safe }}</span>

View file

@ -0,0 +1,22 @@
<ul class="list-inline pull-right" style="margin-top:1em;">
<li>
<label id="list-feeds">Atom feed:</label>
<div class="btn-group" role="group" aria-labelledby="list-feeds">
<a class="btn btn-default" title="Feed of all changes" href="{% if clist.group %}{% url "community_group_feed" clist.group.acronym %}{% else %}{% url "community_personal_feed" clist.user.username %}{% endif %}">All changes</a>
<a class="btn btn-default" title="Feed of only significant state changes" href="{% if clist.group %}{% url "community_group_feed" clist.group.acronym %}{% else %}{% url "community_personal_feed" clist.user.username %}{% endif %}?significant=1">Significant</a>
</div>
</li>
{% if clist.pk != None %}
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_subscription" clist.group.acronym %}{% else %}{% url "community_personal_subscription" clist.user.username %}{% endif %}">
<i class="glyphicon glyphicon-envelope"></i>
{% if subscribed %}
Change subscription
{% else %}
Subscribe to changes
{% endif %}
</a></li>
{% endif %}
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_csv" clist.group.acronym %}{% else %}{% url "community_personal_csv" clist.user.username %}{% endif %}"><i class="glyphicon glyphicon-list"></i> Export as CSV</a></li>
</ul>

View file

@ -49,8 +49,8 @@
<tr>
<td>{{ doc.display_name }}</td>
<td>{{ doc.get_state }}</td>
<td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></td>
<td><a class="btn btn-danger btn-xs" href="{% url "community_remove_document" cl.pk doc.pk %}">Remove</a></td>
<td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></td>
<td><a class="btn btn-danger btn-xs" href="{% if cl.user %}{% url "community_personal_untrack_document" doc.pk %}{% else %}{% url "community_group_untrack_document" %}{% endif %}">Remove</a></td>
</tr>
{% endfor %}
</tbody>

View file

@ -0,0 +1,143 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% load staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block title %}Manage {{ clist.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Manage {{ clist.long_name }}</h1>
<noscript>This page depends on Javascript being enabled to work properly.</noscript>
{% bootstrap_messages %}
<p>The list currently tracks <a href="{{ clist.get_absolute_url }}">{{ total_count }} document{{ total_count|pluralize }}</a>.</p>
<p><a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a></p>
<h2>Individual documents</h2>
{% if individually_added %}
<p>The list tracks {{ individually_added|length }} individually added document{{ individually_added|length|pluralize }}:</p>
<table class="table table-condensed table-striped">
<tbody>
{% for d in individually_added %}
<tr>
<td>{{ d.name }}</td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" name="document" value="{{ d.pk }}">
<button class="btn btn-danger btn-xs" name="action" value="remove_document">Remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>The list does not track any individually added documents yet.</p>
{% endif %}
{% if clist.group %}
<p>Add individual documents here:</p>
{% else %}
<p>Conveniently track individual documents in your personal list with the track icon <span class="fa fa-bookmark-o"></span> in <a href="/doc/search/">search results</a>.</p>
<p>You can also add documents here:</p>
{% endif %}
<form class="form add-document" method="post">
{% csrf_token %}
{% bootstrap_field add_doc_form.documents show_label=False %}
<button class="btn btn-primary" name="action" value="add_documents">Add documents</button>
</form>
<h2>Search rules</h2>
<p>You can track documents with a search rule. When a document fulfills the search criteria, it will automatically show up in the list.</p>
{% if rules %}
<table class="table table-condensed table-striped">
<thead>
<tr><th>Rule</th><th>Value</th><th>Documents</th><th></th></tr>
</thead>
<tbody>
{% for rule in rules %}
<tr id="r{{ rule.pk }}">
<td>{{ rule.get_rule_type_display }}</td>
<td>
{% if "group" in rule.rule_type or "area" in rule.rule_type %}
{{ rule.group.acronym }}
{% elif "state_" in rule.rule_type %}
{{ rule.state }}
{% elif "author" in rule.rule_type or rule.rule_type == "ad" or "shepherd" in rule.rule_type %}
{{ rule.person }}
{% elif "name_contains" in rule.rule_type %}
{{ rule.text }}
{% endif %}
</td>
<td>{{ rule.matching_documents_count }} match{{ rule.matching_documents_count|pluralize:"es" }}</td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" name="rule" value="{{ rule.pk }}">
<button class="btn btn-danger btn-xs" name="action" value="remove_rule">Remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No rules defined.</p>
{% endif %}
<div><a class="btn btn-primary" data-toggle="collapse" data-target="#add-new-rule">Add a new rule</a></div>
<div id="add-new-rule" {% if not rule_type_form.errors and not rule_form %}class="collapse"{% endif %}>
<h3>Add a new rule</h3>
<form method="post">
{% csrf_token %}
{% bootstrap_form rule_type_form %}
<div class="form-content-placeholder">
{% if rule_form %}
{% bootstrap_form rule_form %}
{% endif %}
</div>
{% buttons %}
<button type="submit" class="btn btn-primary" name="action" value="add_rule">Add rule</button>
{% endbuttons %}
</form>
<div class="empty-forms hide">
{% for rule_type, f in empty_rule_forms.items %}
<div class="rule-type {{ rule_type }}">
{% bootstrap_form f %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
<script src="{% static 'ietf/js/manage-community-list.js' %}"></script>
{% endblock %}

View file

@ -3,14 +3,14 @@ Hello,
This is a notification from the {{ clist.long_name }}.
Document: {{ notification.doc }},
https://datatracker.ietf.org/doc/{{ notification.doc }}
Document: {{ event.doc }},
https://datatracker.ietf.org/doc/{{ event.doc_id }}/
Change:
{{ notification.desc|textify|striptags }}
{{ event.desc|textify|striptags }}
Best regards,
The datatracker draft tracking service
The Datatracker draft tracking service
(for the IETF Secretariat)
{% endautoescape %}

View file

@ -1,30 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Subscribe to {{ cl.long_name }}{% endblock %}
{% block content %}
{% origin %}
{% if success %}
<h1>Subscription successful</h1>
<p>We have sent an email to your email address with instructions to complete your subscription.</p>
{% else %}
<h1>Subscribe to {{ cl.long_name }}</h1>
<p>Subscribe to the email list for notifications of {% if significant %}significant {% endif %}changes on {{ cl.long_name }}.</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Subscribe</button>
{% endbuttons %}
</form>
{% endif %}
{% endblock %}

View file

@ -1,12 +0,0 @@
{% autoescape off %}
Hello,
In order to complete your subscription for {% if significant %}significant {% endif %}changes on {{ clist.long_name }}, please follow this link or copy it and paste it in your web browser:
https://{{ domain }}{% if significant %}{% url "confirm_significant_subscription" clist.id to_email today auth %}{% else %}{% url "confirm_subscription" clist.id to_email today auth %}{% endif %}
Best regards,
The datatracker login manager service
(for the IETF Secretariat)
{% endautoescape %}

View file

@ -1,16 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Subscription to {{ cl.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Subscription to {{ cl.long_name }}</h1>
<p>Your email address {{ email }} has been successfully subscribed to {{ cl.long_name }}.</p>
<p>
<a class="btn btn-primary" href="{% url "view_personal_list" secret=cl.secret %}">Back</a>
</p>
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Cancel subscription to {{ cl.long_name }}{% endblock %}
{% block content %}
{% origin %}
{% if success %}
<h1>Cancellation successful</h1>
<p>
You will receive a confirmation email shortly containing further instructions on how to cancel your subscription.
</p>
{% else %}
<h1>Cancel subscription to {{ cl.long_name }}</h1>
<p>
Cancel your subscription to the email list for notifications of {% if significant %}significant {% endif %}changes on {{ cl.long_name }}.
</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Subscribe</button>
{% endbuttons %}
</form>
{% endif %}
{% endblock %}

View file

@ -1,12 +0,0 @@
{% autoescape off %}
Hello,
In order to complete the cancelation of your subscription to {% if significant %}significant {% endif %}changes on {{ clist.long_name }}, please follow this link or copy it and paste it in your web browser:
https://{{ domain }}{% if significant %}{% url "confirm_significant_unsubscription" clist.id to_email today auth %}{% else %}{% url "confirm_unsubscription" clist.id to_email today auth %}{% endif %}
Best regards,
The datatracker login manager service
(for the IETF Secretariat)
{% endautoescape %}

View file

@ -1,18 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Cancelled subscription to {{ cl.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Cancelled subscription to {{ cl.long_name }}</h1>
<p>
Your email address {{ email }} has been successfully removed from the {{ cl.long_name }} {% if significant %}significant {% endif %}changes mailing list.
</p>
<p>
<a class="btn btn-primary" href="{{ cl.get_public_url }}">Back</a>
</p>
{% endblock %}

View file

@ -1,23 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block pagehead %}
<link rel="alternate" type="application/atom+xml" title="Changes on {{ cl.long_name }}" href="../changes/feed/" />
<link rel="alternate" type="application/atom+xml" title="Significant changes on {{ cl.long_name }}" href="../changes/significant/feed/" />
{% endblock %}
{% block title %}{{ cl.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>{{ cl.long_name }}</h1>
<p>
Subscribe to notification email lists:
</p>
<ul>
<li><a href="../subscribe/">All changes email list</a></li>
<li><a href="../subscribe/significant/">Significant changes email list</a></li>
</ul>
{% include "community/view_list.html" %}
{% endblock %}

View file

@ -1,50 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load community_tags %}
{% load future %}
{% with cl.get_rfcs_and_drafts as documents %}
{% with dc.get_active_fields as fields %}
<h2>Internet-Drafts</h2>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
{% for field in fields %}
<th>{{ field.description }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for doc in documents.1 %}
<tr>
{% for field in fields %}
<td>{% show_field field doc %}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endwith %}
{% with dc.get_active_fields as fields %}
<h2>RFCs</h2>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
{% for field in fields %}
<th>{{ field.rfcDescription }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for doc in documents.0 %}
<tr>
{% for field in fields %}
<td>{% show_field field doc %}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endwith %}
{% endwith %}

View file

@ -0,0 +1,58 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Subscription to {{ clist.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Subscription to {{ clist.long_name }}</h1>
{% bootstrap_messages %}
<p>Get notified when changes happen to any of the tracked documents.</p>
{% if existing_subscriptions %}
<h2>Existing subscriptions</h2>
<ul class="list-group">
{% for s in existing_subscriptions %}
<li class="list-group-item email-subscription">
<form method="post">
{% csrf_token %}
<code>{{ s.email.address }}</code> - {{ s.get_notify_on_display }}
<input type="hidden" name="subscription_id" value="{{ s.pk }}">
<button class="btn btn-danger btn-sm" type="submit" name="action" value="unsubscribe">Unsubscribe</button>
</form>
</li>
{% endfor %}
</ul>
<p><a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a></p>
{% endif %}
<h2>Add new subscription</h2>
<p class="text-muted">The email addresses you can choose between are those registered in <a href="{% url "account_profile" %}">your profile</a>.</p>
{% if form.fields.email.queryset %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a>
<button type="submit" name="action" value="subscribe" class="btn btn-primary">Subscribe</button>
{% endbuttons %}
</form>
{% else %}
<div class="alert alert-danger">You do not have any active email addresses registered with your account. Go to <a href="{% url "account_profile" %}">your profile and add or activate one</a>.</div>
<a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,16 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load bootstrap3 %}
{% block title %}Track document {{ name }}{% endblock %}
{% bootstrap_messages %}
<form method="post">
{% csrf_token %}
<p>Add {{ name }} to the list?</p>
{% buttons %}
<input type="submit" class="btn btn-primary" value="Track document">
{% endbuttons %}
</form>

View file

@ -0,0 +1,16 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load bootstrap3 %}
{% block title %}Remove tracking of document {{ name }}{% endblock %}
{% bootstrap_messages %}
<form method="post">
{% csrf_token %}
<p>Remove {{ name }} from the list?</p>
{% buttons %}
<input type="submit" class="btn btn-primary" value="Remove tracking of document">
{% endbuttons %}
</form>

View file

@ -1,4 +1,23 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load community_tags %}
{% get_clist_view cl %}
{% load origin %}
{% load bootstrap3 %}
{% block title %}{{ clist.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>{{ clist.long_name }}</h1>
{% bootstrap_messages %}
{% if can_manage_list %}
<a class="btn btn-primary" href="{% url "community_personal_manage_list" clist.user.username %}">
<i class="glyphicon glyphicon-cog"></i>
Manage list
</a>
{% endif %}
{% include "doc/search/search_results.html" with skip_no_matches_warning=True %}
{% include "community/list_menu.html" %}
{% endblock %}

View file

@ -490,11 +490,8 @@
</ul>
</div>
{% if user.is_authenticated %}
{% if tracking_document %}
<a class="btn btn-default btn-xs community-list-add-remove-doc" href="{% url "community_remove_track_document" doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark-o"></span> Untrack</a>
{% else %}
<a class="btn btn-default btn-xs community-list-add-remove-doc" href="{% url "community_add_track_document" doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark"></span> Track</a>
{% endif %}
<a class="btn btn-default btn-xs track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "community_personal_untrack_document" user.username doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark"></span> Untrack</a>
<a class="btn btn-default btn-xs track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "community_personal_track_document" user.username doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark-o"></span> Track</a>
{% endif %}
{% if can_edit and iesg_state %}

View file

@ -42,10 +42,10 @@
<label class="control-label" for="id_olddrafts">{{ form.olddrafts }} Internet-Draft (expired, replaced or withdrawn)</label>
</div>
{% for doc_type in form.doctypes %}
{% for value, label in form.fields.doctypes.choices %}
<div class="checkbox">
<label class="control-label" for="id_{{doc_type.slug}}">
<input type="checkbox" class="advdoctype" {% if doc_type.slug in meta.checked %}checked{% endif %} name="include-{{doc_type.slug}}" id="id_{{doc_type.slug}}"/>{{doc_type|safe|capfirst_allcaps}}
<label class="control-label" for="id_doctypes_{{ value }}">
<input type="checkbox" class="advdoctype" {% if value in form.doctypes.value %}checked{% endif %} name="doctypes" value="{{ value }}" id="id_doctypes_{{ value }}"/>{{ label|safe|capfirst_allcaps}}
</label>
</div>
{% endfor %}
@ -54,7 +54,7 @@
<div class="form-group search_field">
<div class="col-sm-4">
<input type="radio" name="by" value="author" {% if meta.by == "author" %}checked{% endif %}/>
<input type="radio" name="by" value="author" {% if form.by.value == "author" %}checked{% endif %} id="id_author"/>
<label for="id_author" class="control-label">Author</label>
</div>
<div class="col-sm-8">
@ -64,7 +64,7 @@
<div class="form-group search_field">
<div class="col-sm-4">
<input type="radio" name="by" value="group" {% if meta.by == "group" %}checked{% endif %}/>
<input type="radio" name="by" value="group" {% if form.by.value == "group" %}checked{% endif %} id="id_group"/>
<label for="id_group" class="control-label">WG</label>
</div>
<div class="col-sm-8">
@ -75,7 +75,7 @@
<div class="form-group search_field">
<div class="col-sm-4">
<input type="radio" name="by" value="area" {% if meta.by == "area" %}checked{% endif %}/>
<input type="radio" name="by" value="area" {% if form.by.value == "area" %}checked{% endif %} id="id_area"/>
<label for="id_area" class="control-label">Area</label>
</div>
<div class="col-sm-8">
@ -85,7 +85,7 @@
<div class="form-group search_field">
<div class="col-sm-4">
<input type="radio" name="by" value="ad" {% if meta.by == "ad" %}checked{% endif %}/>
<input type="radio" name="by" value="ad" {% if form.by.value == "ad" %}checked{% endif %} id="id_ad"/>
<label for="id_ad" class="control-label">AD</label>
</div>
<div class="col-sm-8">
@ -95,7 +95,7 @@
<div class="form-group search_field">
<div class="col-sm-4">
<input type="radio" name="by" value="state" {% if meta.by == "state" %}checked{% endif %}/>
<input type="radio" name="by" value="state" {% if form.by.value == "state" %}checked{% endif %} id="id_state"/>
<label for="id_state" class="control-label">IESG State</label>
</div>
<div class="col-sm-4">
@ -108,7 +108,7 @@
<div class="form-group search_field">
<div class="col-sm-4">
<input type="radio" name="by" value="stream" {% if meta.by == "stream" %}checked{% endif %}/>
<input type="radio" name="by" value="stream" {% if form.by.value == "stream" %}checked{% endif %} id="id_stream"/>
<label for="id_stream" class="control-label">Stream</label>
</div>
<div class="col-sm-4">

View file

@ -13,15 +13,12 @@
<td>
{% if user.is_authenticated %}
{% if doc.name in doc_is_tracked %}
<a href="{% url "community_remove_track_document" doc.name %}" class="community-list-add-remove-doc" title="Remove from your personal ID list">
<span class="fa fa-bookmark"></span>
</a>
{% else %}
<a href="{% url "community_add_track_document" doc.name %}" class="community-list-add-remove-doc" title="Add to your personal ID list">
<span class="fa fa-bookmark-o"></span>
</a>
{% endif %}
<a href="{% url "community_personal_untrack_document" request.user.username doc.name %}" class="track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" title="Remove from your personal ID list">
<span class="fa fa-bookmark"></span>
</a>
<a href="{% url "community_personal_track_document" request.user.username doc.name %}" class="track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" title="Add to your personal ID list">
<span class="fa fa-bookmark-o"></span>
</a>
{% endif %}
</td>

View file

@ -8,5 +8,6 @@
{% origin %}
{% include "doc/search/search_results.html" %}
{% include "doc/search/search_results.html" with docs=docs_related meta=meta_related skip_no_matches_warning=True %}
{% include "community/list_menu.html" %}
{% endblock group_content %}