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:
parent
1c3ec64e03
commit
5f4082d595
|
@ -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',
|
||||
]
|
|
@ -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 '✔'
|
||||
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 '✔'
|
||||
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 '✔'
|
||||
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__()]
|
|
@ -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?")
|
||||
|
|
|
@ -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
|
108
ietf/community/migrations/0003_cleanup.py
Normal file
108
ietf/community/migrations/0003_cleanup.py
Normal 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([]),
|
||||
),
|
||||
]
|
183
ietf/community/migrations/0004_cleanup_data.py
Normal file
183
ietf/community/migrations/0004_cleanup_data.py
Normal 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',
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 — %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 — %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 — %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 — %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__()]
|
||||
|
||||
|
|
@ -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
|
|
@ -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"])
|
||||
|
||||
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
27
ietf/static/ietf/js/manage-community-list.js
Normal file
27
ietf/static/ietf/js/manage-community-list.js
Normal 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");
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
14
ietf/templates/community/confirm_email.txt
Normal file
14
ietf/templates/community/confirm_email.txt
Normal 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 %}
|
19
ietf/templates/community/confirm_subscription.html
Normal file
19
ietf/templates/community/confirm_subscription.html
Normal 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 %}
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
|
||||
<span class="displayField displayField-{{ field.codename }}">{{ value|safe }}</span>
|
|
@ -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 %}
|
123
ietf/templates/community/manage_list.html
Normal file
123
ietf/templates/community/manage_list.html
Normal 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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
43
ietf/templates/community/subscription.html
Normal file
43
ietf/templates/community/subscription.html
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue