Skip to content

Commit c7589f9

Browse files
committed
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
1 parent cdcad43 commit c7589f9

23 files changed

Lines changed: 363 additions & 159 deletions

ietf/community/forms.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ def restrict_state(state_type, slug=None):
8787
for name, f in self.fields.iteritems():
8888
f.required = True
8989

90+
def clean_text(self):
91+
return self.cleaned_data["text"].strip().lower() # names are always lower case
92+
9093

9194
class SubscriptionForm(forms.ModelForm):
9295
def __init__(self, user, clist, *args, **kwargs):

ietf/community/migrations/0003_cleanup.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class Migration(migrations.Migration):
8888
migrations.AddField(
8989
model_name='searchrule',
9090
name='text',
91-
field=models.CharField(default=b'', max_length=255, blank=True),
91+
field=models.CharField(default=b'', max_length=255, verbose_name=b'Text/RegExp', blank=True),
9292
preserve_default=True,
9393
),
9494
migrations.RemoveField(
@@ -98,7 +98,7 @@ class Migration(migrations.Migration):
9898
migrations.AlterField(
9999
model_name='searchrule',
100100
name='rule_type',
101-
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')]),
101+
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')]),
102102
preserve_default=True,
103103
),
104104
migrations.AlterUniqueTogether(
@@ -111,4 +111,10 @@ class Migration(migrations.Migration):
111111
field=models.CharField(default=b'all', max_length=30, choices=[(b'all', b'All changes'), (b'significant', b'Only significant state changes')]),
112112
preserve_default=True,
113113
),
114+
migrations.AddField(
115+
model_name='searchrule',
116+
name='name_contains_index',
117+
field=models.ManyToManyField(to='doc.Document'),
118+
preserve_default=True,
119+
),
114120
]

ietf/community/migrations/0004_cleanup_data.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,31 @@ def fill_in_notify_on(apps, schema_editor):
194194
EmailSubscription.objects.filter(significant=False, notify_on="all")
195195
EmailSubscription.objects.filter(significant=True, notify_on="significant")
196196

197+
def add_group_community_lists(apps, schema_editor):
198+
Group = apps.get_model("group", "Group")
199+
Document = apps.get_model("doc", "Document")
200+
State = apps.get_model("doc", "State")
201+
CommunityList = apps.get_model("community", "CommunityList")
202+
SearchRule = apps.get_model("community", "SearchRule")
203+
204+
active_state = State.objects.get(slug="active", type="draft")
205+
rfc_state = State.objects.get(slug="rfc", type="draft")
206+
207+
for g in Group.objects.filter(type__in=("rg", "wg")):
208+
clist = CommunityList.objects.filter(group=g).first()
209+
if clist:
210+
SearchRule.objects.get_or_create(community_list=clist, rule_type="group", group=g, state=active_state)
211+
SearchRule.objects.get_or_create(community_list=clist, rule_type="group_rfc", group=g, state=rfc_state)
212+
r, _ = SearchRule.objects.get_or_create(community_list=clist, rule_type="name_contains", text=r"^draft-[^-]+-%s-" % g.acronym, state=active_state)
213+
r.name_contains_index = Document.objects.filter(docalias__name__regex=r.text)
214+
215+
else:
216+
clist = CommunityList.objects.create(group=g)
217+
SearchRule.objects.create(community_list=clist, rule_type="group", group=g, state=active_state)
218+
SearchRule.objects.create(community_list=clist, rule_type="group_rfc", group=g, state=rfc_state)
219+
r = SearchRule.objects.create(community_list=clist, rule_type="name_contains", text=r"^draft-[^-]+-%s-" % g.acronym, state=active_state)
220+
r.name_contains_index = Document.objects.filter(docalias__name__regex=r.text)
221+
197222
def noop(apps, schema_editor):
198223
pass
199224

