Commit f59cc89813f4d24fe99873e80b451770775aacea

Authored by Sergio Oliveira
2 parents 256ed785 058dc7b0

Merge branch 'private_mailman_lists' into 'master'

Private mailman lists

Only show public lists by default.
Private lists are only visible if user if a member of that list.
That applies to /dashboard /archives/thread and /accounts/username pages.
So you cannot view a collaboration of an user in a private-list in their profile if you are not also in the privatelist.
Note: You'll need mailman-api running to see this functionality.
The tests uses mocked data from the mailman-api.
colab/accounts/utils/mailman.py
... ... @@ -38,7 +38,7 @@ def unsubscribe(listname, address):
38 38  
39 39  
40 40 def update_subscription(address, lists):
41   - current_lists = address_lists(address)
  41 + current_lists = mailing_lists(address=address)
42 42  
43 43 for maillist in current_lists:
44 44 if maillist not in lists:
... ... @@ -49,14 +49,11 @@ def update_subscription(address, lists):
49 49 subscribe(maillist, address)
50 50  
51 51  
52   -def address_lists(address, description=''):
  52 +def mailing_lists(**kwargs):
53 53 url = get_url()
54 54  
55   - params = {'address': address,
56   - 'description': description}
57   -
58 55 try:
59   - lists = requests.get(url, timeout=TIMEOUT, params=params)
  56 + lists = requests.get(url, timeout=TIMEOUT, params=kwargs)
60 57 except:
61 58 LOGGER.exception('Unable to list mailing lists')
62 59 return []
... ... @@ -64,15 +61,22 @@ def address_lists(address, description=''):
64 61 return lists.json()
65 62  
66 63  
67   -def all_lists(*args, **kwargs):
68   - return address_lists('', *args, **kwargs)
  64 +def is_private_list(name):
  65 + try:
  66 + return dict(all_lists(private=True))[name]
  67 + except KeyError:
  68 + return []
  69 +
  70 +
  71 +def all_lists(**kwargs):
  72 + return mailing_lists(**kwargs)
69 73  
70 74  
71 75 def user_lists(user):
72 76 list_set = set()
73 77  
74 78 for email in user.emails.values_list('address', flat=True):
75   - list_set.update(address_lists(email))
  79 + list_set.update(mailing_lists(address=email))
76 80  
77 81 return tuple(list_set)
78 82  
... ... @@ -97,3 +101,16 @@ def list_users(listname):
97 101 return []
98 102  
99 103 return users.json()
  104 +
  105 +
  106 +def get_user_mailinglists(user):
  107 + lists_for_user = []
  108 + emails = ''
  109 +
  110 + if user:
  111 + emails = user.emails.values_list('address', flat=True)
  112 +
  113 + for email in emails:
  114 + lists_for_user.extend(mailing_lists(address=email))
  115 +
  116 + return lists_for_user
