Merge in changes from community-list-cleanup
- Legacy-Id: 10968
This commit is contained in:
commit
c061caaf04
|
@ -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 = 'Last revision'
|
||||
rfcDescription = 'Published'
|
||||
|
||||
def get_value(self, document, raw=False):
|
||||
date = document.latest_event(type='new_revision')
|
||||
if date:
|
||||
return date.time.strftime('%Y-%m-%d')
|
||||
return document.time.strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
class StatusField(DisplayField):
|
||||
codename = 'status'
|
||||
description = 'Status in the IETF process'
|
||||
rfcDescription = description
|
||||
|
||||
def get_value(self, document, raw=False):
|
||||
draft_state = document.get_state('draft')
|
||||
stream_state = document.get_state('draft-stream-%s' % (document.stream.slug)) if document.stream else None
|
||||
iesg_state = document.get_state('draft-iesg') or ''
|
||||
rfceditor_state = document.get_state('draft-rfceditor')
|
||||
if draft_state.slug == 'rfc':
|
||||
state = draft_state.name
|
||||
else:
|
||||
state = ""
|
||||
if stream_state:
|
||||
state = state + ("%s<br/>" % stream_state.name)
|
||||
if iesg_state:
|
||||
state = state + ("%s<br/>" % iesg_state.name)
|
||||
if rfceditor_state:
|
||||
state = state + ("%s<br/>" % rfceditor_state.name)
|
||||
#
|
||||
if draft_state.slug == 'rfc':
|
||||
tags = ""
|
||||
else:
|
||||
tags = [ tag.name for tag in document.tags.all() ]
|
||||
if tags:
|
||||
tags = '[%s]' % ",".join(tags)
|
||||
else:
|
||||
tags = ''
|
||||
return '%s<br/>%s' % (state, tags)
|
||||
|
||||
class WGField(DisplayField):
|
||||
codename = 'wg_rg'
|
||||
description = 'Associated WG or RG'
|
||||
rfcDescription = description
|
||||
|
||||
def get_value(self, document, raw=False):
|
||||
if raw or not document.group.type_id in ['wg','rg']:
|
||||
return document.group.acronym
|
||||
else:
|
||||
return '<a href="%s">%s</a>' % (urlreverse('group_home', kwargs=dict(group_type=document.group.type_id, acronym=document.group.acronym)), document.group.acronym) if (document.group and document.group.acronym != 'none') else ''
|
||||
|
||||
|
||||
class ADField(DisplayField):
|
||||
codename = 'ad'
|
||||
description = 'Associated AD, if any'
|
||||
rfcDescription = description
|
||||
|
||||
def get_value(self, document, raw=False):
|
||||
return document.ad or ''
|
||||
|
||||
|
||||
class OneDayField(DisplayField):
|
||||
codename = '1_day'
|
||||
description = 'Changed within the last 1 day'
|
||||
rfcDescription = description
|
||||
|
||||
def get_value(self, document, raw=False):
|
||||
now = datetime.datetime.now()
|
||||
last = now - datetime.timedelta(days=1)
|
||||
if document.docevent_set.filter(time__gte=last):
|
||||
return raw and 'YES' or '✔'
|
||||
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 '-documentchangedates__new_version_date'
|
||||
|
||||
class ChangeSort(SortMethod):
|
||||
codename = 'recent_change'
|
||||
description = 'Date of most recent change of status of any type'
|
||||
|
||||
def get_sort_field(self):
|
||||
return '-documentchangedates__normal_change_date'
|
||||
|
||||
|
||||
class SignificantSort(SortMethod):
|
||||
codename = 'recent_significant'
|
||||
description = 'Date of most recent significant change of status'
|
||||
|
||||
def get_sort_field(self):
|
||||
return '-documentchangedates__significant_change_date'
|
||||
|
||||
|
||||
TYPES_OF_SORT = [(i.codename, i.description) for i in SortMethod.__subclasses__()]
|
|
@ -1,101 +1,114 @@
|
|||
import hashlib
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db.models import Q
|
||||
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.community.models import Rule, DisplayConfiguration, RuleManager
|
||||
from ietf.community.display import DisplayField
|
||||
from ietf.community.models import SearchRule, EmailSubscription
|
||||
from ietf.doc.fields import SearchableDocumentsField
|
||||
from ietf.person.models import Person
|
||||
from ietf.person.fields import SearchablePersonField
|
||||
|
||||
class AddDocumentsForm(forms.Form):
|
||||
documents = SearchableDocumentsField(label="Add documents to track", doc_type="draft")
|
||||
|
||||
class RuleForm(forms.ModelForm):
|
||||
class SearchRuleTypeForm(forms.Form):
|
||||
rule_type = forms.ChoiceField(choices=[('', '--------------')] + SearchRule.RULE_TYPES)
|
||||
|
||||
class SearchRuleForm(forms.ModelForm):
|
||||
person = SearchablePersonField()
|
||||
|
||||
class Meta:
|
||||
model = Rule
|
||||
fields = ('rule_type', 'value')
|
||||
model = SearchRule
|
||||
fields = ('state', 'group', 'person', 'text')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.clist = kwargs.pop('clist', None)
|
||||
super(RuleForm, self).__init__(*args, **kwargs)
|
||||
def __init__(self, clist, rule_type, *args, **kwargs):
|
||||
kwargs["prefix"] = rule_type # add prefix to avoid mixups in the Javascript
|
||||
super(SearchRuleForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
self.instance.community_list = self.clist
|
||||
super(RuleForm, self).save()
|
||||
def restrict_state(state_type, slug=None):
|
||||
f = self.fields['state']
|
||||
f.queryset = f.queryset.filter(used=True).filter(type=state_type)
|
||||
if slug:
|
||||
f.queryset = f.queryset.filter(slug=slug)
|
||||
if len(f.queryset) == 1:
|
||||
f.initial = f.queryset[0].pk
|
||||
f.widget = forms.HiddenInput()
|
||||
|
||||
def get_all_options(self):
|
||||
result = []
|
||||
for i in RuleManager.__subclasses__():
|
||||
options = i(None).options()
|
||||
if options:
|
||||
result.append({'type': i.codename,
|
||||
'options': options})
|
||||
return result
|
||||
|
||||
if rule_type in ['group', 'group_rfc', 'area', 'area_rfc']:
|
||||
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
|
||||
|
||||
class DisplayForm(forms.ModelForm):
|
||||
if rule_type.startswith("area"):
|
||||
self.fields["group"].label = "Area"
|
||||
self.fields["group"].queryset = self.fields["group"].queryset.filter(Q(type="area") | Q(acronym="irtf")).order_by("acronym")
|
||||
else:
|
||||
self.fields["group"].queryset = self.fields["group"].queryset.filter(type__in=("wg", "rg")).order_by("acronym")
|
||||
|
||||
del self.fields["person"]
|
||||
del self.fields["text"]
|
||||
|
||||
elif rule_type.startswith("state_"):
|
||||
mapping = {
|
||||
"state_iab": "draft-stream-iab",
|
||||
"state_iana": "draft-iana-review",
|
||||
"state_iesg": "draft-iesg",
|
||||
"state_irtf": "draft-stream-irtf",
|
||||
"state_ise": "draft-stream-ise",
|
||||
"state_rfceditor": "draft-rfceditor",
|
||||
"state_ietf": "draft-stream-ietf",
|
||||
}
|
||||
restrict_state(mapping[rule_type])
|
||||
|
||||
del self.fields["group"]
|
||||
del self.fields["person"]
|
||||
del self.fields["text"]
|
||||
|
||||
elif rule_type in ["author", "author_rfc", "shepherd", "ad"]:
|
||||
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
|
||||
|
||||
if rule_type.startswith("author"):
|
||||
self.fields["person"].label = "Author"
|
||||
elif rule_type.startswith("shepherd"):
|
||||
self.fields["person"].label = "Shepherd"
|
||||
elif rule_type.startswith("ad"):
|
||||
self.fields["person"].label = "Area Director"
|
||||
self.fields["person"] = forms.ModelChoiceField(queryset=Person.objects.filter(role__name__in=("ad", "pre-ad"), role__group__state="active").distinct().order_by("name"))
|
||||
|
||||
del self.fields["group"]
|
||||
del self.fields["text"]
|
||||
|
||||
elif rule_type == "name_contains":
|
||||
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
|
||||
|
||||
del self.fields["person"]
|
||||
del self.fields["group"]
|
||||
|
||||
if 'group' in self.fields:
|
||||
self.fields['group'].queryset = self.fields['group'].queryset.filter(state="active").order_by("acronym")
|
||||
self.fields['group'].choices = [(g.pk, u"%s - %s" % (g.acronym, g.name)) for g in self.fields['group'].queryset]
|
||||
|
||||
for name, f in self.fields.iteritems():
|
||||
f.required = True
|
||||
|
||||
def clean_text(self):
|
||||
return self.cleaned_data["text"].strip().lower() # names are always lower case
|
||||
|
||||
|
||||
class SubscriptionForm(forms.ModelForm):
|
||||
def __init__(self, user, clist, *args, **kwargs):
|
||||
self.clist = clist
|
||||
self.user = user
|
||||
|
||||
super(SubscriptionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices)
|
||||
self.fields["email"].queryset = self.fields["email"].queryset.filter(person__user=user, active=True).order_by("-primary")
|
||||
self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]])
|
||||
|
||||
if self.fields["email"].queryset:
|
||||
self.fields["email"].initial = self.fields["email"].queryset[0]
|
||||
|
||||
def clean(self):
|
||||
if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], notify_on=self.cleaned_data["notify_on"]).exists():
|
||||
raise forms.ValidationError("You already have a subscription like this.")
|
||||
|
||||
class Meta:
|
||||
model = DisplayConfiguration
|
||||
fields = ('sort_method', )
|
||||
|
||||
def save(self):
|
||||
data = self.data
|
||||
fields = []
|
||||
for i in DisplayField.__subclasses__():
|
||||
if data.get(i.codename, None):
|
||||
fields.append(i.codename)
|
||||
self.instance.display_fields = ','.join(fields)
|
||||
super(DisplayForm, self).save()
|
||||
|
||||
|
||||
class SubscribeForm(forms.Form):
|
||||
|
||||
email = forms.EmailField(label="Your email")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.clist = kwargs.pop('clist')
|
||||
self.significant = kwargs.pop('significant')
|
||||
super(SubscribeForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.send_email()
|
||||
return True
|
||||
|
||||
def send_email(self):
|
||||
domain = Site.objects.get_current().domain
|
||||
today = datetime.date.today().strftime('%Y%m%d')
|
||||
subject = 'Confirm list subscription: %s' % self.clist
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
to_email = self.cleaned_data['email']
|
||||
auth = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, to_email, 'subscribe', self.significant)).hexdigest()
|
||||
context = {
|
||||
'domain': domain,
|
||||
'clist': self.clist,
|
||||
'today': today,
|
||||
'auth': auth,
|
||||
'to_email': to_email,
|
||||
'significant': self.significant,
|
||||
}
|
||||
send_mail(None, to_email, from_email, subject, 'community/public/subscribe_email.txt', context)
|
||||
|
||||
|
||||
class UnSubscribeForm(SubscribeForm):
|
||||
|
||||
def send_email(self):
|
||||
domain = Site.objects.get_current().domain
|
||||
today = datetime.date.today().strftime('%Y%m%d')
|
||||
subject = 'Confirm list subscription cancelation: %s' % self.clist
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
to_email = self.cleaned_data['email']
|
||||
auth = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, to_email, 'unsubscribe', self.significant)).hexdigest()
|
||||
context = {
|
||||
'domain': domain,
|
||||
'clist': self.clist,
|
||||
'today': today,
|
||||
'auth': auth,
|
||||
'to_email': to_email,
|
||||
'significant': self.significant,
|
||||
}
|
||||
send_mail(None, to_email, from_email, subject, 'community/public/unsubscribe_email.txt', context)
|
||||
model = EmailSubscription
|
||||
fields = ("notify_on", "email")
|
||||
|
|
|
@ -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
|
|
@ -1,38 +0,0 @@
|
|||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from ietf.community.constants import SIGNIFICANT_STATES
|
||||
from ietf.community.models import DocumentChangeDates
|
||||
from ietf.doc.models import Document
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (u"Update drafts in community lists by reviewing their rules")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
documents = Document.objects.filter(type='draft')
|
||||
index = 1
|
||||
total = documents.count()
|
||||
|
||||
for doc in documents.iterator():
|
||||
(changes, created) = DocumentChangeDates.objects.get_or_create(document=doc)
|
||||
new_version = doc.latest_event(type='new_revision')
|
||||
normal_change = doc.latest_event()
|
||||
significant_change = None
|
||||
for event in doc.docevent_set.filter(type='changed_document'):
|
||||
for state in SIGNIFICANT_STATES:
|
||||
if ('<b>%s</b>' % state) in event.desc:
|
||||
significant_change = event
|
||||
break
|
||||
|
||||
changes.new_version_date = new_version and new_version.time.date()
|
||||
changes.normal_change_date = normal_change and normal_change.time.date()
|
||||
changes.significant_change_date = significant_change and significant_change.time.date()
|
||||
|
||||
changes.save()
|
||||
|
||||
sys.stdout.write('Document %s/%s\r' % (index, total))
|
||||
sys.stdout.flush()
|
||||
index += 1
|
||||
print
|
120
ietf/community/migrations/0003_cleanup.py
Normal file
120
ietf/community/migrations/0003_cleanup.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('person', '0004_auto_20150308_0440'),
|
||||
('doc', '0010_auto_20150930_0251'),
|
||||
('group', '0006_auto_20150718_0509'),
|
||||
('community', '0002_auto_20141222_1749'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Rule',
|
||||
new_name='SearchRule',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='displayconfiguration',
|
||||
name='community_list',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DisplayConfiguration',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='documentchangedates',
|
||||
name='document',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DocumentChangeDates',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='expectedchange',
|
||||
name='community_list',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='expectedchange',
|
||||
name='document',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ExpectedChange',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='listnotification',
|
||||
name='event',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ListNotification',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='searchrule',
|
||||
name='cached_ids',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='communitylist',
|
||||
old_name='added_ids',
|
||||
new_name='added_docs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='communitylist',
|
||||
name='cached',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='communitylist',
|
||||
name='secret',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchrule',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, to='group.Group', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchrule',
|
||||
name='person',
|
||||
field=models.ForeignKey(blank=True, to='person.Person', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchrule',
|
||||
name='state',
|
||||
field=models.ForeignKey(blank=True, to='doc.State', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchrule',
|
||||
name='text',
|
||||
field=models.CharField(default=b'', max_length=255, verbose_name=b'Text/RegExp', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='searchrule',
|
||||
name='last_updated',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchrule',
|
||||
name='rule_type',
|
||||
field=models.CharField(max_length=30, choices=[(b'group', b'All I-Ds associated with a particular group'), (b'area', b'All I-Ds associated with all groups in a particular Area'), (b'group_rfc', b'All RFCs associated with a particular group'), (b'area_rfc', b'All RFCs associated with all groups in a particular Area'), (b'state_iab', b'All I-Ds that are in a particular IAB state'), (b'state_iana', b'All I-Ds that are in a particular IANA state'), (b'state_iesg', b'All I-Ds that are in a particular IESG state'), (b'state_irtf', b'All I-Ds that are in a particular IRTF state'), (b'state_ise', b'All I-Ds that are in a particular ISE state'), (b'state_rfceditor', b'All I-Ds that are in a particular RFC Editor state'), (b'state_ietf', b'All I-Ds that are in a particular Working Group state'), (b'author', b'All I-Ds with a particular author'), (b'author_rfc', b'All RFCs with a particular author'), (b'ad', b'All I-Ds with a particular responsible AD'), (b'shepherd', b'All I-Ds with a particular document shepherd'), (b'name_contains', b'All I-Ds with particular text/regular expression in the name')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='searchrule',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='emailsubscription',
|
||||
name='notify_on',
|
||||
field=models.CharField(default=b'all', max_length=30, choices=[(b'all', b'All changes'), (b'significant', b'Only significant state changes')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchrule',
|
||||
name='name_contains_index',
|
||||
field=models.ManyToManyField(to='doc.Document'),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
252
ietf/community/migrations/0004_cleanup_data.py
Normal file
252
ietf/community/migrations/0004_cleanup_data.py
Normal file
|
@ -0,0 +1,252 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def port_rules_to_typed_system(apps, schema_editor):
|
||||
SearchRule = apps.get_model("community", "SearchRule")
|
||||
State = apps.get_model("doc", "State")
|
||||
Group = apps.get_model("group", "Group")
|
||||
Person = apps.get_model("person", "Person")
|
||||
|
||||
draft_active = State.objects.get(type="draft", slug="active")
|
||||
draft_rfc = State.objects.get(type="draft", slug="rfc")
|
||||
|
||||
def try_to_uniquify_person(rule, person_qs):
|
||||
if rule.community_list.user and len(person_qs) > 1:
|
||||
user_specific_qs = person_qs.filter(user=rule.community_list.user)
|
||||
if len(user_specific_qs) > 0 and len(user_specific_qs) < len(person_qs):
|
||||
return user_specific_qs
|
||||
|
||||
return person_qs
|
||||
|
||||
|
||||
for rule in SearchRule.objects.all().iterator():
|
||||
handled = False
|
||||
|
||||
if rule.rule_type in ['wg_asociated', 'area_asociated', 'wg_asociated_rfc', 'area_asociated_rfc']:
|
||||
try:
|
||||
rule.group = Group.objects.get(acronym=rule.value)
|
||||
|
||||
if rule.rule_type in ['wg_asociated_rfc', 'area_asociated_rfc']:
|
||||
rule.state = draft_rfc
|
||||
else:
|
||||
rule.state = draft_active
|
||||
handled = True
|
||||
except Group.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
elif rule.rule_type in ['in_iab_state', 'in_iana_state', 'in_iesg_state', 'in_irtf_state', 'in_ise_state', 'in_rfcEdit_state', 'in_wg_state']:
|
||||
state_types = {
|
||||
'in_iab_state': 'draft-stream-iab',
|
||||
'in_iana_state': 'draft-iana-review',
|
||||
'in_iesg_state': 'draft-iesg',
|
||||
'in_irtf_state': 'draft-stream-irtf',
|
||||
'in_ise_state': 'draft-stream-ise',
|
||||
'in_rfcEdit_state': 'draft-rfceditor',
|
||||
'in_wg_state': 'draft-stream-ietf',
|
||||
}
|
||||
|
||||
try:
|
||||
rule.state = State.objects.get(type=state_types[rule.rule_type], slug=rule.value)
|
||||
handled = True
|
||||
except State.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
elif rule.rule_type in ["author", "author_rfc"]:
|
||||
found_persons = list(try_to_uniquify_person(rule, Person.objects.filter(email__documentauthor__id__gte=1).filter(name__icontains=rule.value).distinct()))
|
||||
|
||||
if found_persons:
|
||||
rule.person = found_persons[0]
|
||||
rule.state = draft_active
|
||||
|
||||
for p in found_persons[1:]:
|
||||
SearchRule.objects.create(
|
||||
community_list=rule.community_list,
|
||||
rule_type=rule.rule_type,
|
||||
state=rule.state,
|
||||
person=p,
|
||||
)
|
||||
#print "created", rule.rule_type, p.name
|
||||
|
||||
handled = True
|
||||
|
||||
elif rule.rule_type == "ad_responsible":
|
||||
try:
|
||||
rule.person = Person.objects.get(id=rule.value)
|
||||
rule.state = draft_active
|
||||
handled = True
|
||||
except Person.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
elif rule.rule_type == "shepherd":
|
||||
found_persons = list(try_to_uniquify_person(rule, Person.objects.filter(email__shepherd_document_set__type="draft").filter(name__icontains=rule.value).distinct()))
|
||||
|
||||
if found_persons:
|
||||
rule.person = found_persons[0]
|
||||
rule.state = draft_active
|
||||
|
||||
for p in found_persons[1:]:
|
||||
SearchRule.objects.create(
|
||||
community_list=rule.community_list,
|
||||
rule_type=rule.rule_type,
|
||||
state=rule.state,
|
||||
person=p,
|
||||
)
|
||||
#print "created", rule.rule_type, p.name
|
||||
|
||||
handled = True
|
||||
|
||||
elif rule.rule_type == "with_text":
|
||||
rule.state = draft_active
|
||||
|
||||
if rule.value:
|
||||
rule.text = rule.value
|
||||
handled = True
|
||||
|
||||
if handled:
|
||||
rule.save()
|
||||
else:
|
||||
rule.delete()
|
||||
#print "NOT HANDLED", rule.pk, rule.rule_type, rule.value
|
||||
|
||||
def delete_extra_person_rules(apps, schema_editor):
|
||||
SearchRule = apps.get_model("community", "SearchRule")
|
||||
SearchRule.objects.exclude(person=None).filter(value="").delete()
|
||||
|
||||
RENAMED_RULES = [
|
||||
('wg_asociated', 'group'),
|
||||
('area_asociated', 'area'),
|
||||
('wg_asociated_rfc', 'group_rfc'),
|
||||
('area_asociated_rfc', 'area_rfc'),
|
||||
|
||||
('in_iab_state', 'state_iab'),
|
||||
('in_iana_state', 'state_iana'),
|
||||
('in_iesg_state', 'state_iesg'),
|
||||
('in_irtf_state', 'state_irtf'),
|
||||
('in_ise_state', 'state_ise'),
|
||||
('in_rfcEdit_state', 'state_rfceditor'),
|
||||
('in_wg_state', 'state_ietf'),
|
||||
|
||||
('ad_responsible', 'ad'),
|
||||
|
||||
('with_text', 'name_contains'),
|
||||
]
|
||||
|
||||
def rename_rule_type_forwards(apps, schema_editor):
|
||||
SearchRule = apps.get_model("community", "SearchRule")
|
||||
|
||||
renamings = dict(RENAMED_RULES)
|
||||
|
||||
for r in SearchRule.objects.all():
|
||||
if r.rule_type in renamings:
|
||||
r.rule_type = renamings[r.rule_type]
|
||||
r.save()
|
||||
|
||||
def rename_rule_type_backwards(apps, schema_editor):
|
||||
SearchRule = apps.get_model("community", "SearchRule")
|
||||
|
||||
renamings = dict((to, fro) for fro, to in RENAMED_RULES)
|
||||
|
||||
for r in SearchRule.objects.all():
|
||||
if r.rule_type in renamings:
|
||||
r.rule_type = renamings[r.rule_type]
|
||||
r.save()
|
||||
|
||||
def get_rid_of_empty_lists(apps, schema_editor):
|
||||
CommunityList = apps.get_model("community", "CommunityList")
|
||||
|
||||
for cl in CommunityList.objects.all():
|
||||
if not cl.added_docs.exists() and not cl.searchrule_set.exists() and not cl.emailsubscription_set.exists():
|
||||
cl.delete()
|
||||
|
||||
def move_email_subscriptions_to_preregistered_email(apps, schema_editor):
|
||||
EmailSubscription = apps.get_model("community", "EmailSubscription")
|
||||
Email = apps.get_model("person", "Email")
|
||||
Person = apps.get_model("person", "Person")
|
||||
|
||||
for e in EmailSubscription.objects.all():
|
||||
email_obj = None
|
||||
try:
|
||||
email_obj = Email.objects.get(address=e.email)
|
||||
except Email.DoesNotExist:
|
||||
if e.community_list.user:
|
||||
person = Person.objects.filter(user=e.community_list.user).first()
|
||||
|
||||
#print "creating", e.email, person.ascii
|
||||
# we'll register it on the user, on the assumption
|
||||
# that the user and the subscriber is the same person
|
||||
email_obj = Email.objects.create(
|
||||
address=e.email,
|
||||
person=person,
|
||||
)
|
||||
|
||||
if not email_obj:
|
||||
print "deleting", e.email
|
||||
e.delete()
|
||||
|
||||
def fill_in_notify_on(apps, schema_editor):
|
||||
EmailSubscription = apps.get_model("community", "EmailSubscription")
|
||||
|
||||
EmailSubscription.objects.filter(significant=False, notify_on="all")
|
||||
EmailSubscription.objects.filter(significant=True, notify_on="significant")
|
||||
|
||||
def add_group_community_lists(apps, schema_editor):
|
||||
Group = apps.get_model("group", "Group")
|
||||
Document = apps.get_model("doc", "Document")
|
||||
State = apps.get_model("doc", "State")
|
||||
CommunityList = apps.get_model("community", "CommunityList")
|
||||
SearchRule = apps.get_model("community", "SearchRule")
|
||||
|
||||
active_state = State.objects.get(slug="active", type="draft")
|
||||
rfc_state = State.objects.get(slug="rfc", type="draft")
|
||||
|
||||
for g in Group.objects.filter(type__in=("rg", "wg")):
|
||||
clist = CommunityList.objects.filter(group=g).first()
|
||||
if clist:
|
||||
SearchRule.objects.get_or_create(community_list=clist, rule_type="group", group=g, state=active_state)
|
||||
SearchRule.objects.get_or_create(community_list=clist, rule_type="group_rfc", group=g, state=rfc_state)
|
||||
r, _ = SearchRule.objects.get_or_create(community_list=clist, rule_type="name_contains", text=r"^draft-[^-]+-%s-" % g.acronym, state=active_state)
|
||||
r.name_contains_index = Document.objects.filter(docalias__name__regex=r.text)
|
||||
|
||||
else:
|
||||
clist = CommunityList.objects.create(group=g)
|
||||
SearchRule.objects.create(community_list=clist, rule_type="group", group=g, state=active_state)
|
||||
SearchRule.objects.create(community_list=clist, rule_type="group_rfc", group=g, state=rfc_state)
|
||||
r = SearchRule.objects.create(community_list=clist, rule_type="name_contains", text=r"^draft-[^-]+-%s-" % g.acronym, state=active_state)
|
||||
r.name_contains_index = Document.objects.filter(docalias__name__regex=r.text)
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('community', '0003_cleanup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(port_rules_to_typed_system, delete_extra_person_rules),
|
||||
migrations.RunPython(rename_rule_type_forwards, rename_rule_type_backwards),
|
||||
migrations.RunPython(move_email_subscriptions_to_preregistered_email, noop),
|
||||
migrations.RunPython(get_rid_of_empty_lists, noop),
|
||||
migrations.RunPython(fill_in_notify_on, noop),
|
||||
migrations.RunPython(add_group_community_lists, noop),
|
||||
migrations.RemoveField(
|
||||
model_name='searchrule',
|
||||
name='value',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='emailsubscription',
|
||||
name='email',
|
||||
field=models.ForeignKey(to='person.Email'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='emailsubscription',
|
||||
name='significant',
|
||||
),
|
||||
]
|
|
@ -1,263 +1,102 @@
|
|||
import hashlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models import signals, Q
|
||||
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.doc.models import Document, DocEvent
|
||||
from ietf.group.models import Group, Role
|
||||
|
||||
from ietf.community.rules import TYPES_OF_RULES, RuleManager
|
||||
from ietf.community.display import (TYPES_OF_SORT, DisplayField,
|
||||
SortMethod)
|
||||
from ietf.community.constants import SIGNIFICANT_STATES
|
||||
from django.db.models import signals
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.doc.models import Document, DocEvent, State
|
||||
from ietf.group.models import Group
|
||||
from ietf.person.models import Person, Email
|
||||
|
||||
class CommunityList(models.Model):
|
||||
|
||||
user = models.ForeignKey(User, blank=True, null=True)
|
||||
group = models.ForeignKey(Group, blank=True, null=True)
|
||||
added_ids = models.ManyToManyField(Document)
|
||||
secret = models.CharField(max_length=255, null=True, blank=True)
|
||||
cached = models.TextField(null=True, blank=True)
|
||||
|
||||
def check_manager(self, user):
|
||||
if user == self.user:
|
||||
return True
|
||||
if not self.group or self.group.type.slug not in ('area', 'wg'):
|
||||
return False
|
||||
try:
|
||||
person = user.person
|
||||
except:
|
||||
return False
|
||||
if self.group.type.slug == 'area':
|
||||
return bool(Role.objects.filter(name__slug='ad', email__in=person.email_set.all(), group=self.group).count())
|
||||
elif self.group.type.slug == 'wg':
|
||||
return bool(Role.objects.filter(name__slug='chair', email__in=person.email_set.all(), group=self.group).count())
|
||||
return False
|
||||
|
||||
def short_name(self):
|
||||
if self.user:
|
||||
return 'mine'
|
||||
else:
|
||||
return '%s' % self.group.acronym
|
||||
added_docs = models.ManyToManyField(Document)
|
||||
|
||||
def long_name(self):
|
||||
if self.user:
|
||||
return 'Personal ID list of %s' % self.user.username
|
||||
else:
|
||||
elif self.group:
|
||||
return 'ID list for %s' % self.group.name
|
||||
else:
|
||||
return 'ID list'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.long_name()
|
||||
|
||||
def get_public_url(self):
|
||||
def get_absolute_url(self):
|
||||
if self.user:
|
||||
return reverse('view_personal_list', None, args=(self.user.username, ))
|
||||
else:
|
||||
return reverse('view_group_list', None, args=(self.group.acronym, ))
|
||||
|
||||
def get_manage_url(self):
|
||||
if self.user:
|
||||
return reverse('manage_personal_list', None, args=())
|
||||
else:
|
||||
return reverse('manage_group_list', None, args=(self.group.acronym, ))
|
||||
|
||||
def get_display_config(self):
|
||||
dconfig = getattr(self, '_cached_dconfig', None)
|
||||
if not dconfig:
|
||||
try:
|
||||
self._cached_dconfig = DisplayConfiguration.objects.get(community_list=self)
|
||||
except DisplayConfiguration.DoesNotExist:
|
||||
self._cached_dconfig = DisplayConfiguration(community_list=self)
|
||||
return self._cached_dconfig
|
||||
return self._cached_dconfig
|
||||
|
||||
def get_documents(self):
|
||||
if hasattr(self, '_cached_documents'):
|
||||
return self._cached_documents
|
||||
docs = self.added_ids.all().distinct().select_related('type', 'group', 'ad')
|
||||
for rule in self.rule_set.all():
|
||||
docs = docs | rule.cached_ids.all().distinct()
|
||||
sort_field = self.get_display_config().get_sort_method().get_sort_field()
|
||||
docs = docs.distinct().order_by(sort_field)
|
||||
self._cached_documents = docs
|
||||
return self._cached_documents
|
||||
|
||||
def get_rfcs_and_drafts(self):
|
||||
if hasattr(self, '_cached_rfcs_and_drafts'):
|
||||
return self._cached_rfcs_and_drafts
|
||||
docs = self.get_documents()
|
||||
sort_method = self.get_display_config().get_sort_method()
|
||||
sort_field = sort_method.get_sort_field()
|
||||
if hasattr(sort_method, 'get_full_rfc_sort'):
|
||||
rfcs = sort_method.get_full_rfc_sort(docs.filter(states__name='rfc').distinct())
|
||||
else:
|
||||
rfcs = docs.filter(states__name='rfc').distinct().order_by(sort_field)
|
||||
if hasattr(sort_method, 'get_full_draft_sort'):
|
||||
drafts = sort_method.get_full_draft_sort(docs.exclude(pk__in=rfcs).distinct())
|
||||
else:
|
||||
drafts = docs.exclude(pk__in=rfcs).distinct().order_by(sort_field)
|
||||
self._cached_rfcs_and_drafts = (rfcs, drafts)
|
||||
return self._cached_rfcs_and_drafts
|
||||
|
||||
def add_subscriptor(self, email, significant):
|
||||
self.emailsubscription_set.get_or_create(email=email, significant=significant)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(CommunityList, self).save(*args, **kwargs)
|
||||
if not self.secret:
|
||||
self.secret = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, self.id, self.user and self.user.id or '', self.group and self.group.id or '')).hexdigest()
|
||||
self.save()
|
||||
|
||||
def update(self):
|
||||
self.cached=None
|
||||
self.save()
|
||||
return urlreverse("community_personal_view_list", kwargs={ 'username': self.user.username })
|
||||
elif self.group:
|
||||
return urlreverse("group_docs", kwargs={ 'acronym': self.group.acronym })
|
||||
return ""
|
||||
|
||||
|
||||
class Rule(models.Model):
|
||||
class SearchRule(models.Model):
|
||||
# these types define the UI for setting up the rule, and also
|
||||
# helps when interpreting the rule and matching documents
|
||||
RULE_TYPES = [
|
||||
('group', 'All I-Ds associated with a particular group'),
|
||||
('area', 'All I-Ds associated with all groups in a particular Area'),
|
||||
('group_rfc', 'All RFCs associated with a particular group'),
|
||||
('area_rfc', 'All RFCs associated with all groups in a particular Area'),
|
||||
|
||||
('state_iab', 'All I-Ds that are in a particular IAB state'),
|
||||
('state_iana', 'All I-Ds that are in a particular IANA state'),
|
||||
('state_iesg', 'All I-Ds that are in a particular IESG state'),
|
||||
('state_irtf', 'All I-Ds that are in a particular IRTF state'),
|
||||
('state_ise', 'All I-Ds that are in a particular ISE state'),
|
||||
('state_rfceditor', 'All I-Ds that are in a particular RFC Editor state'),
|
||||
('state_ietf', 'All I-Ds that are in a particular Working Group state'),
|
||||
|
||||
('author', 'All I-Ds with a particular author'),
|
||||
('author_rfc', 'All RFCs with a particular author'),
|
||||
|
||||
('ad', 'All I-Ds with a particular responsible AD'),
|
||||
|
||||
('shepherd', 'All I-Ds with a particular document shepherd'),
|
||||
|
||||
('name_contains', 'All I-Ds with particular text/regular expression in the name'),
|
||||
]
|
||||
|
||||
community_list = models.ForeignKey(CommunityList)
|
||||
cached_ids = models.ManyToManyField(Document)
|
||||
rule_type = models.CharField(max_length=30, choices=TYPES_OF_RULES)
|
||||
value = models.CharField(max_length=255)
|
||||
rule_type = models.CharField(max_length=30, choices=RULE_TYPES)
|
||||
|
||||
class Meta:
|
||||
unique_together= ("community_list", "rule_type", "value")
|
||||
# these are filled in depending on the type
|
||||
state = models.ForeignKey(State, blank=True, null=True)
|
||||
group = models.ForeignKey(Group, blank=True, null=True)
|
||||
person = models.ForeignKey(Person, blank=True, null=True)
|
||||
text = models.CharField(verbose_name="Text/RegExp", max_length=255, blank=True, default="")
|
||||
|
||||
last_updated = models.DateTimeField(
|
||||
auto_now=True)
|
||||
|
||||
def get_callable_rule(self):
|
||||
for i in RuleManager.__subclasses__():
|
||||
if i.codename == self.rule_type:
|
||||
return i(self.value)
|
||||
return RuleManager(self.value)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Rule, self).save(*args, **kwargs)
|
||||
rule = self.get_callable_rule()
|
||||
self.cached_ids = rule.get_documents()
|
||||
self.community_list.update()
|
||||
|
||||
def delete(self):
|
||||
self.community_list.update()
|
||||
super(Rule, self).delete()
|
||||
|
||||
|
||||
class DisplayConfiguration(models.Model):
|
||||
|
||||
community_list = models.ForeignKey(CommunityList)
|
||||
sort_method = models.CharField(
|
||||
max_length=100,
|
||||
choices=TYPES_OF_SORT,
|
||||
default='by_filename',
|
||||
blank=False,
|
||||
null=False)
|
||||
display_fields = models.TextField(
|
||||
default='filename,title,date')
|
||||
|
||||
def get_display_fields_config(self):
|
||||
fields = self.display_fields and self.display_fields.split(',') or []
|
||||
config = []
|
||||
for i in DisplayField.__subclasses__():
|
||||
config.append({
|
||||
'codename': i.codename,
|
||||
'description': i.description,
|
||||
'active': i.codename in fields,
|
||||
})
|
||||
return config
|
||||
|
||||
def get_active_fields(self):
|
||||
fields = self.display_fields and self.display_fields.split(',') or ''
|
||||
active_fields = [i for i in DisplayField.__subclasses__() if i.codename in fields]
|
||||
return active_fields
|
||||
|
||||
def get_all_fields(self):
|
||||
all_fields = [i for i in DisplayField.__subclasses__()]
|
||||
return all_fields
|
||||
|
||||
def get_sort_method(self):
|
||||
for i in SortMethod.__subclasses__():
|
||||
if i.codename == self.sort_method:
|
||||
return i()
|
||||
return SortMethod()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(DisplayConfiguration, self).save(*args, **kwargs)
|
||||
self.community_list.update()
|
||||
|
||||
def delete(self):
|
||||
self.community_list.update()
|
||||
super(DisplayConfiguration, self).delete()
|
||||
|
||||
|
||||
class ExpectedChange(models.Model):
|
||||
|
||||
community_list = models.ForeignKey(CommunityList)
|
||||
document = models.ForeignKey(Document)
|
||||
expected_date = models.DateField(
|
||||
verbose_name='Expected date'
|
||||
)
|
||||
# store a materialized view/index over which documents are matched
|
||||
# by the name_contains rule to avoid having to scan the whole
|
||||
# database - we update this manually when the rule is changed and
|
||||
# when new documents are submitted
|
||||
name_contains_index = models.ManyToManyField(Document)
|
||||
|
||||
|
||||
class EmailSubscription(models.Model):
|
||||
community_list = models.ForeignKey(CommunityList)
|
||||
email = models.CharField(max_length=200)
|
||||
significant = models.BooleanField(default=False)
|
||||
email = models.ForeignKey(Email)
|
||||
|
||||
NOTIFICATION_CHOICES = [
|
||||
("all", "All changes"),
|
||||
("significant", "Only significant state changes")
|
||||
]
|
||||
notify_on = models.CharField(max_length=30, choices=NOTIFICATION_CHOICES, default="all")
|
||||
|
||||
class ListNotification(models.Model):
|
||||
|
||||
event = models.ForeignKey(DocEvent)
|
||||
significant = models.BooleanField(default=False)
|
||||
|
||||
def notify_by_email(self):
|
||||
clists = CommunityList.objects.filter(
|
||||
Q(added_ids=self.event.doc) | Q(rule__cached_ids=self.event.doc)).distinct()
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
for l in clists:
|
||||
subject = '%s notification: Changes on %s' % (l.long_name(), self.event.doc.name)
|
||||
context = {'notification': self.event,
|
||||
'clist': l}
|
||||
filter_subscription = {'community_list': l}
|
||||
if not self.significant:
|
||||
filter_subscription['significant'] = False
|
||||
for to_email in list(set([i.email for i in EmailSubscription.objects.filter(**filter_subscription)])):
|
||||
send_mail(None, to_email, from_email, subject, 'community/public/notification_email.txt', context)
|
||||
def __unicode__(self):
|
||||
return u"%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on)
|
||||
|
||||
|
||||
def notify_events(sender, instance, **kwargs):
|
||||
if not isinstance(instance, DocEvent):
|
||||
return
|
||||
if instance.doc.type.slug != 'draft' or instance.type == 'added_comment':
|
||||
|
||||
if instance.doc.type_id != 'draft':
|
||||
return
|
||||
(changes, created) = DocumentChangeDates.objects.get_or_create(document=instance.doc)
|
||||
changes.normal_change_date = instance.time
|
||||
significant = False
|
||||
if instance.type == 'changed_document' and 'tate changed' in instance.desc:
|
||||
for i in SIGNIFICANT_STATES:
|
||||
if ('<b>%s</b>' % i) in instance.desc:
|
||||
significant = True
|
||||
changes.significant_change_date = instance.time
|
||||
break
|
||||
elif instance.type == 'new_revision':
|
||||
changes.new_version_date = instance.time
|
||||
changes.save()
|
||||
notification = ListNotification.objects.create(
|
||||
event=instance,
|
||||
significant=significant,
|
||||
)
|
||||
notification.notify_by_email()
|
||||
|
||||
from ietf.community.utils import notify_event_to_subscribers
|
||||
notify_event_to_subscribers(instance)
|
||||
|
||||
|
||||
signals.post_save.connect(notify_events)
|
||||
|
||||
|
||||
class DocumentChangeDates(models.Model):
|
||||
|
||||
document = models.ForeignKey(Document)
|
||||
new_version_date = models.DateTimeField(blank=True, null=True)
|
||||
normal_change_date = models.DateTimeField(blank=True, null=True)
|
||||
significant_change_date = models.DateTimeField(blank=True, null=True)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
# Autogenerated by the mkresources management command 2014-11-13 23:53
|
||||
from tastypie.resources import ModelResource
|
||||
from ietf.api import ToOneField
|
||||
from tastypie.fields import ToManyField
|
||||
from tastypie.fields import ToOneField, ToManyField
|
||||
from tastypie.constants import ALL, ALL_WITH_RELATIONS
|
||||
from tastypie.cache import SimpleCache
|
||||
|
||||
from ietf import api
|
||||
|
||||
from ietf.community.models import ( CommunityList, ExpectedChange, DisplayConfiguration,
|
||||
ListNotification, Rule, EmailSubscription, DocumentChangeDates )
|
||||
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
|
||||
|
||||
|
||||
from ietf.doc.resources import DocumentResource
|
||||
from ietf.group.resources import GroupResource
|
||||
|
@ -16,7 +15,7 @@ from ietf.utils.resources import UserResource
|
|||
class CommunityListResource(ModelResource):
|
||||
user = ToOneField(UserResource, 'user', null=True)
|
||||
group = ToOneField(GroupResource, 'group', null=True)
|
||||
added_ids = ToManyField(DocumentResource, 'added_ids', null=True)
|
||||
added_docs = ToManyField(DocumentResource, 'added_docs', null=True)
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = CommunityList.objects.all()
|
||||
|
@ -28,75 +27,24 @@ class CommunityListResource(ModelResource):
|
|||
"cached": ALL,
|
||||
"user": ALL_WITH_RELATIONS,
|
||||
"group": ALL_WITH_RELATIONS,
|
||||
"added_ids": ALL_WITH_RELATIONS,
|
||||
"added_docs": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(CommunityListResource())
|
||||
|
||||
from ietf.doc.resources import DocumentResource
|
||||
class ExpectedChangeResource(ModelResource):
|
||||
community_list = ToOneField(CommunityListResource, 'community_list')
|
||||
document = ToOneField(DocumentResource, 'document')
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = ExpectedChange.objects.all()
|
||||
serializer = api.Serializer()
|
||||
#resource_name = 'expectedchange'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"expected_date": ALL,
|
||||
"community_list": ALL_WITH_RELATIONS,
|
||||
"document": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(ExpectedChangeResource())
|
||||
|
||||
class DisplayConfigurationResource(ModelResource):
|
||||
class SearchRuleResource(ModelResource):
|
||||
community_list = ToOneField(CommunityListResource, 'community_list')
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = DisplayConfiguration.objects.all()
|
||||
serializer = api.Serializer()
|
||||
#resource_name = 'displayconfiguration'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"sort_method": ALL,
|
||||
"display_fields": ALL,
|
||||
"community_list": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(DisplayConfigurationResource())
|
||||
|
||||
from ietf.doc.resources import DocEventResource
|
||||
class ListNotificationResource(ModelResource):
|
||||
event = ToOneField(DocEventResource, 'event')
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = ListNotification.objects.all()
|
||||
serializer = api.Serializer()
|
||||
#resource_name = 'listnotification'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"significant": ALL,
|
||||
"event": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(ListNotificationResource())
|
||||
|
||||
from ietf.doc.resources import DocumentResource
|
||||
class RuleResource(ModelResource):
|
||||
community_list = ToOneField(CommunityListResource, 'community_list')
|
||||
cached_ids = ToManyField(DocumentResource, 'cached_ids', null=True)
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = Rule.objects.all()
|
||||
queryset = SearchRule.objects.all()
|
||||
serializer = api.Serializer()
|
||||
#resource_name = 'rule'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"rule_type": ALL,
|
||||
"value": ALL,
|
||||
"last_updated": ALL,
|
||||
"community_list": ALL_WITH_RELATIONS,
|
||||
"cached_ids": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(RuleResource())
|
||||
api.community.register(SearchRuleResource())
|
||||
|
||||
class EmailSubscriptionResource(ModelResource):
|
||||
community_list = ToOneField(CommunityListResource, 'community_list')
|
||||
|
@ -107,26 +55,8 @@ class EmailSubscriptionResource(ModelResource):
|
|||
#resource_name = 'emailsubscription'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"email": ALL,
|
||||
"significant": ALL,
|
||||
"email": ALL_WITH_RELATIONS,
|
||||
"notify_on": ALL,
|
||||
"community_list": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(EmailSubscriptionResource())
|
||||
|
||||
from ietf.doc.resources import DocumentResource
|
||||
class DocumentChangeDatesResource(ModelResource):
|
||||
document = ToOneField(DocumentResource, 'document')
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = DocumentChangeDates.objects.all()
|
||||
serializer = api.Serializer()
|
||||
#resource_name = 'documentchangedates'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"new_version_date": ALL,
|
||||
"normal_change_date": ALL,
|
||||
"significant_change_date": ALL,
|
||||
"document": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.community.register(DocumentChangeDatesResource())
|
||||
|
||||
|
|
|
@ -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,45 +0,0 @@
|
|||
from django import template
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.community.models import CommunityList
|
||||
from ietf.group.models import Role
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.assignment_tag
|
||||
def get_user_managed_lists(user):
|
||||
if not (user and hasattr(user, "is_authenticated") and user.is_authenticated()):
|
||||
return ''
|
||||
lists = {'personal': CommunityList.objects.get_or_create(user=user)[0]}
|
||||
try:
|
||||
person = user.person
|
||||
groups = []
|
||||
managed_areas = [i.group for i in Role.objects.filter(name__slug='ad', group__type__slug='area', group__state__slug='active', email__in=person.email_set.all())]
|
||||
for area in managed_areas:
|
||||
groups.append(CommunityList.objects.get_or_create(group=area)[0])
|
||||
managed_wg = [i.group for i in Role.objects.filter(name__slug='chair', group__type__slug='wg', group__state__slug__in=('active','bof'), email__in=person.email_set.all())]
|
||||
for wg in managed_wg:
|
||||
groups.append(CommunityList.objects.get_or_create(group=wg)[0])
|
||||
lists['group'] = groups
|
||||
except:
|
||||
pass
|
||||
return lists
|
||||
|
||||
@register.inclusion_tag('community/display_field.html', takes_context=False)
|
||||
def show_field(field, doc):
|
||||
return {'field': field,
|
||||
'value': field.get_value(doc),
|
||||
}
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_clist_view(clist):
|
||||
if settings.DEBUG or not clist.cached:
|
||||
clist.cached = render_to_string('community/raw_view.html', {
|
||||
'cl': clist,
|
||||
'dc': clist.get_display_config()
|
||||
})
|
||||
clist.save()
|
||||
return clist.cached
|
354
ietf/community/tests.py
Normal file
354
ietf/community/tests.py
Normal file
|
@ -0,0 +1,354 @@
|
|||
import json
|
||||
|
||||
from pyquery import PyQuery
|
||||
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
|
||||
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
|
||||
from ietf.community.utils import reset_name_contains_index_for_rule
|
||||
from ietf.group.utils import setup_default_community_list_for_group
|
||||
from ietf.doc.models import State
|
||||
from ietf.doc.utils import add_state_change_event
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.utils.test_data import make_test_data
|
||||
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
|
||||
from ietf.utils.mail import outbox
|
||||
|
||||
class CommunityListTests(TestCase):
|
||||
def test_rule_matching(self):
|
||||
draft = make_test_data()
|
||||
iesg_state = State.objects.get(type="draft-iesg", slug="lc")
|
||||
draft.set_state(iesg_state)
|
||||
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
|
||||
rule_group = SearchRule.objects.create(rule_type="group", group=draft.group, state=State.objects.get(type="draft", slug="active"), community_list=clist)
|
||||
rule_group_rfc = SearchRule.objects.create(rule_type="group_rfc", group=draft.group, state=State.objects.get(type="draft", slug="rfc"), community_list=clist)
|
||||
rule_area = SearchRule.objects.create(rule_type="area", group=draft.group.parent, state=State.objects.get(type="draft", slug="active"), community_list=clist)
|
||||
|
||||
rule_state_iesg = SearchRule.objects.create(rule_type="state_iesg", state=State.objects.get(type="draft-iesg", slug="lc"), community_list=clist)
|
||||
|
||||
rule_author = SearchRule.objects.create(rule_type="author", state=State.objects.get(type="draft", slug="active"), person=Person.objects.filter(email__documentauthor__document=draft).first(), community_list=clist)
|
||||
|
||||
rule_ad = SearchRule.objects.create(rule_type="ad", state=State.objects.get(type="draft", slug="active"), person=draft.ad, community_list=clist)
|
||||
|
||||
rule_shepherd = SearchRule.objects.create(rule_type="shepherd", state=State.objects.get(type="draft", slug="active"), person=draft.shepherd.person, community_list=clist)
|
||||
|
||||
rule_name_contains = SearchRule.objects.create(rule_type="name_contains", state=State.objects.get(type="draft", slug="active"), text="draft-.*" + "-".join(draft.name.split("-")[2:]), community_list=clist)
|
||||
reset_name_contains_index_for_rule(rule_name_contains)
|
||||
|
||||
# doc -> rules
|
||||
matching_rules = list(community_list_rules_matching_doc(draft))
|
||||
self.assertTrue(rule_group in matching_rules)
|
||||
self.assertTrue(rule_group_rfc not in matching_rules)
|
||||
self.assertTrue(rule_area in matching_rules)
|
||||
self.assertTrue(rule_state_iesg in matching_rules)
|
||||
self.assertTrue(rule_author in matching_rules)
|
||||
self.assertTrue(rule_ad in matching_rules)
|
||||
self.assertTrue(rule_shepherd in matching_rules)
|
||||
self.assertTrue(rule_name_contains in matching_rules)
|
||||
|
||||
# rule -> docs
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_group)))
|
||||
self.assertTrue(draft not in list(docs_matching_community_list_rule(rule_group_rfc)))
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_area)))
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_state_iesg)))
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_author)))
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_ad)))
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_shepherd)))
|
||||
self.assertTrue(draft in list(docs_matching_community_list_rule(rule_name_contains)))
|
||||
|
||||
def test_view_list(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_view_list", kwargs={ "username": "plain" })
|
||||
|
||||
# without list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# with list
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(draft.name in r.content)
|
||||
|
||||
def test_manage_personal_list(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_manage_list", kwargs={ "username": "plain" })
|
||||
login_testing_unauthorized(self, "plain", url)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# add document
|
||||
r = self.client.post(url, { "action": "add_documents", "documents": draft.pk })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertTrue(clist.added_docs.filter(pk=draft.pk))
|
||||
|
||||
# document shows up on GET
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(draft.name in r.content)
|
||||
|
||||
# remove document
|
||||
r = self.client.post(url, { "action": "remove_document", "document": draft.pk })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertTrue(not clist.added_docs.filter(pk=draft.pk))
|
||||
|
||||
# add rule
|
||||
r = self.client.post(url, {
|
||||
"action": "add_rule",
|
||||
"rule_type": "author_rfc",
|
||||
"author_rfc-person": Person.objects.filter(email__documentauthor__document=draft).first().pk,
|
||||
"author_rfc-state": State.objects.get(type="draft", slug="rfc").pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc"))
|
||||
|
||||
# add name_contains rule
|
||||
r = self.client.post(url, {
|
||||
"action": "add_rule",
|
||||
"rule_type": "name_contains",
|
||||
"name_contains-text": "draft.*mars",
|
||||
"name_contains-state": State.objects.get(type="draft", slug="active").pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains"))
|
||||
|
||||
# rule shows up on GET
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
rule = clist.searchrule_set.filter(rule_type="author_rfc").first()
|
||||
q = PyQuery(r.content)
|
||||
self.assertEqual(len(q('#r%s' % rule.pk)), 1)
|
||||
|
||||
# remove rule
|
||||
r = self.client.post(url, {
|
||||
"action": "remove_rule",
|
||||
"rule": rule.pk,
|
||||
})
|
||||
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc"))
|
||||
|
||||
def test_manage_group_list(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_group_manage_list", kwargs={ "acronym": draft.group.acronym })
|
||||
setup_default_community_list_for_group(draft.group)
|
||||
login_testing_unauthorized(self, "marschairman", url)
|
||||
|
||||
# test GET, rest is tested with personal list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_track_untrack_document(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
|
||||
login_testing_unauthorized(self, "plain", url)
|
||||
|
||||
# track
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [draft])
|
||||
|
||||
# untrack
|
||||
url = urlreverse("community_personal_untrack_document", kwargs={ "username": "plain", "name": draft.name })
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [])
|
||||
|
||||
def test_track_untrack_document_through_ajax(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
|
||||
login_testing_unauthorized(self, "plain", url)
|
||||
|
||||
# track
|
||||
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(json.loads(r.content)["success"], True)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [draft])
|
||||
|
||||
# untrack
|
||||
url = urlreverse("community_personal_untrack_document", kwargs={ "username": "plain", "name": draft.name })
|
||||
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(json.loads(r.content)["success"], True)
|
||||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [])
|
||||
|
||||
def test_csv(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_csv", kwargs={ "username": "plain" })
|
||||
|
||||
# without list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# with list
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
# this is a simple-minded test, we don't actually check the fields
|
||||
self.assertTrue(draft.name in r.content)
|
||||
|
||||
def test_csv_for_group(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_group_csv", kwargs={ "acronym": draft.group.acronym })
|
||||
|
||||
setup_default_community_list_for_group(draft.group)
|
||||
|
||||
# test GET, rest is tested with personal list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_feed(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_feed", kwargs={ "username": "plain" })
|
||||
|
||||
# without list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# with list
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(draft.name in r.content)
|
||||
|
||||
# only significant
|
||||
r = self.client.get(url + "?significant=1")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue('<entry>' not in r.content)
|
||||
|
||||
def test_feed_for_group(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_group_feed", kwargs={ "acronym": draft.group.acronym })
|
||||
|
||||
setup_default_community_list_for_group(draft.group)
|
||||
|
||||
# test GET, rest is tested with personal list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_subscription(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_subscription", kwargs={ "username": "plain" })
|
||||
|
||||
login_testing_unauthorized(self, "plain", url)
|
||||
|
||||
# subscription without list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# subscription with list
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# subscribe
|
||||
email = Email.objects.filter(person__user__username="plain").first()
|
||||
r = self.client.post(url, { "email": email.pk, "notify_on": "significant", "action": "subscribe" })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
subscription = EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").first()
|
||||
|
||||
self.assertTrue(subscription)
|
||||
|
||||
# delete subscription
|
||||
r = self.client.post(url, { "subscription_id": subscription.pk, "action": "unsubscribe" })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0)
|
||||
|
||||
def test_subscription_for_group(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_group_subscription", kwargs={ "acronym": draft.group.acronym })
|
||||
|
||||
setup_default_community_list_for_group(draft.group)
|
||||
|
||||
login_testing_unauthorized(self, "marschairman", url)
|
||||
|
||||
# test GET, rest is tested with personal list
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_notification(self):
|
||||
draft = make_test_data()
|
||||
|
||||
clist = CommunityList.objects.create(user=User.objects.get(username="plain"))
|
||||
clist.added_docs.add(draft)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
state=State.objects.get(type="draft", slug="active"),
|
||||
text="test",
|
||||
)
|
||||
|
||||
EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant")
|
||||
|
||||
mailbox_before = len(outbox)
|
||||
active_state = State.objects.get(type="draft", slug="active")
|
||||
system = Person.objects.get(name="(System)")
|
||||
add_state_change_event(draft, system, None, active_state)
|
||||
self.assertEqual(len(outbox), mailbox_before)
|
||||
|
||||
mailbox_before = len(outbox)
|
||||
rfc_state = State.objects.get(type="draft", slug="rfc")
|
||||
add_state_change_event(draft, system, active_state, rfc_state)
|
||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||
self.assertTrue(draft.name in outbox[-1]["Subject"])
|
||||
|
||||
|
|
@ -1,34 +1,13 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns('ietf.community.views',
|
||||
url(r'^personal/$', 'manage_personal_list', name='manage_personal_list'),
|
||||
url(r'^personal/csv/$', 'csv_personal_list', name='csv_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/view/$', 'view_personal_list', name='view_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/csv/$', 'view_csv_personal_list', name='view_csv_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/changes/feed/$', 'changes_personal_list', name='changes_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/changes/significant/feed/$', 'significant_personal_list', name='significant_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/$', 'subscribe_personal_list', {'significant': False}, name='subscribe_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/significant/$', 'subscribe_personal_list', {'significant': True}, name='subscribe_significant_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/$', 'unsubscribe_personal_list', {'significant': False}, name='unsubscribe_personal_list'),
|
||||
url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/significant/$', 'unsubscribe_personal_list', {'significant': True}, name='unsubscribe_significant_personal_list'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^personal/(?P<username>[^/]+)/$', 'ietf.community.views.view_list', name='community_personal_view_list'),
|
||||
url(r'^personal/(?P<username>[^/]+)/manage/$', 'ietf.community.views.manage_list', name='community_personal_manage_list'),
|
||||
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.track_document', name='community_personal_track_document'),
|
||||
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.untrack_document', name='community_personal_untrack_document'),
|
||||
url(r'^personal/(?P<username>[^/]+)/csv/$', 'ietf.community.views.export_to_csv', name='community_personal_csv'),
|
||||
url(r'^personal/(?P<username>[^/]+)/feed/$', 'ietf.community.views.feed', name='community_personal_feed'),
|
||||
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'ietf.community.views.subscription', name='community_personal_subscription'),
|
||||
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/$', 'manage_group_list', name='manage_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/view/$', 'view_group_list', name='view_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/changes/feed/$', 'changes_group_list', name='changes_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/changes/significant/feed/$', 'significant_group_list', name='significant_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'csv_group_list', name='csv_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/subscribe/$', 'subscribe_group_list', {'significant': False}, name='subscribe_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/subscribe/significant/$', 'subscribe_group_list', {'significant': True}, name='subscribe_significant_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/unsubscribe/$', 'unsubscribe_group_list', {'significant': False}, name='unsubscribe_group_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/unsubscribe/significant/$', 'unsubscribe_group_list', {'significant': True}, name='unsubscribe_significant_group_list'),
|
||||
|
||||
url(r'^add_track_document/(?P<document_name>[^/]+)/$', 'add_track_document', name='community_add_track_document'),
|
||||
url(r'^remove_track_document/(?P<document_name>[^/]+)/$', 'remove_track_document', name='community_remove_track_document'),
|
||||
url(r'^(?P<list_id>[\d]+)/remove_document/(?P<document_name>[^/]+)/$', 'remove_document', name='community_remove_document'),
|
||||
url(r'^(?P<list_id>[\d]+)/remove_rule/(?P<rule_id>[^/]+)/$', 'remove_rule', name='community_remove_rule'),
|
||||
url(r'^(?P<list_id>[\d]+)/subscribe/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_subscription', name='confirm_subscription'),
|
||||
url(r'^(?P<list_id>[\d]+)/subscribe/significant/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_significant_subscription', name='confirm_significant_subscription'),
|
||||
url(r'^(?P<list_id>[\d]+)/unsubscribe/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_unsubscription', name='confirm_unsubscription'),
|
||||
url(r'^(?P<list_id>[\d]+)/unsubscribe/significant/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_significant_unsubscription', name='confirm_significant_unsubscription'),
|
||||
)
|
||||
|
|
183
ietf/community/utils.py
Normal file
183
ietf/community/utils.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
import re
|
||||
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.community.models import CommunityList, EmailSubscription, SearchRule
|
||||
from ietf.doc.models import Document, State
|
||||
from ietf.group.models import Role, Group
|
||||
from ietf.person.models import Person
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from ietf.utils.mail import send_mail
|
||||
|
||||
def states_of_significant_change():
|
||||
return State.objects.filter(used=True).filter(
|
||||
Q(type="draft-stream-ietf", slug__in=['adopt-wg', 'wg-lc', 'writeupw', 'parked', 'dead']) |
|
||||
Q(type="draft-iesg", slug__in=['pub-req', 'lc', 'iesg-eva', 'rfcqueue']) |
|
||||
Q(type="draft-stream-iab", slug__in=['active', 'review-c', 'rfc-edit']) |
|
||||
Q(type="draft-stream-irtf", slug__in=['active', 'rg-lc', 'irsg-w', 'iesg-rev', 'rfc-edit', 'iesghold']) |
|
||||
Q(type="draft-stream-ise", slug__in=['receive', 'ise-rev', 'iesg-rev', 'rfc-edit', 'iesghold']) |
|
||||
Q(type="draft", slug__in=['rfc', 'dead'])
|
||||
)
|
||||
|
||||
def lookup_community_list(username=None, acronym=None):
|
||||
assert username or acronym
|
||||
|
||||
if acronym:
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group)
|
||||
else:
|
||||
user = get_object_or_404(User, username=username)
|
||||
clist = CommunityList.objects.filter(user=user).first() or CommunityList(user=user)
|
||||
|
||||
return clist
|
||||
|
||||
def can_manage_community_list(user, clist):
|
||||
if not user or not user.is_authenticated():
|
||||
return False
|
||||
|
||||
if clist.user:
|
||||
return user == clist.user
|
||||
elif clist.group:
|
||||
if has_role(user, 'Secretariat'):
|
||||
return True
|
||||
|
||||
if clist.group.type_id == 'area':
|
||||
return Role.objects.filter(name__slug='ad', person__user=user, group=clist.group).exists()
|
||||
elif clist.group.type_id in ('wg', 'rg'):
|
||||
return Role.objects.filter(name__slug='chair', person__user=user, group=clist.group).exists()
|
||||
|
||||
return False
|
||||
|
||||
def augment_docs_with_tracking_info(docs, user):
|
||||
"""Add attribute to each document with whether the document is tracked
|
||||
by the user or not."""
|
||||
|
||||
tracked = set()
|
||||
|
||||
if user and user.is_authenticated():
|
||||
clist = CommunityList.objects.filter(user=user).first()
|
||||
if clist:
|
||||
tracked.update(docs_tracked_by_community_list(clist).filter(pk__in=docs).values_list("pk", flat=True))
|
||||
|
||||
for d in docs:
|
||||
d.tracked_in_personal_community_list = d.pk in tracked
|
||||
|
||||
def reset_name_contains_index_for_rule(rule):
|
||||
if not rule.rule_type == "name_contains":
|
||||
return
|
||||
|
||||
rule.name_contains_index = Document.objects.filter(docalias__name__regex=rule.text)
|
||||
|
||||
def update_name_contains_indexes_with_new_doc(doc):
|
||||
for r in SearchRule.objects.filter(rule_type="name_contains"):
|
||||
# in theory we could use the database to do this query, but
|
||||
# Django doesn't support a reversed regex operator, and regexp
|
||||
# support needs backend-specific code so custom SQL is a bit
|
||||
# cumbersome too
|
||||
if re.search(r.text, doc.name):
|
||||
r.name_contains_index.add(doc)
|
||||
|
||||
def docs_matching_community_list_rule(rule):
|
||||
docs = Document.objects.all()
|
||||
if rule.rule_type in ['group', 'area', 'group_rfc', 'area_rfc']:
|
||||
return docs.filter(Q(group=rule.group_id) | Q(group__parent=rule.group_id), states=rule.state)
|
||||
elif rule.rule_type.startswith("state_"):
|
||||
return docs.filter(states=rule.state)
|
||||
elif rule.rule_type in ["author", "author_rfc"]:
|
||||
return docs.filter(states=rule.state, documentauthor__author__person=rule.person)
|
||||
elif rule.rule_type == "ad":
|
||||
return docs.filter(states=rule.state, ad=rule.person)
|
||||
elif rule.rule_type == "shepherd":
|
||||
return docs.filter(states=rule.state, shepherd__person=rule.person)
|
||||
elif rule.rule_type == "name_contains":
|
||||
return docs.filter(states=rule.state, searchrule=rule)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def community_list_rules_matching_doc(doc):
|
||||
states = list(doc.states.values_list("pk", flat=True))
|
||||
|
||||
rules = SearchRule.objects.none()
|
||||
|
||||
if doc.group_id:
|
||||
groups = [doc.group_id]
|
||||
if doc.group.parent_id:
|
||||
groups.append(doc.group.parent_id)
|
||||
rules |= SearchRule.objects.filter(
|
||||
rule_type__in=['group', 'area', 'group_rfc', 'area_rfc'],
|
||||
state__in=states,
|
||||
group__in=groups
|
||||
)
|
||||
|
||||
rules |= SearchRule.objects.filter(
|
||||
rule_type__in=['state_iab', 'state_iana', 'state_iesg', 'state_irtf', 'state_ise', 'state_rfceditor', 'state_ietf'],
|
||||
state__in=states,
|
||||
)
|
||||
|
||||
rules |= SearchRule.objects.filter(
|
||||
rule_type__in=["author", "author_rfc"],
|
||||
state__in=states,
|
||||
person__in=list(Person.objects.filter(email__documentauthor__document=doc)),
|
||||
)
|
||||
|
||||
if doc.ad_id:
|
||||
rules |= SearchRule.objects.filter(
|
||||
rule_type="ad",
|
||||
state__in=states,
|
||||
person=doc.ad_id,
|
||||
)
|
||||
|
||||
if doc.shepherd_id:
|
||||
rules |= SearchRule.objects.filter(
|
||||
rule_type="shepherd",
|
||||
state__in=states,
|
||||
person__email=doc.shepherd_id,
|
||||
)
|
||||
|
||||
rules |= SearchRule.objects.filter(
|
||||
rule_type="name_contains",
|
||||
state__in=states,
|
||||
name_contains_index=doc, # search our materialized index to avoid full scan
|
||||
)
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def docs_tracked_by_community_list(clist):
|
||||
if clist.pk is None:
|
||||
return Document.objects.none()
|
||||
|
||||
# in theory, we could use an OR query, but databases seem to have
|
||||
# trouble with OR queries and complicated joins so do the OR'ing
|
||||
# manually
|
||||
doc_ids = set(clist.added_docs.values_list("pk", flat=True))
|
||||
for rule in clist.searchrule_set.all():
|
||||
doc_ids = doc_ids | set(docs_matching_community_list_rule(rule).values_list("pk", flat=True))
|
||||
|
||||
return Document.objects.filter(pk__in=doc_ids)
|
||||
|
||||
def community_lists_tracking_doc(doc):
|
||||
return CommunityList.objects.filter(Q(added_docs=doc) | Q(searchrule__in=community_list_rules_matching_doc(doc)))
|
||||
|
||||
|
||||
def notify_event_to_subscribers(event):
|
||||
significant = event.type == "changed_state" and event.state_id in [s.pk for s in states_of_significant_change()]
|
||||
|
||||
subscriptions = EmailSubscription.objects.filter(community_list__in=community_lists_tracking_doc(event.doc)).distinct()
|
||||
|
||||
if not significant:
|
||||
subscriptions = subscriptions.filter(notify_on="all")
|
||||
|
||||
for sub in subscriptions.select_related("community_list", "email"):
|
||||
clist = sub.community_list
|
||||
subject = '%s notification: Changes to %s' % (clist.long_name(), event.doc.name)
|
||||
|
||||
send_mail(None, sub.email.address, settings.DEFAULT_FROM_EMAIL, subject, 'community/notification_email.txt',
|
||||
context = {
|
||||
'event': event,
|
||||
'clist': clist,
|
||||
})
|
|
@ -1,352 +1,268 @@
|
|||
import csv
|
||||
import uuid
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.utils.http import urlquote
|
||||
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from ietf.community.models import CommunityList, Rule, EmailSubscription
|
||||
from ietf.community.forms import RuleForm, DisplayForm, SubscribeForm, UnSubscribeForm
|
||||
from ietf.group.models import Group
|
||||
from ietf.doc.models import DocEvent, DocAlias
|
||||
from ietf.community.models import SearchRule, EmailSubscription
|
||||
from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
|
||||
from ietf.community.utils import lookup_community_list, can_manage_community_list
|
||||
from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule
|
||||
from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule
|
||||
from ietf.doc.models import DocEvent, Document
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
|
||||
def view_list(request, username=None):
|
||||
clist = lookup_community_list(username)
|
||||
|
||||
def _manage_list(request, clist):
|
||||
display_config = clist.get_display_config()
|
||||
if request.method == 'POST' and request.POST.get('save_rule', None):
|
||||
rule_form = RuleForm(request.POST, clist=clist)
|
||||
display_form = DisplayForm(instance=display_config)
|
||||
if rule_form.is_valid():
|
||||
try:
|
||||
rule_form.save()
|
||||
except IntegrityError:
|
||||
pass;
|
||||
rule_form = RuleForm(clist=clist)
|
||||
display_form = DisplayForm(instance=display_config)
|
||||
elif request.method == 'POST' and request.POST.get('save_display', None):
|
||||
display_form = DisplayForm(request.POST, instance=display_config)
|
||||
rule_form = RuleForm(clist=clist)
|
||||
if display_form.is_valid():
|
||||
display_form.save()
|
||||
rule_form = RuleForm(clist=clist)
|
||||
display_form = DisplayForm(instance=display_config)
|
||||
docs = docs_tracked_by_community_list(clist)
|
||||
docs, meta = prepare_document_table(request, docs, request.GET)
|
||||
|
||||
subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
|
||||
|
||||
return render(request, 'community/view_list.html', {
|
||||
'clist': clist,
|
||||
'docs': docs,
|
||||
'meta': meta,
|
||||
'can_manage_list': can_manage_community_list(request.user, clist),
|
||||
'subscribed': subscribed,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def manage_list(request, username=None, acronym=None, group_type=None):
|
||||
# we need to be a bit careful because clist may not exist in the
|
||||
# database so we can't call related stuff on it yet
|
||||
clist = lookup_community_list(username, acronym)
|
||||
|
||||
if not can_manage_community_list(request.user, clist):
|
||||
return HttpResponseForbidden("You do not have permission to access this view")
|
||||
|
||||
action = request.POST.get('action')
|
||||
|
||||
if request.method == 'POST' and action == 'add_documents':
|
||||
add_doc_form = AddDocumentsForm(request.POST)
|
||||
if add_doc_form.is_valid():
|
||||
if clist.pk is None:
|
||||
clist.save()
|
||||
|
||||
for d in add_doc_form.cleaned_data['documents']:
|
||||
clist.added_docs.add(d)
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
else:
|
||||
rule_form = RuleForm(clist=clist)
|
||||
display_form = DisplayForm(instance=display_config)
|
||||
clist = CommunityList.objects.get(id=clist.id)
|
||||
return render_to_response('community/manage_clist.html',
|
||||
{'cl': clist,
|
||||
'dc': display_config,
|
||||
'display_form': display_form,
|
||||
'rule_form': rule_form},
|
||||
context_instance=RequestContext(request))
|
||||
add_doc_form = AddDocumentsForm()
|
||||
|
||||
if request.method == 'POST' and action == 'remove_document':
|
||||
document_pk = request.POST.get('document')
|
||||
if clist.pk is not None and document_pk:
|
||||
document = get_object_or_404(clist.added_docs, pk=document_pk)
|
||||
clist.added_docs.remove(document)
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
|
||||
if request.method == 'POST' and action == 'add_rule':
|
||||
rule_type_form = SearchRuleTypeForm(request.POST)
|
||||
if rule_type_form.is_valid():
|
||||
rule_type = rule_type_form.cleaned_data['rule_type']
|
||||
|
||||
if rule_type:
|
||||
rule_form = SearchRuleForm(clist, rule_type, request.POST)
|
||||
if rule_form.is_valid():
|
||||
if clist.pk is None:
|
||||
clist.save()
|
||||
|
||||
rule = rule_form.save(commit=False)
|
||||
rule.community_list = clist
|
||||
rule.rule_type = rule_type
|
||||
rule.save()
|
||||
if rule.rule_type == "name_contains":
|
||||
reset_name_contains_index_for_rule(rule)
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
else:
|
||||
rule_type_form = SearchRuleTypeForm()
|
||||
rule_form = None
|
||||
|
||||
if request.method == 'POST' and action == 'remove_rule':
|
||||
rule_pk = request.POST.get('rule')
|
||||
if clist.pk is not None and rule_pk:
|
||||
rule = get_object_or_404(SearchRule, pk=rule_pk, community_list=clist)
|
||||
rule.delete()
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
|
||||
rules = clist.searchrule_set.all() if clist.pk is not None else []
|
||||
for r in rules:
|
||||
r.matching_documents_count = docs_matching_community_list_rule(r).count()
|
||||
|
||||
empty_rule_forms = { rule_type: SearchRuleForm(clist, rule_type) for rule_type, _ in SearchRule.RULE_TYPES }
|
||||
|
||||
total_count = docs_tracked_by_community_list(clist).count()
|
||||
|
||||
return render(request, 'community/manage_list.html', {
|
||||
'clist': clist,
|
||||
'rules': rules,
|
||||
'individually_added': clist.added_docs.all() if clist.pk is not None else [],
|
||||
'rule_type_form': rule_type_form,
|
||||
'rule_form': rule_form,
|
||||
'empty_rule_forms': empty_rule_forms,
|
||||
'total_count': total_count,
|
||||
'add_doc_form': add_doc_form,
|
||||
})
|
||||
|
||||
|
||||
def manage_personal_list(request):
|
||||
user = request.user
|
||||
if not user.is_authenticated():
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
clist = CommunityList.objects.get_or_create(user=request.user)[0]
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
return _manage_list(request, clist)
|
||||
@login_required
|
||||
def track_document(request, name, username=None, acronym=None):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
|
||||
if request.method == "POST":
|
||||
clist = lookup_community_list(username, acronym)
|
||||
if not can_manage_community_list(request.user, clist):
|
||||
return HttpResponseForbidden("You do not have permission to access this view")
|
||||
|
||||
if clist.pk is None:
|
||||
clist.save()
|
||||
|
||||
clist.added_docs.add(doc)
|
||||
|
||||
if request.is_ajax():
|
||||
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
|
||||
else:
|
||||
return HttpResponseRedirect(clist.get_absolute_url())
|
||||
|
||||
return render(request, "community/track_document.html", {
|
||||
"name": doc.name,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def untrack_document(request, name, username=None, acronym=None):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
clist = lookup_community_list(username, acronym)
|
||||
if not can_manage_community_list(request.user, clist):
|
||||
return HttpResponseForbidden("You do not have permission to access this view")
|
||||
|
||||
if request.method == "POST":
|
||||
if clist.pk is not None:
|
||||
clist.added_docs.remove(doc)
|
||||
|
||||
if request.is_ajax():
|
||||
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
|
||||
else:
|
||||
return HttpResponseRedirect(clist.get_absolute_url())
|
||||
|
||||
return render(request, "community/untrack_document.html", {
|
||||
"name": doc.name,
|
||||
})
|
||||
|
||||
|
||||
def manage_group_list(request, acronym):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
if group.type.slug not in ('area', 'wg'):
|
||||
raise Http404
|
||||
clist = CommunityList.objects.get_or_create(group=group)[0]
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
return _manage_list(request, clist)
|
||||
def export_to_csv(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
|
||||
def add_track_document(request, document_name):
|
||||
"""supports the "Track this document" functionality
|
||||
|
||||
This is exposed in the document view and in document search results."""
|
||||
if not request.user.is_authenticated():
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
doc = get_object_or_404(DocAlias, name=document_name).document
|
||||
clist = CommunityList.objects.get_or_create(user=request.user)[0]
|
||||
clist.update()
|
||||
return add_document_to_list(request, clist, doc)
|
||||
if clist.group:
|
||||
filename = "%s-draft-list.csv" % clist.group.acronym
|
||||
else:
|
||||
filename = "draft-list.csv"
|
||||
|
||||
def remove_track_document(request, document_name):
|
||||
"""supports the "Untrack this document" functionality
|
||||
|
||||
This is exposed in the document view and in document search results."""
|
||||
clist = CommunityList.objects.get_or_create(user=request.user)[0]
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
doc = get_object_or_404(DocAlias, name=document_name).document
|
||||
clist.added_ids.remove(doc)
|
||||
clist.update()
|
||||
return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
|
||||
response['Content-Disposition'] = 'attachment; filename=%s' % filename
|
||||
|
||||
def remove_document(request, list_id, document_name):
|
||||
clist = get_object_or_404(CommunityList, pk=list_id)
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
doc = get_object_or_404(DocAlias, name=document_name).document
|
||||
clist.added_ids.remove(doc)
|
||||
clist.update()
|
||||
return HttpResponseRedirect(clist.get_manage_url())
|
||||
writer = csv.writer(response, dialect=csv.excel, delimiter=',')
|
||||
|
||||
def add_document_to_list(request, clist, doc):
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
clist.added_ids.add(doc)
|
||||
return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
|
||||
header = [
|
||||
"Name",
|
||||
"Title",
|
||||
"Date of latest revision",
|
||||
"Status in the IETF process",
|
||||
"Associated group",
|
||||
"Associated AD",
|
||||
"Date of latest change",
|
||||
]
|
||||
writer.writerow(header)
|
||||
|
||||
docs = docs_tracked_by_community_list(clist).select_related('type', 'group', 'ad')
|
||||
for doc in docs.prefetch_related("states", "tags"):
|
||||
row = []
|
||||
row.append(doc.name)
|
||||
row.append(doc.title)
|
||||
e = doc.latest_event(type='new_revision')
|
||||
row.append(e.time.strftime("%Y-%m-%d") if e else "")
|
||||
row.append(strip_tags(doc.friendly_state()))
|
||||
row.append(doc.group.acronym if doc.group else "")
|
||||
row.append(unicode(doc.ad) if doc.ad else "")
|
||||
e = doc.latest_event()
|
||||
row.append(e.time.strftime("%Y-%m-%d") if e else "")
|
||||
writer.writerow([v.encode("utf-8") for v in row])
|
||||
|
||||
def remove_rule(request, list_id, rule_id):
|
||||
clist = get_object_or_404(CommunityList, pk=list_id)
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
rule = get_object_or_404(Rule, pk=rule_id)
|
||||
rule.delete()
|
||||
return HttpResponseRedirect(clist.get_manage_url())
|
||||
return response
|
||||
|
||||
def feed(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
|
||||
def _view_list(request, clist):
|
||||
display_config = clist.get_display_config()
|
||||
return render_to_response('community/public/view_list.html',
|
||||
{'cl': clist,
|
||||
'dc': display_config,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
significant = request.GET.get('significant', '') == '1'
|
||||
|
||||
documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True)
|
||||
since = datetime.datetime.now() - datetime.timedelta(days=14)
|
||||
|
||||
def view_personal_list(request, secret):
|
||||
clist = get_object_or_404(CommunityList, secret=secret)
|
||||
return _view_list(request, clist)
|
||||
events = DocEvent.objects.filter(
|
||||
doc__in=documents,
|
||||
time__gte=since,
|
||||
).distinct().order_by('-time', '-id').select_related("doc")
|
||||
|
||||
|
||||
def view_group_list(request, acronym):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
return _view_list(request, clist)
|
||||
|
||||
|
||||
def _atom_view(request, clist, significant=False):
|
||||
documents = [i['pk'] for i in clist.get_documents().values('pk')]
|
||||
startDate = datetime.datetime.now() - datetime.timedelta(days=14)
|
||||
|
||||
notifications = DocEvent.objects.filter(doc__pk__in=documents, time__gte=startDate)\
|
||||
.distinct()\
|
||||
.order_by('-time', '-id')
|
||||
if significant:
|
||||
notifications = notifications.filter(listnotification__significant=True)
|
||||
events = events.filter(type="changed_state", statedocevent__state__in=list(states_of_significant_change()))
|
||||
|
||||
host = request.get_host()
|
||||
feed_url = 'https://%s%s' % (host, request.get_full_path())
|
||||
feed_id = uuid.uuid5(uuid.NAMESPACE_URL, feed_url.encode('utf-8'))
|
||||
title = '%s RSS Feed' % clist.long_name()
|
||||
title = u'%s RSS Feed' % clist.long_name()
|
||||
if significant:
|
||||
subtitle = 'Document significant changes'
|
||||
subtitle = 'Significant document changes'
|
||||
else:
|
||||
subtitle = 'Document changes'
|
||||
|
||||
return render_to_response('community/public/atom.xml',
|
||||
{'cl': clist,
|
||||
'entries': notifications,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'id': feed_id.get_urn(),
|
||||
'updated': datetime.datetime.today(),
|
||||
},
|
||||
content_type='text/xml',
|
||||
context_instance=RequestContext(request))
|
||||
return render(request, 'community/atom.xml', {
|
||||
'clist': clist,
|
||||
'entries': events[:50],
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'id': feed_id.get_urn(),
|
||||
'updated': datetime.datetime.now(),
|
||||
}, content_type='text/xml')
|
||||
|
||||
|
||||
def changes_personal_list(request, secret):
|
||||
clist = get_object_or_404(CommunityList, secret=secret)
|
||||
return _atom_view(request, clist)
|
||||
|
||||
|
||||
def changes_group_list(request, acronym):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
return _atom_view(request, clist)
|
||||
|
||||
|
||||
def significant_personal_list(request, secret):
|
||||
clist = get_object_or_404(CommunityList, secret=secret)
|
||||
return _atom_view(request, clist, significant=True)
|
||||
|
||||
|
||||
def significant_group_list(request, acronym):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
return _atom_view(request, clist, significant=True)
|
||||
|
||||
|
||||
def _csv_list(request, clist):
|
||||
display_config = clist.get_display_config()
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename=draft-list.csv'
|
||||
|
||||
writer = csv.writer(response, dialect=csv.excel, delimiter=',')
|
||||
header = []
|
||||
fields = display_config.get_all_fields()
|
||||
for field in fields:
|
||||
header.append(field.description)
|
||||
writer.writerow(header)
|
||||
|
||||
for doc in clist.get_documents():
|
||||
row = []
|
||||
for field in fields:
|
||||
row.append(field().get_value(doc, raw=True))
|
||||
writer.writerow(row)
|
||||
return response
|
||||
|
||||
|
||||
def csv_personal_list(request):
|
||||
user = request.user
|
||||
if not user.is_authenticated():
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
clist = CommunityList.objects.get_or_create(user=user)[0]
|
||||
if not clist.check_manager(user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
return _csv_list(request, clist)
|
||||
|
||||
|
||||
def csv_group_list(request, acronym):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
if group.type.slug not in ('area', 'wg'):
|
||||
@login_required
|
||||
def subscription(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
if clist.pk is None:
|
||||
raise Http404
|
||||
clist = CommunityList.objects.get_or_create(group=group)[0]
|
||||
if not clist.check_manager(request.user):
|
||||
path = urlquote(request.get_full_path())
|
||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
||||
return _csv_list(request, clist)
|
||||
|
||||
def view_csv_personal_list(request, secret):
|
||||
clist = get_object_or_404(CommunityList, secret=secret)
|
||||
return _csv_list(request, clist)
|
||||
existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
|
||||
|
||||
def _subscribe_list(request, clist, significant):
|
||||
success = False
|
||||
if request.method == 'POST':
|
||||
form = SubscribeForm(data=request.POST, clist=clist, significant=significant)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
success = True
|
||||
action = request.POST.get("action")
|
||||
if action == "subscribe":
|
||||
form = SubscriptionForm(request.user, clist, request.POST)
|
||||
if form.is_valid():
|
||||
subscription = form.save(commit=False)
|
||||
subscription.community_list = clist
|
||||
subscription.save()
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
|
||||
elif action == "unsubscribe":
|
||||
existing_subscriptions.filter(pk=request.POST.get("subscription_id")).delete()
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
else:
|
||||
form = SubscribeForm(clist=clist, significant=significant)
|
||||
return render_to_response('community/public/subscribe.html',
|
||||
{'cl': clist,
|
||||
'form': form,
|
||||
'success': success,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
form = SubscriptionForm(request.user, clist)
|
||||
|
||||
|
||||
def _unsubscribe_list(request, clist, significant):
|
||||
success = False
|
||||
if request.method == 'POST':
|
||||
form = UnSubscribeForm(data=request.POST, clist=clist, significant=significant)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
success = True
|
||||
else:
|
||||
form = UnSubscribeForm(clist=clist, significant=significant)
|
||||
return render_to_response('community/public/unsubscribe.html',
|
||||
{'cl': clist,
|
||||
'form': form,
|
||||
'success': success,
|
||||
'significant': significant,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def subscribe_personal_list(request, secret, significant=False):
|
||||
clist = get_object_or_404(CommunityList, secret=secret)
|
||||
return _subscribe_list(request, clist, significant=significant)
|
||||
|
||||
|
||||
def subscribe_group_list(request, acronym, significant=False):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
return _subscribe_list(request, clist, significant=significant)
|
||||
|
||||
|
||||
def unsubscribe_personal_list(request, secret, significant=False):
|
||||
clist = get_object_or_404(CommunityList, secret=secret)
|
||||
return _unsubscribe_list(request, clist, significant=significant)
|
||||
|
||||
|
||||
def unsubscribe_group_list(request, acronym, significant=False):
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
return _unsubscribe_list(request, clist, significant=significant)
|
||||
|
||||
|
||||
def confirm_subscription(request, list_id, email, date, confirm_hash, significant=False):
|
||||
clist = get_object_or_404(CommunityList, pk=list_id)
|
||||
valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, email, 'subscribe', significant)).hexdigest() == confirm_hash
|
||||
if not valid:
|
||||
raise Http404
|
||||
(subscription, created) = EmailSubscription.objects.get_or_create(
|
||||
community_list=clist,
|
||||
email=email,
|
||||
significant=significant)
|
||||
return render_to_response('community/public/subscription_confirm.html',
|
||||
{'cl': clist,
|
||||
'significant': significant,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def confirm_significant_subscription(request, list_id, email, date, confirm_hash):
|
||||
return confirm_subscription(request, list_id, email, date, confirm_hash, significant=True)
|
||||
|
||||
|
||||
def confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=False):
|
||||
clist = get_object_or_404(CommunityList, pk=list_id)
|
||||
valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, email, 'unsubscribe', significant)).hexdigest() == confirm_hash
|
||||
if not valid:
|
||||
raise Http404
|
||||
EmailSubscription.objects.filter(
|
||||
community_list=clist,
|
||||
email=email,
|
||||
significant=significant).delete()
|
||||
return render_to_response('community/public/unsubscription_confirm.html',
|
||||
{'cl': clist,
|
||||
'significant': significant,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def confirm_significant_unsubscription(request, list_id, email, date, confirm_hash):
|
||||
return confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=True)
|
||||
return render(request, 'community/subscription.html', {
|
||||
'clist': clist,
|
||||
'form': form,
|
||||
'existing_subscriptions': existing_subscriptions,
|
||||
})
|
||||
|
|
26
ietf/doc/templatetags/managed_groups.py
Normal file
26
ietf/doc/templatetags/managed_groups.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django import template
|
||||
|
||||
from ietf.group.models import Group
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def managed_groups(user):
|
||||
if not (user and hasattr(user, "is_authenticated") and user.is_authenticated()):
|
||||
return []
|
||||
|
||||
groups = []
|
||||
# groups.extend(Group.objects.filter(
|
||||
# role__name__slug='ad',
|
||||
# role__person__user=user,
|
||||
# type__slug='area',
|
||||
# state__slug='active').select_related("type"))
|
||||
|
||||
groups.extend(Group.objects.filter(
|
||||
role__name__slug='chair',
|
||||
role__person__user=user,
|
||||
type__slug__in=('rg', 'wg'),
|
||||
state__slug__in=('active', 'bof')).select_related("type"))
|
||||
|
||||
return groups
|
||||
|
189
ietf/doc/utils_search.py
Normal file
189
ietf/doc/utils_search.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent
|
||||
from ietf.doc.expire import expirable_draft
|
||||
from ietf.community.utils import augment_docs_with_tracking_info
|
||||
|
||||
def wrap_value(v):
|
||||
return lambda: v
|
||||
|
||||
def fill_in_document_table_attributes(docs):
|
||||
# fill in some attributes for the document table results to save
|
||||
# some hairy template code and avoid repeated SQL queries
|
||||
|
||||
docs_dict = dict((d.pk, d) for d in docs)
|
||||
doc_ids = docs_dict.keys()
|
||||
|
||||
rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=doc_ids).values_list("document_id", "name"))
|
||||
|
||||
# latest event cache
|
||||
event_types = ("published_rfc",
|
||||
"changed_ballot_position",
|
||||
"started_iesg_process",
|
||||
"new_revision")
|
||||
for d in docs:
|
||||
d.latest_event_cache = dict()
|
||||
for e in event_types:
|
||||
d.latest_event_cache[e] = None
|
||||
|
||||
for e in DocEvent.objects.filter(doc__in=doc_ids, type__in=event_types).order_by('time'):
|
||||
docs_dict[e.doc_id].latest_event_cache[e.type] = e
|
||||
|
||||
# telechat date, can't do this with above query as we need to get TelechatDocEvents out
|
||||
seen = set()
|
||||
for e in TelechatDocEvent.objects.filter(doc__in=doc_ids, type="scheduled_for_telechat").order_by('-time'):
|
||||
if e.doc_id not in seen:
|
||||
d = docs_dict[e.doc_id]
|
||||
d.telechat_date = wrap_value(d.telechat_date(e))
|
||||
seen.add(e.doc_id)
|
||||
|
||||
# misc
|
||||
for d in docs:
|
||||
# emulate canonical name which is used by a lot of the utils
|
||||
d.canonical_name = wrap_value(rfc_aliases[d.pk] if d.pk in rfc_aliases else d.name)
|
||||
|
||||
if d.rfc_number() != None and d.latest_event_cache["published_rfc"]:
|
||||
d.latest_revision_date = d.latest_event_cache["published_rfc"].time
|
||||
elif d.latest_event_cache["new_revision"]:
|
||||
d.latest_revision_date = d.latest_event_cache["new_revision"].time
|
||||
else:
|
||||
d.latest_revision_date = d.time
|
||||
|
||||
if d.type_id == "draft":
|
||||
if d.get_state_slug() == "rfc":
|
||||
d.search_heading = "RFC"
|
||||
elif d.get_state_slug() in ("ietf-rm", "auth-rm"):
|
||||
d.search_heading = "Withdrawn Internet-Draft"
|
||||
else:
|
||||
d.search_heading = "%s Internet-Draft" % d.get_state()
|
||||
else:
|
||||
d.search_heading = "%s" % (d.type,);
|
||||
|
||||
d.expirable = expirable_draft(d)
|
||||
|
||||
if d.get_state_slug() != "rfc":
|
||||
d.milestones = d.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
|
||||
|
||||
|
||||
|
||||
# RFCs
|
||||
|
||||
# errata
|
||||
erratas = set(Document.objects.filter(tags="errata", name__in=rfc_aliases.keys()).distinct().values_list("name", flat=True))
|
||||
for d in docs:
|
||||
d.has_errata = d.name in erratas
|
||||
|
||||
# obsoleted/updated by
|
||||
for a in rfc_aliases:
|
||||
d = docs_dict[a]
|
||||
d.obsoleted_by_list = []
|
||||
d.updated_by_list = []
|
||||
|
||||
xed_by = RelatedDocument.objects.filter(target__name__in=rfc_aliases.values(),
|
||||
relationship__in=("obs", "updates")).select_related('target__document_id')
|
||||
rel_rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc",
|
||||
document__in=[rel.source_id for rel in xed_by]).values_list('document_id', 'name'))
|
||||
for rel in xed_by:
|
||||
d = docs_dict[rel.target.document_id]
|
||||
if rel.relationship_id == "obs":
|
||||
l = d.obsoleted_by_list
|
||||
elif rel.relationship_id == "updates":
|
||||
l = d.updated_by_list
|
||||
l.append(rel_rfc_aliases[rel.source_id].upper())
|
||||
l.sort()
|
||||
|
||||
|
||||
def prepare_document_table(request, docs, query=None, max_results=500):
|
||||
"""Take a queryset of documents and a QueryDict with sorting info
|
||||
and return list of documents with attributes filled in for
|
||||
displaying a full table of information about the documents, plus
|
||||
dict with information about the columns."""
|
||||
|
||||
if not isinstance(docs, list):
|
||||
# evaluate and fill in attribute results immediately to decrease
|
||||
# the number of queries
|
||||
docs = docs.select_related("ad", "ad__person", "std_level", "intended_std_level", "group", "stream")
|
||||
docs = docs.prefetch_related("states__type", "tags")
|
||||
|
||||
docs = list(docs[:max_results])
|
||||
|
||||
fill_in_document_table_attributes(docs)
|
||||
augment_docs_with_tracking_info(docs, request.user)
|
||||
|
||||
meta = {}
|
||||
|
||||
sort_key = query and query.get('sort') or ""
|
||||
sort_reversed = sort_key.startswith("-")
|
||||
sort_key = sort_key.lstrip("-")
|
||||
|
||||
# sort
|
||||
def generate_sort_key(d):
|
||||
res = []
|
||||
|
||||
rfc_num = d.rfc_number()
|
||||
|
||||
|
||||
if d.type_id == "draft":
|
||||
res.append(["Active", "Expired", "Replaced", "Withdrawn", "RFC"].index(d.search_heading.split()[0]))
|
||||
else:
|
||||
res.append(d.type_id);
|
||||
res.append("-");
|
||||
res.append(d.get_state_slug());
|
||||
res.append("-");
|
||||
|
||||
if sort_key == "title":
|
||||
res.append(d.title)
|
||||
elif sort_key == "date":
|
||||
res.append(str(d.latest_revision_date))
|
||||
elif sort_key == "status":
|
||||
if rfc_num != None:
|
||||
res.append(int(rfc_num))
|
||||
else:
|
||||
res.append(d.get_state().order if d.get_state() else None)
|
||||
elif sort_key == "ipr":
|
||||
res.append(len(d.ipr()))
|
||||
elif sort_key == "ad":
|
||||
if rfc_num != None:
|
||||
res.append(int(rfc_num))
|
||||
elif d.get_state_slug() == "active":
|
||||
if d.get_state("draft-iesg"):
|
||||
res.append(d.get_state("draft-iesg").order)
|
||||
else:
|
||||
res.append(0)
|
||||
else:
|
||||
if rfc_num != None:
|
||||
res.append(int(rfc_num))
|
||||
else:
|
||||
res.append(d.canonical_name())
|
||||
|
||||
return res
|
||||
|
||||
docs.sort(key=generate_sort_key, reverse=sort_reversed)
|
||||
|
||||
# fill in a meta dict with some information for rendering the table
|
||||
if len(docs) == max_results:
|
||||
meta['max'] = max_results
|
||||
|
||||
meta['headers'] = [{'title': 'Document', 'key':'document'},
|
||||
{'title': 'Title', 'key':'title'},
|
||||
{'title': 'Date', 'key':'date'},
|
||||
{'title': 'Status', 'key':'status'},
|
||||
{'title': 'IPR', 'key':'ipr'},
|
||||
{'title': 'AD / Shepherd', 'key':'ad'}]
|
||||
|
||||
if query and hasattr(query, "urlencode"): # fed a Django QueryDict
|
||||
d = query.copy()
|
||||
for h in meta['headers']:
|
||||
h["sort_url"] = "?" + d.urlencode()
|
||||
if h['key'] == sort_key:
|
||||
h['sorted'] = True
|
||||
if sort_reversed:
|
||||
h['direction'] = 'desc'
|
||||
d["sort"] = h["key"]
|
||||
else:
|
||||
h['direction'] = 'asc'
|
||||
d["sort"] = "-" + h["key"]
|
||||
else:
|
||||
d["sort"] = h["key"]
|
||||
|
||||
return (docs, meta)
|
||||
|
||||
|
|
@ -36,7 +36,6 @@ from django.http import HttpResponse, Http404 , HttpResponseForbidden
|
|||
from django.shortcuts import render, render_to_response, get_object_or_404, redirect
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
|
@ -50,7 +49,7 @@ from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_wi
|
|||
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
|
||||
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
||||
get_initial_notify, make_notify_changed_event, crawl_history)
|
||||
from ietf.community.models import CommunityList
|
||||
from ietf.community.utils import augment_docs_with_tracking_info
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.utils import can_manage_group_type, can_manage_materials
|
||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
|
||||
|
@ -345,15 +344,7 @@ def document_main(request, name, rev=None):
|
|||
elif can_edit_stream_info and (not iesg_state or iesg_state.slug == 'watching'):
|
||||
actions.append(("Submit to IESG for Publication", urlreverse('doc_to_iesg', kwargs=dict(name=doc.name))))
|
||||
|
||||
tracking_document = False
|
||||
if request.user.is_authenticated():
|
||||
try:
|
||||
clist = CommunityList.objects.get(user=request.user)
|
||||
clist.update()
|
||||
if clist.get_documents().filter(name=doc.name).count() > 0:
|
||||
tracking_document = True
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
augment_docs_with_tracking_info([doc], request.user)
|
||||
|
||||
replaces = [d.name for d in doc.related_that_doc("replaces")]
|
||||
replaced_by = [d.name for d in doc.related_that("replaces")]
|
||||
|
@ -420,7 +411,6 @@ def document_main(request, name, rev=None):
|
|||
shepherd_writeup=shepherd_writeup,
|
||||
search_archive=search_archive,
|
||||
actions=actions,
|
||||
tracking_document=tracking_document,
|
||||
presentations=presentations,
|
||||
),
|
||||
context_instance=RequestContext(request))
|
||||
|
|
|
@ -881,6 +881,7 @@ class ShepherdWriteupUploadForm(forms.Form):
|
|||
def clean_txt(self):
|
||||
return get_cleaned_text_file_content(self.cleaned_data["txt"])
|
||||
|
||||
@login_required
|
||||
def edit_shepherd_writeup(request, name):
|
||||
"""Change this document's shepherd writeup"""
|
||||
doc = get_object_or_404(Document, type="draft", name=name)
|
||||
|
|
|
@ -35,7 +35,6 @@ import datetime, re
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.db.models import Q
|
||||
from django.http import Http404, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect
|
||||
|
@ -44,16 +43,15 @@ from django.utils.cache import _generate_cache_key
|
|||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.community.models import CommunityList
|
||||
from ietf.doc.models import ( Document, DocHistory, DocAlias, State, RelatedDocument,
|
||||
DocEvent, LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
|
||||
from ietf.doc.expire import expirable_draft
|
||||
from ietf.doc.models import ( Document, DocHistory, DocAlias, State,
|
||||
LastCallDocEvent, IESG_SUBSTATE_TAGS )
|
||||
from ietf.doc.fields import select2_id_doc_name_json
|
||||
from ietf.group.models import Group
|
||||
from ietf.idindex.index import active_drafts_index_by_group
|
||||
from ietf.name.models import DocTagName, DocTypeName, StreamName
|
||||
from ietf.person.models import Person
|
||||
from ietf.utils.draft_search import normalize_draftname
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
|
@ -62,7 +60,7 @@ class SearchForm(forms.Form):
|
|||
activedrafts = forms.BooleanField(required=False, initial=True)
|
||||
olddrafts = forms.BooleanField(required=False, initial=False)
|
||||
|
||||
by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state','stream')], required=False, initial='wg')
|
||||
by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state','stream')], required=False, initial='group')
|
||||
author = forms.CharField(required=False)
|
||||
group = forms.CharField(required=False)
|
||||
stream = forms.ModelChoiceField(StreamName.objects.all().order_by('name'), empty_label="any stream", required=False)
|
||||
|
@ -81,7 +79,7 @@ class SearchForm(forms.Form):
|
|||
("ad", "AD"), ("-ad", "AD (desc)"), ),
|
||||
required=False, widget=forms.HiddenInput)
|
||||
|
||||
doctypes = DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name');
|
||||
doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name'), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SearchForm, self).__init__(*args, **kwargs)
|
||||
|
@ -123,126 +121,27 @@ class SearchForm(forms.Form):
|
|||
q['state'] = q['substate'] = None
|
||||
return q
|
||||
|
||||
def wrap_value(v):
|
||||
return lambda: v
|
||||
|
||||
def fill_in_search_attributes(docs):
|
||||
# fill in some attributes for the search results to save some
|
||||
# hairy template code and avoid repeated SQL queries
|
||||
|
||||
docs_dict = dict((d.pk, d) for d in docs)
|
||||
doc_ids = docs_dict.keys()
|
||||
|
||||
rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=doc_ids).values_list("document_id", "name"))
|
||||
|
||||
# latest event cache
|
||||
event_types = ("published_rfc",
|
||||
"changed_ballot_position",
|
||||
"started_iesg_process",
|
||||
"new_revision")
|
||||
for d in docs:
|
||||
d.latest_event_cache = dict()
|
||||
for e in event_types:
|
||||
d.latest_event_cache[e] = None
|
||||
|
||||
for e in DocEvent.objects.filter(doc__in=doc_ids, type__in=event_types).order_by('time'):
|
||||
docs_dict[e.doc_id].latest_event_cache[e.type] = e
|
||||
|
||||
# telechat date, can't do this with above query as we need to get TelechatDocEvents out
|
||||
seen = set()
|
||||
for e in TelechatDocEvent.objects.filter(doc__in=doc_ids, type="scheduled_for_telechat").order_by('-time'):
|
||||
if e.doc_id not in seen:
|
||||
d = docs_dict[e.doc_id]
|
||||
d.telechat_date = wrap_value(d.telechat_date(e))
|
||||
seen.add(e.doc_id)
|
||||
|
||||
# misc
|
||||
for d in docs:
|
||||
# emulate canonical name which is used by a lot of the utils
|
||||
d.canonical_name = wrap_value(rfc_aliases[d.pk] if d.pk in rfc_aliases else d.name)
|
||||
|
||||
if d.rfc_number() != None and d.latest_event_cache["published_rfc"]:
|
||||
d.latest_revision_date = d.latest_event_cache["published_rfc"].time
|
||||
elif d.latest_event_cache["new_revision"]:
|
||||
d.latest_revision_date = d.latest_event_cache["new_revision"].time
|
||||
else:
|
||||
d.latest_revision_date = d.time
|
||||
|
||||
if d.type_id == "draft":
|
||||
if d.get_state_slug() == "rfc":
|
||||
d.search_heading = "RFC"
|
||||
elif d.get_state_slug() in ("ietf-rm", "auth-rm"):
|
||||
d.search_heading = "Withdrawn Internet-Draft"
|
||||
else:
|
||||
d.search_heading = "%s Internet-Draft" % d.get_state()
|
||||
else:
|
||||
d.search_heading = "%s" % (d.type,);
|
||||
|
||||
d.expirable = expirable_draft(d)
|
||||
|
||||
if d.get_state_slug() != "rfc":
|
||||
d.milestones = d.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
|
||||
|
||||
|
||||
|
||||
# RFCs
|
||||
|
||||
# errata
|
||||
erratas = set(Document.objects.filter(tags="errata", name__in=rfc_aliases.keys()).distinct().values_list("name", flat=True))
|
||||
for d in docs:
|
||||
d.has_errata = d.name in erratas
|
||||
|
||||
# obsoleted/updated by
|
||||
for a in rfc_aliases:
|
||||
d = docs_dict[a]
|
||||
d.obsoleted_by_list = []
|
||||
d.updated_by_list = []
|
||||
|
||||
xed_by = RelatedDocument.objects.filter(target__name__in=rfc_aliases.values(),
|
||||
relationship__in=("obs", "updates")).select_related('target__document_id')
|
||||
rel_rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc",
|
||||
document__in=[rel.source_id for rel in xed_by]).values_list('document_id', 'name'))
|
||||
for rel in xed_by:
|
||||
d = docs_dict[rel.target.document_id]
|
||||
if rel.relationship_id == "obs":
|
||||
l = d.obsoleted_by_list
|
||||
elif rel.relationship_id == "updates":
|
||||
l = d.updated_by_list
|
||||
l.append(rel_rfc_aliases[rel.source_id].upper())
|
||||
l.sort()
|
||||
|
||||
|
||||
def retrieve_search_results(form, all_types=False):
|
||||
|
||||
"""Takes a validated SearchForm and return the results."""
|
||||
|
||||
if not form.is_valid():
|
||||
raise ValueError("SearchForm doesn't validate: %s" % form.errors)
|
||||
|
||||
query = form.cleaned_data
|
||||
|
||||
types=[];
|
||||
meta = {}
|
||||
|
||||
if (query['activedrafts'] or query['olddrafts'] or query['rfcs']):
|
||||
types.append('draft')
|
||||
|
||||
# Advanced document types are data-driven, so we need to read them from the
|
||||
# raw form.data field (and track their checked/unchecked state ourselves)
|
||||
meta['checked'] = {}
|
||||
alltypes = DocTypeName.objects.exclude(slug='draft').order_by('name');
|
||||
for doctype in alltypes:
|
||||
if form.data.__contains__('include-' + doctype.slug):
|
||||
types.append(doctype.slug)
|
||||
meta['checked'][doctype.slug] = True
|
||||
|
||||
if len(types) == 0 and not all_types:
|
||||
return ([], {})
|
||||
|
||||
MAX = 500
|
||||
|
||||
if all_types:
|
||||
docs = Document.objects.all()
|
||||
else:
|
||||
types = []
|
||||
|
||||
if query['activedrafts'] or query['olddrafts'] or query['rfcs']:
|
||||
types.append('draft')
|
||||
|
||||
types.extend(query["doctypes"])
|
||||
|
||||
if not types:
|
||||
return []
|
||||
|
||||
docs = Document.objects.filter(type__in=types)
|
||||
|
||||
# name
|
||||
|
@ -281,104 +180,7 @@ def retrieve_search_results(form, all_types=False):
|
|||
elif by == "stream":
|
||||
docs = docs.filter(stream=query["stream"])
|
||||
|
||||
# evaluate and fill in attribute results immediately to cut down
|
||||
# the number of queries
|
||||
docs = docs.select_related("ad", "ad__person", "std_level", "intended_std_level", "group", "stream")
|
||||
docs = docs.prefetch_related("states__type", "tags")
|
||||
results = list(docs[:MAX])
|
||||
|
||||
fill_in_search_attributes(results)
|
||||
|
||||
# sort
|
||||
def sort_key(d):
|
||||
res = []
|
||||
|
||||
rfc_num = d.rfc_number()
|
||||
|
||||
|
||||
if d.type_id == "draft":
|
||||
res.append(["Active", "Expired", "Replaced", "Withdrawn", "RFC"].index(d.search_heading.split()[0] ))
|
||||
else:
|
||||
res.append(d.type_id);
|
||||
res.append("-");
|
||||
res.append(d.get_state_slug());
|
||||
res.append("-");
|
||||
|
||||
if query["sort"] in ["title", "-title"]:
|
||||
res.append(d.title)
|
||||
elif query["sort"] in ["date", "-date" ]:
|
||||
res.append(str(d.latest_revision_date))
|
||||
elif query["sort"] in ["status", "-status"]:
|
||||
if rfc_num != None:
|
||||
res.append(int(rfc_num))
|
||||
else:
|
||||
res.append(d.get_state().order if d.get_state() else None)
|
||||
elif query["sort"] in ["ipr", "-ipr"]:
|
||||
res.append(len(d.ipr()))
|
||||
elif query["sort"] in ["ad", "-ad"]:
|
||||
if rfc_num != None:
|
||||
res.append(int(rfc_num))
|
||||
elif d.get_state_slug() == "active":
|
||||
if d.get_state("draft-iesg"):
|
||||
res.append(d.get_state("draft-iesg").order)
|
||||
else:
|
||||
res.append(0)
|
||||
else:
|
||||
if rfc_num != None:
|
||||
res.append(int(rfc_num))
|
||||
else:
|
||||
res.append(d.canonical_name())
|
||||
|
||||
return res
|
||||
|
||||
results.sort(key=sort_key, reverse=query["sort"].startswith("-"))
|
||||
|
||||
# fill in a meta dict with some information for rendering the result table
|
||||
if len(results) == MAX:
|
||||
meta['max'] = MAX
|
||||
meta['by'] = query['by']
|
||||
meta['advanced'] = bool(query['by'] or len(meta['checked']))
|
||||
|
||||
meta['headers'] = [{'title': 'Document', 'key':'document'},
|
||||
{'title': 'Title', 'key':'title'},
|
||||
{'title': 'Date', 'key':'date'},
|
||||
{'title': 'Status', 'key':'status'},
|
||||
{'title': 'IPR', 'key':'ipr'},
|
||||
{'title': 'AD / Shepherd', 'key':'ad'}]
|
||||
|
||||
if hasattr(form.data, "urlencode"): # form was fed a Django QueryDict, not local plain dict
|
||||
d = form.data.copy()
|
||||
for h in meta['headers']:
|
||||
sort = query.get('sort')
|
||||
if sort.endswith(h['key']):
|
||||
h['sorted'] = True
|
||||
if sort.startswith('-'):
|
||||
h['direction'] = 'desc'
|
||||
d["sort"] = h["key"]
|
||||
else:
|
||||
h['direction'] = 'asc'
|
||||
d["sort"] = "-" + h["key"]
|
||||
else:
|
||||
d["sort"] = h["key"]
|
||||
h["sort_url"] = "?" + d.urlencode()
|
||||
|
||||
return (results, meta)
|
||||
|
||||
|
||||
def get_doc_is_tracked(request, results):
|
||||
# Determine whether each document is being tracked or not, and remember
|
||||
# that so we can display the proper track/untrack option.
|
||||
doc_is_tracked = { }
|
||||
if request.user.is_authenticated():
|
||||
try:
|
||||
clist = CommunityList.objects.get(user=request.user)
|
||||
clist.update()
|
||||
except ObjectDoesNotExist:
|
||||
return doc_is_tracked
|
||||
for doc in results:
|
||||
if clist.get_documents().filter(name=doc.name).count() > 0:
|
||||
doc_is_tracked[doc.name] = True
|
||||
return doc_is_tracked
|
||||
return docs
|
||||
|
||||
def search(request):
|
||||
if request.GET:
|
||||
|
@ -395,17 +197,16 @@ def search(request):
|
|||
if not form.is_valid():
|
||||
return HttpResponseBadRequest("form not valid: %s" % form.errors)
|
||||
|
||||
results, meta = retrieve_search_results(form)
|
||||
results = retrieve_search_results(form)
|
||||
results, meta = prepare_document_table(request, results, get_params)
|
||||
meta['searching'] = True
|
||||
else:
|
||||
form = SearchForm()
|
||||
results = []
|
||||
meta = { 'by': None, 'advanced': False, 'searching': False }
|
||||
|
||||
doc_is_tracked = get_doc_is_tracked(request, results)
|
||||
meta = { 'by': None, 'searching': False }
|
||||
|
||||
return render(request, 'doc/search/search.html', {
|
||||
'form':form, 'docs':results, 'doc_is_tracked':doc_is_tracked, 'meta':meta, },
|
||||
'form':form, 'docs':results, 'meta':meta, },
|
||||
)
|
||||
|
||||
def frontpage(request):
|
||||
|
@ -472,7 +273,7 @@ def search_for_name(request, name):
|
|||
else:
|
||||
for t in doctypenames:
|
||||
if n.startswith(t.prefix):
|
||||
search_args += "&include-%s=on" % t.slug
|
||||
search_args += "&doctypes=%s" % t.slug
|
||||
break
|
||||
else:
|
||||
search_args += "&rfcs=on&activedrafts=on&olddrafts=on"
|
||||
|
@ -579,8 +380,9 @@ def docs_for_ad(request, name):
|
|||
raise Http404
|
||||
form = SearchForm({'by':'ad','ad': ad.id,
|
||||
'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
|
||||
'sort': 'status'})
|
||||
results, meta = retrieve_search_results(form, all_types=True)
|
||||
'sort': 'status',
|
||||
'doctypes': list(DocTypeName.objects.filter(used=True).exclude(slug='draft').values_list("pk", flat=True))})
|
||||
results, meta = prepare_document_table(request, retrieve_search_results(form), form.data)
|
||||
results.sort(key=ad_dashboard_sort_key)
|
||||
del meta["headers"][-1]
|
||||
#
|
||||
|
@ -594,7 +396,7 @@ def docs_for_ad(request, name):
|
|||
def drafts_in_last_call(request):
|
||||
lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
|
||||
form = SearchForm({'by':'state','state': lc_state, 'rfcs':'on', 'activedrafts':'on'})
|
||||
results, meta = retrieve_search_results(form)
|
||||
results, meta = prepare_document_table(request, retrieve_search_results(form), form.data)
|
||||
|
||||
return render(request, 'doc/drafts_in_last_call.html', {
|
||||
'form':form, 'docs':results, 'meta':meta
|
||||
|
|
|
@ -17,7 +17,7 @@ from ietf.doc.utils_charter import charter_name_for_group
|
|||
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
|
||||
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
|
||||
from ietf.group.utils import save_group_in_history, can_manage_group_type
|
||||
from ietf.group.utils import get_group_or_404
|
||||
from ietf.group.utils import get_group_or_404, setup_default_community_list_for_group
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.person.fields import SearchableEmailsField
|
||||
from ietf.person.models import Person, Email
|
||||
|
@ -231,6 +231,9 @@ def edit(request, group_type=None, acronym=None, action="edit"):
|
|||
state=clean["state"]
|
||||
)
|
||||
|
||||
if group.features.has_documents:
|
||||
setup_default_community_list_for_group(group)
|
||||
|
||||
e = ChangeStateGroupEvent(group=group, type="changed_state")
|
||||
e.time = group.time
|
||||
e.by = request.user.person
|
||||
|
|
|
@ -39,7 +39,7 @@ from tempfile import mkstemp
|
|||
import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.template.loader import render_to_string
|
||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
|
@ -48,14 +48,16 @@ from django.views.decorators.cache import cache_page
|
|||
from django.db.models import Q
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from ietf.doc.views_search import SearchForm, retrieve_search_results, get_doc_is_tracked
|
||||
from ietf.doc.models import Document, State, DocAlias, RelatedDocument
|
||||
from ietf.doc.utils import get_chartering_type
|
||||
from ietf.doc.templatetags.ietf_filters import clean_whitespace
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
from ietf.group.models import Group, Role, ChangeStateGroupEvent
|
||||
from ietf.name.models import GroupTypeName
|
||||
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
|
||||
from ietf.group.utils import can_manage_materials, get_group_or_404
|
||||
from ietf.community.utils import docs_tracked_by_community_list, can_manage_community_list
|
||||
from ietf.community.models import CommunityList, EmailSubscription
|
||||
from ietf.utils.pipe import pipe
|
||||
from ietf.settings import MAILING_LIST_INFO_URL
|
||||
from ietf.mailtrigger.utils import gather_relevant_expansions
|
||||
|
@ -369,6 +371,11 @@ def construct_group_menu_context(request, group, selected, group_type, others):
|
|||
if group.state_id != "proposed" and (is_chair or can_manage):
|
||||
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
|
||||
|
||||
if group.features.has_documents:
|
||||
clist = CommunityList.objects.filter(group=group).first()
|
||||
if clist and can_manage_community_list(request.user, clist):
|
||||
actions.append((u'Manage document list', urlreverse('community_group_manage_list', kwargs=kwargs)))
|
||||
|
||||
if group.features.has_materials and can_manage_materials(request.user, group):
|
||||
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
|
||||
|
||||
|
@ -393,36 +400,23 @@ def construct_group_menu_context(request, group, selected, group_type, others):
|
|||
|
||||
return d
|
||||
|
||||
def search_for_group_documents(group):
|
||||
form = SearchForm({ 'by':'group', 'group': group.acronym or "", 'rfcs':'on', 'activedrafts': 'on' })
|
||||
docs, meta = retrieve_search_results(form)
|
||||
|
||||
# get the related docs
|
||||
form_related = SearchForm({ 'by':'group', 'name': u'-%s-' % group.acronym, 'activedrafts': 'on' })
|
||||
raw_docs_related, meta_related = retrieve_search_results(form_related)
|
||||
def prepare_group_documents(request, group, clist):
|
||||
found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET)
|
||||
|
||||
docs = []
|
||||
docs_related = []
|
||||
for d in raw_docs_related:
|
||||
parts = d.name.split("-", 2);
|
||||
# canonical form draft-<name|ietf|irtf>-wg-etc
|
||||
if len(parts) >= 3 and parts[1] not in ("ietf", "irtf") and parts[2].startswith(group.acronym + "-") and d not in docs:
|
||||
|
||||
# split results
|
||||
for d in found_docs:
|
||||
# non-WG drafts and call for WG adoption are considered related
|
||||
if (d.group != group
|
||||
or (d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"))):
|
||||
d.search_heading = "Related Internet-Draft"
|
||||
docs_related.append(d)
|
||||
|
||||
# move call for WG adoption to related
|
||||
cleaned_docs = []
|
||||
docs_related_names = set(d.name for d in docs_related)
|
||||
for d in docs:
|
||||
if d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"):
|
||||
if d.name not in docs_related_names:
|
||||
d.search_heading = "Related Internet-Draft"
|
||||
docs_related.append(d)
|
||||
else:
|
||||
cleaned_docs.append(d)
|
||||
docs.append(d)
|
||||
|
||||
docs = cleaned_docs
|
||||
|
||||
docs_related.sort(key=lambda d: d.name)
|
||||
meta_related = meta.copy()
|
||||
|
||||
return docs, meta, docs_related, meta_related
|
||||
|
||||
|
@ -438,17 +432,19 @@ def group_documents(request, acronym, group_type=None):
|
|||
if not group.features.has_documents:
|
||||
raise Http404
|
||||
|
||||
docs, meta, docs_related, meta_related = search_for_group_documents(group)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
|
||||
doc_is_tracked = get_doc_is_tracked(request, docs)
|
||||
doc_is_tracked.update(get_doc_is_tracked(request, docs_related))
|
||||
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
|
||||
|
||||
subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
|
||||
|
||||
context = construct_group_menu_context(request, group, "documents", group_type, {
|
||||
'docs': docs,
|
||||
'meta': meta,
|
||||
'docs_related': docs_related,
|
||||
'meta_related': meta_related,
|
||||
'doc_is_tracked': doc_is_tracked,
|
||||
'subscribed': subscribed,
|
||||
'clist': clist,
|
||||
})
|
||||
|
||||
return render(request, 'group/group_documents.html', context)
|
||||
|
@ -459,7 +455,9 @@ def group_documents_txt(request, acronym, group_type=None):
|
|||
if not group.features.has_documents:
|
||||
raise Http404
|
||||
|
||||
docs, meta, docs_related, meta_related = search_for_group_documents(group)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
|
||||
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
|
||||
|
||||
for d in docs:
|
||||
d.prefix = d.get_state().name
|
||||
|
|
|
@ -15,7 +15,7 @@ from django.core.urlresolvers import NoReverseMatch
|
|||
|
||||
from ietf.doc.models import Document, DocAlias, DocEvent, State
|
||||
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
|
||||
from ietf.group.utils import save_group_in_history
|
||||
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
|
||||
from ietf.name.models import DocTagName, GroupStateName, GroupTypeName
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.utils.test_utils import TestCase, unicontent
|
||||
|
@ -186,20 +186,21 @@ class GroupPagesTests(TestCase):
|
|||
name=draft2.name,
|
||||
)
|
||||
|
||||
setup_default_community_list_for_group(group)
|
||||
|
||||
url = urlreverse('ietf.group.info.group_documents', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(draft.name in unicontent(r))
|
||||
self.assertTrue(group.name in unicontent(r))
|
||||
self.assertTrue(group.acronym in unicontent(r))
|
||||
|
||||
self.assertTrue(draft2.name in unicontent(r))
|
||||
|
||||
# Make sure that a logged in user is presented with an opportunity to add results to their community list
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.community-list-add-remove-doc')]))
|
||||
self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.track-untrack-doc')]))
|
||||
|
||||
# test the txt version too while we're at it
|
||||
url = urlreverse('ietf.group.info.group_documents_txt', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
|
||||
|
|
|
@ -5,6 +5,10 @@ urlpatterns = patterns('',
|
|||
(r'^$', 'ietf.group.info.group_home', None, "group_home"),
|
||||
(r'^documents/txt/$', 'ietf.group.info.group_documents_txt'),
|
||||
(r'^documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
|
||||
(r'^documents/manage/$', 'ietf.community.views.manage_list', None, "community_group_manage_list"),
|
||||
(r'^documents/csv/$', 'ietf.community.views.export_to_csv', None, 'community_group_csv'),
|
||||
(r'^documents/feed/$', 'ietf.community.views.feed', None, 'community_group_feed'),
|
||||
(r'^documents/subscription/$', 'ietf.community.views.subscription', None, 'community_group_subscription'),
|
||||
(r'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
|
||||
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
|
||||
(r'^history/$','ietf.group.info.history'),
|
||||
|
|
|
@ -8,6 +8,9 @@ from ietf.group.models import Group, RoleHistory
|
|||
from ietf.person.models import Email
|
||||
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.community.models import CommunityList, SearchRule
|
||||
from ietf.community.utils import reset_name_contains_index_for_rule
|
||||
from ietf.doc.models import State
|
||||
|
||||
|
||||
def save_group_in_history(group):
|
||||
|
@ -107,3 +110,25 @@ def get_group_or_404(acronym, group_type):
|
|||
possible_groups = possible_groups.filter(type=group_type)
|
||||
|
||||
return get_object_or_404(possible_groups, acronym=acronym)
|
||||
|
||||
def setup_default_community_list_for_group(group):
|
||||
clist = CommunityList.objects.create(group=group)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="group",
|
||||
group=group,
|
||||
state=State.objects.get(slug="active", type="draft"),
|
||||
)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="group_rfc",
|
||||
group=group,
|
||||
state=State.objects.get(slug="rfc", type="draft"),
|
||||
)
|
||||
related_docs_rule = SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
text=r"^draft-[^-]+-%s-" % group.acronym,
|
||||
state=State.objects.get(slug="active", type="draft"),
|
||||
)
|
||||
reset_name_contains_index_for_rule(related_docs_rule)
|
||||
|
|
|
@ -5,7 +5,8 @@ from django.template import RequestContext
|
|||
from django.http import Http404, HttpResponseForbidden
|
||||
from django import forms
|
||||
|
||||
from ietf.doc.views_search import SearchForm, retrieve_search_results
|
||||
from ietf.doc.models import Document
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
from ietf.group.models import Group, GroupEvent, Role
|
||||
from ietf.group.utils import save_group_in_history
|
||||
from ietf.ietfauth.utils import has_role
|
||||
|
@ -27,9 +28,9 @@ def stream_documents(request, acronym):
|
|||
group = get_object_or_404(Group, acronym=acronym)
|
||||
editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
|
||||
stream = StreamName.objects.get(slug=acronym)
|
||||
form = SearchForm({'by':'stream', 'stream':acronym,
|
||||
'rfcs':'on', 'activedrafts':'on'})
|
||||
docs, meta = retrieve_search_results(form)
|
||||
|
||||
qs = Document.objects.filter(states__type="draft", states__slug__in=["active", "rfc"], stream=acronym)
|
||||
docs, meta = prepare_document_table(request, qs)
|
||||
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable }, context_instance=RequestContext(request))
|
||||
|
||||
class StreamEditForm(forms.Form):
|
||||
|
|
|
@ -60,7 +60,7 @@ from ietf.iesg.models import TelechatDate
|
|||
from ietf.iesg.utils import telechat_page_count
|
||||
from ietf.ietfauth.utils import has_role, role_required, user_is_person
|
||||
from ietf.person.models import Person
|
||||
from ietf.doc.views_search import fill_in_search_attributes
|
||||
from ietf.doc.utils_search import fill_in_document_table_attributes
|
||||
|
||||
def review_decisions(request, year=None):
|
||||
events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
|
||||
|
@ -370,7 +370,7 @@ def agenda_documents(request):
|
|||
sections = agenda_sections()
|
||||
# augment the docs with the search attributes, since we're using
|
||||
# the search_result_row view to display them (which expects them)
|
||||
fill_in_search_attributes(docs_by_date[date])
|
||||
fill_in_document_table_attributes(docs_by_date[date])
|
||||
fill_in_agenda_docs(date, sections, docs_by_date[date])
|
||||
|
||||
telechats.append({
|
||||
|
|
|
@ -10,7 +10,7 @@ urlpatterns = patterns('ietf.ietfauth.views',
|
|||
url(r'^logout/$', logout),
|
||||
# url(r'^loggedin/$', 'ietf_loggedin'),
|
||||
# url(r'^loggedout/$', 'logged_out'),
|
||||
url(r'^profile/$', 'profile'),
|
||||
url(r'^profile/$', 'profile', name="account_profile"),
|
||||
# (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'url_login'),
|
||||
url(r'^testemail/$', 'test_email'),
|
||||
url(r'^create/$', 'create_account', name='create_account'),
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.shortcuts import render_to_response, get_object_or_404, redirect
|
|||
from django.template import RequestContext
|
||||
|
||||
from ietf.group.models import Group, GroupMilestone, ChangeStateGroupEvent, GroupEvent, GroupURL, Role
|
||||
from ietf.group.utils import save_group_in_history, get_charter_text
|
||||
from ietf.group.utils import save_group_in_history, get_charter_text, setup_default_community_list_for_group
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.person.models import Person
|
||||
from ietf.secr.groups.forms import GroupModelForm, GroupMilestoneForm, RoleForm, SearchForm
|
||||
|
@ -102,6 +102,9 @@ def add(request):
|
|||
awp.group = group
|
||||
awp.save()
|
||||
|
||||
if group.features.has_documents:
|
||||
setup_default_community_list_for_group(group)
|
||||
|
||||
# create GroupEvent(s)
|
||||
# always create started event
|
||||
ChangeStateGroupEvent.objects.create(group=group,
|
||||
|
|
|
@ -448,3 +448,14 @@ form.navbar-form input.form-control.input-sm { width: 141px; }
|
|||
padding: inherit;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
/* Community lists */
|
||||
|
||||
label#list-feeds {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.email-subscription button[type=submit] {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
|
|
@ -100,24 +100,20 @@ $(document).ready(function () {
|
|||
}
|
||||
|
||||
// search results
|
||||
$('.community-list-add-remove-doc').click(function(e) {
|
||||
$('.track-untrack-doc').click(function(e) {
|
||||
e.preventDefault();
|
||||
var trigger = $(this);
|
||||
$.ajax({
|
||||
var trigger = $(this);
|
||||
$.ajax({
|
||||
url: trigger.attr('href'),
|
||||
type: 'GET',
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
success: function(response){
|
||||
if (response.success) {
|
||||
trigger.parent().find(".tooltip").remove();
|
||||
trigger.find("span.fa").toggleClass("fa-bookmark fa-bookmark-o");
|
||||
if (trigger.hasClass('btn')) {
|
||||
trigger.attr('disabled', true).blur();
|
||||
} else {
|
||||
trigger.contents().unwrap().blur();
|
||||
trigger.parent().find(".tooltip").remove();
|
||||
trigger.addClass("hide");
|
||||
trigger.parent().find(".track-untrack-doc").not(trigger).removeClass("hide");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
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");
|
||||
});
|
||||
});
|
|
@ -20,6 +20,7 @@ from ietf.person.models import Person
|
|||
from ietf.group.models import Group
|
||||
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
|
||||
from ietf.submit.models import Submission, Preapproval
|
||||
from ietf.group.utils import setup_default_community_list_for_group
|
||||
|
||||
class SubmitTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -143,7 +144,8 @@ class SubmitTests(TestCase):
|
|||
def submit_new_wg(self, formats):
|
||||
# submit new -> supply submitter info -> approve
|
||||
draft = make_test_data()
|
||||
|
||||
setup_default_community_list_for_group(draft.group)
|
||||
|
||||
# prepare draft to suggest replace
|
||||
sug_replaced_draft = Document.objects.create(
|
||||
name="draft-ietf-ames-sug-replaced",
|
||||
|
|
|
@ -13,6 +13,7 @@ from ietf.group.models import Group
|
|||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.name.models import StreamName
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.community.utils import update_name_contains_indexes_with_new_doc
|
||||
from ietf.submit.mail import announce_to_lists, announce_new_version, announce_to_authors
|
||||
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
|
||||
from ietf.utils import unaccent
|
||||
|
@ -126,7 +127,7 @@ def post_submission(request, submission):
|
|||
if not (group.type_id == "individ" and draft.group and draft.group.type_id == "area"):
|
||||
# don't overwrite an assigned area if it's still an individual
|
||||
# submission
|
||||
draft.group_id = group.pk
|
||||
draft.group = group
|
||||
draft.rev = submission.rev
|
||||
draft.pages = submission.pages
|
||||
draft.abstract = submission.abstract
|
||||
|
@ -205,6 +206,8 @@ def post_submission(request, submission):
|
|||
|
||||
new_replaces, new_possibly_replaces = update_replaces_from_submission(request, submission, draft)
|
||||
|
||||
update_name_contains_indexes_with_new_doc(draft)
|
||||
|
||||
announce_to_lists(request, submission)
|
||||
announce_new_version(request, submission, draft, state_change_msg)
|
||||
announce_to_authors(request, submission)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
|
||||
{% load ietf_filters community_tags wg_menu streams_menu active_groups_menu %}
|
||||
{% load ietf_filters managed_groups wg_menu streams_menu active_groups_menu %}
|
||||
|
||||
{% if flavor != "top" %}
|
||||
{% include "base/menu_user.html" %}
|
||||
|
@ -51,14 +51,14 @@
|
|||
<li><a href="{% url "submit_approvals" %}">Approve a draft</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% get_user_managed_lists user as community_lists %}
|
||||
{% if community_lists %}
|
||||
<li><a href="{{ community_lists.personal.get_manage_url }}">My tracked docs</a></li>
|
||||
{% for cl in community_lists.group %}
|
||||
<li><a href="{{ cl.get_manage_url }}">{{ cl.short_name }} {{cl.group.type.slug}} docs</a></li>
|
||||
{% if user and user.is_authenticated %}
|
||||
<li><a href="{% url "community_personal_view_list" user.username %}">My tracked docs</a></li>
|
||||
|
||||
{% for g in user|managed_groups %}
|
||||
<li><a href="{% url "group_docs" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in to track docs</a></li>
|
||||
<li><a rel="nofollow" href="/accounts/login/?next={{ request.get_full_path|urlencode }}">Sign in to track docs</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% if user|has_role:"Area Director,Secretariat" %}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}{% origin %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
<form method="post" action="#custom">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form display_form %}
|
||||
|
||||
<div class="form-group">
|
||||
<label>Show fields</label>
|
||||
{% for field in dc.get_display_fields_config %}
|
||||
<div class="checkbox">
|
||||
<label for="id_{{ field.codename }}">
|
||||
<input id="id_{{ field.codename }}" type="checkbox" name="{{ field.codename }}" {% if field.active %}checked{% endif %}>
|
||||
{{ field.description }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% buttons %}
|
||||
<input type="submit" class="btn btn-primary" name="save_display" value="Save configuration">
|
||||
{% endbuttons %}
|
||||
</form>
|
|
@ -1,2 +0,0 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
|
||||
<span class="displayField displayField-{{ field.codename }}">{{ value|safe }}</span>
|
22
ietf/templates/community/list_menu.html
Normal file
22
ietf/templates/community/list_menu.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<ul class="list-inline pull-right" style="margin-top:1em;">
|
||||
<li>
|
||||
<label id="list-feeds">Atom feed:</label>
|
||||
<div class="btn-group" role="group" aria-labelledby="list-feeds">
|
||||
<a class="btn btn-default" title="Feed of all changes" href="{% if clist.group %}{% url "community_group_feed" clist.group.acronym %}{% else %}{% url "community_personal_feed" clist.user.username %}{% endif %}">All changes</a>
|
||||
<a class="btn btn-default" title="Feed of only significant state changes" href="{% if clist.group %}{% url "community_group_feed" clist.group.acronym %}{% else %}{% url "community_personal_feed" clist.user.username %}{% endif %}?significant=1">Significant</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% if clist.pk != None %}
|
||||
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_subscription" clist.group.acronym %}{% else %}{% url "community_personal_subscription" clist.user.username %}{% endif %}">
|
||||
<i class="glyphicon glyphicon-envelope"></i>
|
||||
{% if subscribed %}
|
||||
Change subscription
|
||||
{% else %}
|
||||
Subscribe to changes
|
||||
{% endif %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_csv" clist.group.acronym %}{% else %}{% url "community_personal_csv" clist.user.username %}{% endif %}"><i class="glyphicon glyphicon-list"></i> Export as CSV</a></li>
|
||||
</ul>
|
|
@ -49,8 +49,8 @@
|
|||
<tr>
|
||||
<td>{{ doc.display_name }}</td>
|
||||
<td>{{ doc.get_state }}</td>
|
||||
<td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></td>
|
||||
<td><a class="btn btn-danger btn-xs" href="{% url "community_remove_document" cl.pk doc.pk %}">Remove</a></td>
|
||||
<td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></td>
|
||||
<td><a class="btn btn-danger btn-xs" href="{% if cl.user %}{% url "community_personal_untrack_document" doc.pk %}{% else %}{% url "community_group_untrack_document" %}{% endif %}">Remove</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
143
ietf/templates/community/manage_list.html
Normal file
143
ietf/templates/community/manage_list.html
Normal file
|
@ -0,0 +1,143 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load bootstrap3 %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Manage {{ clist.long_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Manage {{ clist.long_name }}</h1>
|
||||
|
||||
<noscript>This page depends on Javascript being enabled to work properly.</noscript>
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<p>The list currently tracks <a href="{{ clist.get_absolute_url }}">{{ total_count }} document{{ total_count|pluralize }}</a>.</p>
|
||||
|
||||
<p><a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a></p>
|
||||
|
||||
<h2>Individual documents</h2>
|
||||
|
||||
{% if individually_added %}
|
||||
<p>The list tracks {{ individually_added|length }} individually added document{{ individually_added|length|pluralize }}:</p>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
{% for d in individually_added %}
|
||||
<tr>
|
||||
<td>{{ d.name }}</td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="document" value="{{ d.pk }}">
|
||||
<button class="btn btn-danger btn-xs" name="action" value="remove_document">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>The list does not track any individually added documents yet.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if clist.group %}
|
||||
<p>Add individual documents here:</p>
|
||||
{% else %}
|
||||
<p>Conveniently track individual documents in your personal list with the track icon <span class="fa fa-bookmark-o"></span> in <a href="/doc/search/">search results</a>.</p>
|
||||
|
||||
<p>You can also add documents here:</p>
|
||||
{% endif %}
|
||||
|
||||
<form class="form add-document" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field add_doc_form.documents show_label=False %}
|
||||
<button class="btn btn-primary" name="action" value="add_documents">Add documents</button>
|
||||
</form>
|
||||
|
||||
|
||||
<h2>Search rules</h2>
|
||||
|
||||
<p>You can track documents with a search rule. When a document fulfills the search criteria, it will automatically show up in the list.</p>
|
||||
|
||||
{% if rules %}
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr><th>Rule</th><th>Value</th><th>Documents</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rule in rules %}
|
||||
<tr id="r{{ rule.pk }}">
|
||||
<td>{{ rule.get_rule_type_display }}</td>
|
||||
<td>
|
||||
{% if "group" in rule.rule_type or "area" in rule.rule_type %}
|
||||
{{ rule.group.acronym }}
|
||||
{% elif "state_" in rule.rule_type %}
|
||||
{{ rule.state }}
|
||||
{% elif "author" in rule.rule_type or rule.rule_type == "ad" or "shepherd" in rule.rule_type %}
|
||||
{{ rule.person }}
|
||||
{% elif "name_contains" in rule.rule_type %}
|
||||
{{ rule.text }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ rule.matching_documents_count }} match{{ rule.matching_documents_count|pluralize:"es" }}</td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="rule" value="{{ rule.pk }}">
|
||||
<button class="btn btn-danger btn-xs" name="action" value="remove_rule">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>No rules defined.</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div><a class="btn btn-primary" data-toggle="collapse" data-target="#add-new-rule">Add a new rule</a></div>
|
||||
|
||||
<div id="add-new-rule" {% if not rule_type_form.errors and not rule_form %}class="collapse"{% endif %}>
|
||||
<h3>Add a new rule</h3>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form rule_type_form %}
|
||||
|
||||
<div class="form-content-placeholder">
|
||||
{% if rule_form %}
|
||||
{% bootstrap_form rule_form %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary" name="action" value="add_rule">Add rule</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
<div class="empty-forms hide">
|
||||
{% for rule_type, f in empty_rule_forms.items %}
|
||||
<div class="rule-type {{ rule_type }}">
|
||||
{% bootstrap_form f %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'select2/select2.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/manage-community-list.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -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,30 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}Subscribe to {{ cl.long_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
{% if success %}
|
||||
<h1>Subscription successful</h1>
|
||||
|
||||
<p>We have sent an email to your email address with instructions to complete your subscription.</p>
|
||||
{% else %}
|
||||
<h1>Subscribe to {{ cl.long_name }}</h1>
|
||||
|
||||
<p>Subscribe to the email list for notifications of {% if significant %}significant {% endif %}changes on {{ cl.long_name }}.</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -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,34 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}Cancel subscription to {{ cl.long_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
{% if success %}
|
||||
<h1>Cancellation successful</h1>
|
||||
|
||||
<p>
|
||||
You will receive a confirmation email shortly containing further instructions on how to cancel your subscription.
|
||||
</p>
|
||||
{% else %}
|
||||
<h1>Cancel subscription to {{ cl.long_name }}</h1>
|
||||
|
||||
<p>
|
||||
Cancel your subscription to the email list for notifications of {% if significant %}significant {% endif %}changes on {{ cl.long_name }}.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,12 +0,0 @@
|
|||
{% autoescape off %}
|
||||
Hello,
|
||||
|
||||
In order to complete the cancelation of your subscription to {% if significant %}significant {% endif %}changes on {{ clist.long_name }}, please follow this link or copy it and paste it in your web browser:
|
||||
|
||||
https://{{ domain }}{% if significant %}{% url "confirm_significant_unsubscription" clist.id to_email today auth %}{% else %}{% url "confirm_unsubscription" clist.id to_email today auth %}{% endif %}
|
||||
|
||||
Best regards,
|
||||
|
||||
The datatracker login manager service
|
||||
(for the IETF Secretariat)
|
||||
{% endautoescape %}
|
|
@ -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>Internet-Drafts</h2>
|
||||
<table class="table table-condensed table-striped tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
<th>{{ field.description }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in documents.1 %}
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
<td>{% show_field field doc %}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endwith %}
|
||||
|
||||
{% with dc.get_active_fields as fields %}
|
||||
<h2>RFCs</h2>
|
||||
<table class="table table-condensed table-striped tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
<th>{{ field.rfcDescription }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in documents.0 %}
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
<td>{% show_field field doc %}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
58
ietf/templates/community/subscription.html
Normal file
58
ietf/templates/community/subscription.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}Subscription to {{ clist.long_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>Subscription to {{ clist.long_name }}</h1>
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<p>Get notified when changes happen to any of the tracked documents.</p>
|
||||
|
||||
{% if existing_subscriptions %}
|
||||
<h2>Existing subscriptions</h2>
|
||||
|
||||
<ul class="list-group">
|
||||
{% for s in existing_subscriptions %}
|
||||
<li class="list-group-item email-subscription">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<code>{{ s.email.address }}</code> - {{ s.get_notify_on_display }}
|
||||
<input type="hidden" name="subscription_id" value="{{ s.pk }}">
|
||||
<button class="btn btn-danger btn-sm" type="submit" name="action" value="unsubscribe">Unsubscribe</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<p><a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a></p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<h2>Add new subscription</h2>
|
||||
|
||||
<p class="text-muted">The email addresses you can choose between are those registered in <a href="{% url "account_profile" %}">your profile</a>.</p>
|
||||
|
||||
{% if form.fields.email.queryset %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a>
|
||||
|
||||
<button type="submit" name="action" value="subscribe" class="btn btn-primary">Subscribe</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">You do not have any active email addresses registered with your account. Go to <a href="{% url "account_profile" %}">your profile and add or activate one</a>.</div>
|
||||
|
||||
<a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
16
ietf/templates/community/track_document.html
Normal file
16
ietf/templates/community/track_document.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}{% origin %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}Track document {{ name }}{% endblock %}
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>Add {{ name }} to the list?</p>
|
||||
|
||||
{% buttons %}
|
||||
<input type="submit" class="btn btn-primary" value="Track document">
|
||||
{% endbuttons %}
|
||||
</form>
|
16
ietf/templates/community/untrack_document.html
Normal file
16
ietf/templates/community/untrack_document.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}{% origin %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}Remove tracking of document {{ name }}{% endblock %}
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>Remove {{ name }} from the list?</p>
|
||||
|
||||
{% buttons %}
|
||||
<input type="submit" class="btn btn-primary" value="Remove tracking of document">
|
||||
{% endbuttons %}
|
||||
</form>
|
|
@ -1,4 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}{% origin %}
|
||||
{% load community_tags %}
|
||||
{% get_clist_view cl %}
|
||||
{% load origin %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}{{ clist.long_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>{{ clist.long_name }}</h1>
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
{% if can_manage_list %}
|
||||
<a class="btn btn-primary" href="{% url "community_personal_manage_list" clist.user.username %}">
|
||||
<i class="glyphicon glyphicon-cog"></i>
|
||||
Manage list
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% include "doc/search/search_results.html" with skip_no_matches_warning=True %}
|
||||
{% include "community/list_menu.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -490,11 +490,8 @@
|
|||
</ul>
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
{% if tracking_document %}
|
||||
<a class="btn btn-default btn-xs community-list-add-remove-doc" href="{% url "community_remove_track_document" doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark-o"></span> Untrack</a>
|
||||
{% else %}
|
||||
<a class="btn btn-default btn-xs community-list-add-remove-doc" href="{% url "community_add_track_document" doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark"></span> Track</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-default btn-xs track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "community_personal_untrack_document" user.username doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark"></span> Untrack</a>
|
||||
<a class="btn btn-default btn-xs track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "community_personal_track_document" user.username doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark-o"></span> Track</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit and iesg_state %}
|
||||
|
|
|
@ -42,10 +42,10 @@
|
|||
<label class="control-label" for="id_olddrafts">{{ form.olddrafts }} Internet-Draft (expired, replaced or withdrawn)</label>
|
||||
</div>
|
||||
|
||||
{% for doc_type in form.doctypes %}
|
||||
{% for value, label in form.fields.doctypes.choices %}
|
||||
<div class="checkbox">
|
||||
<label class="control-label" for="id_{{doc_type.slug}}">
|
||||
<input type="checkbox" class="advdoctype" {% if doc_type.slug in meta.checked %}checked{% endif %} name="include-{{doc_type.slug}}" id="id_{{doc_type.slug}}"/>{{doc_type|safe|capfirst_allcaps}}
|
||||
<label class="control-label" for="id_doctypes_{{ value }}">
|
||||
<input type="checkbox" class="advdoctype" {% if value in form.doctypes.value %}checked{% endif %} name="doctypes" value="{{ value }}" id="id_doctypes_{{ value }}"/>{{ label|safe|capfirst_allcaps}}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -54,7 +54,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<input type="radio" name="by" value="author" {% if meta.by == "author" %}checked{% endif %}/>
|
||||
<input type="radio" name="by" value="author" {% if form.by.value == "author" %}checked{% endif %} id="id_author"/>
|
||||
<label for="id_author" class="control-label">Author</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
|
@ -64,7 +64,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<input type="radio" name="by" value="group" {% if meta.by == "group" %}checked{% endif %}/>
|
||||
<input type="radio" name="by" value="group" {% if form.by.value == "group" %}checked{% endif %} id="id_group"/>
|
||||
<label for="id_group" class="control-label">WG</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<input type="radio" name="by" value="area" {% if meta.by == "area" %}checked{% endif %}/>
|
||||
<input type="radio" name="by" value="area" {% if form.by.value == "area" %}checked{% endif %} id="id_area"/>
|
||||
<label for="id_area" class="control-label">Area</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
|
@ -85,7 +85,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<input type="radio" name="by" value="ad" {% if meta.by == "ad" %}checked{% endif %}/>
|
||||
<input type="radio" name="by" value="ad" {% if form.by.value == "ad" %}checked{% endif %} id="id_ad"/>
|
||||
<label for="id_ad" class="control-label">AD</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
|
@ -95,7 +95,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<input type="radio" name="by" value="state" {% if meta.by == "state" %}checked{% endif %}/>
|
||||
<input type="radio" name="by" value="state" {% if form.by.value == "state" %}checked{% endif %} id="id_state"/>
|
||||
<label for="id_state" class="control-label">IESG State</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
|
@ -108,7 +108,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<input type="radio" name="by" value="stream" {% if meta.by == "stream" %}checked{% endif %}/>
|
||||
<input type="radio" name="by" value="stream" {% if form.by.value == "stream" %}checked{% endif %} id="id_stream"/>
|
||||
<label for="id_stream" class="control-label">Stream</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
|
|
|
@ -13,15 +13,12 @@
|
|||
|
||||
<td>
|
||||
{% if user.is_authenticated %}
|
||||
{% if doc.name in doc_is_tracked %}
|
||||
<a href="{% url "community_remove_track_document" doc.name %}" class="community-list-add-remove-doc" title="Remove from your personal ID list">
|
||||
<span class="fa fa-bookmark"></span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url "community_add_track_document" doc.name %}" class="community-list-add-remove-doc" title="Add to your personal ID list">
|
||||
<span class="fa fa-bookmark-o"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "community_personal_untrack_document" request.user.username doc.name %}" class="track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" title="Remove from your personal ID list">
|
||||
<span class="fa fa-bookmark"></span>
|
||||
</a>
|
||||
<a href="{% url "community_personal_track_document" request.user.username doc.name %}" class="track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" title="Add to your personal ID list">
|
||||
<span class="fa fa-bookmark-o"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
{% origin %}
|
||||
{% include "doc/search/search_results.html" %}
|
||||
{% include "doc/search/search_results.html" with docs=docs_related meta=meta_related skip_no_matches_warning=True %}
|
||||
{% include "community/list_menu.html" %}
|
||||
|
||||
{% endblock group_content %}
|
||||
|
|
Loading…
Reference in a new issue