Merged community list branch with trunk
- Legacy-Id: 11174
This commit is contained in:
commit
9faae5e915
|
@ -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 import forms
|
||||||
from django.conf import settings
|
from django.db.models import Q
|
||||||
from django.contrib.sites.models import Site
|
|
||||||
|
|
||||||
from ietf.utils.mail import send_mail
|
from ietf.community.models import SearchRule, EmailSubscription
|
||||||
from ietf.community.models import Rule, DisplayConfiguration, RuleManager
|
from ietf.doc.fields import SearchableDocumentsField
|
||||||
from ietf.community.display import DisplayField
|
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:
|
class Meta:
|
||||||
model = Rule
|
model = SearchRule
|
||||||
fields = ('rule_type', 'value')
|
fields = ('state', 'group', 'person', 'text')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, clist, rule_type, *args, **kwargs):
|
||||||
self.clist = kwargs.pop('clist', None)
|
kwargs["prefix"] = rule_type # add prefix to avoid mixups in the Javascript
|
||||||
super(RuleForm, self).__init__(*args, **kwargs)
|
super(SearchRuleForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def save(self):
|
def restrict_state(state_type, slug=None):
|
||||||
self.instance.community_list = self.clist
|
f = self.fields['state']
|
||||||
super(RuleForm, self).save()
|
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):
|
if rule_type in ['group', 'group_rfc', 'area', 'area_rfc']:
|
||||||
result = []
|
restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
|
||||||
for i in RuleManager.__subclasses__():
|
|
||||||
options = i(None).options()
|
|
||||||
if options:
|
|
||||||
result.append({'type': i.codename,
|
|
||||||
'options': options})
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
class Meta:
|
||||||
model = DisplayConfiguration
|
model = EmailSubscription
|
||||||
fields = ('sort_method', )
|
fields = ("notify_on", "email")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
|
@ -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.contrib.auth.models import User
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import signals, Q
|
from django.db.models import signals
|
||||||
|
from django.core.urlresolvers import reverse as urlreverse
|
||||||
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 ietf.doc.models import Document, DocEvent, State
|
||||||
|
from ietf.group.models import Group
|
||||||
|
from ietf.person.models import Person, Email
|
||||||
|
|
||||||
class CommunityList(models.Model):
|
class CommunityList(models.Model):
|
||||||
|
|
||||||
user = models.ForeignKey(User, blank=True, null=True)
|
user = models.ForeignKey(User, blank=True, null=True)
|
||||||
group = models.ForeignKey(Group, blank=True, null=True)
|
group = models.ForeignKey(Group, blank=True, null=True)
|
||||||
added_ids = models.ManyToManyField(Document)
|
added_docs = 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
|
|
||||||
|
|
||||||
def long_name(self):
|
def long_name(self):
|
||||||
if self.user:
|
if self.user:
|
||||||
return 'Personal ID list of %s' % self.user.username
|
return 'Personal ID list of %s' % self.user.username
|
||||||
else:
|
elif self.group:
|
||||||
return 'ID list for %s' % self.group.name
|
return 'ID list for %s' % self.group.name
|
||||||
|
else:
|
||||||
|
return 'ID list'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.long_name()
|
return self.long_name()
|
||||||
|
|
||||||
def get_public_url(self):
|
def get_absolute_url(self):
|
||||||
if self.user:
|
if self.user:
|
||||||
return reverse('view_personal_list', None, args=(self.user.username, ))
|
return urlreverse("community_personal_view_list", kwargs={ 'username': self.user.username })
|
||||||
else:
|
elif self.group:
|
||||||
return reverse('view_group_list', None, args=(self.group.acronym, ))
|
return urlreverse("group_docs", kwargs={ 'acronym': self.group.acronym })
|
||||||
|
return ""
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
community_list = models.ForeignKey(CommunityList)
|
||||||
cached_ids = models.ManyToManyField(Document)
|
rule_type = models.CharField(max_length=30, choices=RULE_TYPES)
|
||||||
rule_type = models.CharField(max_length=30, choices=TYPES_OF_RULES)
|
|
||||||
value = models.CharField(max_length=255)
|
|
||||||
|
|
||||||
class Meta:
|
# these are filled in depending on the type
|
||||||
unique_together= ("community_list", "rule_type", "value")
|
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(
|
# store a materialized view/index over which documents are matched
|
||||||
auto_now=True)
|
# by the name_contains rule to avoid having to scan the whole
|
||||||
|
# database - we update this manually when the rule is changed and
|
||||||
def get_callable_rule(self):
|
# when new documents are submitted
|
||||||
for i in RuleManager.__subclasses__():
|
name_contains_index = models.ManyToManyField(Document)
|
||||||
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'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmailSubscription(models.Model):
|
class EmailSubscription(models.Model):
|
||||||
community_list = models.ForeignKey(CommunityList)
|
community_list = models.ForeignKey(CommunityList)
|
||||||
email = models.CharField(max_length=200)
|
email = models.ForeignKey(Email)
|
||||||
significant = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
|
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):
|
def __unicode__(self):
|
||||||
|
return u"%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on)
|
||||||
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 notify_events(sender, instance, **kwargs):
|
def notify_events(sender, instance, **kwargs):
|
||||||
if not isinstance(instance, DocEvent):
|
if not isinstance(instance, DocEvent):
|
||||||
return
|
return
|
||||||
if instance.doc.type.slug != 'draft' or instance.type == 'added_comment':
|
|
||||||
|
if instance.doc.type_id != 'draft':
|
||||||
return
|
return
|
||||||
(changes, created) = DocumentChangeDates.objects.get_or_create(document=instance.doc)
|
|
||||||
changes.normal_change_date = instance.time
|
from ietf.community.utils import notify_event_to_subscribers
|
||||||
significant = False
|
notify_event_to_subscribers(instance)
|
||||||
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()
|
|
||||||
signals.post_save.connect(notify_events)
|
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
|
# Autogenerated by the mkresources management command 2014-11-13 23:53
|
||||||
from tastypie.resources import ModelResource
|
from tastypie.resources import ModelResource
|
||||||
from ietf.api import ToOneField
|
from tastypie.fields import ToOneField, ToManyField
|
||||||
from tastypie.fields import ToManyField
|
|
||||||
from tastypie.constants import ALL, ALL_WITH_RELATIONS
|
from tastypie.constants import ALL, ALL_WITH_RELATIONS
|
||||||
from tastypie.cache import SimpleCache
|
from tastypie.cache import SimpleCache
|
||||||
|
|
||||||
from ietf import api
|
from ietf import api
|
||||||
|
|
||||||
from ietf.community.models import ( CommunityList, ExpectedChange, DisplayConfiguration,
|
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
|
||||||
ListNotification, Rule, EmailSubscription, DocumentChangeDates )
|
|
||||||
|
|
||||||
from ietf.doc.resources import DocumentResource
|
from ietf.doc.resources import DocumentResource
|
||||||
from ietf.group.resources import GroupResource
|
from ietf.group.resources import GroupResource
|
||||||
|
@ -16,7 +15,7 @@ from ietf.utils.resources import UserResource
|
||||||
class CommunityListResource(ModelResource):
|
class CommunityListResource(ModelResource):
|
||||||
user = ToOneField(UserResource, 'user', null=True)
|
user = ToOneField(UserResource, 'user', null=True)
|
||||||
group = ToOneField(GroupResource, 'group', 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:
|
class Meta:
|
||||||
cache = SimpleCache()
|
cache = SimpleCache()
|
||||||
queryset = CommunityList.objects.all()
|
queryset = CommunityList.objects.all()
|
||||||
|
@ -28,75 +27,24 @@ class CommunityListResource(ModelResource):
|
||||||
"cached": ALL,
|
"cached": ALL,
|
||||||
"user": ALL_WITH_RELATIONS,
|
"user": ALL_WITH_RELATIONS,
|
||||||
"group": ALL_WITH_RELATIONS,
|
"group": ALL_WITH_RELATIONS,
|
||||||
"added_ids": ALL_WITH_RELATIONS,
|
"added_docs": ALL_WITH_RELATIONS,
|
||||||
}
|
}
|
||||||
api.community.register(CommunityListResource())
|
api.community.register(CommunityListResource())
|
||||||
|
|
||||||
from ietf.doc.resources import DocumentResource
|
from ietf.doc.resources import DocumentResource
|
||||||
class ExpectedChangeResource(ModelResource):
|
class SearchRuleResource(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):
|
|
||||||
community_list = ToOneField(CommunityListResource, 'community_list')
|
community_list = ToOneField(CommunityListResource, 'community_list')
|
||||||
class Meta:
|
class Meta:
|
||||||
cache = SimpleCache()
|
cache = SimpleCache()
|
||||||
queryset = DisplayConfiguration.objects.all()
|
queryset = SearchRule.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()
|
|
||||||
serializer = api.Serializer()
|
serializer = api.Serializer()
|
||||||
#resource_name = 'rule'
|
#resource_name = 'rule'
|
||||||
filtering = {
|
filtering = {
|
||||||
"id": ALL,
|
"id": ALL,
|
||||||
"rule_type": ALL,
|
"rule_type": ALL,
|
||||||
"value": ALL,
|
|
||||||
"last_updated": ALL,
|
|
||||||
"community_list": ALL_WITH_RELATIONS,
|
"community_list": ALL_WITH_RELATIONS,
|
||||||
"cached_ids": ALL_WITH_RELATIONS,
|
|
||||||
}
|
}
|
||||||
api.community.register(RuleResource())
|
api.community.register(SearchRuleResource())
|
||||||
|
|
||||||
class EmailSubscriptionResource(ModelResource):
|
class EmailSubscriptionResource(ModelResource):
|
||||||
community_list = ToOneField(CommunityListResource, 'community_list')
|
community_list = ToOneField(CommunityListResource, 'community_list')
|
||||||
|
@ -107,26 +55,8 @@ class EmailSubscriptionResource(ModelResource):
|
||||||
#resource_name = 'emailsubscription'
|
#resource_name = 'emailsubscription'
|
||||||
filtering = {
|
filtering = {
|
||||||
"id": ALL,
|
"id": ALL,
|
||||||
"email": ALL,
|
"email": ALL_WITH_RELATIONS,
|
||||||
"significant": ALL,
|
"notify_on": ALL,
|
||||||
"community_list": ALL_WITH_RELATIONS,
|
"community_list": ALL_WITH_RELATIONS,
|
||||||
}
|
}
|
||||||
api.community.register(EmailSubscriptionResource())
|
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
|
from django.conf.urls import patterns, url
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('ietf.community.views',
|
urlpatterns = patterns('',
|
||||||
url(r'^personal/$', 'manage_personal_list', name='manage_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/$', 'ietf.community.views.view_list', name='community_personal_view_list'),
|
||||||
url(r'^personal/csv/$', 'csv_personal_list', name='csv_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/manage/$', 'ietf.community.views.manage_list', name='community_personal_manage_list'),
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/view/$', 'view_personal_list', name='view_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.track_document', name='community_personal_track_document'),
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/csv/$', 'view_csv_personal_list', name='view_csv_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.untrack_document', name='community_personal_untrack_document'),
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/changes/feed/$', 'changes_personal_list', name='changes_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/csv/$', 'ietf.community.views.export_to_csv', name='community_personal_csv'),
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/changes/significant/feed/$', 'significant_personal_list', name='significant_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/feed/$', 'ietf.community.views.feed', name='community_personal_feed'),
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/$', 'subscribe_personal_list', {'significant': False}, name='subscribe_personal_list'),
|
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'ietf.community.views.subscription', name='community_personal_subscription'),
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/significant/$', 'subscribe_personal_list', {'significant': True}, name='subscribe_significant_personal_list'),
|
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/$', 'unsubscribe_personal_list', {'significant': False}, name='unsubscribe_personal_list'),
|
|
||||||
url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/significant/$', 'unsubscribe_personal_list', {'significant': True}, name='unsubscribe_significant_personal_list'),
|
|
||||||
|
|
||||||
url(r'^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 csv
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.db import IntegrityError
|
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
|
||||||
from django.conf import settings
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
from django.utils.html import strip_tags
|
||||||
from django.shortcuts import get_object_or_404, render_to_response
|
|
||||||
from django.template import RequestContext
|
|
||||||
from django.utils.http import urlquote
|
|
||||||
|
|
||||||
from ietf.community.models import CommunityList, Rule, EmailSubscription
|
from ietf.community.models import SearchRule, EmailSubscription
|
||||||
from ietf.community.forms import RuleForm, DisplayForm, SubscribeForm, UnSubscribeForm
|
from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
|
||||||
from ietf.group.models import Group
|
from ietf.community.utils import lookup_community_list, can_manage_community_list
|
||||||
from ietf.doc.models import DocEvent, DocAlias
|
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):
|
docs = docs_tracked_by_community_list(clist)
|
||||||
display_config = clist.get_display_config()
|
docs, meta = prepare_document_table(request, docs, request.GET)
|
||||||
if request.method == 'POST' and request.POST.get('save_rule', None):
|
|
||||||
rule_form = RuleForm(request.POST, clist=clist)
|
subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
|
||||||
display_form = DisplayForm(instance=display_config)
|
|
||||||
if rule_form.is_valid():
|
return render(request, 'community/view_list.html', {
|
||||||
try:
|
'clist': clist,
|
||||||
rule_form.save()
|
'docs': docs,
|
||||||
except IntegrityError:
|
'meta': meta,
|
||||||
pass;
|
'can_manage_list': can_manage_community_list(request.user, clist),
|
||||||
rule_form = RuleForm(clist=clist)
|
'subscribed': subscribed,
|
||||||
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)
|
@login_required
|
||||||
rule_form = RuleForm(clist=clist)
|
def manage_list(request, username=None, acronym=None, group_type=None):
|
||||||
if display_form.is_valid():
|
# we need to be a bit careful because clist may not exist in the
|
||||||
display_form.save()
|
# database so we can't call related stuff on it yet
|
||||||
rule_form = RuleForm(clist=clist)
|
clist = lookup_community_list(username, acronym)
|
||||||
display_form = DisplayForm(instance=display_config)
|
|
||||||
|
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:
|
else:
|
||||||
rule_form = RuleForm(clist=clist)
|
add_doc_form = AddDocumentsForm()
|
||||||
display_form = DisplayForm(instance=display_config)
|
|
||||||
clist = CommunityList.objects.get(id=clist.id)
|
if request.method == 'POST' and action == 'remove_document':
|
||||||
return render_to_response('community/manage_clist.html',
|
document_pk = request.POST.get('document')
|
||||||
{'cl': clist,
|
if clist.pk is not None and document_pk:
|
||||||
'dc': display_config,
|
document = get_object_or_404(clist.added_docs, pk=document_pk)
|
||||||
'display_form': display_form,
|
clist.added_docs.remove(document)
|
||||||
'rule_form': rule_form},
|
|
||||||
context_instance=RequestContext(request))
|
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):
|
@login_required
|
||||||
user = request.user
|
def track_document(request, name, username=None, acronym=None):
|
||||||
if not user.is_authenticated():
|
doc = get_object_or_404(Document, docalias__name=name)
|
||||||
path = urlquote(request.get_full_path())
|
|
||||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
if request.method == "POST":
|
||||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
clist = lookup_community_list(username, acronym)
|
||||||
clist = CommunityList.objects.get_or_create(user=request.user)[0]
|
if not can_manage_community_list(request.user, clist):
|
||||||
if not clist.check_manager(request.user):
|
return HttpResponseForbidden("You do not have permission to access this view")
|
||||||
path = urlquote(request.get_full_path())
|
|
||||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
if clist.pk is None:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
clist.save()
|
||||||
return _manage_list(request, clist)
|
|
||||||
|
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):
|
def export_to_csv(request, username=None, acronym=None, group_type=None):
|
||||||
group = get_object_or_404(Group, acronym=acronym)
|
clist = lookup_community_list(username, 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)
|
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
|
||||||
def add_track_document(request, document_name):
|
if clist.group:
|
||||||
"""supports the "Track this document" functionality
|
filename = "%s-draft-list.csv" % clist.group.acronym
|
||||||
|
else:
|
||||||
This is exposed in the document view and in document search results."""
|
filename = "draft-list.csv"
|
||||||
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)
|
|
||||||
|
|
||||||
def remove_track_document(request, document_name):
|
response['Content-Disposition'] = 'attachment; filename=%s' % filename
|
||||||
"""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')
|
|
||||||
|
|
||||||
def remove_document(request, list_id, document_name):
|
writer = csv.writer(response, dialect=csv.excel, delimiter=',')
|
||||||
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())
|
|
||||||
|
|
||||||
def add_document_to_list(request, clist, doc):
|
header = [
|
||||||
if not clist.check_manager(request.user):
|
"Name",
|
||||||
path = urlquote(request.get_full_path())
|
"Title",
|
||||||
tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
|
"Date of latest revision",
|
||||||
return HttpResponseRedirect('%s?%s=%s' % tup)
|
"Status in the IETF process",
|
||||||
clist.added_ids.add(doc)
|
"Associated group",
|
||||||
return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
|
"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):
|
return response
|
||||||
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())
|
|
||||||
|
|
||||||
|
def feed(request, username=None, acronym=None, group_type=None):
|
||||||
|
clist = lookup_community_list(username, acronym)
|
||||||
|
|
||||||
def _view_list(request, clist):
|
significant = request.GET.get('significant', '') == '1'
|
||||||
display_config = clist.get_display_config()
|
|
||||||
return render_to_response('community/public/view_list.html',
|
|
||||||
{'cl': clist,
|
|
||||||
'dc': display_config,
|
|
||||||
},
|
|
||||||
context_instance=RequestContext(request))
|
|
||||||
|
|
||||||
|
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):
|
events = DocEvent.objects.filter(
|
||||||
clist = get_object_or_404(CommunityList, secret=secret)
|
doc__in=documents,
|
||||||
return _view_list(request, clist)
|
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:
|
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()
|
host = request.get_host()
|
||||||
feed_url = 'https://%s%s' % (host, request.get_full_path())
|
feed_url = 'https://%s%s' % (host, request.get_full_path())
|
||||||
feed_id = uuid.uuid5(uuid.NAMESPACE_URL, feed_url.encode('utf-8'))
|
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:
|
if significant:
|
||||||
subtitle = 'Document significant changes'
|
subtitle = 'Significant document changes'
|
||||||
else:
|
else:
|
||||||
subtitle = 'Document changes'
|
subtitle = 'Document changes'
|
||||||
|
|
||||||
return render_to_response('community/public/atom.xml',
|
return render(request, 'community/atom.xml', {
|
||||||
{'cl': clist,
|
'clist': clist,
|
||||||
'entries': notifications,
|
'entries': events[:50],
|
||||||
'title': title,
|
'title': title,
|
||||||
'subtitle': subtitle,
|
'subtitle': subtitle,
|
||||||
'id': feed_id.get_urn(),
|
'id': feed_id.get_urn(),
|
||||||
'updated': datetime.datetime.today(),
|
'updated': datetime.datetime.now(),
|
||||||
},
|
}, content_type='text/xml')
|
||||||
content_type='text/xml',
|
|
||||||
context_instance=RequestContext(request))
|
|
||||||
|
|
||||||
|
|
||||||
def changes_personal_list(request, secret):
|
@login_required
|
||||||
clist = get_object_or_404(CommunityList, secret=secret)
|
def subscription(request, username=None, acronym=None, group_type=None):
|
||||||
return _atom_view(request, clist)
|
clist = lookup_community_list(username, acronym)
|
||||||
|
if clist.pk is None:
|
||||||
|
|
||||||
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'):
|
|
||||||
raise Http404
|
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):
|
existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
|
||||||
clist = get_object_or_404(CommunityList, secret=secret)
|
|
||||||
return _csv_list(request, clist)
|
|
||||||
|
|
||||||
def _subscribe_list(request, clist, significant):
|
|
||||||
success = False
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = SubscribeForm(data=request.POST, clist=clist, significant=significant)
|
action = request.POST.get("action")
|
||||||
if form.is_valid():
|
if action == "subscribe":
|
||||||
form.save()
|
form = SubscriptionForm(request.user, clist, request.POST)
|
||||||
success = True
|
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:
|
else:
|
||||||
form = SubscribeForm(clist=clist, significant=significant)
|
form = SubscriptionForm(request.user, clist)
|
||||||
return render_to_response('community/public/subscribe.html',
|
|
||||||
{'cl': clist,
|
|
||||||
'form': form,
|
|
||||||
'success': success,
|
|
||||||
},
|
|
||||||
context_instance=RequestContext(request))
|
|
||||||
|
|
||||||
|
return render(request, 'community/subscription.html', {
|
||||||
def _unsubscribe_list(request, clist, significant):
|
'clist': clist,
|
||||||
success = False
|
'form': form,
|
||||||
if request.method == 'POST':
|
'existing_subscriptions': existing_subscriptions,
|
||||||
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)
|
|
||||||
|
|
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.shortcuts import render, render_to_response, get_object_or_404, redirect
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.core.urlresolvers import reverse as urlreverse
|
from django.core.urlresolvers import reverse as urlreverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django import forms
|
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,
|
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,
|
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
||||||
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus)
|
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus)
|
||||||
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.models import Role
|
||||||
from ietf.group.utils import can_manage_group, can_manage_materials
|
from ietf.group.utils import can_manage_group, can_manage_materials
|
||||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
|
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'):
|
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))))
|
actions.append(("Submit to IESG for Publication", urlreverse('doc_to_iesg', kwargs=dict(name=doc.name))))
|
||||||
|
|
||||||
tracking_document = False
|
augment_docs_with_tracking_info([doc], request.user)
|
||||||
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
|
|
||||||
|
|
||||||
replaces = [d.name for d in doc.related_that_doc("replaces")]
|
replaces = [d.name for d in doc.related_that_doc("replaces")]
|
||||||
replaced_by = [d.name for d in doc.related_that("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,
|
shepherd_writeup=shepherd_writeup,
|
||||||
search_archive=search_archive,
|
search_archive=search_archive,
|
||||||
actions=actions,
|
actions=actions,
|
||||||
tracking_document=tracking_document,
|
|
||||||
presentations=presentations,
|
presentations=presentations,
|
||||||
),
|
),
|
||||||
context_instance=RequestContext(request))
|
context_instance=RequestContext(request))
|
||||||
|
|
|
@ -881,6 +881,7 @@ class ShepherdWriteupUploadForm(forms.Form):
|
||||||
def clean_txt(self):
|
def clean_txt(self):
|
||||||
return get_cleaned_text_file_content(self.cleaned_data["txt"])
|
return get_cleaned_text_file_content(self.cleaned_data["txt"])
|
||||||
|
|
||||||
|
@login_required
|
||||||
def edit_shepherd_writeup(request, name):
|
def edit_shepherd_writeup(request, name):
|
||||||
"""Change this document's shepherd writeup"""
|
"""Change this document's shepherd writeup"""
|
||||||
doc = get_object_or_404(Document, type="draft", name=name)
|
doc = get_object_or_404(Document, type="draft", name=name)
|
||||||
|
|
|
@ -35,7 +35,6 @@ import datetime, re
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.core.urlresolvers import reverse as urlreverse
|
from django.core.urlresolvers import reverse as urlreverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import Http404, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect
|
||||||
|
@ -44,16 +43,15 @@ from django.utils.cache import _generate_cache_key
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.community.models import CommunityList
|
from ietf.doc.models import ( Document, DocHistory, DocAlias, State,
|
||||||
from ietf.doc.models import ( Document, DocHistory, DocAlias, State, RelatedDocument,
|
LastCallDocEvent, IESG_SUBSTATE_TAGS )
|
||||||
DocEvent, LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
|
|
||||||
from ietf.doc.expire import expirable_draft
|
|
||||||
from ietf.doc.fields import select2_id_doc_name_json
|
from ietf.doc.fields import select2_id_doc_name_json
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.idindex.index import active_drafts_index_by_group
|
from ietf.idindex.index import active_drafts_index_by_group
|
||||||
from ietf.name.models import DocTagName, DocTypeName, StreamName
|
from ietf.name.models import DocTagName, DocTypeName, StreamName
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.utils.draft_search import normalize_draftname
|
from ietf.utils.draft_search import normalize_draftname
|
||||||
|
from ietf.doc.utils_search import prepare_document_table
|
||||||
|
|
||||||
|
|
||||||
class SearchForm(forms.Form):
|
class SearchForm(forms.Form):
|
||||||
|
@ -62,7 +60,7 @@ class SearchForm(forms.Form):
|
||||||
activedrafts = forms.BooleanField(required=False, initial=True)
|
activedrafts = forms.BooleanField(required=False, initial=True)
|
||||||
olddrafts = forms.BooleanField(required=False, initial=False)
|
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)
|
author = forms.CharField(required=False)
|
||||||
group = forms.CharField(required=False)
|
group = forms.CharField(required=False)
|
||||||
stream = forms.ModelChoiceField(StreamName.objects.all().order_by('name'), empty_label="any stream", 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)"), ),
|
("ad", "AD"), ("-ad", "AD (desc)"), ),
|
||||||
required=False, widget=forms.HiddenInput)
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SearchForm, self).__init__(*args, **kwargs)
|
super(SearchForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -123,126 +121,27 @@ class SearchForm(forms.Form):
|
||||||
q['state'] = q['substate'] = None
|
q['state'] = q['substate'] = None
|
||||||
return q
|
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):
|
def retrieve_search_results(form, all_types=False):
|
||||||
|
|
||||||
"""Takes a validated SearchForm and return the results."""
|
"""Takes a validated SearchForm and return the results."""
|
||||||
|
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
raise ValueError("SearchForm doesn't validate: %s" % form.errors)
|
raise ValueError("SearchForm doesn't validate: %s" % form.errors)
|
||||||
|
|
||||||
query = form.cleaned_data
|
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:
|
if all_types:
|
||||||
docs = Document.objects.all()
|
docs = Document.objects.all()
|
||||||
else:
|
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)
|
docs = Document.objects.filter(type__in=types)
|
||||||
|
|
||||||
# name
|
# name
|
||||||
|
@ -281,104 +180,7 @@ def retrieve_search_results(form, all_types=False):
|
||||||
elif by == "stream":
|
elif by == "stream":
|
||||||
docs = docs.filter(stream=query["stream"])
|
docs = docs.filter(stream=query["stream"])
|
||||||
|
|
||||||
# evaluate and fill in attribute results immediately to cut down
|
return docs
|
||||||
# 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
|
|
||||||
|
|
||||||
def search(request):
|
def search(request):
|
||||||
if request.GET:
|
if request.GET:
|
||||||
|
@ -395,17 +197,16 @@ def search(request):
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return HttpResponseBadRequest("form not valid: %s" % form.errors)
|
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
|
meta['searching'] = True
|
||||||
else:
|
else:
|
||||||
form = SearchForm()
|
form = SearchForm()
|
||||||
results = []
|
results = []
|
||||||
meta = { 'by': None, 'advanced': False, 'searching': False }
|
meta = { 'by': None, 'searching': False }
|
||||||
|
|
||||||
doc_is_tracked = get_doc_is_tracked(request, results)
|
|
||||||
|
|
||||||
return render(request, 'doc/search/search.html', {
|
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):
|
def frontpage(request):
|
||||||
|
@ -472,7 +273,7 @@ def search_for_name(request, name):
|
||||||
else:
|
else:
|
||||||
for t in doctypenames:
|
for t in doctypenames:
|
||||||
if n.startswith(t.prefix):
|
if n.startswith(t.prefix):
|
||||||
search_args += "&include-%s=on" % t.slug
|
search_args += "&doctypes=%s" % t.slug
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
search_args += "&rfcs=on&activedrafts=on&olddrafts=on"
|
search_args += "&rfcs=on&activedrafts=on&olddrafts=on"
|
||||||
|
@ -579,8 +380,9 @@ def docs_for_ad(request, name):
|
||||||
raise Http404
|
raise Http404
|
||||||
form = SearchForm({'by':'ad','ad': ad.id,
|
form = SearchForm({'by':'ad','ad': ad.id,
|
||||||
'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
|
'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
|
||||||
'sort': 'status'})
|
'sort': 'status',
|
||||||
results, meta = retrieve_search_results(form, all_types=True)
|
'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)
|
results.sort(key=ad_dashboard_sort_key)
|
||||||
del meta["headers"][-1]
|
del meta["headers"][-1]
|
||||||
#
|
#
|
||||||
|
@ -594,7 +396,7 @@ def docs_for_ad(request, name):
|
||||||
def drafts_in_last_call(request):
|
def drafts_in_last_call(request):
|
||||||
lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
|
lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
|
||||||
form = SearchForm({'by':'state','state': lc_state, 'rfcs':'on', 'activedrafts':'on'})
|
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', {
|
return render(request, 'doc/drafts_in_last_call.html', {
|
||||||
'form':form, 'docs':results, 'meta':meta
|
'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,
|
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
|
||||||
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
|
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
|
||||||
from ietf.group.utils import save_group_in_history, can_manage_group
|
from ietf.group.utils import save_group_in_history, can_manage_group
|
||||||
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.ietfauth.utils import has_role
|
||||||
from ietf.person.fields import SearchableEmailsField
|
from ietf.person.fields import SearchableEmailsField
|
||||||
from ietf.person.models import Person, Email
|
from ietf.person.models import Person, Email
|
||||||
|
@ -231,6 +231,9 @@ def edit(request, group_type=None, acronym=None, action="edit"):
|
||||||
state=clean["state"]
|
state=clean["state"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group.features.has_documents:
|
||||||
|
setup_default_community_list_for_group(group)
|
||||||
|
|
||||||
e = ChangeStateGroupEvent(group=group, type="changed_state")
|
e = ChangeStateGroupEvent(group=group, type="changed_state")
|
||||||
e.time = group.time
|
e.time = group.time
|
||||||
e.by = request.user.person
|
e.by = request.user.person
|
||||||
|
|
|
@ -42,7 +42,7 @@ from collections import OrderedDict
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
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.template.loader import render_to_string
|
||||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -51,14 +51,16 @@ from django.views.decorators.cache import cache_page
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.safestring import mark_safe
|
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.models import Document, State, DocAlias, RelatedDocument
|
||||||
from ietf.doc.utils import get_chartering_type
|
from ietf.doc.utils import get_chartering_type
|
||||||
from ietf.doc.templatetags.ietf_filters import clean_whitespace
|
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.group.models import Group, Role, ChangeStateGroupEvent
|
||||||
from ietf.name.models import GroupTypeName
|
from ietf.name.models import GroupTypeName
|
||||||
from ietf.group.utils import get_charter_text, can_manage_group_type, can_manage_group, milestone_reviewer_for_group_type, can_provide_status_update
|
from ietf.group.utils import get_charter_text, can_manage_group_type, can_manage_group, milestone_reviewer_for_group_type, can_provide_status_update
|
||||||
from ietf.group.utils import can_manage_materials, get_group_or_404
|
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.utils.pipe import pipe
|
||||||
from ietf.utils.textupload import get_cleaned_text_file_content
|
from ietf.utils.textupload import get_cleaned_text_file_content
|
||||||
from ietf.settings import MAILING_LIST_INFO_URL
|
from ietf.settings import MAILING_LIST_INFO_URL
|
||||||
|
@ -373,6 +375,11 @@ def construct_group_menu_context(request, group, selected, group_type, others):
|
||||||
if group.state_id != "proposed" and (is_chair or can_manage):
|
if group.state_id != "proposed" and (is_chair or can_manage):
|
||||||
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
|
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):
|
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)))
|
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
|
||||||
|
|
||||||
|
@ -397,36 +404,23 @@ def construct_group_menu_context(request, group, selected, group_type, others):
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def search_for_group_documents(group):
|
def prepare_group_documents(request, group, clist):
|
||||||
form = SearchForm({ 'by':'group', 'group': group.acronym or "", 'rfcs':'on', 'activedrafts': 'on' })
|
found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET)
|
||||||
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)
|
|
||||||
|
|
||||||
|
docs = []
|
||||||
docs_related = []
|
docs_related = []
|
||||||
for d in raw_docs_related:
|
|
||||||
parts = d.name.split("-", 2);
|
# split results
|
||||||
# canonical form draft-<name|ietf|irtf>-wg-etc
|
for d in found_docs:
|
||||||
if len(parts) >= 3 and parts[1] not in ("ietf", "irtf") and parts[2].startswith(group.acronym + "-") and d not in 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"
|
d.search_heading = "Related Internet-Draft"
|
||||||
docs_related.append(d)
|
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:
|
else:
|
||||||
cleaned_docs.append(d)
|
docs.append(d)
|
||||||
|
|
||||||
docs = cleaned_docs
|
meta_related = meta.copy()
|
||||||
|
|
||||||
docs_related.sort(key=lambda d: d.name)
|
|
||||||
|
|
||||||
return docs, meta, docs_related, meta_related
|
return docs, meta, docs_related, meta_related
|
||||||
|
|
||||||
|
@ -442,17 +436,19 @@ def group_documents(request, acronym, group_type=None):
|
||||||
if not group.features.has_documents:
|
if not group.features.has_documents:
|
||||||
raise Http404
|
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)
|
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
|
||||||
doc_is_tracked.update(get_doc_is_tracked(request, docs_related))
|
|
||||||
|
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, {
|
context = construct_group_menu_context(request, group, "documents", group_type, {
|
||||||
'docs': docs,
|
'docs': docs,
|
||||||
'meta': meta,
|
'meta': meta,
|
||||||
'docs_related': docs_related,
|
'docs_related': docs_related,
|
||||||
'meta_related': meta_related,
|
'meta_related': meta_related,
|
||||||
'doc_is_tracked': doc_is_tracked,
|
'subscribed': subscribed,
|
||||||
|
'clist': clist,
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, 'group/group_documents.html', context)
|
return render(request, 'group/group_documents.html', context)
|
||||||
|
@ -463,7 +459,9 @@ def group_documents_txt(request, acronym, group_type=None):
|
||||||
if not group.features.has_documents:
|
if not group.features.has_documents:
|
||||||
raise Http404
|
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:
|
for d in docs:
|
||||||
d.prefix = d.get_state().name
|
d.prefix = d.get_state().name
|
||||||
|
|
|
@ -20,7 +20,7 @@ from django.template.defaultfilters import urlize
|
||||||
|
|
||||||
from ietf.doc.models import Document, DocAlias, DocEvent, State
|
from ietf.doc.models import Document, DocAlias, DocEvent, State
|
||||||
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
|
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.name.models import DocTagName, GroupStateName, GroupTypeName
|
||||||
from ietf.person.models import Person, Email
|
from ietf.person.models import Person, Email
|
||||||
from ietf.utils.test_utils import TestCase, unicontent
|
from ietf.utils.test_utils import TestCase, unicontent
|
||||||
|
@ -191,20 +191,21 @@ class GroupPagesTests(TestCase):
|
||||||
name=draft2.name,
|
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))
|
url = urlreverse('ietf.group.info.group_documents', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertTrue(draft.name in unicontent(r))
|
self.assertTrue(draft.name in unicontent(r))
|
||||||
self.assertTrue(group.name in unicontent(r))
|
self.assertTrue(group.name in unicontent(r))
|
||||||
self.assertTrue(group.acronym in unicontent(r))
|
self.assertTrue(group.acronym in unicontent(r))
|
||||||
|
|
||||||
self.assertTrue(draft2.name 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
|
# 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")
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
q = PyQuery(r.content)
|
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
|
# 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))
|
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'^$', 'ietf.group.info.group_home', None, "group_home"),
|
||||||
(r'^documents/txt/$', 'ietf.group.info.group_documents_txt'),
|
(r'^documents/txt/$', 'ietf.group.info.group_documents_txt'),
|
||||||
(r'^documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
|
(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'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
|
||||||
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
|
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
|
||||||
(r'^about/status/$', 'ietf.group.info.group_about_status'),
|
(r'^about/status/$', 'ietf.group.info.group_about_status'),
|
||||||
|
|
|
@ -8,6 +8,9 @@ from ietf.group.models import Group, RoleHistory
|
||||||
from ietf.person.models import Email
|
from ietf.person.models import Email
|
||||||
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
|
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
|
||||||
from ietf.ietfauth.utils import has_role
|
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):
|
def save_group_in_history(group):
|
||||||
|
@ -124,3 +127,25 @@ def get_group_or_404(acronym, group_type):
|
||||||
possible_groups = possible_groups.filter(type=group_type)
|
possible_groups = possible_groups.filter(type=group_type)
|
||||||
|
|
||||||
return get_object_or_404(possible_groups, acronym=acronym)
|
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.http import Http404, HttpResponseForbidden
|
||||||
from django import forms
|
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.models import Group, GroupEvent, Role
|
||||||
from ietf.group.utils import save_group_in_history
|
from ietf.group.utils import save_group_in_history
|
||||||
from ietf.ietfauth.utils import has_role
|
from ietf.ietfauth.utils import has_role
|
||||||
|
@ -27,9 +28,9 @@ def stream_documents(request, acronym):
|
||||||
group = get_object_or_404(Group, acronym=acronym)
|
group = get_object_or_404(Group, acronym=acronym)
|
||||||
editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
|
editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
|
||||||
stream = StreamName.objects.get(slug=acronym)
|
stream = StreamName.objects.get(slug=acronym)
|
||||||
form = SearchForm({'by':'stream', 'stream':acronym,
|
|
||||||
'rfcs':'on', 'activedrafts':'on'})
|
qs = Document.objects.filter(states__type="draft", states__slug__in=["active", "rfc"], stream=acronym)
|
||||||
docs, meta = retrieve_search_results(form)
|
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))
|
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable }, context_instance=RequestContext(request))
|
||||||
|
|
||||||
class StreamEditForm(forms.Form):
|
class StreamEditForm(forms.Form):
|
||||||
|
|
|
@ -60,7 +60,7 @@ from ietf.iesg.models import TelechatDate
|
||||||
from ietf.iesg.utils import telechat_page_count
|
from ietf.iesg.utils import telechat_page_count
|
||||||
from ietf.ietfauth.utils import has_role, role_required, user_is_person
|
from ietf.ietfauth.utils import has_role, role_required, user_is_person
|
||||||
from ietf.person.models import 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):
|
def review_decisions(request, year=None):
|
||||||
events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
|
events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
|
||||||
|
@ -370,7 +370,7 @@ def agenda_documents(request):
|
||||||
sections = agenda_sections()
|
sections = agenda_sections()
|
||||||
# augment the docs with the search attributes, since we're using
|
# augment the docs with the search attributes, since we're using
|
||||||
# the search_result_row view to display them (which expects them)
|
# 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])
|
fill_in_agenda_docs(date, sections, docs_by_date[date])
|
||||||
|
|
||||||
telechats.append({
|
telechats.append({
|
||||||
|
|
|
@ -10,7 +10,7 @@ urlpatterns = patterns('ietf.ietfauth.views',
|
||||||
url(r'^logout/$', logout),
|
url(r'^logout/$', logout),
|
||||||
# url(r'^loggedin/$', 'ietf_loggedin'),
|
# url(r'^loggedin/$', 'ietf_loggedin'),
|
||||||
# url(r'^loggedout/$', 'logged_out'),
|
# 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'),
|
# (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'url_login'),
|
||||||
url(r'^testemail/$', 'test_email'),
|
url(r'^testemail/$', 'test_email'),
|
||||||
url(r'^create/$', 'create_account', name='create_account'),
|
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 django.template import RequestContext
|
||||||
|
|
||||||
from ietf.group.models import Group, GroupMilestone, ChangeStateGroupEvent, GroupEvent, GroupURL, Role
|
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.ietfauth.utils import role_required
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.secr.groups.forms import GroupModelForm, GroupMilestoneForm, RoleForm, SearchForm
|
from ietf.secr.groups.forms import GroupModelForm, GroupMilestoneForm, RoleForm, SearchForm
|
||||||
|
@ -102,6 +102,9 @@ def add(request):
|
||||||
awp.group = group
|
awp.group = group
|
||||||
awp.save()
|
awp.save()
|
||||||
|
|
||||||
|
if group.features.has_documents:
|
||||||
|
setup_default_community_list_for_group(group)
|
||||||
|
|
||||||
# create GroupEvent(s)
|
# create GroupEvent(s)
|
||||||
# always create started event
|
# always create started event
|
||||||
ChangeStateGroupEvent.objects.create(group=group,
|
ChangeStateGroupEvent.objects.create(group=group,
|
||||||
|
|
|
@ -448,3 +448,14 @@ form.navbar-form input.form-control.input-sm { width: 141px; }
|
||||||
padding: inherit;
|
padding: inherit;
|
||||||
outline: 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
|
// search results
|
||||||
$('.community-list-add-remove-doc').click(function(e) {
|
$('.track-untrack-doc').click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var trigger = $(this);
|
var trigger = $(this);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: trigger.attr('href'),
|
url: trigger.attr('href'),
|
||||||
type: 'GET',
|
type: 'POST',
|
||||||
cache: false,
|
cache: false,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(response){
|
success: function(response){
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
trigger.parent().find(".tooltip").remove();
|
trigger.parent().find(".tooltip").remove();
|
||||||
trigger.find("span.fa").toggleClass("fa-bookmark fa-bookmark-o");
|
trigger.addClass("hide");
|
||||||
if (trigger.hasClass('btn')) {
|
trigger.parent().find(".track-untrack-doc").not(trigger).removeClass("hide");
|
||||||
trigger.attr('disabled', true).blur();
|
|
||||||
} else {
|
|
||||||
trigger.contents().unwrap().blur();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
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");
|
||||||
|
});
|
||||||
|
});
|
|
@ -19,6 +19,7 @@ from ietf.person.models import Person
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
|
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
|
||||||
from ietf.submit.models import Submission, Preapproval
|
from ietf.submit.models import Submission, Preapproval
|
||||||
|
from ietf.group.utils import setup_default_community_list_for_group
|
||||||
|
|
||||||
class SubmitTests(TestCase):
|
class SubmitTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -163,7 +164,8 @@ class SubmitTests(TestCase):
|
||||||
def submit_new_wg(self, formats):
|
def submit_new_wg(self, formats):
|
||||||
# submit new -> supply submitter info -> approve
|
# submit new -> supply submitter info -> approve
|
||||||
draft = make_test_data()
|
draft = make_test_data()
|
||||||
|
setup_default_community_list_for_group(draft.group)
|
||||||
|
|
||||||
# prepare draft to suggest replace
|
# prepare draft to suggest replace
|
||||||
sug_replaced_draft = Document.objects.create(
|
sug_replaced_draft = Document.objects.create(
|
||||||
name="draft-ietf-ames-sug-replaced",
|
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.ietfauth.utils import has_role
|
||||||
from ietf.name.models import StreamName
|
from ietf.name.models import StreamName
|
||||||
from ietf.person.models import Person, Email
|
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.mail import announce_to_lists, announce_new_version, announce_to_authors
|
||||||
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
|
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
|
||||||
from ietf.utils import unaccent
|
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"):
|
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
|
# don't overwrite an assigned area if it's still an individual
|
||||||
# submission
|
# submission
|
||||||
draft.group_id = group.pk
|
draft.group = group
|
||||||
draft.rev = submission.rev
|
draft.rev = submission.rev
|
||||||
draft.pages = submission.pages
|
draft.pages = submission.pages
|
||||||
draft.abstract = submission.abstract
|
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)
|
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_to_lists(request, submission)
|
||||||
announce_new_version(request, submission, draft, state_change_msg)
|
announce_new_version(request, submission, draft, state_change_msg)
|
||||||
announce_to_authors(request, submission)
|
announce_to_authors(request, submission)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
|
{# 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" %}
|
{% if flavor != "top" %}
|
||||||
{% include "base/menu_user.html" %}
|
{% include "base/menu_user.html" %}
|
||||||
|
@ -51,14 +51,14 @@
|
||||||
<li><a href="{% url "submit_approvals" %}">Approve a draft</a></li>
|
<li><a href="{% url "submit_approvals" %}">Approve a draft</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% get_user_managed_lists user as community_lists %}
|
{% if user and user.is_authenticated %}
|
||||||
{% if community_lists %}
|
<li><a href="{% url "community_personal_view_list" user.username %}">My tracked docs</a></li>
|
||||||
<li><a href="{{ community_lists.personal.get_manage_url }}">My tracked docs</a></li>
|
|
||||||
{% for cl in community_lists.group %}
|
{% for g in user|managed_groups %}
|
||||||
<li><a href="{{ cl.get_manage_url }}">{{ cl.short_name }} {{cl.group.type.slug}} docs</a></li>
|
<li><a href="{% url "group_docs" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user|has_role:"Area Director,Secretariat" %}
|
{% 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>
|
<tr>
|
||||||
<td>{{ doc.display_name }}</td>
|
<td>{{ doc.display_name }}</td>
|
||||||
<td>{{ doc.get_state }}</td>
|
<td>{{ doc.get_state }}</td>
|
||||||
<td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></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 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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</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 }}.
|
This is a notification from the {{ clist.long_name }}.
|
||||||
|
|
||||||
Document: {{ notification.doc }},
|
Document: {{ event.doc }},
|
||||||
https://datatracker.ietf.org/doc/{{ notification.doc }}
|
https://datatracker.ietf.org/doc/{{ event.doc_id }}/
|
||||||
|
|
||||||
Change:
|
Change:
|
||||||
{{ notification.desc|textify|striptags }}
|
{{ event.desc|textify|striptags }}
|
||||||
|
|
||||||
Best regards,
|
Best regards,
|
||||||
|
|
||||||
The datatracker draft tracking service
|
The Datatracker draft tracking service
|
||||||
(for the IETF Secretariat)
|
(for the IETF Secretariat)
|
||||||
{% endautoescape %}
|
{% 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 #}
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||||
{% load origin %}{% origin %}
|
{% load origin %}
|
||||||
{% load community_tags %}
|
{% load bootstrap3 %}
|
||||||
{% get_clist_view cl %}
|
|
||||||
|
{% 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 %}
|
||||||
|
|
|
@ -501,11 +501,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if tracking_document %}
|
<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 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>
|
<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>
|
||||||
{% 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 %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if can_edit and iesg_state %}
|
{% 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>
|
<label class="control-label" for="id_olddrafts">{{ form.olddrafts }} Internet-Draft (expired, replaced or withdrawn)</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for doc_type in form.doctypes %}
|
{% for value, label in form.fields.doctypes.choices %}
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label class="control-label" for="id_{{doc_type.slug}}">
|
<label class="control-label" for="id_doctypes_{{ value }}">
|
||||||
<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}}
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
|
|
||||||
<div class="form-group search_field">
|
<div class="form-group search_field">
|
||||||
<div class="col-sm-4">
|
<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>
|
<label for="id_author" class="control-label">Author</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
|
|
||||||
<div class="form-group search_field">
|
<div class="form-group search_field">
|
||||||
<div class="col-sm-4">
|
<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>
|
<label for="id_group" class="control-label">WG</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
|
|
||||||
<div class="form-group search_field">
|
<div class="form-group search_field">
|
||||||
<div class="col-sm-4">
|
<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>
|
<label for="id_area" class="control-label">Area</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
|
|
||||||
<div class="form-group search_field">
|
<div class="form-group search_field">
|
||||||
<div class="col-sm-4">
|
<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>
|
<label for="id_ad" class="control-label">AD</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
|
|
||||||
<div class="form-group search_field">
|
<div class="form-group search_field">
|
||||||
<div class="col-sm-4">
|
<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>
|
<label for="id_state" class="control-label">IESG State</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
|
|
||||||
<div class="form-group search_field">
|
<div class="form-group search_field">
|
||||||
<div class="col-sm-4">
|
<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>
|
<label for="id_stream" class="control-label">Stream</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
|
|
|
@ -13,15 +13,12 @@
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if doc.name in doc_is_tracked %}
|
<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">
|
||||||
<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>
|
||||||
<span class="fa fa-bookmark"></span>
|
</a>
|
||||||
</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">
|
||||||
{% else %}
|
<span class="fa fa-bookmark-o"></span>
|
||||||
<a href="{% url "community_add_track_document" doc.name %}" class="community-list-add-remove-doc" title="Add to your personal ID list">
|
</a>
|
||||||
<span class="fa fa-bookmark-o"></span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
@ -8,5 +8,6 @@
|
||||||
{% origin %}
|
{% origin %}
|
||||||
{% include "doc/search/search_results.html" %}
|
{% 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 "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 %}
|
{% endblock group_content %}
|
||||||
|
|
Loading…
Reference in a new issue