... ...
colab/accounts/views.py
... ... @@ -15,9 +15,10 @@ from django.views.generic import DetailView, UpdateView, TemplateView
15 15 from conversejs import xmpp
16 16 from conversejs.models import XMPPAccount
17 17  
18   -from colab.super_archives.models import (EmailAddress, Message,
  18 +from colab.super_archives.models import (EmailAddress,
19 19 EmailAddressValidation)
20   -from colab.search.utils import get_collaboration_data
  20 +from colab.search.utils import get_collaboration_data, get_visible_threads
  21 +from colab.accounts.models import User
21 22  
22 23 from .forms import (UserCreationForm, UserForm, ListsForm,
23 24 UserUpdateForm, ChangeXMPPPasswordForm)
... ... @@ -60,12 +61,17 @@ class UserProfileDetailView(UserProfileBaseMixin, DetailView):
60 61 template_name = 'accounts/user_detail.html'
61 62  
62 63 def get_context_data(self, **kwargs):
63   - user = self.object
  64 + profile_user = self.object
64 65 context = {}
65 66  
66 67 count_types = OrderedDict()
67 68  
68   - collaborations, count_types_extras = get_collaboration_data(user)
  69 + logged_user = None
  70 + if self.request.user.is_authenticated():
  71 + logged_user = User.objects.get(username=self.request.user)
  72 +
  73 + collaborations, count_types_extras = get_collaboration_data(
  74 + logged_user, profile_user)
69 75  
70 76 collaborations.sort(key=lambda elem: elem.modified, reverse=True)
71 77  
... ... @@ -74,16 +80,13 @@ class UserProfileDetailView(UserProfileBaseMixin, DetailView):
74 80 context['type_count'] = count_types
75 81 context['results'] = collaborations[:10]
76 82  
77   - email_pks = [addr.pk for addr in user.emails.iterator()]
78   - query = Message.objects.filter(from_address__in=email_pks)
79   - query = query.order_by('-received_time')
80   - context['emails'] = query[:10]
  83 + query = get_visible_threads(logged_user, profile_user)
  84 + context['emails'] = query.order_by('-received_time')[:10]
81 85  
82   - messages = Message.objects.filter(from_address__user__pk=user.pk)
83 86 count_by = 'thread__mailinglist__name'
84   - context['list_activity'] = dict(messages.values_list(count_by)
85   - .annotate(Count(count_by))
86   - .order_by(count_by))
  87 + context['list_activity'] = dict(query.values_list(count_by)
  88 + .annotate(Count(count_by))
  89 + .order_by(count_by))
87 90  
88 91 context.update(kwargs)
89 92 return super(UserProfileDetailView, self).get_context_data(**context)
... ... @@ -185,7 +188,7 @@ class ManageUserSubscriptionsView(UserProfileBaseMixin, DetailView):
185 188  
186 189 for email in emails:
187 190 lists = []
188   - lists_for_address = mailman.address_lists(email)
  191 + lists_for_address = mailman.mailing_lists(address=email)
189 192 for listname, description in all_lists:
190 193 if listname in lists_for_address:
191 194 checked = True
... ...
colab/home/views.py
... ... @@ -4,23 +4,45 @@ from django.http import HttpResponse, Http404
4 4  
5 5 from colab.search.utils import get_collaboration_data
6 6 from colab.super_archives.models import Thread
  7 +from colab.accounts.utils import mailman
  8 +from colab.accounts.models import User
  9 +
  10 +
  11 +def get_user_threads(threads, lists_for_user, key):
  12 + visible_threads = []
  13 + for t in threads:
  14 + if not t.mailinglist.is_private or \
  15 + t.mailinglist.name in lists_for_user:
  16 + visible_threads.append(key(t))
  17 +
  18 + return visible_threads
7 19  
8 20  
9 21 def dashboard(request):
10 22 """Dashboard page"""
11 23  
12   - highest_score_threads = Thread.highest_score.all()[:6]
  24 + highest_score_threads = Thread.highest_score.all()
  25 +
  26 + all_threads = Thread.objects.all()
  27 + latest_threads = []
  28 + lists_for_user = []
13 29  
14   - hottest_threads = [t.latest_message for t in highest_score_threads]
  30 + user = None
  31 + if request.user.is_authenticated():
  32 + user = User.objects.get(username=request.user)
  33 + lists_for_user = mailman.get_user_mailinglists(user)
15 34  
16   - latest_threads = Thread.objects.all()[:6]
  35 + latest_threads = get_user_threads(
  36 + all_threads, lists_for_user, lambda t: t)
  37 + hottest_threads = get_user_threads(
  38 + highest_score_threads, lists_for_user, lambda t: t.latest_message)
17 39  
18   - latest_results, count_types = get_collaboration_data()
  40 + latest_results, count_types = get_collaboration_data(user)
19 41 latest_results.sort(key=lambda elem: elem.modified, reverse=True)
20 42  
21 43 context = {
22 44 'hottest_threads': hottest_threads[:6],
23   - 'latest_threads': latest_threads,
  45 + 'latest_threads': latest_threads[:6],
24 46 'type_count': count_types,
25 47 'latest_results': latest_results[:6],
26 48 }
... ...
colab/search/tests.py
... ... @@ -13,7 +13,7 @@ class SearchViewTest(TestCase):
13 13 self.client = Client()
14 14  
15 15 def tearDown(self):
16   - call_command('clear_index', interactive=False,verbosity=0)
  16 + call_command('clear_index', interactive=False, verbosity=0)
17 17  
18 18 def test_search_thread(self):
19 19 request = self.client.get('/search/?q=thread')
... ...
colab/search/utils.py
... ... @@ -6,11 +6,40 @@ from collections import OrderedDict
6 6 from django.core.cache import cache
7 7 from django.utils.translation import ugettext as _
8 8 from django.conf import settings
  9 +from django.db.models import Q as Condition
  10 +
9 11 from colab.super_archives.models import Thread, Message
10 12 from colab.proxy.utils.models import Collaboration
  13 +from colab.accounts.utils import mailman
  14 +
  15 +
  16 +def get_visible_threads_queryset(logged_user):
  17 + queryset = Thread.objects
  18 + lists_for_user = []
  19 + if logged_user:
  20 + lists_for_user = mailman.get_user_mailinglists(logged_user)
  21 +
  22 + user_lists = Condition(mailinglist__name__in=lists_for_user)
  23 + public_lists = Condition(mailinglist__is_private=False)
  24 + queryset = Thread.objects.filter(user_lists | public_lists)
  25 +
  26 + return queryset
  27 +
  28 +
  29 +def get_visible_threads(logged_user, filter_by_user=None):
  30 + thread_qs = get_visible_threads_queryset(logged_user)
  31 + if filter_by_user:
  32 + message_qs = Message.objects.filter(thread__in=thread_qs)
  33 + messages = message_qs.filter(
  34 + from_address__user__pk=filter_by_user.pk)
  35 + else:
  36 + latest_threads = thread_qs.all()
  37 + messages = [t.latest_message for t in latest_threads]
  38 +
  39 + return messages
11 40  
12 41  
13   -def get_collaboration_data(filter_by_user=None):
  42 +def get_collaboration_data(logged_user, filter_by_user=None):
14 43 latest_results = []
15 44 count_types = cache.get('home_chart')
16 45 populate_count_types = False
... ... @@ -18,14 +47,10 @@ def get_collaboration_data(filter_by_user=None):
18 47 if count_types is None:
19 48 populate_count_types = True
20 49 count_types = OrderedDict()
21   - count_types[_('Emails')] = Thread.objects.count()
  50 + visible_threads = get_visible_threads(logged_user)
  51 + count_types[_('Emails')] = len(visible_threads)
22 52  
23   - if filter_by_user:
24   - messages = Message.objects.filter(
25   - from_address__user__pk=filter_by_user.pk)
26   - else:
27   - latest_threads = Thread.objects.all()[:6]
28   - messages = [t.latest_message for t in latest_threads]
  53 + messages = get_visible_threads(logged_user, filter_by_user)
29 54  
30 55 latest_results.extend(messages)
31 56  
... ...
colab/super_archives/fixtures/mailinglistdata.json 0 → 100644
... ... @@ -0,0 +1,186 @@
  1 +[
  2 + {
  3 + "fields": {
  4 + "last_name": "Jar",
  5 + "webpage": null,
  6 + "twitter": null,
  7 + "is_staff": false,
  8 + "user_permissions": [
  9 +
  10 + ],
  11 + "date_joined": "2015-02-24T21:10:35.004Z",
  12 + "google_talk": null,
  13 + "first_name": "Gust",
  14 + "is_superuser": false,
  15 + "last_login": "2015-02-26T17:56:13.378Z",
  16 + "verification_hash": null,
  17 + "role": null,
  18 + "email": "gustmax@hotmail.com",
  19 + "username": "gustmax",
  20 + "bio": null,
  21 + "needs_update": false,
  22 + "is_active": true,
  23 + "facebook": null,
  24 + "groups": [
  25 +
  26 + ],
  27 + "password": "pbkdf2_sha256$12000$ez83ccNOUQZk$vYT/QcYMukXZ7D7L1qQPyYlzCUEEEF20J7/Xjef0Rqg=",
  28 + "institution": null,
  29 + "github": null,
  30 + "modified": "2015-02-24T21:11:22.323Z"
  31 + },
  32 + "model": "accounts.user",
  33 + "pk": 1
  34 + },
  35 + {
  36 + "fields": {
  37 + "real_name": "",
  38 + "user": 1,
  39 + "md5": "ed8f47ae6048f8d4456c0554578f53ff",
  40 + "address": "gustmax@hotmail.com"
  41 + },
  42 + "model": "super_archives.emailaddress",
  43 + "pk": 1
  44 + },
  45 + {
  46 + "fields": {
  47 + "description": "",
  48 + "email": "",
  49 + "logo": "",
  50 + "last_imported_index": 0,
  51 + "is_private": true,
  52 + "name": "mailman"
  53 + },
  54 + "model": "super_archives.mailinglist",
  55 + "pk": 1
  56 + },
  57 + {
  58 + "fields": {
  59 + "description": "",
  60 + "email": "",
  61 + "logo": "",
  62 + "last_imported_index": 0,
  63 + "is_private": false,
  64 + "name": "lista"
  65 + },
  66 + "model": "super_archives.mailinglist",
  67 + "pk": 2
  68 + },
  69 + {
  70 + "fields": {
  71 + "description": "",
  72 + "email": "",
  73 + "logo": "",
  74 + "last_imported_index": 0,
  75 + "is_private": true,
  76 + "name": "privatelist"
  77 + },
  78 + "model": "super_archives.mailinglist",
  79 + "pk": 3
  80 + },
  81 + {
  82 + "fields": {
  83 + "spam": false,
  84 + "subject_token": "no-subject",
  85 + "mailinglist": 1,
  86 + "score": 34,
  87 + "latest_message": 1
  88 + },
  89 + "model": "super_archives.thread",
  90 + "pk": 1
  91 + },
  92 + {
  93 + "fields": {
  94 + "spam": false,
  95 + "subject_token": "no-subject",
  96 + "mailinglist": 2,
  97 + "score": 34,
  98 + "latest_message": 2
  99 + },
  100 + "model": "super_archives.thread",
  101 + "pk": 2
  102 + },
  103 + {
  104 + "fields": {
  105 + "spam": false,
  106 + "subject_token": "no-subject",
  107 + "mailinglist": 3,
  108 + "score": 33,
  109 + "latest_message": 3
  110 + },
  111 + "model": "super_archives.thread",
  112 + "pk": 3
  113 + },
  114 + {
  115 + "fields": {
  116 + "body": "lista Mailman email",
  117 + "received_time": "2015-02-24T14:23:42Z",
  118 + "from_address": 1,
  119 + "thread": 1,
  120 + "spam": false,
  121 + "subject_clean": "(no subject)",
  122 + "message_id": "<20150224142347.9ED2419A5B0@localhost.localdomain>",
  123 + "subject": "[Mailman] (no subject)"
  124 + },
  125 + "model": "super_archives.message",
  126 + "pk": 1
  127 + },
  128 + {
  129 + "fields": {
  130 + "body": "nada",
  131 + "received_time": "2015-02-24T14:15:39Z",
  132 + "from_address": 1,
  133 + "thread": 2,
  134 + "spam": false,
  135 + "subject_clean": "(no subject)",
  136 + "message_id": "<20150224141545.1AECA19A5A0@localhost.localdomain>",
  137 + "subject": "[Lista] (no subject)"
  138 + },
  139 + "model": "super_archives.message",
  140 + "pk": 2
  141 + },
  142 + {
  143 + "fields": {
  144 + "body": "Mensagem da lista privada nada",
  145 + "received_time": "2015-02-24T14:15:39Z",
  146 + "from_address": 1,
  147 + "thread": 3,
  148 + "spam": false,
  149 + "subject_clean": "(no subject)",
  150 + "message_id": "<20150224141545.1AECA19A5A0@localhost.localdomain>",
  151 + "subject": "[PrivateList] (no subject)"
  152 + },
  153 + "model": "super_archives.message",
  154 + "pk": 3
  155 + },
  156 + {
  157 + "fields": {
  158 + "text": "lista Mailman email\n",
  159 + "message": 1,
  160 + "is_reply": false,
  161 + "order": 0
  162 + },
  163 + "model": "super_archives.messageblock",
  164 + "pk": 1
  165 + },
  166 + {
  167 + "fields": {
  168 + "text": "nada\n",
  169 + "message": 2,
  170 + "is_reply": false,
  171 + "order": 0
  172 + },
  173 + "model": "super_archives.messageblock",
  174 + "pk": 2
  175 + },
  176 + {
  177 + "fields": {
  178 + "text": "Mensagem da lista privada nada\n",
  179 + "message": 3,
  180 + "is_reply": false,
  181 + "order": 0
  182 + },
  183 + "model": "super_archives.messageblock",
  184 + "pk": 3
  185 + }
  186 +]
... ...
colab/super_archives/management/commands/import_emails.py
... ... @@ -295,3 +295,7 @@ class Command(BaseCommand, object):
295 295 raise
296 296 finally:
297 297 os.remove(self.lock_file)
  298 +
  299 + for mlist in MailingList.objects.all():
  300 + mlist.update_privacy()
  301 + mlist.save()
... ...
colab/super_archives/management/commands/message.py
... ... @@ -79,7 +79,7 @@ class Message(mailbox.mboxMessage):
79 79 return body.strip()
80 80  
81 81 def get_received_datetime(self):
82   - if self not in ('Received'):
  82 + if 'Received' not in self:
83 83 return None
84 84 # The time received should always be the last element
85 85 # in the `Received` attribute from the message headers
... ...
colab/super_archives/migrations/0002_mailinglist_is_private.py 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals
  3 +
  4 +from django.db import models, migrations
  5 +
  6 +
  7 +class Migration(migrations.Migration):
  8 +
  9 + dependencies = [
  10 + ('super_archives', '0001_initial'),
  11 + ]
  12 +
  13 + operations = [
  14 + migrations.AddField(
  15 + model_name='mailinglist',
  16 + name='is_private',
  17 + field=models.BooleanField(default=False),
  18 + preserve_default=True,
  19 + ),
  20 + ]
... ...
colab/super_archives/models.py
... ... @@ -19,6 +19,7 @@ from hitcounter.models import HitCounterModelMixin
19 19 from .managers import NotSpamManager, MostVotedManager, HighestScore
20 20 from .utils import blocks, email
21 21 from .utils.etiquetador import etiquetador
  22 +from colab.accounts.utils import mailman
22 23  
23 24  
24 25 def get_validation_key():
... ... @@ -79,6 +80,10 @@ class MailingList(models.Model):
79 80 description = models.TextField()
80 81 logo = models.FileField(upload_to='list_logo') # TODO
81 82 last_imported_index = models.IntegerField(default=0)
  83 + is_private = models.BooleanField(default=False)
  84 +
  85 + def update_privacy(self):
  86 + self.is_private = mailman.is_private_list(self.name)
82 87  
83 88 def get_absolute_url(self):
84 89 params = {
... ...
colab/super_archives/search_indexes.py
... ... @@ -85,10 +85,12 @@ class ThreadIndex(BaseIndex, indexes.Indexable):
85 85 return u'thread'
86 86  
87 87 def index_queryset(self, using=None):
88   - return self.get_model().objects.filter(
89   - spam=False
  88 + elements = self.get_model().objects.filter(
  89 + spam=False, mailinglist__is_private=False
90 90 ).exclude(subject_token='')
91 91  
  92 + return elements
  93 +
92 94 def get_boost(self, obj):
93 95 boost = super(ThreadIndex, self).get_boost(obj)
94 96  
... ...
colab/super_archives/tests/__init__.py 0 → 100644
colab/super_archives/tests/test_privatelist.py 0 → 100644
... ... @@ -0,0 +1,66 @@
  1 +# -*- coding:utf-8 -*-
  2 +import mock
  3 +
  4 +from colab.accounts.utils import mailman
  5 +from django.test import TestCase, Client
  6 +
  7 +
  8 +class ArchivesViewTest(TestCase):
  9 +
  10 + fixtures = ['mailinglistdata.json']
  11 +
  12 + def setUp(self):
  13 + self.client = Client()
  14 +
  15 + def authenticate_user(self):
  16 + self.client.login(username='gustmax', password='1234')
  17 +
  18 + def test_see_only_private_list_if_member(self):
  19 + mailman.get_user_mailinglists = mock.Mock(
  20 + return_value="['privatelist']")
  21 +
  22 + self.authenticate_user()
  23 + request = self.client.get('/archives/thread/')
  24 +
  25 + list_data = request.context['lists']
  26 +
  27 + self.assertEqual('lista', list_data[0][0])
  28 + self.assertEqual('privatelist', list_data[1][0])
  29 + self.assertEqual(2, len(list_data))
  30 +
  31 + def test_see_only_public_if_not_logged_in(self):
  32 + request = self.client.get('/archives/thread/')
  33 +
  34 + list_data = request.context['lists']
  35 +
  36 + self.assertEqual('lista', list_data[0][0])
  37 + self.assertEqual(1, len(list_data))
  38 +
  39 + def test_see_private_thread_in_dashboard_if_member(self):
  40 + mailman.get_user_mailinglists = mock.Mock(
  41 + return_value="['privatelist']")
  42 +
  43 + self.authenticate_user()
  44 + request = self.client.get('/dashboard')
  45 +
  46 + latest_threads = request.context['latest_threads']
  47 + hottest_threads = request.context['hottest_threads']
  48 +
  49 + self.assertEqual(2, len(latest_threads))
  50 + self.assertEqual(2, len(hottest_threads))
  51 +
  52 + def test_dont_see_private_thread_if_logged_out(self):
  53 + request = self.client.get('/dashboard')
  54 +
  55 + latest_threads = request.context['latest_threads']
  56 + hottest_threads = request.context['hottest_threads']
  57 +
  58 + self.assertEqual(1, len(latest_threads))
  59 + self.assertEqual(1, len(hottest_threads))
  60 +
  61 + def test_dont_see_private_threads_in_profile_if_logged_out(self):
  62 + request = self.client.get('/account/gustmax')
  63 +
  64 + emails = request.context['emails']
  65 +
  66 + self.assertEqual(1, len(emails))
... ...
colab/super_archives/views.py
... ... @@ -12,7 +12,7 @@ from django.contrib import messages
12 12 from django.db import IntegrityError
13 13 from django.views.generic import View
14 14 from django.utils.translation import ugettext as _
15   -from django.core.exceptions import ObjectDoesNotExist
  15 +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
16 16 from django.utils.decorators import method_decorator
17 17 from django.contrib.auth.decorators import login_required
18 18 from django.shortcuts import render, redirect, get_object_or_404
... ... @@ -31,6 +31,18 @@ class ThreadView(View):
31 31  
32 32 thread = get_object_or_404(Thread, subject_token=thread_token,
33 33 mailinglist__name=mailinglist)
  34 +
  35 + all_privates = dict(mailman.all_lists(private=True))
  36 + if all_privates[thread.mailinglist.name]:
  37 + if not request.user.is_authenticated():
  38 + raise PermissionDenied
  39 + else:
  40 + user = User.objects.get(username=request.user)
  41 + emails = user.emails.values_list('address', flat=True)
  42 + lists_for_user = mailman.get_user_mailinglists(user)
  43 + if thread.mailinglist.name not in lists_for_user:
  44 + raise PermissionDenied
  45 +
34 46 thread.hit(request)
35 47  
36 48 try:
... ... @@ -122,21 +134,31 @@ class ThreadDashboardView(View):
122 134 def get(self, request):
123 135 MAX = 6
124 136 context = {}
125   - all_lists = mailman.all_lists(description=True)
  137 +
  138 + all_privates = {}
  139 + private_mailinglist = MailingList.objects.filter(is_private=True)
  140 + for mailinglist in private_mailinglist:
  141 + all_privates[mailinglist.name] = True
126 142  
127 143 context['lists'] = []
128 144  
  145 + lists_for_user = []
  146 + if request.user.is_authenticated():
  147 + user = User.objects.get(username=request.user)
  148 + lists_for_user = mailman.get_user_mailinglists(user)
  149 +
129 150 for list_ in MailingList.objects.order_by('name'):
130   - context['lists'].append((
131   - list_.name,
132   - mailman.get_list_description(list_.name, all_lists),
133   - list_.thread_set.filter(spam=False).order_by(
134   - '-latest_message__received_time'
135   - )[:MAX],
136   - [t.latest_message for t in Thread.highest_score.filter(
137   - mailinglist__name=list_.name)[:MAX]],
138   - len(mailman.list_users(list_.name)),
139   - ))
  151 + if list_.name not in all_privates or list_.name in lists_for_user:
  152 + context['lists'].append((
  153 + list_.name,
  154 + mailman.get_list_description(list_.name),
  155 + list_.thread_set.filter(spam=False).order_by(
  156 + '-latest_message__received_time'
  157 + )[:MAX],
  158 + [t.latest_message for t in Thread.highest_score.filter(
  159 + mailinglist__name=list_.name)[:MAX]],
  160 + len(mailman.list_users(list_.name)),
  161 + ))
140 162  
141 163 return render(request, 'superarchives/thread-dashboard.html', context)
142 164  
... ...