Overhaul of the community list code.

From a user perspective: Use friendlier URLs for lists. Reuse the
search results table for displaying lists. Simplify the management
pages and improve the search rule UI to help fill in the values and
validating them, instead of just providing a text field. Fixes #1874.
Add an explicit button for adding individual documents. Include all
changes in the document change streams, not just some changes. Fix a
concurrency issue that allows changed documents to escape the search
rules. Don't create an empty list just be logging in.

From a code maintenance perspective: Clean up the models. Replace the
background caching scheme with direct queries. Get rid of a big chunk
of code. Speed up the code that adds track buttons to search results.
Add tests of all community views. Fixes #1422. Also fix some minor
bugs and oddities here and there.

There's still some work to do with respect to integrating the group
lists better.
 - Legacy-Id: 10921
This commit is contained in:
Ole Laursen 2016-03-14 10:44:57 +00:00
parent 1c3ec64e03
commit 5f4082d595
41 changed files with 1397 additions and 1529 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 = 'Date of current I-D'
rfcDescription = 'Date of RFC'
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 '-time' # FIXME: latest revision date
class ChangeSort(SortMethod):
codename = 'recent_change'
description = 'Date of most recent change of status of any type'
def get_sort_field(self):
return '-time' # FIXME: latest doc event
class SignificantSort(SortMethod):
codename = 'recent_significant'
description = 'Date of most recent significant change of status'
def get_sort_field(self):
return '-time' # FIXME: latest significant state change
TYPES_OF_SORT = [(i.codename, i.description) for i in SortMethod.__subclasses__()]

View file

@ -1,101 +1,112 @@
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")
class Meta:
model = DisplayConfiguration
fields = ('sort_method', )
del self.fields["person"]
del self.fields["text"]
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()
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
class SubscribeForm(forms.Form):
class SubscriptionForm(forms.Form):
notify_on = forms.ChoiceField(choices=[("all", "All changes"), ("significant", "Only significant state changes")], widget=forms.RadioSelect, initial="all")
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 __init__(self, operation, clist, *args, **kwargs):
self.operation = operation
self.clist = clist
def save(self, *args, **kwargs):
self.send_email()
return True
super(SubscriptionForm, self).__init__(*args, **kwargs)
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)
if operation == "subscribe":
self.fields["notify_on"].label = "Get notified on"
else:
self.fields["notify_on"].label = "For notifications on"
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)
def clean(self):
if self.operation == "subscribe":
if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], significant=self.cleaned_data["notify_on"] == "significant").exists():
raise forms.ValidationError("This email address is already subscribed.")
else:
if not EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], significant=self.cleaned_data["notify_on"] == "significant").exists():
raise forms.ValidationError("Couldn't find a matching subscription?")

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

@ -0,0 +1,108 @@
# -*- 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, 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 in the name')]),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='searchrule',
unique_together=set([]),
),
]

View file

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
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 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(get_rid_of_empty_lists, noop),
migrations.RemoveField(
model_name='searchrule',
name='value',
),
]

View file

@ -1,175 +1,63 @@
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 django.db.models import signals
from ietf.utils.mail import send_mail
from ietf.doc.models import Document, DocEvent
from ietf.doc.models import Document, DocEvent, State
from ietf.group.models import Group
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 ietf.person.models import Person
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)
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):
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, ))
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'),
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
('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'),
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
('author', 'All I-Ds with a particular author'),
('author_rfc', 'All RFCs with a particular author'),
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
('ad', 'All I-Ds with a particular responsible AD'),
def add_subscriptor(self, email, significant):
self.emailsubscription_set.get_or_create(email=email, significant=significant)
('shepherd', 'All I-Ds with a particular document shepherd'),
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()
class Rule(models.Model):
('name_contains', 'All I-Ds with particular text 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)
last_updated = models.DateTimeField(auto_now=True)
rule_type = models.CharField(max_length=30, choices=RULE_TYPES)
class Meta:
unique_together= ("community_list", "rule_type", "value")
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()
# 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(max_length=255, blank=True, default="")
class EmailSubscription(models.Model):
@ -177,41 +65,19 @@ class EmailSubscription(models.Model):
email = models.CharField(max_length=200)
significant = models.BooleanField(default=False)
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, "significant" if self.significant else "all")
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
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
break
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)

View file

