Integrate community lists for groups with the existing group documents
page. Each WG/RG now gets a list with an initial set of rules to populate the list. Refine the community list management interface a bit to support the group lists better - group lists aren't connected to the usual track icons so need to be able to add/remove individual drafts. Change the "name contains" rule to support regular expressions to enable each group to have a default replacement for the previously implemented "related documents" search. Maintain a materialized view of the regexp-matched drafts with a call in the submit code to avoid having to scan all drafts/~1000 group rules all the time. - Legacy-Id: 10963
This commit is contained in:
parent
cdcad43fc0
commit
c7589f9b6a
|
@ -87,6 +87,9 @@ class SearchRuleForm(forms.ModelForm):
|
|||
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):
|
||||
|
|
|
@ -88,7 +88,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='searchrule',
|
||||
name='text',
|
||||
field=models.CharField(default=b'', max_length=255, blank=True),
|
||||
field=models.CharField(default=b'', max_length=255, verbose_name=b'Text/RegExp', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
|
@ -98,7 +98,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='searchrule',
|
||||
name='rule_type',
|
||||
field=models.CharField(max_length=30, choices=[(b'group', b'All I-Ds associated with a particular group'), (b'area', b'All I-Ds associated with all groups in a particular Area'), (b'group_rfc', b'All RFCs associated with a particular group'), (b'area_rfc', b'All RFCs associated with all groups in a particular Area'), (b'state_iab', b'All I-Ds that are in a particular IAB state'), (b'state_iana', b'All I-Ds that are in a particular IANA state'), (b'state_iesg', b'All I-Ds that are in a particular IESG state'), (b'state_irtf', b'All I-Ds that are in a particular IRTF state'), (b'state_ise', b'All I-Ds that are in a particular ISE state'), (b'state_rfceditor', b'All I-Ds that are in a particular RFC Editor state'), (b'state_ietf', b'All I-Ds that are in a particular Working Group state'), (b'author', b'All I-Ds with a particular author'), (b'author_rfc', b'All RFCs with a particular author'), (b'ad', b'All I-Ds with a particular responsible AD'), (b'shepherd', b'All I-Ds with a particular document shepherd'), (b'name_contains', b'All I-Ds with particular text in the name')]),
|
||||
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(
|
||||
|
@ -111,4 +111,10 @@ class Migration(migrations.Migration):
|
|||
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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -194,6 +194,31 @@ def fill_in_notify_on(apps, schema_editor):
|
|||
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
|
||||
|
||||
|
@ -209,6 +234,7 @@ class Migration(migrations.Migration):
|
|||
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',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import signals
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.doc.models import Document, DocEvent, State
|
||||
from ietf.group.models import Group
|
||||
|
@ -22,6 +23,13 @@ class CommunityList(models.Model):
|
|||
def __unicode__(self):
|
||||
return self.long_name()
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.user:
|
||||
return urlreverse("community_personal_view_list", kwargs={ 'username': self.user.username })
|
||||
elif self.group:
|
||||
return urlreverse("group_docs", kwargs={ 'acronym': self.group.acronym })
|
||||
return ""
|
||||
|
||||
|
||||
class SearchRule(models.Model):
|
||||
# these types define the UI for setting up the rule, and also
|
||||
|
@ -47,7 +55,7 @@ class SearchRule(models.Model):
|
|||
|
||||
('shepherd', 'All I-Ds with a particular document shepherd'),
|
||||
|
||||
('name_contains', 'All I-Ds with particular text in the name'),
|
||||
('name_contains', 'All I-Ds with particular text/regular expression in the name'),
|
||||
]
|
||||
|
||||
community_list = models.ForeignKey(CommunityList)
|
||||
|
@ -57,7 +65,13 @@ class SearchRule(models.Model):
|
|||
state = models.ForeignKey(State, blank=True, null=True)
|
||||
group = models.ForeignKey(Group, blank=True, null=True)
|
||||
person = models.ForeignKey(Person, blank=True, null=True)
|
||||
text = models.CharField(max_length=255, blank=True, default="")
|
||||
text = models.CharField(verbose_name="Text/RegExp", max_length=255, blank=True, default="")
|
||||
|
||||
# store a materialized view/index over which documents are matched
|
||||
# by the name_contains rule to avoid having to scan the whole
|
||||
# database - we update this manually when the rule is changed and
|
||||
# when new documents are submitted
|
||||
name_contains_index = models.ManyToManyField(Document)
|
||||
|
||||
|
||||
class EmailSubscription(models.Model):
|
||||
|
|
|
@ -7,6 +7,8 @@ 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
|
||||
|
@ -34,7 +36,8 @@ class CommunityListTests(TestCase):
|
|||
|
||||
rule_shepherd = SearchRule.objects.create(rule_type="shepherd", state=State.objects.get(type="draft", slug="active"), person=draft.shepherd.person, community_list=clist)
|
||||
|
||||
rule_name_contains = SearchRule.objects.create(rule_type="name_contains", state=State.objects.get(type="draft", slug="active"), text="-".join(draft.name.split("-")[2:]), community_list=clist)
|
||||
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))
|
||||
|
@ -79,7 +82,7 @@ class CommunityListTests(TestCase):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(draft.name in r.content)
|
||||
|
||||
def test_manage_list(self):
|
||||
def test_manage_personal_list(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_personal_manage_list", kwargs={ "username": "plain" })
|
||||
|
@ -94,6 +97,17 @@ class CommunityListTests(TestCase):
|
|||
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",
|
||||
|
@ -105,6 +119,17 @@ class CommunityListTests(TestCase):
|
|||
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)
|
||||
|
@ -121,7 +146,43 @@ class CommunityListTests(TestCase):
|
|||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc"))
|
||||
|
||||
def test_track_untrack_document_for_personal_list_through_ajax(self):
|
||||
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 })
|
||||
|
@ -142,31 +203,6 @@ class CommunityListTests(TestCase):
|
|||
clist = CommunityList.objects.get(user__username="plain")
|
||||
self.assertEqual(list(clist.added_docs.all()), [])
|
||||
|
||||
def test_track_untrack_document_for_group_list(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse("community_group_track_document", kwargs={ "acronym": draft.group.acronym, "name": draft.name })
|
||||
login_testing_unauthorized(self, "marschairman", 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(group__acronym=draft.group.acronym)
|
||||
self.assertEqual(list(clist.added_docs.all()), [draft])
|
||||
|
||||
# untrack
|
||||
url = urlreverse("community_group_untrack_document", kwargs={ "acronym": draft.group.acronym, "name": draft.name })
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
clist = CommunityList.objects.get(group__acronym=draft.group.acronym)
|
||||
self.assertEqual(list(clist.added_docs.all()), [])
|
||||
|
||||
def test_csv(self):
|
||||
draft = make_test_data()
|
||||
|
||||
|
@ -190,6 +226,17 @@ class CommunityListTests(TestCase):
|
|||
# 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()
|
||||
|
||||
|
@ -217,6 +264,17 @@ class CommunityListTests(TestCase):
|
|||
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()
|
||||
|
||||
|
@ -254,6 +312,19 @@ class CommunityListTests(TestCase):
|
|||
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()
|
||||
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns('ietf.community.views',
|
||||
url(r'^personal/(?P<username>[^/]+)/$', 'view_list', name='community_personal_view_list'),
|
||||
url(r'^personal/(?P<username>[^/]+)/manage/$', 'manage_list', name='community_personal_manage_list'),
|
||||
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_personal_track_document'),
|
||||
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_personal_untrack_document'),
|
||||
url(r'^personal/(?P<username>[^/]+)/csv/$', 'export_to_csv', name='community_personal_csv'),
|
||||
url(r'^personal/(?P<username>[^/]+)/feed/$', 'feed', name='community_personal_feed'),
|
||||
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'subscription', name='community_personal_subscription'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^personal/(?P<username>[^/]+)/$', 'ietf.community.views.view_list', name='community_personal_view_list'),
|
||||
url(r'^personal/(?P<username>[^/]+)/manage/$', 'ietf.community.views.manage_list', name='community_personal_manage_list'),
|
||||
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.track_document', name='community_personal_track_document'),
|
||||
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.untrack_document', name='community_personal_untrack_document'),
|
||||
url(r'^personal/(?P<username>[^/]+)/csv/$', 'ietf.community.views.export_to_csv', name='community_personal_csv'),
|
||||
url(r'^personal/(?P<username>[^/]+)/feed/$', 'ietf.community.views.feed', name='community_personal_feed'),
|
||||
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'ietf.community.views.subscription', name='community_personal_subscription'),
|
||||
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/$', 'view_list', name='community_group_view_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/manage/$', 'manage_list', name='community_group_manage_list'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_group_track_document'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_group_untrack_document'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'export_to_csv', name='community_group_csv'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/feed/$', 'feed', name='community_group_feed'),
|
||||
url(r'^group/(?P<acronym>[\w.@+-]+)/subscription/$', 'subscription', name='community_group_subscription'),
|
||||
)
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
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
|
||||
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
|
||||
|
||||
|
@ -18,6 +23,18 @@ def states_of_significant_change():
|
|||
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
|
||||
|
@ -25,6 +42,9 @@ def can_manage_community_list(user, clist):
|
|||
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'):
|
||||
|
@ -46,6 +66,21 @@ def augment_docs_with_tracking_info(docs, user):
|
|||
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']:
|
||||
|
@ -59,13 +94,11 @@ def docs_matching_community_list_rule(rule):
|
|||
elif rule.rule_type == "shepherd":
|
||||
return docs.filter(states=rule.state, shepherd__person=rule.person)
|
||||
elif rule.rule_type == "name_contains":
|
||||
return docs.filter(states=rule.state, name__icontains=rule.text)
|
||||
return docs.filter(states=rule.state, searchrule=rule)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def community_list_rules_matching_doc(doc):
|
||||
from django.db import connection
|
||||
|
||||
states = list(doc.states.values_list("pk", flat=True))
|
||||
|
||||
rules = SearchRule.objects.none()
|
||||
|
@ -108,10 +141,7 @@ def community_list_rules_matching_doc(doc):
|
|||
rules |= SearchRule.objects.filter(
|
||||
rule_type="name_contains",
|
||||
state__in=states,
|
||||
).extra(
|
||||
# we need a reverse icontains here, unfortunately this means we need concatenation which isn't quite cross-platform
|
||||
where=["%s like '%%' || text || '%%'" if connection.vendor == "sqlite" else "%s like concat('%%', text, '%%')"],
|
||||
params=[doc.name]
|
||||
name_contains_index=doc, # search our materialized index to avoid full scan
|
||||
)
|
||||
|
||||
return rules
|
||||
|
|
|
@ -4,35 +4,20 @@ import datetime
|
|||
import json
|
||||
|
||||
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
|
||||
from ietf.community.models import SearchRule, EmailSubscription
|
||||
from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
|
||||
from ietf.community.utils import can_manage_community_list
|
||||
from ietf.community.utils import lookup_community_list, can_manage_community_list
|
||||
from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule
|
||||
from ietf.community.utils import states_of_significant_change
|
||||
from ietf.group.models import Group
|
||||
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 lookup_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 view_list(request, username=None, acronym=None):
|
||||
clist = lookup_list(username, acronym)
|
||||
def view_list(request, username=None):
|
||||
clist = lookup_community_list(username)
|
||||
|
||||
docs = docs_tracked_by_community_list(clist)
|
||||
docs, meta = prepare_document_table(request, docs, request.GET)
|
||||
|
@ -48,10 +33,10 @@ def view_list(request, username=None, acronym=None):
|
|||
})
|
||||
|
||||
@login_required
|
||||
def manage_list(request, username=None, acronym=None):
|
||||
def manage_list(request, username=None, acronym=None, group_type=None):
|
||||
# we need to be a bit careful because clist may not exist in the
|
||||
# database so we can't call related stuff on it yet
|
||||
clist = lookup_list(username, acronym)
|
||||
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")
|
||||
|
@ -71,6 +56,14 @@ def manage_list(request, username=None, acronym=None):
|
|||
else:
|
||||
add_doc_form = AddDocumentsForm()
|
||||
|
||||
if request.method == 'POST' and action == 'remove_document':
|
||||
document_pk = request.POST.get('document')
|
||||
if clist.pk is not None and document_pk:
|
||||
document = get_object_or_404(clist.added_docs, pk=document_pk)
|
||||
clist.added_docs.remove(document)
|
||||
|
||||
return HttpResponseRedirect("")
|
||||
|
||||
if request.method == 'POST' and action == 'add_rule':
|
||||
rule_type_form = SearchRuleTypeForm(request.POST)
|
||||
if rule_type_form.is_valid():
|
||||
|
@ -86,6 +79,8 @@ def manage_list(request, username=None, acronym=None):
|
|||
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:
|
||||
|
@ -111,7 +106,7 @@ def manage_list(request, username=None, acronym=None):
|
|||
return render(request, 'community/manage_list.html', {
|
||||
'clist': clist,
|
||||
'rules': rules,
|
||||
'individually_added': clist.added_docs.count() if clist.pk is not None else 0,
|
||||
'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,
|
||||
|
@ -125,7 +120,7 @@ def track_document(request, name, username=None, acronym=None):
|
|||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
|
||||
if request.method == "POST":
|
||||
clist = lookup_list(username, acronym)
|
||||
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")
|
||||
|
||||
|
@ -137,10 +132,7 @@ def track_document(request, name, username=None, acronym=None):
|
|||
if request.is_ajax():
|
||||
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
|
||||
else:
|
||||
if clist.group:
|
||||
return redirect('community_group_view_list', acronym=clist.group.acronym)
|
||||
else:
|
||||
return redirect('community_personal_view_list', username=clist.user.username)
|
||||
return HttpResponseRedirect(clist.get_absolute_url())
|
||||
|
||||
return render(request, "community/track_document.html", {
|
||||
"name": doc.name,
|
||||
|
@ -149,7 +141,7 @@ def track_document(request, name, username=None, acronym=None):
|
|||
@login_required
|
||||
def untrack_document(request, name, username=None, acronym=None):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
clist = lookup_list(username, acronym)
|
||||
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")
|
||||
|
||||
|
@ -160,18 +152,15 @@ def untrack_document(request, name, username=None, acronym=None):
|
|||
if request.is_ajax():
|
||||
return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
|
||||
else:
|
||||
if clist.group:
|
||||
return redirect('community_group_view_list', acronym=clist.group.acronym)
|
||||
else:
|
||||
return redirect('community_personal_view_list', username=clist.user.username)
|
||||
return HttpResponseRedirect(clist.get_absolute_url())
|
||||
|
||||
return render(request, "community/untrack_document.html", {
|
||||
"name": doc.name,
|
||||
})
|
||||
|
||||
|
||||
def export_to_csv(request, username=None, acronym=None):
|
||||
clist = lookup_list(username, acronym)
|
||||
def export_to_csv(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
|
||||
|
@ -211,8 +200,8 @@ def export_to_csv(request, username=None, acronym=None):
|
|||
|
||||
return response
|
||||
|
||||
def feed(request, username=None, acronym=None):
|
||||
clist = lookup_list(username, acronym)
|
||||
def feed(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
|
||||
significant = request.GET.get('significant', '') == '1'
|
||||
|
||||
|
@ -247,8 +236,8 @@ def feed(request, username=None, acronym=None):
|
|||
|
||||
|
||||
@login_required
|
||||
def subscription(request, username=None, acronym=None):
|
||||
clist = lookup_list(username, acronym)
|
||||
def subscription(request, username=None, acronym=None, group_type=None):
|
||||
clist = lookup_community_list(username, acronym)
|
||||
if clist.pk is None:
|
||||
raise Http404
|
||||
|
||||
|
|
|
@ -10,11 +10,11 @@ def managed_groups(user):
|
|||
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='ad',
|
||||
# role__person__user=user,
|
||||
# type__slug='area',
|
||||
# state__slug='active').select_related("type"))
|
||||
|
||||
groups.extend(Group.objects.filter(
|
||||
role__name__slug='chair',
|
||||
|
|
|
@ -17,7 +17,7 @@ from ietf.doc.utils_charter import charter_name_for_group
|
|||
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
|
||||
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
|
||||
from ietf.group.utils import save_group_in_history, can_manage_group_type
|
||||
from ietf.group.utils import get_group_or_404
|
||||
from ietf.group.utils import get_group_or_404, setup_default_community_list_for_group
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.person.fields import SearchableEmailsField
|
||||
from ietf.person.models import Person, Email
|
||||
|
@ -231,6 +231,9 @@ def edit(request, group_type=None, acronym=None, action="edit"):
|
|||
state=clean["state"]
|
||||
)
|
||||
|
||||
if group.features.has_documents:
|
||||
setup_default_community_list_for_group(group)
|
||||
|
||||
e = ChangeStateGroupEvent(group=group, type="changed_state")
|
||||
e.time = group.time
|
||||
e.by = request.user.person
|
||||
|
|
|
@ -38,7 +38,7 @@ import re
|
|||
from tempfile import mkstemp
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.template.loader import render_to_string
|
||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
|
@ -55,6 +55,8 @@ from ietf.group.models import Group, Role, ChangeStateGroupEvent
|
|||
from ietf.name.models import GroupTypeName
|
||||
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
|
||||
from ietf.group.utils import can_manage_materials, get_group_or_404
|
||||
from ietf.community.utils import docs_tracked_by_community_list, can_manage_community_list
|
||||
from ietf.community.models import CommunityList, EmailSubscription
|
||||
from ietf.utils.pipe import pipe
|
||||
from ietf.settings import MAILING_LIST_INFO_URL
|
||||
from ietf.mailtrigger.utils import gather_relevant_expansions
|
||||
|
@ -354,6 +356,11 @@ def construct_group_menu_context(request, group, selected, group_type, others):
|
|||
if group.state_id != "proposed" and (is_chair or can_manage):
|
||||
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
|
||||
|
||||
if group.features.has_documents:
|
||||
clist = CommunityList.objects.filter(group=group).first()
|
||||
if clist and can_manage_community_list(request.user, clist):
|
||||
actions.append((u'Manage document list', urlreverse('community_group_manage_list', kwargs=kwargs)))
|
||||
|
||||
if group.features.has_materials and can_manage_materials(request.user, group):
|
||||
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
|
||||
|
||||
|
@ -378,36 +385,23 @@ def construct_group_menu_context(request, group, selected, group_type, others):
|
|||
|
||||
return d
|
||||
|
||||
def search_for_group_documents(group, request):
|
||||
qs = Document.objects.filter(states__type="draft", states__slug__in=["active", "rfc"], group=group)
|
||||
docs, meta = prepare_document_table(request, qs)
|
||||
|
||||
# get the related docs
|
||||
qs_related = Document.objects.filter(states__type="draft", states__slug="active", name__contains="-%s-" % group.acronym)
|
||||
raw_docs_related, meta_related = prepare_document_table(request, qs_related)
|
||||
def prepare_group_documents(request, group, clist):
|
||||
found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET)
|
||||
|
||||
docs = []
|
||||
docs_related = []
|
||||
for d in raw_docs_related:
|
||||
parts = d.name.split("-", 2);
|
||||
# canonical form draft-<name|ietf|irtf>-wg-etc
|
||||
if len(parts) >= 3 and parts[1] not in ("ietf", "irtf") and parts[2].startswith(group.acronym + "-") and d not in docs:
|
||||
|
||||
# split results
|
||||
for d in found_docs:
|
||||
# non-WG drafts and call for WG adoption are considered related
|
||||
if (d.group != group
|
||||
or (d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"))):
|
||||
d.search_heading = "Related Internet-Draft"
|
||||
docs_related.append(d)
|
||||
|
||||
# move call for WG adoption to related
|
||||
cleaned_docs = []
|
||||
docs_related_names = set(d.name for d in docs_related)
|
||||
for d in docs:
|
||||
if d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"):
|
||||
if d.name not in docs_related_names:
|
||||
d.search_heading = "Related Internet-Draft"
|
||||
docs_related.append(d)
|
||||
else:
|
||||
cleaned_docs.append(d)
|
||||
docs.append(d)
|
||||
|
||||
docs = cleaned_docs
|
||||
|
||||
docs_related.sort(key=lambda d: d.name)
|
||||
meta_related = meta.copy()
|
||||
|
||||
return docs, meta, docs_related, meta_related
|
||||
|
||||
|
@ -423,13 +417,19 @@ def group_documents(request, acronym, group_type=None):
|
|||
if not group.features.has_documents:
|
||||
raise Http404
|
||||
|
||||
docs, meta, docs_related, meta_related = search_for_group_documents(group, request)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
|
||||
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
|
||||
|
||||
subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
|
||||
|
||||
context = construct_group_menu_context(request, group, "documents", group_type, {
|
||||
'docs': docs,
|
||||
'meta': meta,
|
||||
'docs_related': docs_related,
|
||||
'meta_related': meta_related,
|
||||
'subscribed': subscribed,
|
||||
'clist': clist,
|
||||
})
|
||||
|
||||
return render(request, 'group/group_documents.html', context)
|
||||
|
@ -440,7 +440,9 @@ def group_documents_txt(request, acronym, group_type=None):
|
|||
if not group.features.has_documents:
|
||||
raise Http404
|
||||
|
||||
docs, meta, docs_related, meta_related = search_for_group_documents(group, request)
|
||||
clist = get_object_or_404(CommunityList, group=group)
|
||||
|
||||
docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
|
||||
|
||||
for d in docs:
|
||||
d.prefix = d.get_state().name
|
||||
|
|
|
@ -15,7 +15,7 @@ from django.core.urlresolvers import NoReverseMatch
|
|||
|
||||
from ietf.doc.models import Document, DocAlias, DocEvent, State
|
||||
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
|
||||
from ietf.group.utils import save_group_in_history
|
||||
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
|
||||
from ietf.name.models import DocTagName, GroupStateName, GroupTypeName
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.utils.test_utils import TestCase, unicontent
|
||||
|
@ -184,13 +184,14 @@ class GroupPagesTests(TestCase):
|
|||
name=draft2.name,
|
||||
)
|
||||
|
||||
setup_default_community_list_for_group(group)
|
||||
|
||||
url = urlreverse('ietf.group.info.group_documents', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(draft.name in unicontent(r))
|
||||
self.assertTrue(group.name in unicontent(r))
|
||||
self.assertTrue(group.acronym in unicontent(r))
|
||||
|
||||
self.assertTrue(draft2.name in unicontent(r))
|
||||
|
||||
# Make sure that a logged in user is presented with an opportunity to add results to their community list
|
||||
|
|
|
@ -5,6 +5,10 @@ urlpatterns = patterns('',
|
|||
(r'^$', 'ietf.group.info.group_home', None, "group_home"),
|
||||
(r'^documents/txt/$', 'ietf.group.info.group_documents_txt'),
|
||||
(r'^documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
|
||||
(r'^documents/manage/$', 'ietf.community.views.manage_list', None, "community_group_manage_list"),
|
||||
(r'^documents/csv/$', 'ietf.community.views.export_to_csv', None, 'community_group_csv'),
|
||||
(r'^documents/feed/$', 'ietf.community.views.feed', None, 'community_group_feed'),
|
||||
(r'^documents/subscription/$', 'ietf.community.views.subscription', None, 'community_group_subscription'),
|
||||
(r'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
|
||||
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
|
||||
(r'^history/$','ietf.group.info.history'),
|
||||
|
|
|
@ -8,6 +8,9 @@ from ietf.group.models import Group, RoleHistory
|
|||
from ietf.person.models import Email
|
||||
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.community.models import CommunityList, SearchRule
|
||||
from ietf.community.utils import reset_name_contains_index_for_rule
|
||||
from ietf.doc.models import State
|
||||
|
||||
|
||||
def save_group_in_history(group):
|
||||
|
@ -107,3 +110,25 @@ def get_group_or_404(acronym, group_type):
|
|||
possible_groups = possible_groups.filter(type=group_type)
|
||||
|
||||
return get_object_or_404(possible_groups, acronym=acronym)
|
||||
|
||||
def setup_default_community_list_for_group(group):
|
||||
clist = CommunityList.objects.create(group=group)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="group",
|
||||
group=group,
|
||||
state=State.objects.get(slug="active", type="draft"),
|
||||
)
|
||||
SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="group_rfc",
|
||||
group=group,
|
||||
state=State.objects.get(slug="rfc", type="draft"),
|
||||
)
|
||||
related_docs_rule = SearchRule.objects.create(
|
||||
community_list=clist,
|
||||
rule_type="name_contains",
|
||||
text=r"^draft-[^-]+-%s-" % group.acronym,
|
||||
state=State.objects.get(slug="active", type="draft"),
|
||||
)
|
||||
reset_name_contains_index_for_rule(related_docs_rule)
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.shortcuts import render_to_response, get_object_or_404, redirect
|
|||
from django.template import RequestContext
|
||||
|
||||
from ietf.group.models import Group, GroupMilestone, ChangeStateGroupEvent, GroupEvent, GroupURL, Role
|
||||
from ietf.group.utils import save_group_in_history, get_charter_text
|
||||
from ietf.group.utils import save_group_in_history, get_charter_text, setup_default_community_list_for_group
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.person.models import Person
|
||||
from ietf.secr.groups.forms import GroupModelForm, GroupMilestoneForm, RoleForm, SearchForm
|
||||
|
@ -102,6 +102,9 @@ def add(request):
|
|||
awp.group = group
|
||||
awp.save()
|
||||
|
||||
if group.features.has_documents:
|
||||
setup_default_community_list_for_group(group)
|
||||
|
||||
# create GroupEvent(s)
|
||||
# always create started event
|
||||
ChangeStateGroupEvent.objects.create(group=group,
|
||||
|
|
|
@ -21,6 +21,7 @@ from ietf.person.models import Person
|
|||
from ietf.group.models import Group
|
||||
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
|
||||
from ietf.submit.models import Submission, Preapproval
|
||||
from ietf.group.utils import setup_default_community_list_for_group
|
||||
|
||||
class SubmitTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -135,7 +136,8 @@ class SubmitTests(TestCase):
|
|||
def submit_new_wg(self, formats):
|
||||
# submit new -> supply submitter info -> approve
|
||||
draft = make_test_data()
|
||||
|
||||
setup_default_community_list_for_group(draft.group)
|
||||
|
||||
# prepare draft to suggest replace
|
||||
sug_replaced_draft = Document.objects.create(
|
||||
name="draft-ietf-ames-sug-replaced",
|
||||
|
|
|
@ -14,6 +14,7 @@ from ietf.group.models import Group
|
|||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.name.models import StreamName
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.community.utils import update_name_contains_indexes_with_new_doc
|
||||
from ietf.submit.mail import announce_to_lists, announce_new_version, announce_to_authors
|
||||
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
|
||||
from ietf.utils import unaccent
|
||||
|
@ -223,6 +224,8 @@ def post_submission(request, submission):
|
|||
|
||||
new_replaces, new_possibly_replaces = update_replaces_from_submission(request, submission, draft)
|
||||
|
||||
update_name_contains_indexes_with_new_doc(draft)
|
||||
|
||||
announce_to_lists(request, submission)
|
||||
announce_new_version(request, submission, draft, state_change_msg)
|
||||
announce_to_authors(request, submission)
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<li><a href="{% url "community_personal_view_list" user.username %}">My tracked docs</a></li>
|
||||
|
||||
{% for g in user|managed_groups %}
|
||||
<li><a href="{% url "community_group_view_list" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
|
||||
<li><a href="{% url "group_docs" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li><a rel="nofollow" href="/accounts/login/?next={{ request.get_full_path|urlencode }}">Sign in to track docs</a></li>
|
||||
|
|
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>
|
|
@ -19,18 +19,38 @@
|
|||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<p>The list currently tracks <a href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">{{ total_count }} document{{ total_count|pluralize }}</a>.</p>
|
||||
<p>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="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">Back to list</a></p>
|
||||
<p><a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a></p>
|
||||
|
||||
<h2>Individual documents</h2>
|
||||
|
||||
<p>The list tracks {{ individually_added }} individually added document{{ individually_added|pluralize }}.</p>
|
||||
|
||||
{% if clist.group %}
|
||||
<p>You can add individual documents here:</p>
|
||||
{% 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>You can conveniently track individual documents in your personal list with the track icon <span class="fa fa-bookmark-o"></span> in <a href="/doc/search/">search results</a>.</p>
|
||||
<p>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 %}
|
||||
|
|
|
@ -30,9 +30,10 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">Back to list</a></p>
|
||||
<p><a class="btn btn-default" href="{{ clist.get_absolute_url }}">Back to list</a></p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<h2>Add new subscription</h2>
|
||||
|
||||
|
@ -44,12 +45,14 @@
|
|||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{% if clist.group %}{% url "community_group_view_list" clist.group.acronym %}{% else %}{% url "community_personal_view_list" clist.user.username %}{% endif%}">Back to list</a>
|
||||
<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 %}
|
||||
|
|
|
@ -11,30 +11,13 @@
|
|||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<ul class="list-inline">
|
||||
{% if can_manage_list %}
|
||||
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_manage_list" clist.group.acronym %}{% else %}{% url "community_personal_manage_list" clist.user.username %}{% endif%}">Manage list</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% if clist.pk != None %}
|
||||
{% if subscribed %}
|
||||
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_subscribe" clist.group.acronym %}{% else %}{% url "community_personal_subscription" clist.user.username %}{% endif%}">Change subscription</a></li>
|
||||
{% else %}
|
||||
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_subscribe" clist.group.acronym %}{% else %}{% url "community_personal_subscription" clist.user.username %}{% endif%}">Subscribe to changes</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<li><a class="btn btn-default" href="{% if clist.group %}{% url "community_group_csv" clist.group.acronym %}{% else %}{% url "community_personal_csv" clist.user.username %}{% endif%}">Export as CSV</a></li>
|
||||
|
||||
<li>
|
||||
<label id="list-feeds">Atom feed of document changes:</label>
|
||||
<div class="btn-group" role="group" aria-labelledby="list-feeds">
|
||||
<a class="btn btn-default" title="Feed of all changes" href="{% if clist.group %}{% url "community_group_feed" clist.group.acronym %}{% else %}{% url "community_personal_feed" clist.user.username %}{% endif%}">All</a>
|
||||
<a class="btn btn-default" title="Feed of only significant state changes" href="{% if clist.group %}{% url "community_group_feed" clist.group.acronym %}{% else %}{% url "community_personal_feed" clist.user.username %}{% endif%}?significant=1">Significant</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
{% 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 %}
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
{% origin %}
|
||||
{% include "doc/search/search_results.html" %}
|
||||
{% include "doc/search/search_results.html" with docs=docs_related meta=meta_related skip_no_matches_warning=True %}
|
||||
{% include "community/list_menu.html" %}
|
||||
|
||||
{% endblock group_content %}
|
||||
|
|
Loading…
Reference in a new issue