@@ -209,6 +234,7 @@ class Migration(migrations.Migration):
209234
migrations.RunPython(move_email_subscriptions_to_preregistered_email, noop),
210235
migrations.RunPython(get_rid_of_empty_lists, noop),
211236
migrations.RunPython(fill_in_notify_on, noop),
237+
migrations.RunPython(add_group_community_lists, noop),
212238
migrations.RemoveField(
213239
model_name='searchrule',
214240
name='value',

ietf/community/models.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib.auth.models import User
22
from django.db import models
33
from django.db.models import signals
4+
from django.core.urlresolvers import reverse as urlreverse
45

56
from ietf.doc.models import Document, DocEvent, State
67
from ietf.group.models import Group
@@ -22,6 +23,13 @@ def long_name(self):
2223
def __unicode__(self):
2324
return self.long_name()
2425

26+
def get_absolute_url(self):
27+
if self.user:
28+
return urlreverse("community_personal_view_list", kwargs={ 'username': self.user.username })
29+
elif self.group:
30+
return urlreverse("group_docs", kwargs={ 'acronym': self.group.acronym })
31+
return ""
32+
2533

2634
class SearchRule(models.Model):
2735
# these types define the UI for setting up the rule, and also
@@ -47,7 +55,7 @@ class SearchRule(models.Model):
4755

4856
('shepherd', 'All I-Ds with a particular document shepherd'),
4957

50-
('name_contains', 'All I-Ds with particular text in the name'),
58+
('name_contains', 'All I-Ds with particular text/regular expression in the name'),
5159
]
5260

5361
community_list = models.ForeignKey(CommunityList)
@@ -57,7 +65,13 @@ class SearchRule(models.Model):
5765
state = models.ForeignKey(State, blank=True, null=True)
5866
group = models.ForeignKey(Group, blank=True, null=True)
5967
person = models.ForeignKey(Person, blank=True, null=True)
60-
text = models.CharField(max_length=255, blank=True, default="")
68+
text = models.CharField(verbose_name="Text/RegExp", max_length=255, blank=True, default="")
69+
70+
# store a materialized view/index over which documents are matched
71+
# by the name_contains rule to avoid having to scan the whole
72+
# database - we update this manually when the rule is changed and
73+
# when new documents are submitted
74+
name_contains_index = models.ManyToManyField(Document)
6175

6276

6377
class EmailSubscription(models.Model):

ietf/community/tests.py

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
99
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
10+
from ietf.community.utils import reset_name_contains_index_for_rule
11+
from ietf.group.utils import setup_default_community_list_for_group
1012
from ietf.doc.models import State
1113
from ietf.doc.utils import add_state_change_event
1214
from ietf.person.models import Person, Email
@@ -34,7 +36,8 @@ def test_rule_matching(self):
3436

3537
rule_shepherd = SearchRule.objects.create(rule_type="shepherd", state=State.objects.get(type="draft", slug="active"), person=draft.shepherd.person, community_list=clist)
3638

37-
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)
39+
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)
40+
reset_name_contains_index_for_rule(rule_name_contains)
3841

3942
# doc -> rules
4043
matching_rules = list(community_list_rules_matching_doc(draft))
@@ -79,7 +82,7 @@ def test_view_list(self):
7982
self.assertEqual(r.status_code, 200)
8083
self.assertTrue(draft.name in r.content)
8184

82-
def test_manage_list(self):
85+
def test_manage_personal_list(self):
8386
draft = make_test_data()
8487

8588
url = urlreverse("community_personal_manage_list", kwargs={ "username": "plain" })
@@ -94,6 +97,17 @@ def test_manage_list(self):
9497
clist = CommunityList.objects.get(user__username="plain")
9598
self.assertTrue(clist.added_docs.filter(pk=draft.pk))
9699