@ -14,7 +14,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:
queryset = CommunityList.objects.all()
serializer = api.Serializer()
@ -25,55 +25,23 @@ 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())
class DisplayConfigurationResource(ModelResource):
community_list = ToOneField(CommunityListResource, 'community_list')
class Meta:
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:
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):
class SearchRuleResource(ModelResource):
community_list = ToOneField(CommunityListResource, 'community_list')
cached_ids = ToManyField(DocumentResource, 'cached_ids', null=True)
class Meta:
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')

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,22 +0,0 @@
from django import template
from django.template.loader import render_to_string
from django.conf import settings
register = template.Library()
@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

View file

@ -1,16 +1,130 @@
import json
from django.core.urlresolvers import reverse as urlreverse
from pyquery import PyQuery
from ietf.community.models import CommunityList
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.doc.models import State
from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person
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="-".join(draft.name.split("-")[2:]), community_list=clist)
# 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_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))
# 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"))
# 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_track_untrack_document_for_personal_list_through_ajax(self):
draft = make_test_data()
url = urlreverse("community_personal_track_document", kwargs={ "name": draft.name })
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
login_testing_unauthorized(self, "plain", url)
# track
@ -18,20 +132,20 @@ class CommunityListTests(TestCase):
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_ids.all()), [draft])
self.assertEqual(list(clist.added_docs.all()), [draft])
# untrack
url = urlreverse("community_personal_untrack_document", kwargs={ "name": draft.name })
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_ids.all()), [])
self.assertEqual(list(clist.added_docs.all()), [])
def test_track_untrack_document_for_group_list(self):
draft = make_test_data()
url = urlreverse("community_group_track_document", kwargs={ "name": draft.name, "acronym": draft.group.acronym })
url = urlreverse("community_group_track_document", kwargs={ "acronym": draft.group.acronym, "name": draft.name })
login_testing_unauthorized(self, "marschairman", url)
# track
@ -41,15 +155,162 @@ class CommunityListTests(TestCase):
r = self.client.post(url)
self.assertEqual(r.status_code, 302)
clist = CommunityList.objects.get(group__acronym=draft.group.acronym)
self.assertEqual(list(clist.added_ids.all()), [draft])
self.assertEqual(list(clist.added_docs.all()), [draft])
# untrack
url = urlreverse("community_group_untrack_document", kwargs={ "name": draft.name, "acronym": draft.group.acronym })
url = urlreverse("community_group_untrack_document", kwargs={ "acronym": draft.group.acronym, "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(group__acronym=draft.group.acronym)
self.assertEqual(list(clist.added_ids.all()), [])
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_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 extract_confirm_url(self, confirm_email):
# dig out confirm_email link
msg = confirm_email.get_payload(decode=True)
line_start = "http"
confirm_url = None
for line in msg.split("\n"):
if line.strip().startswith(line_start):
confirm_url = line.strip()
self.assertTrue(confirm_url)
return confirm_url
def test_subscription(self):
draft = make_test_data()
url = urlreverse("community_personal_subscription", kwargs={ "operation": "subscribe", "username": "plain" })
# subscribe without list
r = self.client.get(url)
self.assertEqual(r.status_code, 404)
# subscribe 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)
# do subscribe
mailbox_before = len(outbox)
r = self.client.post(url, { "email": "subscriber@example.com", "notify_on": "significant" })
self.assertEqual(r.status_code, 200)
self.assertEqual(len(outbox), mailbox_before + 1)
# go to confirm page
confirm_url = self.extract_confirm_url(outbox[-1])
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
# confirm subscribe
r = self.client.post(confirm_url, { 'action': 'confirm' })
self.assertEqual(r.status_code, 302)
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email="subscriber@example.com", significant=True).count(), 1)
# unsubscribe
url = urlreverse("community_personal_subscription", kwargs={ "operation": "unsubscribe", "username": "plain" })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# do unsubscribe
mailbox_before = len(outbox)
r = self.client.post(url, { "email": "subscriber@example.com", "notify_on": "significant" })
self.assertEqual(r.status_code, 200)
self.assertEqual(len(outbox), mailbox_before + 1)
# go to confirm page
confirm_url = self.extract_confirm_url(outbox[-1])
r = self.client.get(confirm_url)
self.assertEqual(r.status_code, 200)
# confirm unsubscribe
r = self.client.post(confirm_url, { 'action': 'confirm' })
self.assertEqual(r.status_code, 302)
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email="subscriber@example.com", significant=True).count(), 0)
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="subscriber@example.com", significant=True)
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

