diff --git a/ietf/community/constants.py b/ietf/community/constants.py
deleted file mode 100644
index 9bcad3fb5..000000000
--- a/ietf/community/constants.py
+++ /dev/null
@@ -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',
-]
diff --git a/ietf/community/display.py b/ietf/community/display.py
deleted file mode 100644
index f96ae1ab2..000000000
--- a/ietf/community/display.py
+++ /dev/null
@@ -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 '&#10004;'
-        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 '&#10004;'
-        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 '&#10004;'
-        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__()]
diff --git a/ietf/community/forms.py b/ietf/community/forms.py
index 3797b7816..d6dce1a68 100644
--- a/ietf/community/forms.py
+++ b/ietf/community/forms.py
@@ -1,101 +1,114 @@
-import hashlib
-import datetime
-
 from django import forms
-from django.conf import settings
-from django.contrib.sites.models import Site
+from django.db.models import Q
 
-from ietf.utils.mail import send_mail
-from ietf.community.models import Rule, DisplayConfiguration, RuleManager
-from ietf.community.display import DisplayField
+from ietf.community.models import SearchRule, EmailSubscription
+from ietf.doc.fields import SearchableDocumentsField
+from ietf.person.models import Person
+from ietf.person.fields import SearchablePersonField
 
+class AddDocumentsForm(forms.Form):
+    documents = SearchableDocumentsField(label="Add documents to track", doc_type="draft")
 
-class RuleForm(forms.ModelForm):
+class SearchRuleTypeForm(forms.Form):
+    rule_type = forms.ChoiceField(choices=[('', '--------------')] + SearchRule.RULE_TYPES)
+
+class SearchRuleForm(forms.ModelForm):
+    person = SearchablePersonField()
 
     class Meta:
-        model = Rule
-        fields = ('rule_type', 'value')
+        model = SearchRule
+        fields = ('state', 'group', 'person', 'text')
 
-    def __init__(self, *args, **kwargs):
-        self.clist = kwargs.pop('clist', None)
-        super(RuleForm, self).__init__(*args, **kwargs)
+    def __init__(self, clist, rule_type, *args, **kwargs):
+        kwargs["prefix"] = rule_type # add prefix to avoid mixups in the Javascript
+        super(SearchRuleForm, self).__init__(*args, **kwargs)
 
-    def save(self):
-        self.instance.community_list = self.clist
-        super(RuleForm, self).save()
+        def restrict_state(state_type, slug=None):
+            f = self.fields['state']
+            f.queryset = f.queryset.filter(used=True).filter(type=state_type)
+            if slug:
+                f.queryset = f.queryset.filter(slug=slug)
+            if len(f.queryset) == 1:
+                f.initial = f.queryset[0].pk
+                f.widget = forms.HiddenInput()
 
-    def get_all_options(self):
-        result = []
-        for i in RuleManager.__subclasses__():
-            options = i(None).options()
-            if options:
-                result.append({'type': i.codename,
-                               'options': options})
-        return result
-        
+        if rule_type in ['group', 'group_rfc', 'area', 'area_rfc']:
+            restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
 
-class DisplayForm(forms.ModelForm):
+            if rule_type.startswith("area"):
+                self.fields["group"].label = "Area"
+                self.fields["group"].queryset = self.fields["group"].queryset.filter(Q(type="area") | Q(acronym="irtf")).order_by("acronym")
+            else:
+                self.fields["group"].queryset = self.fields["group"].queryset.filter(type__in=("wg", "rg")).order_by("acronym")
+
+            del self.fields["person"]
+            del self.fields["text"]
+
+        elif rule_type.startswith("state_"):
+            mapping = {
+                "state_iab": "draft-stream-iab",
+                "state_iana": "draft-iana-review",
+                "state_iesg": "draft-iesg",
+                "state_irtf": "draft-stream-irtf",
+                "state_ise": "draft-stream-ise",
+                "state_rfceditor": "draft-rfceditor",
+                "state_ietf": "draft-stream-ietf",
+            }
+            restrict_state(mapping[rule_type])
+
+            del self.fields["group"]
+            del self.fields["person"]
+            del self.fields["text"]
+
+        elif rule_type in ["author", "author_rfc", "shepherd", "ad"]:
+            restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
+
+            if rule_type.startswith("author"):
+                self.fields["person"].label = "Author"
+            elif rule_type.startswith("shepherd"):
+                self.fields["person"].label = "Shepherd"
+            elif rule_type.startswith("ad"):
+                self.fields["person"].label = "Area Director"
+                self.fields["person"] = forms.ModelChoiceField(queryset=Person.objects.filter(role__name__in=("ad", "pre-ad"), role__group__state="active").distinct().order_by("name"))
+
+            del self.fields["group"]
+            del self.fields["text"]
+
+        elif rule_type == "name_contains":
+            restrict_state("draft", "rfc" if rule_type.endswith("rfc") else "active")
+
+            del self.fields["person"]
+            del self.fields["group"]
+
+        if 'group' in self.fields:
+            self.fields['group'].queryset = self.fields['group'].queryset.filter(state="active").order_by("acronym")
+            self.fields['group'].choices = [(g.pk, u"%s - %s" % (g.acronym, g.name)) for g in self.fields['group'].queryset]
+
+        for name, f in self.fields.iteritems():
+            f.required = True
+
+    def clean_text(self):
+        return self.cleaned_data["text"].strip().lower() # names are always lower case
+
+
+class SubscriptionForm(forms.ModelForm):
+    def __init__(self, user, clist, *args, **kwargs):
+        self.clist = clist
+        self.user = user
+
+        super(SubscriptionForm, self).__init__(*args, **kwargs)
+
+        self.fields["notify_on"].widget = forms.RadioSelect(choices=self.fields["notify_on"].choices)
+        self.fields["email"].queryset = self.fields["email"].queryset.filter(person__user=user, active=True).order_by("-primary")
+        self.fields["email"].widget = forms.RadioSelect(choices=[t for t in self.fields["email"].choices if t[0]])
+
+        if self.fields["email"].queryset:
+            self.fields["email"].initial = self.fields["email"].queryset[0]
+
+    def clean(self):
+        if EmailSubscription.objects.filter(community_list=self.clist, email=self.cleaned_data["email"], notify_on=self.cleaned_data["notify_on"]).exists():
+            raise forms.ValidationError("You already have a subscription like this.")
 
     class Meta:
-        model = DisplayConfiguration
-        fields = ('sort_method', )
-
-    def save(self):
-        data = self.data
-        fields = []
-        for i in DisplayField.__subclasses__():
-            if data.get(i.codename, None):
-                fields.append(i.codename)
-        self.instance.display_fields = ','.join(fields)
-        super(DisplayForm, self).save()
-
-
-class SubscribeForm(forms.Form):
-
-    email = forms.EmailField(label="Your email")
-
-    def __init__(self, *args, **kwargs):
-        self.clist = kwargs.pop('clist')
-        self.significant = kwargs.pop('significant')
-        super(SubscribeForm, self).__init__(*args, **kwargs)
-
-    def save(self, *args, **kwargs):
-        self.send_email()
-        return True
-
-    def send_email(self):
-        domain = Site.objects.get_current().domain
-        today = datetime.date.today().strftime('%Y%m%d')
-        subject = 'Confirm list subscription: %s' % self.clist
-        from_email = settings.DEFAULT_FROM_EMAIL
-        to_email = self.cleaned_data['email']
-        auth = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, to_email, 'subscribe', self.significant)).hexdigest()
-        context = {
-            'domain': domain,
-            'clist': self.clist,
-            'today': today,
-            'auth': auth,
-            'to_email': to_email,
-            'significant': self.significant,
-        }
-        send_mail(None, to_email, from_email, subject, 'community/public/subscribe_email.txt', context)
-
-
-class UnSubscribeForm(SubscribeForm):
-
-    def send_email(self):
-        domain = Site.objects.get_current().domain
-        today = datetime.date.today().strftime('%Y%m%d')
-        subject = 'Confirm list subscription cancelation: %s' % self.clist
-        from_email = settings.DEFAULT_FROM_EMAIL
-        to_email = self.cleaned_data['email']
-        auth = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, to_email, 'unsubscribe', self.significant)).hexdigest()
-        context = {
-            'domain': domain,
-            'clist': self.clist,
-            'today': today,
-            'auth': auth,
-            'to_email': to_email,
-            'significant': self.significant,
-        }
-        send_mail(None, to_email, from_email, subject, 'community/public/unsubscribe_email.txt', context)
+        model = EmailSubscription
+        fields = ("notify_on", "email")
diff --git a/ietf/community/management/__init__.py b/ietf/community/management/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ietf/community/management/commands/__init__.py b/ietf/community/management/commands/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ietf/community/management/commands/update_community_lists.py b/ietf/community/management/commands/update_community_lists.py
deleted file mode 100644
index 4191c6f5d..000000000
--- a/ietf/community/management/commands/update_community_lists.py
+++ /dev/null
@@ -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
diff --git a/ietf/community/management/commands/update_doc_change_dates.py b/ietf/community/management/commands/update_doc_change_dates.py
deleted file mode 100644
index 5f946cf04..000000000
--- a/ietf/community/management/commands/update_doc_change_dates.py
+++ /dev/null
@@ -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
diff --git a/ietf/community/migrations/0003_cleanup.py b/ietf/community/migrations/0003_cleanup.py
new file mode 100644
index 000000000..f90283127
--- /dev/null
+++ b/ietf/community/migrations/0003_cleanup.py
@@ -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,
+        ),
+    ]
diff --git a/ietf/community/migrations/0004_cleanup_data.py b/ietf/community/migrations/0004_cleanup_data.py
new file mode 100644
index 000000000..6790230ac
--- /dev/null
+++ b/ietf/community/migrations/0004_cleanup_data.py
@@ -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',
+        ),
+    ]
diff --git a/ietf/community/models.py b/ietf/community/models.py
index a644a18fc..b3e265767 100644
--- a/ietf/community/models.py
+++ b/ietf/community/models.py
@@ -1,263 +1,102 @@
-import hashlib
-
-from django.conf import settings
 from django.contrib.auth.models import User
-from django.core.urlresolvers import reverse
 from django.db import models
-from django.db.models import signals, Q
-
-from ietf.utils.mail import send_mail
-from ietf.doc.models import Document, DocEvent
-from ietf.group.models import Group, Role
-
-from ietf.community.rules import TYPES_OF_RULES, RuleManager
-from ietf.community.display import (TYPES_OF_SORT, DisplayField,
-                                    SortMethod)
-from ietf.community.constants import SIGNIFICANT_STATES
+from django.db.models import signals
+from django.core.urlresolvers import reverse as urlreverse
 
+from ietf.doc.models import Document, DocEvent, State
+from ietf.group.models import Group
+from ietf.person.models import Person, Email
 
 class CommunityList(models.Model):
-
     user = models.ForeignKey(User, blank=True, null=True)
     group = models.ForeignKey(Group, blank=True, null=True)
-    added_ids = models.ManyToManyField(Document)
-    secret = models.CharField(max_length=255, null=True, blank=True)
-    cached = models.TextField(null=True, blank=True)
-
-    def check_manager(self, user):
-        if user == self.user:
-            return True
-        if not self.group or self.group.type.slug not in ('area', 'wg'):
-            return False
-        try:
-            person = user.person
-        except:
-            return False
-        if self.group.type.slug == 'area':
-            return bool(Role.objects.filter(name__slug='ad', email__in=person.email_set.all(), group=self.group).count())
-        elif self.group.type.slug == 'wg':
-            return bool(Role.objects.filter(name__slug='chair', email__in=person.email_set.all(), group=self.group).count())
-        return False
-
-    def short_name(self):
-        if self.user:
-            return 'mine'
-        else:
-            return '%s' % self.group.acronym
+    added_docs = models.ManyToManyField(Document)
 
     def long_name(self):
         if self.user:
             return 'Personal ID list of %s' % self.user.username
-        else:
+        elif self.group:
             return 'ID list for %s' % self.group.name
+        else:
+            return 'ID list'
 
     def __unicode__(self):
         return self.long_name()
 
-    def get_public_url(self):
+    def get_absolute_url(self):
         if self.user:
-            return reverse('view_personal_list', None, args=(self.user.username, ))
-        else:
-            return reverse('view_group_list', None, args=(self.group.acronym, ))
-
-    def get_manage_url(self):
-        if self.user:
-            return reverse('manage_personal_list', None, args=())
-        else:
-            return reverse('manage_group_list', None, args=(self.group.acronym, ))
-
-    def get_display_config(self):
-        dconfig = getattr(self, '_cached_dconfig', None)
-        if not dconfig:
-            try:
-                self._cached_dconfig = DisplayConfiguration.objects.get(community_list=self)
-            except DisplayConfiguration.DoesNotExist:
-                self._cached_dconfig = DisplayConfiguration(community_list=self)
-            return self._cached_dconfig
-        return self._cached_dconfig
-
-    def get_documents(self):
-        if hasattr(self, '_cached_documents'):
-            return self._cached_documents
-        docs = self.added_ids.all().distinct().select_related('type', 'group', 'ad')
-        for rule in self.rule_set.all():
-            docs = docs | rule.cached_ids.all().distinct()
-        sort_field = self.get_display_config().get_sort_method().get_sort_field()
-        docs = docs.distinct().order_by(sort_field)
-        self._cached_documents = docs
-        return self._cached_documents
-
-    def get_rfcs_and_drafts(self):
-        if hasattr(self, '_cached_rfcs_and_drafts'):
-            return self._cached_rfcs_and_drafts
-        docs = self.get_documents()
-        sort_method = self.get_display_config().get_sort_method()
-        sort_field = sort_method.get_sort_field()
-        if hasattr(sort_method, 'get_full_rfc_sort'):
-            rfcs = sort_method.get_full_rfc_sort(docs.filter(states__name='rfc').distinct())
-        else:
-            rfcs = docs.filter(states__name='rfc').distinct().order_by(sort_field)
-        if hasattr(sort_method, 'get_full_draft_sort'):
-            drafts = sort_method.get_full_draft_sort(docs.exclude(pk__in=rfcs).distinct())
-        else:
-            drafts = docs.exclude(pk__in=rfcs).distinct().order_by(sort_field)
-        self._cached_rfcs_and_drafts = (rfcs, drafts)
-        return self._cached_rfcs_and_drafts
-
-    def add_subscriptor(self, email, significant):
-        self.emailsubscription_set.get_or_create(email=email, significant=significant)
-
-    def save(self, *args, **kwargs):
-        super(CommunityList, self).save(*args, **kwargs)
-        if not self.secret:
-            self.secret = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, self.id, self.user and self.user.id or '', self.group and self.group.id or '')).hexdigest()
-            self.save()
-
-    def update(self):
-        self.cached=None
-        self.save()
+            return urlreverse("community_personal_view_list", kwargs={ 'username': self.user.username })
+        elif self.group:
+            return urlreverse("group_docs", kwargs={ 'acronym': self.group.acronym })
+        return ""
 
 
-class Rule(models.Model):
+class SearchRule(models.Model):
+    # these types define the UI for setting up the rule, and also
+    # helps when interpreting the rule and matching documents
+    RULE_TYPES = [
+        ('group', 'All I-Ds associated with a particular group'),
+        ('area', 'All I-Ds associated with all groups in a particular Area'),
+        ('group_rfc', 'All RFCs associated with a particular group'),
+        ('area_rfc', 'All RFCs associated with all groups in a particular Area'),
+
+        ('state_iab', 'All I-Ds that are in a particular IAB state'),
+        ('state_iana', 'All I-Ds that are in a particular IANA state'),
+        ('state_iesg', 'All I-Ds that are in a particular IESG state'),
+        ('state_irtf', 'All I-Ds that are in a particular IRTF state'),
+        ('state_ise', 'All I-Ds that are in a particular ISE state'),
+        ('state_rfceditor', 'All I-Ds that are in a particular RFC Editor state'),
+        ('state_ietf', 'All I-Ds that are in a particular Working Group state'),
+
+        ('author', 'All I-Ds with a particular author'),
+        ('author_rfc', 'All RFCs with a particular author'),
+
+        ('ad', 'All I-Ds with a particular responsible AD'),
+
+        ('shepherd', 'All I-Ds with a particular document shepherd'),
+
+        ('name_contains', 'All I-Ds with particular text/regular expression in the name'),
+    ]
 
     community_list = models.ForeignKey(CommunityList)
-    cached_ids = models.ManyToManyField(Document)
-    rule_type = models.CharField(max_length=30, choices=TYPES_OF_RULES)
-    value = models.CharField(max_length=255)
+    rule_type = models.CharField(max_length=30, choices=RULE_TYPES)
 
-    class Meta:
-        unique_together= ("community_list", "rule_type", "value")
+    # these are filled in depending on the type
+    state = models.ForeignKey(State, blank=True, null=True)
+    group = models.ForeignKey(Group, blank=True, null=True)
+    person = models.ForeignKey(Person, blank=True, null=True)
+    text = models.CharField(verbose_name="Text/RegExp", max_length=255, blank=True, default="")
 
-    last_updated = models.DateTimeField(
-        auto_now=True)
-
-    def get_callable_rule(self):
-        for i in RuleManager.__subclasses__():
-            if i.codename == self.rule_type:
-                return i(self.value)
-        return RuleManager(self.value)
-
-    def save(self, *args, **kwargs):
-        super(Rule, self).save(*args, **kwargs)
-        rule = self.get_callable_rule()
-        self.cached_ids = rule.get_documents()
-        self.community_list.update()
-
-    def delete(self):
-        self.community_list.update()
-        super(Rule, self).delete()
-
-
-class DisplayConfiguration(models.Model):
-
-    community_list = models.ForeignKey(CommunityList)
-    sort_method = models.CharField(
-        max_length=100,
-        choices=TYPES_OF_SORT,
-        default='by_filename',
-        blank=False,
-        null=False)
-    display_fields = models.TextField(
-        default='filename,title,date')
-
-    def get_display_fields_config(self):
-        fields = self.display_fields and self.display_fields.split(',') or []
-        config = []
-        for i in DisplayField.__subclasses__():
-            config.append({
-                'codename': i.codename,
-                'description': i.description,
-                'active': i.codename in fields,
-            })
-        return config
-
-    def get_active_fields(self):
-        fields = self.display_fields and self.display_fields.split(',') or ''
-        active_fields = [i for i in DisplayField.__subclasses__() if i.codename in fields]
-        return active_fields
-
-    def get_all_fields(self):
-        all_fields = [i for i in DisplayField.__subclasses__()]
-        return all_fields
-
-    def get_sort_method(self):
-        for i in SortMethod.__subclasses__():
-            if i.codename == self.sort_method:
-                return i()
-        return SortMethod()
-
-    def save(self, *args, **kwargs):
-        super(DisplayConfiguration, self).save(*args, **kwargs)
-        self.community_list.update()
-
-    def delete(self):
-        self.community_list.update()
-        super(DisplayConfiguration, self).delete()
-
-
-class ExpectedChange(models.Model):
-
-    community_list = models.ForeignKey(CommunityList)
-    document = models.ForeignKey(Document)
-    expected_date = models.DateField(
-        verbose_name='Expected date'
-        )
+    # store a materialized view/index over which documents are matched
+    # by the name_contains rule to avoid having to scan the whole
+    # database - we update this manually when the rule is changed and
+    # when new documents are submitted
+    name_contains_index = models.ManyToManyField(Document)
 
 
 class EmailSubscription(models.Model):
     community_list = models.ForeignKey(CommunityList)
-    email = models.CharField(max_length=200)
-    significant = models.BooleanField(default=False)
+    email = models.ForeignKey(Email)
 
+    NOTIFICATION_CHOICES = [
+        ("all", "All changes"),
+        ("significant", "Only significant state changes")
+    ]
+    notify_on = models.CharField(max_length=30, choices=NOTIFICATION_CHOICES, default="all")
 
-class ListNotification(models.Model):
-
-    event = models.ForeignKey(DocEvent)
-    significant = models.BooleanField(default=False)
-
-    def notify_by_email(self):
-        clists = CommunityList.objects.filter(
-            Q(added_ids=self.event.doc) | Q(rule__cached_ids=self.event.doc)).distinct()
-        from_email = settings.DEFAULT_FROM_EMAIL
-        for l in clists:
-            subject = '%s notification: Changes on %s' % (l.long_name(), self.event.doc.name)
-            context = {'notification': self.event,
-                       'clist': l}
-            filter_subscription = {'community_list': l}
-            if not self.significant:
-                filter_subscription['significant'] = False
-            for to_email in list(set([i.email for i in EmailSubscription.objects.filter(**filter_subscription)])):
-                send_mail(None, to_email, from_email, subject, 'community/public/notification_email.txt', context)
+    def __unicode__(self):
+        return u"%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on)
 
 
 def notify_events(sender, instance, **kwargs):
     if not isinstance(instance, DocEvent):
         return
-    if instance.doc.type.slug != 'draft' or instance.type == 'added_comment':
+
+    if instance.doc.type_id != 'draft':
         return
-    (changes, created) = DocumentChangeDates.objects.get_or_create(document=instance.doc)
-    changes.normal_change_date = instance.time
-    significant = False
-    if instance.type == 'changed_document' and 'tate changed' in instance.desc:
-        for i in SIGNIFICANT_STATES:
-            if ('<b>%s</b>' % i) in instance.desc:
-                significant = True
-                changes.significant_change_date = instance.time
-                break
-    elif instance.type == 'new_revision':
-        changes.new_version_date = instance.time
-    changes.save()
-    notification = ListNotification.objects.create(
-        event=instance,
-        significant=significant,
-    )
-    notification.notify_by_email()
+
+    from ietf.community.utils import notify_event_to_subscribers
+    notify_event_to_subscribers(instance)
+
+
 signals.post_save.connect(notify_events)
-
-
-class DocumentChangeDates(models.Model):
-
-    document = models.ForeignKey(Document)
-    new_version_date = models.DateTimeField(blank=True, null=True)
-    normal_change_date = models.DateTimeField(blank=True, null=True)
-    significant_change_date = models.DateTimeField(blank=True, null=True)
diff --git a/ietf/community/resources.py b/ietf/community/resources.py
index 641c523b5..d2c9f9209 100644
--- a/ietf/community/resources.py
+++ b/ietf/community/resources.py
@@ -1,14 +1,13 @@
 # Autogenerated by the mkresources management command 2014-11-13 23:53
 from tastypie.resources import ModelResource
-from ietf.api import ToOneField
-from tastypie.fields import ToManyField
+from tastypie.fields import ToOneField, ToManyField
 from tastypie.constants import ALL, ALL_WITH_RELATIONS
 from tastypie.cache import SimpleCache
 
 from ietf import api
 
-from ietf.community.models import ( CommunityList, ExpectedChange, DisplayConfiguration,
-    ListNotification, Rule, EmailSubscription, DocumentChangeDates ) 
+from ietf.community.models import CommunityList, SearchRule, EmailSubscription
+
 
 from ietf.doc.resources import DocumentResource
 from ietf.group.resources import GroupResource
@@ -16,7 +15,7 @@ from ietf.utils.resources import UserResource
 class CommunityListResource(ModelResource):
     user             = ToOneField(UserResource, 'user', null=True)
     group            = ToOneField(GroupResource, 'group', null=True)
-    added_ids        = ToManyField(DocumentResource, 'added_ids', null=True)
+    added_docs       = ToManyField(DocumentResource, 'added_docs', null=True)
     class Meta:
         cache = SimpleCache()
         queryset = CommunityList.objects.all()
@@ -28,75 +27,24 @@ class CommunityListResource(ModelResource):
             "cached": ALL,
             "user": ALL_WITH_RELATIONS,
             "group": ALL_WITH_RELATIONS,
-            "added_ids": ALL_WITH_RELATIONS,
+            "added_docs": ALL_WITH_RELATIONS,
         }
 api.community.register(CommunityListResource())
 
 from ietf.doc.resources import DocumentResource