100+
# document shows up on GET
101+
r = self.client.get(url)
102+
self.assertEqual(r.status_code, 200)
103+
self.assertTrue(draft.name in r.content)
104+
105+
# remove document
106+
r = self.client.post(url, { "action": "remove_document", "document": draft.pk })
107+
self.assertEqual(r.status_code, 302)
108+
clist = CommunityList.objects.get(user__username="plain")
109+
self.assertTrue(not clist.added_docs.filter(pk=draft.pk))
110+
97111
# add rule
98112
r = self.client.post(url, {
99113
"action": "add_rule",
@@ -105,6 +119,17 @@ def test_manage_list(self):
105119
clist = CommunityList.objects.get(user__username="plain")
106120
self.assertTrue(clist.searchrule_set.filter(rule_type="author_rfc"))
107121

122+
# add name_contains rule
123+
r = self.client.post(url, {
124+
"action": "add_rule",
125+
"rule_type": "name_contains",
126+
"name_contains-text": "draft.*mars",
127+
"name_contains-state": State.objects.get(type="draft", slug="active").pk,
128+
})
129+
self.assertEqual(r.status_code, 302)
130+
clist = CommunityList.objects.get(user__username="plain")
131+
self.assertTrue(clist.searchrule_set.filter(rule_type="name_contains"))
132+
108133
# rule shows up on GET
109134
r = self.client.get(url)
110135
self.assertEqual(r.status_code, 200)
@@ -121,50 +146,61 @@ def test_manage_list(self):
121146
clist = CommunityList.objects.get(user__username="plain")
122147
self.assertTrue(not clist.searchrule_set.filter(rule_type="author_rfc"))
123148

124-
def test_track_untrack_document_for_personal_list_through_ajax(self):
149+
def test_manage_group_list(self):
150+
draft = make_test_data()
151+
152+
url = urlreverse("community_group_manage_list", kwargs={ "acronym": draft.group.acronym })
153+
setup_default_community_list_for_group(draft.group)
154+
login_testing_unauthorized(self, "marschairman", url)
155+
156+
# test GET, rest is tested with personal list
157+
r = self.client.get(url)
158+
self.assertEqual(r.status_code, 200)
159+
160+
def test_track_untrack_document(self):
125161
draft = make_test_data()
126162

127163
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
128164
login_testing_unauthorized(self, "plain", url)
129165

130166
# track
131-
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
167+
r = self.client.get(url)
132168
self.assertEqual(r.status_code, 200)
133-
self.assertEqual(json.loads(r.content)["success"], True)
169+
170+
r = self.client.post(url)
171+
self.assertEqual(r.status_code, 302)
134172
clist = CommunityList.objects.get(user__username="plain")
135173
self.assertEqual(list(clist.added_docs.all()), [draft])
136174

137175
# untrack
138176
url = urlreverse("community_personal_untrack_document", kwargs={ "username": "plain", "name": draft.name })
139-
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
177+
r = self.client.get(url)
140178
self.assertEqual(r.status_code, 200)
141-
self.assertEqual(json.loads(r.content)["success"], True)
179+
180+
r = self.client.post(url)
181+
self.assertEqual(r.status_code, 302)
142182
clist = CommunityList.objects.get(user__username="plain")
143183
self.assertEqual(list(clist.added_docs.all()), [])
144184

145-
def test_track_untrack_document_for_group_list(self):
185+
def test_track_untrack_document_through_ajax(self):
146186
draft = make_test_data()
147187

148-
url = urlreverse("community_group_track_document", kwargs={ "acronym": draft.group.acronym, "name": draft.name })
149-
login_testing_unauthorized(self, "marschairman", url)
188+
url = urlreverse("community_personal_track_document", kwargs={ "username": "plain", "name": draft.name })
189+
login_testing_unauthorized(self, "plain", url)
150190

151191
# track
152-
r = self.client.get(url)
192+
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
153193
self.assertEqual(r.status_code, 200)
154-
155-
r = self.client.post(url)
156-
self.assertEqual(r.status_code, 302)
157-
clist = CommunityList.objects.get(group__acronym=draft.group.acronym)
194+
self.assertEqual(json.loads(r.content)["success"], True)
195+
clist = CommunityList.objects.get(user__username="plain")
158196
self.assertEqual(list(clist.added_docs.all()), [draft])
159197

160198
# untrack
161-
url = urlreverse("community_group_untrack_document", kwargs={ "acronym": draft.group.acronym, "name": draft.name })
162-
r = self.client.get(url)
199+
url = urlreverse("community_personal_untrack_document", kwargs={ "username": "plain", "name": draft.name })
200+
r = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
163201
self.assertEqual(r.status_code, 200)
164-
165-
r = self.client.post(url)
166-
self.assertEqual(r.status_code, 302)
167-
clist = CommunityList.objects.get(group__acronym=draft.group.acronym)
202+
self.assertEqual(json.loads(r.content)["success"], True)
203+
clist = CommunityList.objects.get(user__username="plain")
168204
self.assertEqual(list(clist.added_docs.all()), [])
169205

170206
def test_csv(self):
@@ -190,6 +226,17 @@ def test_csv(self):
190226
# this is a simple-minded test, we don't actually check the fields
191227
self.assertTrue(draft.name in r.content)
192228

229+
def test_csv_for_group(self):
230+
draft = make_test_data()
231+
232+
url = urlreverse("community_group_csv", kwargs={ "acronym": draft.group.acronym })
233+
234+
setup_default_community_list_for_group(draft.group)
235+
236+
# test GET, rest is tested with personal list
237+
r = self.client.get(url)
238+
self.assertEqual(r.status_code, 200)
239+
193240
def test_feed(self):
194241
draft = make_test_data()
195242

@@ -217,6 +264,17 @@ def test_feed(self):
217264
self.assertEqual(r.status_code, 200)
218265
self.assertTrue('<entry>' not in r.content)
219266

267+
def test_feed_for_group(self):
268+
draft = make_test_data()
269+
270+
url = urlreverse("community_group_feed", kwargs={ "acronym": draft.group.acronym })
271+
272+
setup_default_community_list_for_group(draft.group)
273+
274+
# test GET, rest is tested with personal list
275+
r = self.client.get(url)
276+
self.assertEqual(r.status_code, 200)
277+
220278
def test_subscription(self):
221279
draft = make_test_data()
222280

@@ -254,6 +312,19 @@ def test_subscription(self):
254312
self.assertEqual(r.status_code, 302)
255313
self.assertEqual(EmailSubscription.objects.filter(community_list=clist, email=email, notify_on="significant").count(), 0)
256314

315+
def test_subscription_for_group(self):
316+
draft = make_test_data()
317+
318+
url = urlreverse("community_group_subscription", kwargs={ "acronym": draft.group.acronym })
319+
320+
setup_default_community_list_for_group(draft.group)
321+
322+
login_testing_unauthorized(self, "marschairman", url)
323+
324+
# test GET, rest is tested with personal list
325+
r = self.client.get(url)
326+
self.assertEqual(r.status_code, 200)
327+
257328
def test_notification(self):
258329
draft = make_test_data()
259330

ietf/community/urls.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
from django.conf.urls import patterns, url
22

33

4-
urlpatterns = patterns('ietf.community.views',
5-
url(r'^personal/(?P<username>[^/]+)/$', 'view_list', name='community_personal_view_list'),
6-
url(r'^personal/(?P<username>[^/]+)/manage/$', 'manage_list', name='community_personal_manage_list'),
7-
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_personal_track_document'),
8-
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_personal_untrack_document'),
9-
url(r'^personal/(?P<username>[^/]+)/csv/$', 'export_to_csv', name='community_personal_csv'),
10-
url(r'^personal/(?P<username>[^/]+)/feed/$', 'feed', name='community_personal_feed'),
11-
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'subscription', name='community_personal_subscription'),
4+
urlpatterns = patterns('',
5+
url(r'^personal/(?P<username>[^/]+)/$', 'ietf.community.views.view_list', name='community_personal_view_list'),
6+
url(r'^personal/(?P<username>[^/]+)/manage/$', 'ietf.community.views.manage_list', name='community_personal_manage_list'),
7+
url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.track_document', name='community_personal_track_document'),
8+
url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.untrack_document', name='community_personal_untrack_document'),
9+
url(r'^personal/(?P<username>[^/]+)/csv/$', 'ietf.community.views.export_to_csv', name='community_personal_csv'),
10+
url(r'^personal/(?P<username>[^/]+)/feed/$', 'ietf.community.views.feed', name='community_personal_feed'),
11+
url(r'^personal/(?P<username>[^/]+)/subscription/$', 'ietf.community.views.subscription', name='community_personal_subscription'),
1212

13-
url(r'^group/(?P<acronym>[\w.@+-]+)/$', 'view_list', name='community_group_view_list'),
14-
url(r'^group/(?P<acronym>[\w.@+-]+)/manage/$', 'manage_list', name='community_group_manage_list'),
15-
url(r'^group/(?P<acronym>[\w.@+-]+)/trackdocument/(?P<name>[^/]+)/$', 'track_document', name='community_group_track_document'),
16-
url(r'^group/(?P<acronym>[\w.@+-]+)/untrackdocument/(?P<name>[^/]+)/$', 'untrack_document', name='community_group_untrack_document'),
17-
url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'export_to_csv', name='community_group_csv'),
18-
url(r'^group/(?P<acronym>[\w.@+-]+)/feed/$', 'feed', name='community_group_feed'),
19-
url(r'^group/(?P<acronym>[\w.@+-]+)/subscription/$', 'subscription', name='community_group_subscription'),
2013
)

0 commit comments

Comments
 (0)