@ -2,35 +2,21 @@ 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'),
url(r'^personal/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_personal_track_document'),
url(r'^personal/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_personal_untrack_document'),
url(r'^personal/(?P<username>[^/]+)/$', 'view_list', name='community_personal_view_list'),
url(r'^personal/(?P<username>[^/]+)/manage/$', 'manage_list', name='community_personal_manage_list'),
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_personal_track_document'),
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_personal_untrack_document'),
url(r'^personal/(?P<username>[^/]+)/csv/$', 'export_to_csv', name='community_personal_csv'),
url(r'^personal/(?P<username>[^/]+)/feed/$', 'feed', name='community_personal_feed'),
url(r'^personal/(?P<username>[^/]+)/(?P<operation>subscribe|unsubscribe)/$', 'subscription', name='community_personal_subscription'),
url(r'^personal/(?P<username>[^/]+)/(?P<operation>subscribe|unsubscribe)/confirm/(?P<auth>[^/]+)/$', 'confirm_subscription', name='community_personal_confirm_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'^group/(?P<acronym>[\w.@+-]+)/$', 'view_list', name='community_group_view_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/manage/$', 'manage_list', name='community_group_manage_list'),
url(r'^group/(?P<acronym>[\w.@+-]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_group_track_document'),
url(r'^group/(?P<acronym>[\w.@+-]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_group_untrack_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'),
url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'export_to_csv', name='community_group_csv'),
url(r'^group/(?P<acronym>[\w.@+-]+)/feed/$', 'feed', name='community_group_feed'),
url(r'^group/(?P<acronym>[^/]+)/(?P<operation>subscribe|unsubscribe)/$', 'subscription', name='community_group_subscription'),
url(r'^group/(?P<acronym>[^/]+)/(?P<operation>subscribe|unsubscribe)/confirm/(?P<auth>[^/]+)/$', 'confirm_subscription', name='community_group_confirm_subscription'),
)

View file

@ -1,16 +1,39 @@
from ietf.community.models import CommunityList
from django.db.models import Q
from django.conf import settings
from django.contrib.sites.models import Site
from django.http import Http404
import django.core.signing
from ietf.community.models import CommunityList, EmailSubscription, SearchRule
from ietf.doc.models import Document, State
from ietf.group.models import Role
from ietf.person.models import Person
def can_manage_community_list_for_group(user, group):
if not user or not user.is_authenticated() or not group:
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 can_manage_community_list(user, clist):
if not user or not user.is_authenticated():
return False
if group.type_id == 'area':
return Role.objects.filter(name__slug='ad', person__user=user, group=group).exists()
elif group.type_id in ('wg', 'rg'):
return Role.objects.filter(name__slug='chair', person__user=user, group=group).exists()
else:
return False
if clist.user:
return user == clist.user
elif clist.group:
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
@ -21,7 +44,148 @@ def augment_docs_with_tracking_info(docs, user):
if user and user.is_authenticated():
clist = CommunityList.objects.filter(user=user).first()
if clist:
tracked.update(clist.get_documents().filter(pk__in=docs).values_list("pk", flat=True))
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 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, name__icontains=rule.text)
raise NotImplementedError
def community_list_rules_matching_doc(doc):
from django.db import connection
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,
).extra(
# we need a reverse icontains here, unfortunately this means we need concatenation which isn't quite cross-platform
where=["%s like '%%' || text || '%%'" if connection.vendor == "sqlite" else "%s like concat('%%', text, '%%')"],
params=[doc.name]
)
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(significant=False)
for sub in subscriptions.select_related("community_list"):
clist = sub.community_list
subject = '%s notification: Changes to %s' % (clist.long_name(), event.doc.name)
send_mail(None, sub.email, settings.DEFAULT_FROM_EMAIL, subject, 'community/notification_email.txt',
context = {
'event': event,
'clist': clist,
})
def confirmation_salt(operation, clist):
return ":".join(["community",
operation,
"personal" if clist.user else "group",
clist.user.username if clist.user else clist.group.acronym])
def send_subscription_confirmation_email(request, clist, operation, to_email, significant):
domain = Site.objects.get_current().domain
subject = 'Confirm list subscription: %s' % clist
from_email = settings.DEFAULT_FROM_EMAIL
auth = django.core.signing.dumps([to_email, 1 if significant else 0], salt=confirmation_salt("subscribe", clist))
send_mail(request, to_email, from_email, subject, 'community/confirm_email.txt', {
'domain': domain,
'clist': clist,
'auth': auth,
'operation': operation,
})
def verify_confirmation_data(auth, clist, operation):
try:
data = django.core.signing.loads(auth, salt=confirmation_salt(operation, clist), max_age=24 * 60 * 60)
except django.core.signing.BadSignature:
raise Http404("Invalid or expired auth")
try:
to_email, significant = data[:2]
except ValueError:
raise Http404("Invalid data")
return to_email, bool(significant)