-class ExpectedChangeResource(ModelResource):
-    community_list   = ToOneField(CommunityListResource, 'community_list')
-    document         = ToOneField(DocumentResource, 'document')
-    class Meta:
-        cache = SimpleCache()
-        queryset = ExpectedChange.objects.all()
-        serializer = api.Serializer()
-        #resource_name = 'expectedchange'
-        filtering = { 
-            "id": ALL,
-            "expected_date": ALL,
-            "community_list": ALL_WITH_RELATIONS,
-            "document": ALL_WITH_RELATIONS,
-        }
-api.community.register(ExpectedChangeResource())
-
-class DisplayConfigurationResource(ModelResource):
+class SearchRuleResource(ModelResource):
     community_list   = ToOneField(CommunityListResource, 'community_list')
     class Meta:
         cache = SimpleCache()
-        queryset = DisplayConfiguration.objects.all()
-        serializer = api.Serializer()
-        #resource_name = 'displayconfiguration'
-        filtering = { 
-            "id": ALL,
-            "sort_method": ALL,
-            "display_fields": ALL,
-            "community_list": ALL_WITH_RELATIONS,
-        }
-api.community.register(DisplayConfigurationResource())
-
-from ietf.doc.resources import DocEventResource
-class ListNotificationResource(ModelResource):
-    event            = ToOneField(DocEventResource, 'event')
-    class Meta:
-        cache = SimpleCache()
-        queryset = ListNotification.objects.all()
-        serializer = api.Serializer()
-        #resource_name = 'listnotification'
-        filtering = { 
-            "id": ALL,
-            "significant": ALL,
-            "event": ALL_WITH_RELATIONS,
-        }
-api.community.register(ListNotificationResource())
-
-from ietf.doc.resources import DocumentResource
-class RuleResource(ModelResource):
-    community_list   = ToOneField(CommunityListResource, 'community_list')
-    cached_ids       = ToManyField(DocumentResource, 'cached_ids', null=True)
-    class Meta:
-        cache = SimpleCache()
-        queryset = Rule.objects.all()
+        queryset = SearchRule.objects.all()
         serializer = api.Serializer()
         #resource_name = 'rule'
         filtering = { 
             "id": ALL,
             "rule_type": ALL,
-            "value": ALL,
-            "last_updated": ALL,
             "community_list": ALL_WITH_RELATIONS,
-            "cached_ids": ALL_WITH_RELATIONS,
         }
-api.community.register(RuleResource())
+api.community.register(SearchRuleResource())
 
 class EmailSubscriptionResource(ModelResource):
     community_list   = ToOneField(CommunityListResource, 'community_list')
@@ -107,26 +55,8 @@ class EmailSubscriptionResource(ModelResource):
         #resource_name = 'emailsubscription'
         filtering = { 
             "id": ALL,
-            "email": ALL,
-            "significant": ALL,
+            "email": ALL_WITH_RELATIONS,
+            "notify_on": ALL,
             "community_list": ALL_WITH_RELATIONS,
         }
 api.community.register(EmailSubscriptionResource())
-
-from ietf.doc.resources import DocumentResource
-class DocumentChangeDatesResource(ModelResource):
-    document         = ToOneField(DocumentResource, 'document')
-    class Meta:
-        cache = SimpleCache()
-        queryset = DocumentChangeDates.objects.all()
-        serializer = api.Serializer()
-        #resource_name = 'documentchangedates'
-        filtering = { 
-            "id": ALL,
-            "new_version_date": ALL,
-            "normal_change_date": ALL,
-            "significant_change_date": ALL,
-            "document": ALL_WITH_RELATIONS,
-        }
-api.community.register(DocumentChangeDatesResource())
-
diff --git a/ietf/community/rules.py b/ietf/community/rules.py
deleted file mode 100644
index a77b36570..000000000
--- a/ietf/community/rules.py
+++ /dev/null
@@ -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 &mdash; %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 &mdash; %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 &mdash; %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 &mdash; %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__()]
-
-
diff --git a/ietf/community/templatetags/__init__.py b/ietf/community/templatetags/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ietf/community/templatetags/community_tags.py b/ietf/community/templatetags/community_tags.py
deleted file mode 100644
index c88e6b289..000000000
--- a/ietf/community/templatetags/community_tags.py
+++ /dev/null
@@ -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
diff --git a/ietf/community/tests.py b/ietf/community/tests.py
new file mode 100644
index 000000000..6cbd5ad34
--- /dev/null
+++ b/ietf/community/tests.py
@@ -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"])
+        
+        
diff --git a/ietf/community/urls.py b/ietf/community/urls.py
index 0f60ba63c..73d31c8c6 100644
--- a/ietf/community/urls.py
+++ b/ietf/community/urls.py
@@ -1,34 +1,13 @@
 from django.conf.urls import patterns, url
 
 
-urlpatterns = patterns('ietf.community.views',
-    url(r'^personal/$', 'manage_personal_list', name='manage_personal_list'),
-    url(r'^personal/csv/$', 'csv_personal_list', name='csv_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/view/$', 'view_personal_list', name='view_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/csv/$', 'view_csv_personal_list', name='view_csv_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/changes/feed/$', 'changes_personal_list', name='changes_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/changes/significant/feed/$', 'significant_personal_list', name='significant_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/$', 'subscribe_personal_list', {'significant': False}, name='subscribe_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/subscribe/significant/$', 'subscribe_personal_list', {'significant': True}, name='subscribe_significant_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/$', 'unsubscribe_personal_list', {'significant': False}, name='unsubscribe_personal_list'),
-    url(r'^personal/(?P<secret>[a-f0-9]+)/unsubscribe/significant/$', 'unsubscribe_personal_list', {'significant': True}, name='unsubscribe_significant_personal_list'),
+urlpatterns = patterns('',
+    url(r'^personal/(?P<username>[^/]+)/$', 'ietf.community.views.view_list', name='community_personal_view_list'),
+    url(r'^personal/(?P<username>[^/]+)/manage/$', 'ietf.community.views.manage_list', name='community_personal_manage_list'),
+    url(r'^personal/(?P<username>[^/]+)/trackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.track_document', name='community_personal_track_document'),
+    url(r'^personal/(?P<username>[^/]+)/untrackdocument/(?P<name>[^/]+)/$', 'ietf.community.views.untrack_document', name='community_personal_untrack_document'),
+    url(r'^personal/(?P<username>[^/]+)/csv/$', 'ietf.community.views.export_to_csv', name='community_personal_csv'),
+    url(r'^personal/(?P<username>[^/]+)/feed/$', 'ietf.community.views.feed', name='community_personal_feed'),
+    url(r'^personal/(?P<username>[^/]+)/subscription/$', 'ietf.community.views.subscription', name='community_personal_subscription'),
 
-    url(r'^group/(?P<acronym>[\w.@+-]+)/$', 'manage_group_list', name='manage_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/view/$', 'view_group_list', name='view_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/changes/feed/$', 'changes_group_list', name='changes_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/changes/significant/feed/$', 'significant_group_list', name='significant_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/csv/$', 'csv_group_list', name='csv_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/subscribe/$', 'subscribe_group_list', {'significant': False}, name='subscribe_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/subscribe/significant/$', 'subscribe_group_list', {'significant': True}, name='subscribe_significant_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/unsubscribe/$', 'unsubscribe_group_list', {'significant': False}, name='unsubscribe_group_list'),
-    url(r'^group/(?P<acronym>[\w.@+-]+)/unsubscribe/significant/$', 'unsubscribe_group_list', {'significant': True}, name='unsubscribe_significant_group_list'),
-
-    url(r'^add_track_document/(?P<document_name>[^/]+)/$', 'add_track_document', name='community_add_track_document'),
-    url(r'^remove_track_document/(?P<document_name>[^/]+)/$', 'remove_track_document', name='community_remove_track_document'),
-    url(r'^(?P<list_id>[\d]+)/remove_document/(?P<document_name>[^/]+)/$', 'remove_document', name='community_remove_document'),
-    url(r'^(?P<list_id>[\d]+)/remove_rule/(?P<rule_id>[^/]+)/$', 'remove_rule', name='community_remove_rule'),
-    url(r'^(?P<list_id>[\d]+)/subscribe/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_subscription', name='confirm_subscription'),
-    url(r'^(?P<list_id>[\d]+)/subscribe/significant/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_significant_subscription', name='confirm_significant_subscription'),
-    url(r'^(?P<list_id>[\d]+)/unsubscribe/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_unsubscription', name='confirm_unsubscription'),
-    url(r'^(?P<list_id>[\d]+)/unsubscribe/significant/confirm/(?P<email>[\w.@+-]+)/(?P<date>[\d]+)/(?P<confirm_hash>[a-f0-9]+)/$', 'confirm_significant_unsubscription', name='confirm_significant_unsubscription'),
 )
diff --git a/ietf/community/utils.py b/ietf/community/utils.py
new file mode 100644
index 000000000..ee8c87042
--- /dev/null
+++ b/ietf/community/utils.py
@@ -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,
+                  })
diff --git a/ietf/community/views.py b/ietf/community/views.py
index 87c2a9702..c841d29bc 100644
--- a/ietf/community/views.py
+++ b/ietf/community/views.py
@@ -1,352 +1,268 @@
 import csv
 import uuid
 import datetime
-import hashlib
 import json
 
-from django.db import IntegrityError
-from django.conf import settings
-from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.http import HttpResponse, Http404, HttpResponseRedirect
-from django.shortcuts import get_object_or_404, render_to_response
-from django.template import RequestContext
-from django.utils.http import urlquote
+from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
+from django.shortcuts import get_object_or_404, render
+from django.contrib.auth.decorators import login_required
+from django.utils.html import strip_tags
 
-from ietf.community.models import CommunityList, Rule, EmailSubscription
-from ietf.community.forms import RuleForm, DisplayForm, SubscribeForm, UnSubscribeForm
-from ietf.group.models import Group
-from ietf.doc.models import DocEvent, DocAlias
+from ietf.community.models import SearchRule, EmailSubscription
+from ietf.community.forms import SearchRuleTypeForm, SearchRuleForm, AddDocumentsForm, SubscriptionForm
+from ietf.community.utils import lookup_community_list, can_manage_community_list
+from ietf.community.utils import docs_tracked_by_community_list, docs_matching_community_list_rule
+from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule
+from ietf.doc.models import DocEvent, Document
+from ietf.doc.utils_search import prepare_document_table
 
+def view_list(request, username=None):
+    clist = lookup_community_list(username)
 
-def _manage_list(request, clist):
-    display_config = clist.get_display_config()
-    if request.method == 'POST' and request.POST.get('save_rule', None):
-        rule_form = RuleForm(request.POST, clist=clist)
-        display_form = DisplayForm(instance=display_config)
-        if rule_form.is_valid():
-            try:
-                rule_form.save()
-            except IntegrityError:
-                pass;
-            rule_form = RuleForm(clist=clist)
-            display_form = DisplayForm(instance=display_config)
-    elif request.method == 'POST' and request.POST.get('save_display', None):
-        display_form = DisplayForm(request.POST, instance=display_config)
-        rule_form = RuleForm(clist=clist)
-        if display_form.is_valid():
-            display_form.save()
-            rule_form = RuleForm(clist=clist)
-            display_form = DisplayForm(instance=display_config)
+    docs = docs_tracked_by_community_list(clist)
+    docs, meta = prepare_document_table(request, docs, request.GET)
+
+    subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
+
+    return render(request, 'community/view_list.html', {
+        'clist': clist,
+        'docs': docs,
+        'meta': meta,
+        'can_manage_list': can_manage_community_list(request.user, clist),
+        'subscribed': subscribed,
+    })
+
+@login_required
+def manage_list(request, username=None, acronym=None, group_type=None):
+    # we need to be a bit careful because clist may not exist in the
+    # database so we can't call related stuff on it yet
+    clist = lookup_community_list(username, acronym)
+
+    if not can_manage_community_list(request.user, clist):
+        return HttpResponseForbidden("You do not have permission to access this view")
+
+    action = request.POST.get('action')
+
+    if request.method == 'POST' and action == 'add_documents':
+        add_doc_form = AddDocumentsForm(request.POST)
+        if add_doc_form.is_valid():
+            if clist.pk is None:
+                clist.save()
+
+            for d in add_doc_form.cleaned_data['documents']:
+                clist.added_docs.add(d)
+
+            return HttpResponseRedirect("")
     else:
-        rule_form = RuleForm(clist=clist)
-        display_form = DisplayForm(instance=display_config)
-    clist = CommunityList.objects.get(id=clist.id)
-    return render_to_response('community/manage_clist.html',
-                              {'cl': clist,
-                               'dc': display_config,
-                               'display_form': display_form,
-                               'rule_form': rule_form},
-                              context_instance=RequestContext(request))
+        add_doc_form = AddDocumentsForm()
+
+    if request.method == 'POST' and action == 'remove_document':
+        document_pk = request.POST.get('document')
+        if clist.pk is not None and document_pk:
+            document = get_object_or_404(clist.added_docs, pk=document_pk)
+            clist.added_docs.remove(document)
+
+            return HttpResponseRedirect("")
+
+    if request.method == 'POST' and action == 'add_rule':
+        rule_type_form = SearchRuleTypeForm(request.POST)
+        if rule_type_form.is_valid():
+            rule_type = rule_type_form.cleaned_data['rule_type']
+
+        if rule_type:
+            rule_form = SearchRuleForm(clist, rule_type, request.POST)
+            if rule_form.is_valid():
+                if clist.pk is None:
+                    clist.save()
+
+                rule = rule_form.save(commit=False)
+                rule.community_list = clist
+                rule.rule_type = rule_type
+                rule.save()
+                if rule.rule_type == "name_contains":
+                    reset_name_contains_index_for_rule(rule)
+
+                return HttpResponseRedirect("")
+    else:
+        rule_type_form = SearchRuleTypeForm()
+        rule_form = None
+
+    if request.method == 'POST' and action == 'remove_rule':
+        rule_pk = request.POST.get('rule')
+        if clist.pk is not None and rule_pk:
+            rule = get_object_or_404(SearchRule, pk=rule_pk, community_list=clist)
+            rule.delete()
+
+        return HttpResponseRedirect("")
+
+    rules = clist.searchrule_set.all() if clist.pk is not None else []
+    for r in rules:
+        r.matching_documents_count = docs_matching_community_list_rule(r).count()
+
+    empty_rule_forms = { rule_type: SearchRuleForm(clist, rule_type) for rule_type, _ in SearchRule.RULE_TYPES }
+
+    total_count = docs_tracked_by_community_list(clist).count()
+
+    return render(request, 'community/manage_list.html', {
+        'clist': clist,
+        'rules': rules,
+        'individually_added': clist.added_docs.all() if clist.pk is not None else [],
+        'rule_type_form': rule_type_form,
+        'rule_form': rule_form,
+        'empty_rule_forms': empty_rule_forms,
+        'total_count': total_count,
+        'add_doc_form': add_doc_form,
+    })
 
 
-def manage_personal_list(request):
-    user = request.user
-    if not user.is_authenticated():
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    clist = CommunityList.objects.get_or_create(user=request.user)[0]
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    return _manage_list(request, clist)
+@login_required
+def track_document(request, name, username=None, acronym=None):
+    doc = get_object_or_404(Document, docalias__name=name)
+
+    if request.method == "POST":
+        clist = lookup_community_list(username, acronym)
+        if not can_manage_community_list(request.user, clist):
+            return HttpResponseForbidden("You do not have permission to access this view")
+
+        if clist.pk is None:
+            clist.save()
+
+        clist.added_docs.add(doc)
+
+        if request.is_ajax():
+            return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
+        else:
+            return HttpResponseRedirect(clist.get_absolute_url())
+
+    return render(request, "community/track_document.html", {
+        "name": doc.name,
+    })
+
+@login_required
+def untrack_document(request, name, username=None, acronym=None):
+    doc = get_object_or_404(Document, docalias__name=name)
+    clist = lookup_community_list(username, acronym)
+    if not can_manage_community_list(request.user, clist):
+        return HttpResponseForbidden("You do not have permission to access this view")
+
+    if request.method == "POST":
+        if clist.pk is not None:
+            clist.added_docs.remove(doc)
+
+        if request.is_ajax():
+            return HttpResponse(json.dumps({ 'success': True }), content_type='text/plain')
+        else:
+            return HttpResponseRedirect(clist.get_absolute_url())
+
+    return render(request, "community/untrack_document.html", {
+        "name": doc.name,
+    })
 
 
-def manage_group_list(request, acronym):
-    group = get_object_or_404(Group, acronym=acronym)
-    if group.type.slug not in ('area', 'wg'):
-        raise Http404
-    clist = CommunityList.objects.get_or_create(group=group)[0]
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    return _manage_list(request, clist)
+def export_to_csv(request, username=None, acronym=None, group_type=None):
+    clist = lookup_community_list(username, acronym)
 
+    response = HttpResponse(content_type='text/csv')
 
-def add_track_document(request, document_name):
-    """supports the "Track this document" functionality
-    
-    This is exposed in the document view and in document search results."""
-    if not request.user.is_authenticated():
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    doc = get_object_or_404(DocAlias, name=document_name).document
-    clist = CommunityList.objects.get_or_create(user=request.user)[0]
-    clist.update()
-    return add_document_to_list(request, clist, doc)
+    if clist.group:
+        filename = "%s-draft-list.csv" % clist.group.acronym
+    else:
+        filename = "draft-list.csv"
 
-def remove_track_document(request, document_name):
-    """supports the "Untrack this document" functionality
-    
-    This is exposed in the document view and in document search results."""
-    clist = CommunityList.objects.get_or_create(user=request.user)[0]
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    doc = get_object_or_404(DocAlias, name=document_name).document
-    clist.added_ids.remove(doc)
-    clist.update()
-    return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
+    response['Content-Disposition'] = 'attachment; filename=%s' % filename
 
-def remove_document(request, list_id, document_name):
-    clist = get_object_or_404(CommunityList, pk=list_id)
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    doc = get_object_or_404(DocAlias, name=document_name).document
-    clist.added_ids.remove(doc)
-    clist.update()
-    return HttpResponseRedirect(clist.get_manage_url())
+    writer = csv.writer(response, dialect=csv.excel, delimiter=',')
 
-def add_document_to_list(request, clist, doc):
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    clist.added_ids.add(doc)
-    return HttpResponse(json.dumps({'success': True}), content_type='text/plain')
+    header = [
+        "Name",
+        "Title",
+        "Date of latest revision",
+        "Status in the IETF process",
+        "Associated group",
+        "Associated AD",
+        "Date of latest change",
+    ]
+    writer.writerow(header)
 
+    docs = docs_tracked_by_community_list(clist).select_related('type', 'group', 'ad')
+    for doc in docs.prefetch_related("states", "tags"):
+        row = []
+        row.append(doc.name)
+        row.append(doc.title)
+        e = doc.latest_event(type='new_revision')
+        row.append(e.time.strftime("%Y-%m-%d") if e else "")
+        row.append(strip_tags(doc.friendly_state()))
+        row.append(doc.group.acronym if doc.group else "")
+        row.append(unicode(doc.ad) if doc.ad else "")
+        e = doc.latest_event()
+        row.append(e.time.strftime("%Y-%m-%d") if e else "")
+        writer.writerow([v.encode("utf-8") for v in row])
 
-def remove_rule(request, list_id, rule_id):
-    clist = get_object_or_404(CommunityList, pk=list_id)
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    rule = get_object_or_404(Rule, pk=rule_id)
-    rule.delete()
-    return HttpResponseRedirect(clist.get_manage_url())
+    return response
 
+def feed(request, username=None, acronym=None, group_type=None):
+    clist = lookup_community_list(username, acronym)
 
-def _view_list(request, clist):
-    display_config = clist.get_display_config()
-    return render_to_response('community/public/view_list.html',
-                              {'cl': clist,
-                               'dc': display_config,
-                              },
-                              context_instance=RequestContext(request))
+    significant = request.GET.get('significant', '') == '1'
 
+    documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True)
+    since = datetime.datetime.now() - datetime.timedelta(days=14)
 
-def view_personal_list(request, secret):
-    clist = get_object_or_404(CommunityList, secret=secret)
-    return _view_list(request, clist)
+    events = DocEvent.objects.filter(
+        doc__in=documents,
+        time__gte=since,
+    ).distinct().order_by('-time', '-id').select_related("doc")
 
-
-def view_group_list(request, acronym):
-    group = get_object_or_404(Group, acronym=acronym)
-    clist = get_object_or_404(CommunityList, group=group)
-    return _view_list(request, clist)
-
-
-def _atom_view(request, clist, significant=False):
-    documents = [i['pk'] for i in clist.get_documents().values('pk')]
-    startDate = datetime.datetime.now() - datetime.timedelta(days=14)
-
-    notifications = DocEvent.objects.filter(doc__pk__in=documents, time__gte=startDate)\
-                                            .distinct()\
-                                            .order_by('-time', '-id')
     if significant:
-        notifications = notifications.filter(listnotification__significant=True)
+        events = events.filter(type="changed_state", statedocevent__state__in=list(states_of_significant_change()))
 
     host = request.get_host()
     feed_url = 'https://%s%s' % (host, request.get_full_path())
     feed_id = uuid.uuid5(uuid.NAMESPACE_URL, feed_url.encode('utf-8'))
-    title = '%s RSS Feed' % clist.long_name()
+    title = u'%s RSS Feed' % clist.long_name()
     if significant:
-        subtitle = 'Document significant changes'
+        subtitle = 'Significant document changes'
     else:
         subtitle = 'Document changes'
 