View file

@ -1,324 +1,300 @@
import csv
import uuid
import datetime
import hashlib
import json
from django.db import IntegrityError
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import get_object_or_404, render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
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.community.utils import can_manage_community_list_for_group
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
from ietf.community.utils import 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
from ietf.community.utils import send_subscription_confirmation_email
from ietf.community.utils import verify_confirmation_data
from ietf.group.models import Group
from ietf.doc.models import DocEvent, Document
from ietf.doc.utils_search import prepare_document_table
def lookup_list(username=None, acronym=None):
assert username or acronym
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)
if acronym:
group = get_object_or_404(Group, acronym=acronym)
clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group)
else:
rule_form = RuleForm(clist=clist)
display_form = DisplayForm(instance=display_config)
clist = CommunityList.objects.get(id=clist.id)
return render(request, 'community/manage_clist.html',
{'cl': clist,
'dc': display_config,
'display_form': display_form,
'rule_form': rule_form})
user = get_object_or_404(User, username=username)
clist = CommunityList.objects.filter(user=user).first() or CommunityList(user=user)
return clist
def view_list(request, username=None, acronym=None):
clist = lookup_list(username, acronym)
docs = docs_tracked_by_community_list(clist)
docs, meta = prepare_document_table(request, docs, request.GET)
return render(request, 'community/view_list.html', {
'clist': clist,
'docs': docs,
'meta': meta,
'can_manage_list': can_manage_community_list(request.user, clist),
})
@login_required
def manage_personal_list(request):
clist = CommunityList.objects.get_or_create(user=request.user)[0]
return _manage_list(request, clist)
def manage_list(request, username=None, acronym=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_list(username, acronym)
@login_required
def manage_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if not can_manage_community_list_for_group(request.user, group):
if not can_manage_community_list(request.user, clist):
return HttpResponseForbidden("You do not have permission to access this view")
clist = CommunityList.objects.get_or_create(group=group)[0]
return _manage_list(request, clist)
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:
add_doc_form = AddDocumentsForm()
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()
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.count() if clist.pk is not None else 0,
'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,
})
@login_required
def track_document(request, name, acronym=None):
def track_document(request, name, username=None, acronym=None):
doc = get_object_or_404(Document, docalias__name=name)
if request.method == "POST":
if acronym:
group = get_object_or_404(Group, acronym=acronym)
if not can_manage_community_list_for_group(request.user, group):
return HttpResponseForbidden("You do not have permission to access this view")
clist = lookup_list(username, acronym)
if not can_manage_community_list(request.user, clist):
return HttpResponseForbidden("You do not have permission to access this view")
clist = CommunityList.objects.get_or_create(group=group)[0]
else:
clist = CommunityList.objects.get_or_create(user=request.user)[0]
if clist.pk is None:
clist.save()
clist.added_ids.add(doc)
clist.added_docs.add(doc)
if request.is_ajax():
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
else:
return redirect("manage_personal_list")
if clist.group:
return redirect('community_group_view_list', acronym=clist.group.acronym)
else:
return redirect('community_personal_view_list', username=clist.user.username)
return render(request, "community/track_document.html", {
"name": doc.name,
})
@login_required
def untrack_document(request, name, acronym=None):
def untrack_document(request, name, username=None, acronym=None):
doc = get_object_or_404(Document, docalias__name=name)
if acronym:
group = get_object_or_404(Group, acronym=acronym)
if not can_manage_community_list_for_group(request.user, group):
return HttpResponseForbidden("You do not have permission to access this view")
clist = get_object_or_404(CommunityList, group=group)
else:
clist = get_object_or_404(CommunityList, user=request.user)
clist = lookup_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":
clist.added_ids.remove(doc)
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 redirect("manage_personal_list")
if clist.group:
return redirect('community_group_view_list', acronym=clist.group.acronym)
else:
return redirect('community_personal_view_list', username=clist.user.username)
return render(request, "community/untrack_document.html", {
"name": doc.name,
})
@login_required
def remove_rule(request, list_id, rule_id):
clist = get_object_or_404(CommunityList, pk=list_id)
if ((clist.user and clist.user != request.user)
or (clist.group and not can_manage_community_list_for_group(request.user, clist.group))):
return HttpResponseForbidden("You do not have permission to access this view")
def export_to_csv(request, username=None, acronym=None):
clist = lookup_list(username, acronym)
rule = get_object_or_404(Rule, pk=rule_id)
rule.delete()
return HttpResponseRedirect(clist.get_manage_url())
response = HttpResponse(content_type='text/csv')
if clist.group:
filename = "%s-draft-list.csv" % clist.group.acronym
else:
filename = "draft-list.csv"
def _view_list(request, clist):
display_config = clist.get_display_config()
return render(request, 'community/public/view_list.html',
{'cl': clist,
'dc': display_config,
})
response['Content-Disposition'] = 'attachment; filename=%s' % filename
writer = csv.writer(response, dialect=csv.excel, delimiter=',')
def view_personal_list(request, secret):
clist = get_object_or_404(CommunityList, secret=secret)
return _view_list(request, clist)
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 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)
return response
def feed(request, username=None, acronym=None):
clist = lookup_list(username, acronym)
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)
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)
events = DocEvent.objects.filter(
doc__in=documents,
time__gte=since,
).distinct().order_by('-time', '-id').select_related("doc")
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(request, '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')
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
@login_required
def csv_personal_list(request):
clist = CommunityList.objects.get_or_create(user=request.user)[0]
return _csv_list(request, clist)
@login_required
def csv_group_list(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if not can_manage_community_list_for_group(request.user, group):
return HttpResponseForbidden("You do not have permission to access this view")
clist = CommunityList.objects.get_or_create(group=group)[0]
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)
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
else:
form = SubscribeForm(clist=clist, significant=significant)
return render(request, 'community/public/subscribe.html',
{'cl': clist,
'form': form,
'success': success,
})
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(request, 'community/public/unsubscribe.html',
{'cl': clist,
'form': form,
'success': success,
'significant': significant,
})
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:
def subscription(request, operation, username=None, acronym=None):
clist = lookup_list(username, acronym)
if clist.pk is None:
raise Http404
(subscription, created) = EmailSubscription.objects.get_or_create(
community_list=clist,
email=email,
significant=significant)
return render(request, 'community/public/subscription_confirm.html',
{'cl': clist,
'significant': significant,
})
to_email = None
if request.method == 'POST':
form = SubscriptionForm(operation, clist, request.POST)
if form.is_valid():
to_email = form.cleaned_data['email']
significant = form.cleaned_data['notify_on'] == "significant"
send_subscription_confirmation_email(request, clist, operation, to_email, significant)
else:
form = SubscriptionForm(operation, clist)
return render(request, 'community/subscription.html', {
'clist': clist,
'form': form,
'to_email': to_email,
'operation': operation,
})
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:
def confirm_subscription(request, operation, auth, username=None, acronym=None):
clist = lookup_list(username, acronym)
if clist.pk is None:
raise Http404
EmailSubscription.objects.filter(
community_list=clist,
email=email,
significant=significant).delete()
return render(request, 'community/public/unsubscription_confirm.html',
{'cl': clist,
'significant': significant,
})
to_email, significant = verify_confirmation_data(auth, clist, operation="subscribe")
if request.method == "POST" and request.POST.get("action") == "confirm":
if operation == "subscribe":
if not EmailSubscription.objects.filter(community_list=clist, email__iexact=to_email, significant=significant):
EmailSubscription.objects.create(community_list=clist, email=to_email, significant=significant)
elif operation == "unsubscribe":
EmailSubscription.objects.filter(
community_list=clist,
email__iexact=to_email,
significant=significant).delete()
if clist.group:
return redirect('community_group_view_list', acronym=clist.group.acronym)
else:
return redirect('community_personal_view_list', username=clist.user.username)
return render(request, 'community/confirm_subscription.html', {
'clist': clist,
'to_email': to_email,
'significant': significant,
'operation': operation,
})
def confirm_significant_unsubscription(request, list_id, email, date, confirm_hash):
return confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=True)

View file

@ -871,6 +871,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

@ -451,3 +451,10 @@ form.navbar-form input.form-control.input-sm { width: 141px; }
pointer-events: none;
}
/* Community lists */
label#list-feeds {
display: inline-block;
font-weight: normal;
}

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

@ -52,10 +52,10 @@
{% endif %}
{% if user and user.is_authenticated %}
<li><a href="{% url "manage_personal_list" %}">My tracked docs</a></li>
<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 "manage_group_list" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
<li><a href="{% url "community_group_view_list" 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>

View file

@ -0,0 +1,14 @@
{% autoescape off %}
Hello,
{% filter wordwrap:73 %}In order to {% if operation == "subscribe" %}complete{% else %}cancel{% endif %} your subscription on {% if significant %}significant {% endif %}changes to {{ clist.long_name }}, please follow this link or copy it and paste it in your web browser:{% endfilter %}
https://{{ domain }}{% if clist.user %}{% url "community_personal_confirm_subscription" clist.user.username operation auth %}{% else %}{% url "community_group_confirm_subscription" operation clist.group.acronym auth %}{% endif %}
The link is valid for 24 hours.
Best regards,
The Datatracker draft tracking service
(for the IETF Secretariat)
{% endautoescape %}

View file

@ -0,0 +1,19 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Subscription to {{ clist.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Subscription to {{ clist.long_name }}</h1>
<p>Confirm {% if operation == "subscribe" %}subscription{% else %}cancelling subscription{% endif %} of <code>{{ to_email }}</code> to {% if significant %}significant{% endif %} changes to {{ clist.long_name }}.</p>
<form method="post">{% csrf_token %}
<p>
<a class="btn btn-default" href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif %}">Back to list</a>
<button class="btn btn-primary" type="submit" name="action" value="confirm">Confirm</button>
</p>
</form>
{% endblock %}

View file

@ -1,26 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load bootstrap3 %}
{% bootstrap_messages %}
<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