-    return render_to_response('community/public/atom.xml',
-                              {'cl': clist,
-                               'entries': notifications,
-                               'title': title,
-                               'subtitle': subtitle,
-                               'id': feed_id.get_urn(),
-                               'updated': datetime.datetime.today(),
-                              },
-                              content_type='text/xml',
-                              context_instance=RequestContext(request))
+    return render(request, 'community/atom.xml', {
+        'clist': clist,
+        'entries': events[:50],
+        'title': title,
+        'subtitle': subtitle,
+        'id': feed_id.get_urn(),
+        'updated': datetime.datetime.now(),
+    }, content_type='text/xml')
 
 
-def changes_personal_list(request, secret):
-    clist = get_object_or_404(CommunityList, secret=secret)
-    return _atom_view(request, clist)
-
-
-def changes_group_list(request, acronym):
-    group = get_object_or_404(Group, acronym=acronym)
-    clist = get_object_or_404(CommunityList, group=group)
-    return _atom_view(request, clist)
-
-
-def significant_personal_list(request, secret):
-    clist = get_object_or_404(CommunityList, secret=secret)
-    return _atom_view(request, clist, significant=True)
-
-
-def significant_group_list(request, acronym):
-    group = get_object_or_404(Group, acronym=acronym)
-    clist = get_object_or_404(CommunityList, group=group)
-    return _atom_view(request, clist, significant=True)
-
-
-def _csv_list(request, clist):
-    display_config = clist.get_display_config()
-    response = HttpResponse(content_type='text/csv')
-    response['Content-Disposition'] = 'attachment; filename=draft-list.csv'
-
-    writer = csv.writer(response, dialect=csv.excel, delimiter=',')
-    header = []
-    fields = display_config.get_all_fields()
-    for field in fields:
-        header.append(field.description)
-    writer.writerow(header)
-
-    for doc in clist.get_documents():
-        row = []
-        for field in fields:
-            row.append(field().get_value(doc, raw=True))
-        writer.writerow(row)
-    return response
-
-
-def csv_personal_list(request):
-    user = request.user
-    if not user.is_authenticated():
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    clist = CommunityList.objects.get_or_create(user=user)[0]
-    if not clist.check_manager(user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    return _csv_list(request, clist)
-
-
-def csv_group_list(request, acronym):
-    group = get_object_or_404(Group, acronym=acronym)
-    if group.type.slug not in ('area', 'wg'):
+@login_required
+def subscription(request, username=None, acronym=None, group_type=None):
+    clist = lookup_community_list(username, acronym)
+    if clist.pk is None:
         raise Http404
-    clist = CommunityList.objects.get_or_create(group=group)[0]
-    if not clist.check_manager(request.user):
-        path = urlquote(request.get_full_path())
-        tup = settings.LOGIN_URL, REDIRECT_FIELD_NAME, path
-        return HttpResponseRedirect('%s?%s=%s' % tup)
-    return _csv_list(request, clist)
 
-def view_csv_personal_list(request, secret):
-    clist = get_object_or_404(CommunityList, secret=secret)
-    return _csv_list(request, clist)
+    existing_subscriptions = EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
 
-def _subscribe_list(request, clist, significant):
-    success = False
     if request.method == 'POST':
-        form = SubscribeForm(data=request.POST, clist=clist, significant=significant)
-        if form.is_valid():
-            form.save()
-            success = True
+        action = request.POST.get("action")
+        if action == "subscribe":
+            form = SubscriptionForm(request.user, clist, request.POST)
+            if form.is_valid():
+                subscription = form.save(commit=False)
+                subscription.community_list = clist
+                subscription.save()
+
+                return HttpResponseRedirect("")
+
+        elif action == "unsubscribe":
+            existing_subscriptions.filter(pk=request.POST.get("subscription_id")).delete()
+
+            return HttpResponseRedirect("")
     else:
-        form = SubscribeForm(clist=clist, significant=significant)
-    return render_to_response('community/public/subscribe.html',
-                              {'cl': clist,
-                               'form': form,
-                               'success': success,
-                              },
-                              context_instance=RequestContext(request))
+        form = SubscriptionForm(request.user, clist)
 
-
-def _unsubscribe_list(request, clist, significant):
-    success = False
-    if request.method == 'POST':
-        form = UnSubscribeForm(data=request.POST, clist=clist, significant=significant)
-        if form.is_valid():
-            form.save()
-            success = True
-    else:
-        form = UnSubscribeForm(clist=clist, significant=significant)
-    return render_to_response('community/public/unsubscribe.html',
-                              {'cl': clist,
-                               'form': form,
-                               'success': success,
-                               'significant': significant,
-                              },
-                              context_instance=RequestContext(request))
-
-
-def subscribe_personal_list(request, secret, significant=False):
-    clist = get_object_or_404(CommunityList, secret=secret)
-    return _subscribe_list(request, clist, significant=significant)
-
-
-def subscribe_group_list(request, acronym, significant=False):
-    group = get_object_or_404(Group, acronym=acronym)
-    clist = get_object_or_404(CommunityList, group=group)
-    return _subscribe_list(request, clist, significant=significant)
-
-
-def unsubscribe_personal_list(request, secret, significant=False):
-    clist = get_object_or_404(CommunityList, secret=secret)
-    return _unsubscribe_list(request, clist, significant=significant)
-
-
-def unsubscribe_group_list(request, acronym, significant=False):
-    group = get_object_or_404(Group, acronym=acronym)
-    clist = get_object_or_404(CommunityList, group=group)
-    return _unsubscribe_list(request, clist, significant=significant)
-
-
-def confirm_subscription(request, list_id, email, date, confirm_hash, significant=False):
-    clist = get_object_or_404(CommunityList, pk=list_id)
-    valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, email, 'subscribe', significant)).hexdigest() == confirm_hash
-    if not valid:
-        raise Http404
-    (subscription, created) = EmailSubscription.objects.get_or_create(
-        community_list=clist,
-        email=email,
-        significant=significant)
-    return render_to_response('community/public/subscription_confirm.html',
-                              {'cl': clist,
-                               'significant': significant,
-                              },
-                              context_instance=RequestContext(request))
-
-
-def confirm_significant_subscription(request, list_id, email, date, confirm_hash):
-    return confirm_subscription(request, list_id, email, date, confirm_hash, significant=True)
-
-
-def confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=False):
-    clist = get_object_or_404(CommunityList, pk=list_id)
-    valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, email, 'unsubscribe', significant)).hexdigest() == confirm_hash
-    if not valid:
-        raise Http404
-    EmailSubscription.objects.filter(
-        community_list=clist,
-        email=email,
-        significant=significant).delete()
-    return render_to_response('community/public/unsubscription_confirm.html',
-                              {'cl': clist,
-                               'significant': significant,
-                              },
-                              context_instance=RequestContext(request))
-
-
-def confirm_significant_unsubscription(request, list_id, email, date, confirm_hash):
-    return confirm_unsubscription(request, list_id, email, date, confirm_hash, significant=True)
+    return render(request, 'community/subscription.html', {
+        'clist': clist,
+        'form': form,
+        'existing_subscriptions': existing_subscriptions,
+    })
diff --git a/ietf/doc/templatetags/managed_groups.py b/ietf/doc/templatetags/managed_groups.py
new file mode 100644
index 000000000..113bc0262
--- /dev/null
+++ b/ietf/doc/templatetags/managed_groups.py
@@ -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
+
diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py
new file mode 100644
index 000000000..7b9e3c159
--- /dev/null
+++ b/ietf/doc/utils_search.py
@@ -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)
+
+
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index 89218495f..da9d7a26a 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -36,7 +36,6 @@ from django.http import HttpResponse, Http404 , HttpResponseForbidden
 from django.shortcuts import render, render_to_response, get_object_or_404, redirect
 from django.template import RequestContext
 from django.template.loader import render_to_string
-from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import reverse as urlreverse
 from django.conf import settings
 from django import forms
@@ -50,7 +49,7 @@ from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_wi
     can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
     needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
     get_initial_notify, make_notify_changed_event, crawl_history)
-from ietf.community.models import CommunityList
+from ietf.community.utils import augment_docs_with_tracking_info
 from ietf.group.models import Role
 from ietf.group.utils import can_manage_group_type, can_manage_materials
 from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
@@ -345,15 +344,7 @@ def document_main(request, name, rev=None):
             elif can_edit_stream_info and (not iesg_state or iesg_state.slug == 'watching'):
                 actions.append(("Submit to IESG for Publication", urlreverse('doc_to_iesg', kwargs=dict(name=doc.name))))
 
-        tracking_document = False
-        if request.user.is_authenticated():
-            try:
-                clist = CommunityList.objects.get(user=request.user)
-                clist.update()
-                if clist.get_documents().filter(name=doc.name).count() > 0:
-                    tracking_document = True
-            except ObjectDoesNotExist:
-                pass
+        augment_docs_with_tracking_info([doc], request.user)
 
         replaces = [d.name for d in doc.related_that_doc("replaces")]
         replaced_by = [d.name for d in doc.related_that("replaces")]
@@ -420,7 +411,6 @@ def document_main(request, name, rev=None):
                                        shepherd_writeup=shepherd_writeup,
                                        search_archive=search_archive,
                                        actions=actions,
-                                       tracking_document=tracking_document,
                                        presentations=presentations,
                                        ),
                                   context_instance=RequestContext(request))
diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py
index 89ef2facf..259914339 100644
--- a/ietf/doc/views_draft.py
+++ b/ietf/doc/views_draft.py
@@ -881,6 +881,7 @@ class ShepherdWriteupUploadForm(forms.Form):
     def clean_txt(self):
         return get_cleaned_text_file_content(self.cleaned_data["txt"])
 
+@login_required
 def edit_shepherd_writeup(request, name):
     """Change this document's shepherd writeup"""
     doc = get_object_or_404(Document, type="draft", name=name)
diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py
index cc64cc838..a449002c9 100644
--- a/ietf/doc/views_search.py
+++ b/ietf/doc/views_search.py
@@ -35,7 +35,6 @@ import datetime, re
 from django import forms
 from django.conf import settings
 from django.core.cache import cache
-from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import reverse as urlreverse
 from django.db.models import Q
 from django.http import Http404, HttpResponseBadRequest, HttpResponse, HttpResponseRedirect
@@ -44,16 +43,15 @@ from django.utils.cache import _generate_cache_key
 
 import debug                            # pyflakes:ignore
 
-from ietf.community.models import CommunityList
-from ietf.doc.models import ( Document, DocHistory, DocAlias, State, RelatedDocument,
-    DocEvent, LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
-from ietf.doc.expire import expirable_draft
+from ietf.doc.models import ( Document, DocHistory, DocAlias, State,
+    LastCallDocEvent, IESG_SUBSTATE_TAGS )
 from ietf.doc.fields import select2_id_doc_name_json
 from ietf.group.models import Group
 from ietf.idindex.index import active_drafts_index_by_group
 from ietf.name.models import DocTagName, DocTypeName, StreamName
 from ietf.person.models import Person
 from ietf.utils.draft_search import normalize_draftname
+from ietf.doc.utils_search import prepare_document_table
 
 
 class SearchForm(forms.Form):
@@ -62,7 +60,7 @@ class SearchForm(forms.Form):
     activedrafts = forms.BooleanField(required=False, initial=True)
     olddrafts = forms.BooleanField(required=False, initial=False)
 
-    by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state','stream')], required=False, initial='wg')
+    by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state','stream')], required=False, initial='group')
     author = forms.CharField(required=False)
     group = forms.CharField(required=False)
     stream = forms.ModelChoiceField(StreamName.objects.all().order_by('name'), empty_label="any stream", required=False)
@@ -81,7 +79,7 @@ class SearchForm(forms.Form):
             ("ad", "AD"), ("-ad", "AD (desc)"), ),
         required=False, widget=forms.HiddenInput)
 
-    doctypes = DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name');
+    doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name'), required=False)
 
     def __init__(self, *args, **kwargs):
         super(SearchForm, self).__init__(*args, **kwargs)
@@ -123,126 +121,27 @@ class SearchForm(forms.Form):
             q['state'] = q['substate'] = None
         return q
 
-def wrap_value(v):
-    return lambda: v
-
-def fill_in_search_attributes(docs):
-    # fill in some attributes for the search results to save some
-    # hairy template code and avoid repeated SQL queries
-
-    docs_dict = dict((d.pk, d) for d in docs)
-    doc_ids = docs_dict.keys()
-
-    rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=doc_ids).values_list("document_id", "name"))
-
-    # latest event cache
-    event_types = ("published_rfc",
-                   "changed_ballot_position",
-                   "started_iesg_process",
-                   "new_revision")
-    for d in docs:
-        d.latest_event_cache = dict()
-        for e in event_types:
-            d.latest_event_cache[e] = None
-
-    for e in DocEvent.objects.filter(doc__in=doc_ids, type__in=event_types).order_by('time'):
-        docs_dict[e.doc_id].latest_event_cache[e.type] = e
-
-    # telechat date, can't do this with above query as we need to get TelechatDocEvents out
-    seen = set()
-    for e in TelechatDocEvent.objects.filter(doc__in=doc_ids, type="scheduled_for_telechat").order_by('-time'):
-        if e.doc_id not in seen:
-            d = docs_dict[e.doc_id]
-            d.telechat_date = wrap_value(d.telechat_date(e))
-            seen.add(e.doc_id)
-
-    # misc
-    for d in docs:
-        # emulate canonical name which is used by a lot of the utils
-        d.canonical_name = wrap_value(rfc_aliases[d.pk] if d.pk in rfc_aliases else d.name)
-
-        if d.rfc_number() != None and d.latest_event_cache["published_rfc"]:
-            d.latest_revision_date = d.latest_event_cache["published_rfc"].time
-        elif d.latest_event_cache["new_revision"]:
-            d.latest_revision_date = d.latest_event_cache["new_revision"].time
-        else:
-            d.latest_revision_date = d.time
-
-        if d.type_id == "draft":
-            if d.get_state_slug() == "rfc":
-                d.search_heading = "RFC"
-            elif d.get_state_slug() in ("ietf-rm", "auth-rm"):
-                d.search_heading = "Withdrawn Internet-Draft"
-            else:
-                d.search_heading = "%s Internet-Draft" % d.get_state()
-        else:
-            d.search_heading = "%s" % (d.type,);
-
-        d.expirable = expirable_draft(d)
-
-        if d.get_state_slug() != "rfc":
-            d.milestones = d.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
-
-
-
-    # RFCs
-
-    # errata
-    erratas = set(Document.objects.filter(tags="errata", name__in=rfc_aliases.keys()).distinct().values_list("name", flat=True))
-    for d in docs:
-        d.has_errata = d.name in erratas
-
-    # obsoleted/updated by
-    for a in rfc_aliases:
-        d = docs_dict[a]
-        d.obsoleted_by_list = []
-        d.updated_by_list = []
-
-    xed_by = RelatedDocument.objects.filter(target__name__in=rfc_aliases.values(),
-                                            relationship__in=("obs", "updates")).select_related('target__document_id')
-    rel_rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc",
-                                                   document__in=[rel.source_id for rel in xed_by]).values_list('document_id', 'name'))
-    for rel in xed_by:
-        d = docs_dict[rel.target.document_id]
-        if rel.relationship_id == "obs":
-            l = d.obsoleted_by_list
-        elif rel.relationship_id == "updates":
-            l = d.updated_by_list
-        l.append(rel_rfc_aliases[rel.source_id].upper())
-        l.sort()
-
-
 def retrieve_search_results(form, all_types=False):