@ -1,132 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load future %}
{% load staticfiles %}
{% load bootstrap3 %}
{% block title %}{{ cl.long_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>{{ cl.long_name }}</h1>
{% bootstrap_messages %}
<ul class="nav nav-tabs nav-memory" role="tablist">
<li class="active"><a href="#view" data-toggle="tab">Documents</a></li>
<li><a href="#documents" data-toggle="tab">Explicitly added</a></li>
<li><a href="#rules" data-toggle="tab">Rules</a></li>
<li><a href="#custom" data-toggle="tab">Display customization</a></li>
<li><a href="#info" data-toggle="tab">Exports</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="view">
{% include "community/view_list.html" %}
</div>
<div class="tab-pane" id="documents">
<p>
In order to add some individual documents to your list, you have to:
</p>
<ul>
<li>Search for the document or documents you want to add using the datatracker search form.</li>
<li>In the search results, you'll find a link to add individual documents to your list.</li>
</ul>
<a class="btn btn-default" href="/doc/search/">Document search</a>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Name</th><th>State</th><th>Title</th><th></th>
</tr>
</thead>
<tbody>
{% for doc in cl.added_ids.all %}
<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="{% if cl.user %}{% url "community_personal_untrack_document" doc.pk %}{% else %}{% url "community_group_untrack_document" %}{% endif %}">Remove</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane" id="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 cl.rule_set.all %}
{% with rule.get_callable_rule as callable %}
<tr>
<td>{{ callable.description }}</td>
<td>{{ callable.show_value }}</td>
<td>{% with rule.cached_ids.count as count %}{{ count }} document{{ count|pluralize }}{% endwith %}</td>
<td><a class="btn btn-danger btn-xs" href="{% url "community_remove_rule" cl.pk rule.pk %}">Remove</a></td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
<h3>Add a new rule</h3>
<form method="post">
{% csrf_token %}
{% bootstrap_form rule_form %}
{% buttons %}
<input type="submit" class="btn btn-primary" name="save_rule" value="Add rule">
{% endbuttons %}
</form>
</div>
<div class="tab-pane" id="custom">
{% include "community/customize_display.html" %}
</div>
<div class="tab-pane" id="info">
<p>Feel free to share the following links if you need to:</p>
<ul>
<li><a href="{{ cl.secret }}/view/">Read only view for {{ cl.long_name }}</a></li>
<li><a href="{{ cl.secret }}/changes/feed/">Feed for every change in status of {{ cl.long_name }}</a></li>
<li><a href="{{ cl.secret }}/changes/significant/feed/">Feed for significant change in status of {{ cl.long_name }}</a></li>
<li><a href="{{ cl.secret }}/subscribe/">Subscribe to the mailing list for every change in status of {{ cl.long_name }}</a></li>
<li><a href="{{ cl.secret }}/subscribe/significant/">Subscribe to the mailing list for significant change in status of {{ cl.long_name }}</a></li>
</ul>
<p>Export your list to CSV format:</p>
<ul>
<li><a href="csv/">CSV for {{ cl.long_name }}</a></li>
<li><a href="{{ cl.secret }}/csv/">Read only CSV for {{ cl.long_name }}</a></li>
</ul>
</div>
</div>
{% endblock %}
{% comment %}
XXX scrolling jumps around when using this, unfortunately
<script>
$('.nav-memory a').click(function (e) {
e.preventDefault();
$(this).tab('show');
});
// store the currently selected tab in the hash value
$("ul.nav-tabs > li > a").on("shown.bs.tab", function (e) {
var id = $(e.target).attr("href").substr(1);
window.location.hash = id;
});
// on load of the page: switch to the currently selected tab
var hash = window.location.hash;
$('.nav-memory a[href="' + hash + '"]').tab('show');
</script>
{% endcomment %}

View file

@ -0,0 +1,123 @@
{% 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="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">{{ total_count }} document{{ total_count|pluralize }}</a>.</p>
<p><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">Back to list</a></p>
<h2>Individual documents</h2>
<p>The list tracks {{ individually_added }} individually added document{{ individually_added|pluralize }}.</p>
{% if clist.group %}
<p>You can add individual documents here:</p>
{% else %}
<p>You can 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,32 +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>
{% bootstrap_messages %}
<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,36 +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>
{% bootstrap_messages %}
<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,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>Drafts</h2>
<table class="table table-condensed table-striped">
<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">
<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,43 @@
{% 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 %}
{% if not to_email %}
<h1>Subscription to {{ clist.long_name }}</h1>
{% bootstrap_messages %}
{% if operation == "subscribe" %}
<p>Get notified when changes happen to any of the tracked documents.</p>
{% else %}
<p>Unsubscribe from getting notified when changes happen to any of the tracked documents.</p>
{% endif %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-default" href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">Back to list</a>
<button type="submit" class="btn btn-primary">{% if operation == "subscribe" %}Subscribe{% else %}Unsubscribe{% endif %}</button>
{% endbuttons %}
</form>
{% else %}
<h1>Sent confirmation email</h1>
<p>A message has been sent to <code>{{ to_email }}</code> with
a link for confirming {% if operation == subscribe %}the subscription{% else %}cancelling the subscription{% endif %}.</p>
<p>
<a class="btn btn-default" href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">Back to list</a>
</p>
{% endif %}
{% endblock %}

View file

@ -1,4 +1,38 @@
{% 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 %}
<ul class="list-inline">
{% if can_manage_list %}
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_manage_list" clist.group.acronym %}{% else %}{% url "community_personal_manage_list" clist.user.username %}{% endif%}">Manage list</a></li>
{% endif %}
{% if clist.pk != None %}
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_subscribe" clist.group.acronym %}{% else %}{% url "community_personal_subscription" clist.user.username "subscribe" %}{% endif%}">Subscribe to changes</a></li>
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_subscribe" clist.group.acronym %}{% else %}{% url "community_personal_subscription" clist.user.username "unsubscribe" %}{% endif%}">Unsubscribe</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%}">Export as CSV</a></li>
<li>
<label id="list-feeds">Atom feed of document changes:</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</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>
</ul>
{% include "doc/search/search_results.html" with skip_no_matches_warning=True %}
{% endblock %}

View file

@ -434,8 +434,8 @@
</ul>
</div>
{% if user.is_authenticated %}
<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" 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" doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark-o"></span> Track</a>
<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

@ -13,10 +13,10 @@
<td>
{% if user.is_authenticated %}
<a href="{% url "community_personal_untrack_document" doc.name %}" class="track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" title="Remove from your personal ID list">
<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" doc.name %}" class="track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" title="Add to your personal ID list">
<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 %}