-
     """Takes a validated SearchForm and return the results."""
+
     if not form.is_valid():
         raise ValueError("SearchForm doesn't validate: %s" % form.errors)
 
     query = form.cleaned_data
 
-    types=[];
-    meta = {}
-
-    if (query['activedrafts'] or query['olddrafts'] or query['rfcs']):
-        types.append('draft')
-
-    # Advanced document types are data-driven, so we need to read them from the
-    # raw form.data field (and track their checked/unchecked state ourselves)
-    meta['checked'] = {}
-    alltypes = DocTypeName.objects.exclude(slug='draft').order_by('name');
-    for doctype in alltypes:
-        if form.data.__contains__('include-' + doctype.slug):
-            types.append(doctype.slug)
-            meta['checked'][doctype.slug] = True
-
-    if len(types) == 0 and not all_types:
-        return ([], {})
-
-    MAX = 500
-
     if all_types:
         docs = Document.objects.all()
     else:
+        types = []
+
+        if query['activedrafts'] or query['olddrafts'] or query['rfcs']:
+            types.append('draft')
+
+        types.extend(query["doctypes"])
+
+        if not types:
+            return []
+
         docs = Document.objects.filter(type__in=types)
 
     # name
@@ -281,104 +180,7 @@ def retrieve_search_results(form, all_types=False):
     elif by == "stream":
         docs = docs.filter(stream=query["stream"])
 
-    # evaluate and fill in attribute results immediately to cut down
-    # the number of queries
-    docs = docs.select_related("ad", "ad__person", "std_level", "intended_std_level", "group", "stream")
-    docs = docs.prefetch_related("states__type", "tags")
-    results = list(docs[:MAX])
-
-    fill_in_search_attributes(results)
-
-    # sort
-    def sort_key(d):
-        res = []
-
-        rfc_num = d.rfc_number()
-
-
-        if d.type_id == "draft":
-            res.append(["Active", "Expired", "Replaced", "Withdrawn", "RFC"].index(d.search_heading.split()[0] ))
-        else:
-            res.append(d.type_id);
-            res.append("-");
-            res.append(d.get_state_slug());
-            res.append("-");
-
-        if query["sort"] in ["title", "-title"]:
-            res.append(d.title)
-        elif query["sort"]  in ["date", "-date" ]:
-            res.append(str(d.latest_revision_date))
-        elif query["sort"]  in ["status", "-status"]:
-            if rfc_num != None:
-                res.append(int(rfc_num))
-            else:
-                res.append(d.get_state().order if d.get_state() else None)
-        elif query["sort"]  in ["ipr", "-ipr"]:
-            res.append(len(d.ipr()))
-        elif query["sort"]  in ["ad", "-ad"]:
-            if rfc_num != None:
-                res.append(int(rfc_num))
-            elif d.get_state_slug() == "active":
-                if d.get_state("draft-iesg"):
-                    res.append(d.get_state("draft-iesg").order)
-                else:
-                    res.append(0)
-        else:
-            if rfc_num != None:
-                res.append(int(rfc_num))
-            else:
-                res.append(d.canonical_name())
-
-        return res
-
-    results.sort(key=sort_key, reverse=query["sort"].startswith("-"))
-
-    # fill in a meta dict with some information for rendering the result table
-    if len(results) == MAX:
-        meta['max'] = MAX
-    meta['by'] = query['by']
-    meta['advanced'] = bool(query['by'] or len(meta['checked']))
-
-    meta['headers'] = [{'title': 'Document', 'key':'document'},
-                       {'title': 'Title', 'key':'title'},
-                       {'title': 'Date', 'key':'date'},
-                       {'title': 'Status', 'key':'status'},
-                       {'title': 'IPR', 'key':'ipr'},
-                       {'title': 'AD / Shepherd', 'key':'ad'}]
-
-    if hasattr(form.data, "urlencode"): # form was fed a Django QueryDict, not local plain dict
-        d = form.data.copy()
-        for h in meta['headers']:
-            sort = query.get('sort')
-            if sort.endswith(h['key']):
-                h['sorted'] = True
-                if sort.startswith('-'):
-                    h['direction'] = 'desc'
-                    d["sort"] = h["key"]                    
-                else:
-                    h['direction'] = 'asc'
-                    d["sort"] = "-" + h["key"]
-            else:
-                d["sort"] = h["key"]
-            h["sort_url"] = "?" + d.urlencode()
-                
-    return (results, meta)
-
-
-def get_doc_is_tracked(request, results):
-    # Determine whether each document is being tracked or not, and remember
-    # that so we can display the proper track/untrack option.
-    doc_is_tracked = { }
-    if request.user.is_authenticated():
-        try:
-            clist = CommunityList.objects.get(user=request.user)
-            clist.update()
-        except ObjectDoesNotExist:
-            return doc_is_tracked
-        for doc in results:
-            if clist.get_documents().filter(name=doc.name).count() > 0:
-                doc_is_tracked[doc.name] = True
-    return doc_is_tracked
+    return docs
 
 def search(request):
     if request.GET:
@@ -395,17 +197,16 @@ def search(request):
         if not form.is_valid():
             return HttpResponseBadRequest("form not valid: %s" % form.errors)
 
-        results, meta = retrieve_search_results(form)
+        results = retrieve_search_results(form)
+        results, meta = prepare_document_table(request, results, get_params)
         meta['searching'] = True
     else:
         form = SearchForm()
         results = []
-        meta = { 'by': None, 'advanced': False, 'searching': False }
-
-    doc_is_tracked = get_doc_is_tracked(request, results)
+        meta = { 'by': None, 'searching': False }
 
     return render(request, 'doc/search/search.html', {
-        'form':form, 'docs':results, 'doc_is_tracked':doc_is_tracked, 'meta':meta, },
+        'form':form, 'docs':results, 'meta':meta, },
     )
 
 def frontpage(request):
@@ -472,7 +273,7 @@ def search_for_name(request, name):
     else:
         for t in doctypenames:
             if n.startswith(t.prefix):
-                search_args += "&include-%s=on" % t.slug
+                search_args += "&doctypes=%s" % t.slug
                 break
         else:
             search_args += "&rfcs=on&activedrafts=on&olddrafts=on"
@@ -579,8 +380,9 @@ def docs_for_ad(request, name):
         raise Http404
     form = SearchForm({'by':'ad','ad': ad.id,
                        'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
-                       'sort': 'status'})
-    results, meta = retrieve_search_results(form, all_types=True)
+                       'sort': 'status',
+                       'doctypes': list(DocTypeName.objects.filter(used=True).exclude(slug='draft').values_list("pk", flat=True))})
+    results, meta = prepare_document_table(request, retrieve_search_results(form), form.data)
     results.sort(key=ad_dashboard_sort_key)
     del meta["headers"][-1]
     #
@@ -594,7 +396,7 @@ def docs_for_ad(request, name):
 def drafts_in_last_call(request):
     lc_state = State.objects.get(type="draft-iesg", slug="lc").pk
     form = SearchForm({'by':'state','state': lc_state, 'rfcs':'on', 'activedrafts':'on'})
-    results, meta = retrieve_search_results(form)
+    results, meta = prepare_document_table(request, retrieve_search_results(form), form.data)
 
     return render(request, 'doc/drafts_in_last_call.html', {
         'form':form, 'docs':results, 'meta':meta
diff --git a/ietf/group/edit.py b/ietf/group/edit.py
index b2773d54c..fe19b9e7f 100644
--- a/ietf/group/edit.py
+++ b/ietf/group/edit.py
@@ -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
diff --git a/ietf/group/info.py b/ietf/group/info.py
index fe66fc626..1841f62d9 100644
--- a/ietf/group/info.py
+++ b/ietf/group/info.py
@@ -39,7 +39,7 @@ from tempfile import mkstemp
 import datetime
 from collections import OrderedDict
 
-from django.shortcuts import render, redirect
+from django.shortcuts import render, redirect, get_object_or_404
 from django.template.loader import render_to_string
 from django.http import HttpResponse, Http404, HttpResponseRedirect
 from django.conf import settings
@@ -48,14 +48,16 @@ from django.views.decorators.cache import cache_page
 from django.db.models import Q
 from django.utils.safestring import mark_safe
 
-from ietf.doc.views_search import SearchForm, retrieve_search_results, get_doc_is_tracked
 from ietf.doc.models import Document, State, DocAlias, RelatedDocument
 from ietf.doc.utils import get_chartering_type
 from ietf.doc.templatetags.ietf_filters import clean_whitespace
+from ietf.doc.utils_search import prepare_document_table
 from ietf.group.models import Group, Role, ChangeStateGroupEvent
 from ietf.name.models import GroupTypeName
 from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
 from ietf.group.utils import can_manage_materials, get_group_or_404
+from ietf.community.utils import docs_tracked_by_community_list, can_manage_community_list
+from ietf.community.models import CommunityList, EmailSubscription
 from ietf.utils.pipe import pipe
 from ietf.settings import MAILING_LIST_INFO_URL
 from ietf.mailtrigger.utils import gather_relevant_expansions
@@ -369,6 +371,11 @@ def construct_group_menu_context(request, group, selected, group_type, others):
         if group.state_id != "proposed" and (is_chair or can_manage):
             actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
 
+    if group.features.has_documents:
+        clist = CommunityList.objects.filter(group=group).first()
+        if clist and can_manage_community_list(request.user, clist):
+            actions.append((u'Manage document list', urlreverse('community_group_manage_list', kwargs=kwargs)))
+
     if group.features.has_materials and can_manage_materials(request.user, group):
         actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
 
@@ -393,36 +400,23 @@ def construct_group_menu_context(request, group, selected, group_type, others):
 
     return d
 
-def search_for_group_documents(group):
-    form = SearchForm({ 'by':'group', 'group': group.acronym or "", 'rfcs':'on', 'activedrafts': 'on' })
-    docs, meta = retrieve_search_results(form)
-
-    # get the related docs
-    form_related = SearchForm({ 'by':'group', 'name': u'-%s-' % group.acronym, 'activedrafts': 'on' })
-    raw_docs_related, meta_related = retrieve_search_results(form_related)
+def prepare_group_documents(request, group, clist):
+    found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET)
 
+    docs = []
     docs_related = []
-    for d in raw_docs_related:
-        parts = d.name.split("-", 2);
-        # canonical form draft-<name|ietf|irtf>-wg-etc
-        if len(parts) >= 3 and parts[1] not in ("ietf", "irtf") and parts[2].startswith(group.acronym + "-") and d not in docs:
+
+    # split results
+    for d in found_docs:
+        # non-WG drafts and call for WG adoption are considered related
+        if (d.group != group
+            or (d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"))):
             d.search_heading = "Related Internet-Draft"
             docs_related.append(d)
-
-    # move call for WG adoption to related
-    cleaned_docs = []
-    docs_related_names = set(d.name for d in docs_related)
-    for d in docs:
-        if d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"):
-            if d.name not in docs_related_names:
-                d.search_heading = "Related Internet-Draft"
-                docs_related.append(d)
         else:
-            cleaned_docs.append(d)
+            docs.append(d)
 
-    docs = cleaned_docs
-
-    docs_related.sort(key=lambda d: d.name)
+    meta_related = meta.copy()
 
     return docs, meta, docs_related, meta_related
 
@@ -438,17 +432,19 @@ def group_documents(request, acronym, group_type=None):
     if not group.features.has_documents:
         raise Http404
 
-    docs, meta, docs_related, meta_related = search_for_group_documents(group)
+    clist = get_object_or_404(CommunityList, group=group)
 
-    doc_is_tracked = get_doc_is_tracked(request, docs)
-    doc_is_tracked.update(get_doc_is_tracked(request, docs_related))
+    docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
+
+    subscribed = request.user.is_authenticated() and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user)
 
     context = construct_group_menu_context(request, group, "documents", group_type, {
                 'docs': docs,
                 'meta': meta,
                 'docs_related': docs_related,
                 'meta_related': meta_related,
-                'doc_is_tracked': doc_is_tracked,
+                'subscribed': subscribed,
+                'clist': clist,
                 })
 
     return render(request, 'group/group_documents.html', context)
@@ -459,7 +455,9 @@ def group_documents_txt(request, acronym, group_type=None):
     if not group.features.has_documents:
         raise Http404
 
-    docs, meta, docs_related, meta_related = search_for_group_documents(group)
+    clist = get_object_or_404(CommunityList, group=group)
+
+    docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist)
 
     for d in docs:
         d.prefix = d.get_state().name
diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py
index 5dc560098..ea3919c59 100644
--- a/ietf/group/tests_info.py
+++ b/ietf/group/tests_info.py
@@ -15,7 +15,7 @@ from django.core.urlresolvers import NoReverseMatch
 
 from ietf.doc.models import Document, DocAlias, DocEvent, State
 from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions 
-from ietf.group.utils import save_group_in_history
+from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
 from ietf.name.models import DocTagName, GroupStateName, GroupTypeName
 from ietf.person.models import Person, Email
 from ietf.utils.test_utils import TestCase, unicontent
@@ -186,20 +186,21 @@ class GroupPagesTests(TestCase):
             name=draft2.name,
             )
 
+        setup_default_community_list_for_group(group)
+
         url = urlreverse('ietf.group.info.group_documents', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
         self.assertTrue(draft.name in unicontent(r))
         self.assertTrue(group.name in unicontent(r))
         self.assertTrue(group.acronym in unicontent(r))
-
         self.assertTrue(draft2.name in unicontent(r))
 
         # Make sure that a logged in user is presented with an opportunity to add results to their community list
         self.client.login(username="secretary", password="secretary+password")
         r = self.client.get(url)
         q = PyQuery(r.content)
-        self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.community-list-add-remove-doc')]))
+        self.assertTrue(any([draft2.name in x.attrib['href'] for x in q('table td a.track-untrack-doc')]))
 
         # test the txt version too while we're at it
         url = urlreverse('ietf.group.info.group_documents_txt', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
diff --git a/ietf/group/urls_info_details.py b/ietf/group/urls_info_details.py
index 86b41b37e..89d5b65f5 100644
--- a/ietf/group/urls_info_details.py
+++ b/ietf/group/urls_info_details.py
@@ -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'),
diff --git a/ietf/group/utils.py b/ietf/group/utils.py
index 9e2a37a75..1254221f9 100644
--- a/ietf/group/utils.py
+++ b/ietf/group/utils.py
@@ -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)
diff --git a/ietf/group/views_stream.py b/ietf/group/views_stream.py
index 6ec34702c..9a8d5887c 100644
--- a/ietf/group/views_stream.py
+++ b/ietf/group/views_stream.py
@@ -5,7 +5,8 @@ from django.template import RequestContext
 from django.http import Http404, HttpResponseForbidden
 from django import forms
 
-from ietf.doc.views_search import SearchForm, retrieve_search_results
+from ietf.doc.models import Document
+from ietf.doc.utils_search import prepare_document_table
 from ietf.group.models import Group, GroupEvent, Role
 from ietf.group.utils import save_group_in_history
 from ietf.ietfauth.utils import has_role
@@ -27,9 +28,9 @@ def stream_documents(request, acronym):
     group = get_object_or_404(Group, acronym=acronym)
     editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
     stream = StreamName.objects.get(slug=acronym)
-    form = SearchForm({'by':'stream', 'stream':acronym,
-                       'rfcs':'on', 'activedrafts':'on'})
-    docs, meta = retrieve_search_results(form)
+
+    qs = Document.objects.filter(states__type="draft", states__slug__in=["active", "rfc"], stream=acronym)
+    docs, meta = prepare_document_table(request, qs)
     return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable }, context_instance=RequestContext(request))
 
 class StreamEditForm(forms.Form):
diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py
index e2c15bba4..2747dfbba 100644
--- a/ietf/iesg/views.py
+++ b/ietf/iesg/views.py
@@ -60,7 +60,7 @@ from ietf.iesg.models import TelechatDate
 from ietf.iesg.utils import telechat_page_count
 from ietf.ietfauth.utils import has_role, role_required, user_is_person
 from ietf.person.models import Person
-from ietf.doc.views_search import fill_in_search_attributes
+from ietf.doc.utils_search import fill_in_document_table_attributes
 
 def review_decisions(request, year=None):
     events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
@@ -370,7 +370,7 @@ def agenda_documents(request):
         sections = agenda_sections()
         # augment the docs with the search attributes, since we're using
         # the search_result_row view to display them (which expects them)
-        fill_in_search_attributes(docs_by_date[date])
+        fill_in_document_table_attributes(docs_by_date[date])
         fill_in_agenda_docs(date, sections, docs_by_date[date])
 
         telechats.append({
diff --git a/ietf/ietfauth/urls.py b/ietf/ietfauth/urls.py
index d7156642f..a87da91bc 100644
--- a/ietf/ietfauth/urls.py
+++ b/ietf/ietfauth/urls.py
@@ -10,7 +10,7 @@ urlpatterns = patterns('ietf.ietfauth.views',
         url(r'^logout/$', logout),
 #        url(r'^loggedin/$', 'ietf_loggedin'),
 #        url(r'^loggedout/$', 'logged_out'),
-        url(r'^profile/$', 'profile'),
+        url(r'^profile/$', 'profile', name="account_profile"),
 #        (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'url_login'),
         url(r'^testemail/$', 'test_email'),
         url(r'^create/$', 'create_account', name='create_account'),
diff --git a/ietf/secr/groups/views.py b/ietf/secr/groups/views.py
index feb55d6c1..be14d3fb2 100644
--- a/ietf/secr/groups/views.py
+++ b/ietf/secr/groups/views.py
@@ -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,
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 21ebd7f27..2aacf362b 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -448,3 +448,14 @@ form.navbar-form input.form-control.input-sm { width: 141px; }
     padding: inherit;
     outline: inherit;
 }
+
+/* Community lists */
+
+label#list-feeds {
+  display: inline-block;
+  font-weight: normal;
+}
+
+.email-subscription button[type=submit] {
+  margin-left: 3em;
+}
diff --git a/ietf/static/ietf/js/ietf.js b/ietf/static/ietf/js/ietf.js
index f40e3a5b7..709ec3374 100644
--- a/ietf/static/ietf/js/ietf.js
+++ b/ietf/static/ietf/js/ietf.js
@@ -100,24 +100,20 @@ $(document).ready(function () {
     }
 
     // search results
-    $('.community-list-add-remove-doc').click(function(e) {
+    $('.track-untrack-doc').click(function(e) {
 	e.preventDefault();
-	var trigger = $(this);
-	$.ajax({
+        var trigger = $(this);
+        $.ajax({
 	    url: trigger.attr('href'),
-	    type: 'GET',
+            type: 'POST',
 	    cache: false,
 	    dataType: 'json',
 	    success: function(response){
 		if (response.success) {
-                trigger.parent().find(".tooltip").remove();
-                trigger.find("span.fa").toggleClass("fa-bookmark fa-bookmark-o");
-                if (trigger.hasClass('btn')) {
-                    trigger.attr('disabled', true).blur();
-                } else {
-                    trigger.contents().unwrap().blur();
+                    trigger.parent().find(".tooltip").remove();
+                    trigger.addClass("hide");
+                    trigger.parent().find(".track-untrack-doc").not(trigger).removeClass("hide");
                 }
-            }
 	    }
 	});
     });
diff --git a/ietf/static/ietf/js/manage-community-list.js b/ietf/static/ietf/js/manage-community-list.js
new file mode 100644
index 000000000..f6c1ade8b
--- /dev/null
+++ b/ietf/static/ietf/js/manage-community-list.js
@@ -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");
+    });
+});
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index 310c11d69..6a8c4867c 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -20,6 +20,7 @@ from ietf.person.models import Person
 from ietf.group.models import Group
 from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
 from ietf.submit.models import Submission, Preapproval
+from ietf.group.utils import setup_default_community_list_for_group
 
 class SubmitTests(TestCase):
     def setUp(self):
@@ -143,7 +144,8 @@ class SubmitTests(TestCase):
     def submit_new_wg(self, formats):
         # submit new -> supply submitter info -> approve
         draft = make_test_data()
-        
+        setup_default_community_list_for_group(draft.group)
+
         # prepare draft to suggest replace
         sug_replaced_draft = Document.objects.create(
             name="draft-ietf-ames-sug-replaced",
diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py
index 4562f780f..2e868e46b 100644
--- a/ietf/submit/utils.py
+++ b/ietf/submit/utils.py
@@ -13,6 +13,7 @@ from ietf.group.models import Group
 from ietf.ietfauth.utils import has_role
 from ietf.name.models import StreamName
 from ietf.person.models import Person, Email
+from ietf.community.utils import update_name_contains_indexes_with_new_doc
 from ietf.submit.mail import announce_to_lists, announce_new_version, announce_to_authors
 from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
 from ietf.utils import unaccent
@@ -126,7 +127,7 @@ def post_submission(request, submission):
     if not (group.type_id == "individ" and draft.group and draft.group.type_id == "area"):
         # don't overwrite an assigned area if it's still an individual
         # submission
-        draft.group_id = group.pk
+        draft.group = group
     draft.rev = submission.rev
     draft.pages = submission.pages
     draft.abstract = submission.abstract
@@ -205,6 +206,8 @@ def post_submission(request, submission):
 
     new_replaces, new_possibly_replaces = update_replaces_from_submission(request, submission, draft)
 
+    update_name_contains_indexes_with_new_doc(draft)
+
     announce_to_lists(request, submission)
     announce_new_version(request, submission, draft, state_change_msg)
     announce_to_authors(request, submission)
diff --git a/ietf/templates/base/menu.html b/ietf/templates/base/menu.html
index d79f92a26..b19c555b8 100644
--- a/ietf/templates/base/menu.html
+++ b/ietf/templates/base/menu.html
@@ -1,5 +1,5 @@
 {# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
-{% load ietf_filters community_tags wg_menu streams_menu active_groups_menu %}
+{% load ietf_filters managed_groups wg_menu streams_menu active_groups_menu %}
 
 {% if flavor != "top" %}
   {% include "base/menu_user.html" %}
@@ -51,14 +51,14 @@
     <li><a href="{% url "submit_approvals" %}">Approve a draft</a></li>
   {% endif %}
 
-  {% get_user_managed_lists user as community_lists %}
-  {% if community_lists %}
-    <li><a href="{{ community_lists.personal.get_manage_url }}">My tracked docs</a></li>
-    {% for cl in community_lists.group %}
-      <li><a href="{{ cl.get_manage_url }}">{{ cl.short_name }} {{cl.group.type.slug}} docs</a></li>
+  {% if user and user.is_authenticated %}
+    <li><a href="{% url "community_personal_view_list" user.username %}">My tracked docs</a></li>
+
+    {% for g in user|managed_groups %}
+      <li><a href="{% url "group_docs" g.acronym %}">{{ g.acronym }} {{ g.type.slug }} docs</a></li>
     {% endfor %}
   {% else %}
-    <li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in to track docs</a></li>
+    <li><a rel="nofollow" href="/accounts/login/?next={{ request.get_full_path|urlencode }}">Sign in to track docs</a></li>
   {% endif %}
 
   {% if user|has_role:"Area Director,Secretariat" %}
diff --git a/ietf/templates/community/public/atom.xml b/ietf/templates/community/atom.xml
similarity index 100%
rename from ietf/templates/community/public/atom.xml
rename to ietf/templates/community/atom.xml
diff --git a/ietf/templates/community/customize_display.html b/ietf/templates/community/customize_display.html
deleted file mode 100644
index 57032786a..000000000
--- a/ietf/templates/community/customize_display.html
+++ /dev/null
@@ -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>
diff --git a/ietf/templates/community/display_field.html b/ietf/templates/community/display_field.html
deleted file mode 100644
index 843377c18..000000000
--- a/ietf/templates/community/display_field.html
+++ /dev/null
@@ -1,2 +0,0 @@
-{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
-<span class="displayField displayField-{{ field.codename }}">{{ value|safe }}</span>
diff --git a/ietf/templates/community/list_menu.html b/ietf/templates/community/list_menu.html
new file mode 100644
index 000000000..f1c1fc62e
--- /dev/null
+++ b/ietf/templates/community/list_menu.html
@@ -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>
diff --git a/ietf/templates/community/manage_clist.html b/ietf/templates/community/manage_clist.html
index a6e32521e..624e9b3aa 100644
--- a/ietf/templates/community/manage_clist.html
+++ b/ietf/templates/community/manage_clist.html
@@ -49,8 +49,8 @@
 	    <tr>
 	      <td>{{ doc.display_name }}</td>
 	      <td>{{ doc.get_state }}</td>
-	      <td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></td>
-	      <td><a class="btn btn-danger btn-xs" href="{% url "community_remove_document" cl.pk doc.pk %}">Remove</a></td>
+              <td><a href="{{ doc.get_absolute_url }}">{{ doc.title }}</a></td>
+              <td><a class="btn btn-danger btn-xs" href="{% if cl.user %}{% url "community_personal_untrack_document" doc.pk %}{% else %}{% url "community_group_untrack_document" %}{% endif %}">Remove</a></td>
 	    </tr>
 	  {% endfor %}
         </tbody>
diff --git a/ietf/templates/community/manage_list.html b/ietf/templates/community/manage_list.html
new file mode 100644
index 000000000..89ff91bb5
--- /dev/null
+++ b/ietf/templates/community/manage_list.html
@@ -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 %}
diff --git a/ietf/templates/community/public/notification_email.txt b/ietf/templates/community/notification_email.txt
similarity index 51%
rename from ietf/templates/community/public/notification_email.txt
rename to ietf/templates/community/notification_email.txt
index 6db28028d..73a130d9f 100644
--- a/ietf/templates/community/public/notification_email.txt
+++ b/ietf/templates/community/notification_email.txt
@@ -3,14 +3,14 @@ Hello,
 
 This is a notification from the {{ clist.long_name }}.
 
-Document: {{ notification.doc }},
-https://datatracker.ietf.org/doc/{{ notification.doc }}
+Document: {{ event.doc }},
+https://datatracker.ietf.org/doc/{{ event.doc_id }}/
 
 Change:
-{{ notification.desc|textify|striptags }}
+{{ event.desc|textify|striptags }}
 
 Best regards,
 
-        The datatracker draft tracking service
+        The Datatracker draft tracking service
         (for the IETF Secretariat)
 {% endautoescape %}
diff --git a/ietf/templates/community/public/subscribe.html b/ietf/templates/community/public/subscribe.html
deleted file mode 100644
index 1ad53d026..000000000
--- a/ietf/templates/community/public/subscribe.html
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/public/subscribe_email.txt b/ietf/templates/community/public/subscribe_email.txt
deleted file mode 100644
index b0a8b3fc8..000000000
--- a/ietf/templates/community/public/subscribe_email.txt
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/public/subscription_confirm.html b/ietf/templates/community/public/subscription_confirm.html
deleted file mode 100644
index 55cce1f35..000000000
--- a/ietf/templates/community/public/subscription_confirm.html
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/public/unsubscribe.html b/ietf/templates/community/public/unsubscribe.html
deleted file mode 100644
index 7f7ad4013..000000000
--- a/ietf/templates/community/public/unsubscribe.html
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/public/unsubscribe_email.txt b/ietf/templates/community/public/unsubscribe_email.txt
deleted file mode 100644
index 7b93d2f30..000000000
--- a/ietf/templates/community/public/unsubscribe_email.txt
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/public/unsubscription_confirm.html b/ietf/templates/community/public/unsubscription_confirm.html
deleted file mode 100644
index 8f9a109d7..000000000
--- a/ietf/templates/community/public/unsubscription_confirm.html
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/public/view_list.html b/ietf/templates/community/public/view_list.html
deleted file mode 100644
index 2a475af17..000000000
--- a/ietf/templates/community/public/view_list.html
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/raw_view.html b/ietf/templates/community/raw_view.html
deleted file mode 100644
index 9ec31ca12..000000000
--- a/ietf/templates/community/raw_view.html
+++ /dev/null
@@ -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 %}
diff --git a/ietf/templates/community/subscription.html b/ietf/templates/community/subscription.html
new file mode 100644
index 000000000..0a4c337e5
--- /dev/null
+++ b/ietf/templates/community/subscription.html
@@ -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 %}
diff --git a/ietf/templates/community/track_document.html b/ietf/templates/community/track_document.html
new file mode 100644
index 000000000..b592495f2
--- /dev/null
+++ b/ietf/templates/community/track_document.html
@@ -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>
diff --git a/ietf/templates/community/untrack_document.html b/ietf/templates/community/untrack_document.html
new file mode 100644
index 000000000..ef5156363
--- /dev/null
+++ b/ietf/templates/community/untrack_document.html
@@ -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>
diff --git a/ietf/templates/community/view_list.html b/ietf/templates/community/view_list.html
index deb71d692..e329180ae 100644
--- a/ietf/templates/community/view_list.html
+++ b/ietf/templates/community/view_list.html
@@ -1,4 +1,23 @@
+{% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin %}{% origin %}
-{% load community_tags %}
-{% get_clist_view cl %}
+{% load origin %}
+{% load bootstrap3 %}
+
+{% block title %}{{ clist.long_name }}{% endblock %}
+
+{% block content %}
+  {% origin %}
+  <h1>{{ clist.long_name }}</h1>
+
+  {% bootstrap_messages %}
+
+  {% if can_manage_list %}
+    <a class="btn btn-primary" href="{% url "community_personal_manage_list" clist.user.username %}">
+      <i class="glyphicon glyphicon-cog"></i>
+      Manage list
+    </a>
+  {% endif %}
+
+  {% include "doc/search/search_results.html" with skip_no_matches_warning=True %}
+  {% include "community/list_menu.html" %}
+{% endblock %}
diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html
index 8093350f5..1eb926f83 100644
--- a/ietf/templates/doc/document_draft.html
+++ b/ietf/templates/doc/document_draft.html
@@ -490,11 +490,8 @@
       </ul>
     </div>
     {% if user.is_authenticated %}
-      {% if tracking_document %}
-        <a class="btn btn-default btn-xs community-list-add-remove-doc" href="{% url "community_remove_track_document" doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark-o"></span> Untrack</a>
-      {% else %}
-        <a class="btn btn-default btn-xs community-list-add-remove-doc" href="{% url "community_add_track_document" doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark"></span> Track</a>
-      {% endif %}
+      <a class="btn btn-default btn-xs track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "community_personal_untrack_document" user.username doc.name %}" title="Remove from your personal ID list"><span class="fa fa-bookmark"></span> Untrack</a>
+      <a class="btn btn-default btn-xs track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "community_personal_track_document" user.username doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark-o"></span> Track</a>
     {% endif %}
 
     {% if can_edit and iesg_state %}
diff --git a/ietf/templates/doc/search/search_form.html b/ietf/templates/doc/search/search_form.html
index 573a63c09..4133b12d3 100644
--- a/ietf/templates/doc/search/search_form.html
+++ b/ietf/templates/doc/search/search_form.html
@@ -42,10 +42,10 @@
 		<label class="control-label" for="id_olddrafts">{{ form.olddrafts }} Internet-Draft (expired, replaced or withdrawn)</label>
 	      </div>
 
-	      {% for doc_type in form.doctypes %}
+              {% for value, label in form.fields.doctypes.choices %}
 	        <div class="checkbox">
-		  <label class="control-label" for="id_{{doc_type.slug}}">
-                    <input type="checkbox" class="advdoctype" {% if doc_type.slug in meta.checked %}checked{% endif %} name="include-{{doc_type.slug}}" id="id_{{doc_type.slug}}"/>{{doc_type|safe|capfirst_allcaps}}
+                  <label class="control-label" for="id_doctypes_{{ value }}">
+                    <input type="checkbox" class="advdoctype" {% if value in form.doctypes.value %}checked{% endif %} name="doctypes" value="{{ value }}" id="id_doctypes_{{ value }}"/>{{ label|safe|capfirst_allcaps}}
 		  </label>
 	        </div>
 	      {% endfor %}
@@ -54,7 +54,7 @@
 
 	  <div class="form-group search_field">
 	    <div class="col-sm-4">
-	      <input type="radio" name="by" value="author" {% if meta.by == "author" %}checked{% endif %}/>
+              <input type="radio" name="by" value="author" {% if form.by.value == "author" %}checked{% endif %} id="id_author"/>
 	      <label for="id_author" class="control-label">Author</label>
 	    </div>
 	    <div class="col-sm-8">
@@ -64,7 +64,7 @@
 
 	  <div class="form-group search_field">
 	    <div class="col-sm-4">
-	      <input type="radio" name="by" value="group" {% if meta.by == "group" %}checked{% endif %}/>
+              <input type="radio" name="by" value="group" {% if form.by.value == "group" %}checked{% endif %} id="id_group"/>
 	      <label for="id_group" class="control-label">WG</label>
 	    </div>
 	    <div class="col-sm-8">
@@ -75,7 +75,7 @@
 
 	  <div class="form-group search_field">
 	    <div class="col-sm-4">
-	      <input type="radio" name="by" value="area" {% if meta.by == "area" %}checked{% endif %}/>
+              <input type="radio" name="by" value="area" {% if form.by.value == "area" %}checked{% endif %} id="id_area"/>
 	      <label for="id_area" class="control-label">Area</label>
 	    </div>
 	    <div class="col-sm-8">
@@ -85,7 +85,7 @@
 
 	  <div class="form-group search_field">
 	    <div class="col-sm-4">
-	      <input type="radio" name="by" value="ad" {% if meta.by == "ad" %}checked{% endif %}/>
+              <input type="radio" name="by" value="ad" {% if form.by.value == "ad" %}checked{% endif %} id="id_ad"/>
 	      <label for="id_ad" class="control-label">AD</label>
 	    </div>
 	    <div class="col-sm-8">
@@ -95,7 +95,7 @@
 
 	  <div class="form-group search_field">
 	    <div class="col-sm-4">
-	      <input type="radio" name="by" value="state" {% if meta.by == "state" %}checked{% endif %}/>
+              <input type="radio" name="by" value="state" {% if form.by.value == "state" %}checked{% endif %} id="id_state"/>
 	      <label for="id_state" class="control-label">IESG State</label>
 	    </div>
 	    <div class="col-sm-4">
@@ -108,7 +108,7 @@
 
 	  <div class="form-group search_field">
 	    <div class="col-sm-4">
-	      <input type="radio" name="by" value="stream" {% if meta.by == "stream" %}checked{% endif %}/>
+              <input type="radio" name="by" value="stream" {% if form.by.value == "stream" %}checked{% endif %} id="id_stream"/>
 	      <label for="id_stream" class="control-label">Stream</label>
 	    </div>
 	    <div class="col-sm-4">
diff --git a/ietf/templates/doc/search/search_result_row.html b/ietf/templates/doc/search/search_result_row.html
index e74e3cbc2..71cfc9dc7 100644
--- a/ietf/templates/doc/search/search_result_row.html
+++ b/ietf/templates/doc/search/search_result_row.html
@@ -13,15 +13,12 @@
 
   <td>
     {% if user.is_authenticated %}
-      {% if doc.name in doc_is_tracked %}
-        <a href="{% url "community_remove_track_document" doc.name %}" class="community-list-add-remove-doc" title="Remove from your personal ID list">
-          <span class="fa fa-bookmark"></span>
-        </a>
-      {% else %}
-        <a href="{% url "community_add_track_document" doc.name %}" class="community-list-add-remove-doc" title="Add to your personal ID list">
-          <span class="fa fa-bookmark-o"></span>
-        </a>
-      {% endif %}
+      <a href="{% url "community_personal_untrack_document" request.user.username doc.name %}" class="track-untrack-doc {% if not doc.tracked_in_personal_community_list %}hide{% endif %}" title="Remove from your personal ID list">
+        <span class="fa fa-bookmark"></span>
+      </a>
+      <a href="{% url "community_personal_track_document" request.user.username doc.name %}" class="track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" title="Add to your personal ID list">
+        <span class="fa fa-bookmark-o"></span>
+      </a>
     {% endif %}
   </td>
 
diff --git a/ietf/templates/group/group_documents.html b/ietf/templates/group/group_documents.html
index 87484c6a9..301b8ea55 100644
--- a/ietf/templates/group/group_documents.html
+++ b/ietf/templates/group/group_documents.html
@@ -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 %}