Merge lp:~widelands-dev/widelands-website/anti_spam into lp:widelands-website

Proposed by kaputtnik
Status: Merged
Merged at revision: 423
Proposed branch: lp:~widelands-dev/widelands-website/anti_spam
Merge into: lp:widelands-website
Diff against target: 360 lines (+151/-25)
11 files modified
pybb/admin.py (+2/-2)
pybb/forms.py (+0/-1)
pybb/migrations/0002_auto_20161001_2046.py (+19/-0)
pybb/models.py (+24/-0)
pybb/urls.py (+2/-1)
pybb/views.py (+32/-5)
templates/pybb/forum.html (+35/-4)
templates/pybb/inlines/display_category.html (+16/-7)
templates/pybb/last_posts.html (+7/-5)
templates/pybb/pybb_moderate_info.html (+12/-0)
templates/pybb/topic.html (+2/-0)
To merge this branch: bzr merge lp:~widelands-dev/widelands-website/anti_spam
Reviewer Review Type Date Requested Status
SirVer Approve
GunChleoc Approve
Review via email: mp+307869@code.launchpad.net

Commit message

Hide all posts/topics which are potentially spam using a keyword filter.

Description of the change

Hide all posts/topics which are potentially spam using a keyword filter.

Add a boolean 'hided' field to pybb.Post.

Post filter: Applies if a post contains 'vashikaran' AND 'baba'. This catches each case insentive occurrence of this strings, so 'VaShIkaRan' is caught as well as 'babaco' or 'blabababla'. Because 'Baba' is also known as 'Bye-bye' these keywords must occur both.

Topic name filter: Applies if ' baba ' (as a word) OR 'ji' is used. Also case insensitive. Should catch ' baBaJi' as 'baBa ji' or ' baba '

The filters are set in pybb/views.py

When commiting on the server, ./manage.py migrate must be run before the website is restartet. This adds the 'hided' field to all columns in the database with default value 'False'.

To post a comment you must log in.
Revision history for this message
kaputtnik (franku) wrote :

Hm, the diff is truncated... maybe remove the akismet api?

Revision history for this message
SirVer (sirver) wrote :

That Is Fine. Well review offline then. I hope I get around to it tomorrow. Maybe it will be Saturday.

> Am 06.10.2016 um 20:02 schrieb kaputtnik <email address hidden>:
>
> Hm, the diff is truncated... maybe remove the akismet api?
> --
> https://code.launchpad.net/~widelands-dev/widelands-website/anti_spam/+merge/307869
> You are subscribed to branch lp:widelands-website.

Revision history for this message
kaputtnik (franku) wrote :

Just a thought of last night:
Change the Text of the redirect page saying that EACH Post is hided now and will be moderated.

So if the spammers ever read this, they should assume that spamming has no chance here. If a non spammer read this (i guess this would be very rare, if it ever comes up) we could explain him the text.

And of course, it would be better to have the lists of keywords in local_settings.py.

I am very busy these days with normal work, so on Saturday i am working the whole day and in the evening my son is visiting me.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Just a quick note: past tense of "hide" is "hid" ;)

429. By kaputtnik

changed text of redirect page

430. By GunChleoc

Proofreading.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have decided to rename hided -> hidden right now, because it affects the models.

Not tested but code LGTM as far as I can tell.

I'd say do a final text to make sure that I didn't mess anything up and then go live.

review: Approve
431. By SirVer

Minor nits.

Revision history for this message
SirVer (sirver) wrote :

1) Is it necessary that we ship the askimet client in our repo, i.e. can it not be installed wia pip_requirements.txt? We definitively should remove the .svn directory if possible. Also, for this branch it is completely unused, so I actually now agree with #1 and suggest removing the API for now.

2) I was also changing hided -> hidden, at the very same time than Gun - but she beat me to submitting :).

3) You should also check in the title of the post, not only the body. We had some spam that only had data in the title. You can do that cheaply by just concatenating: text = title + body and then working over text.

4) It might make sense to add a regular expression checking for international phone numbers - I do not think such a post can ever be legit.

5) You are marking any post of a user with 'ji' in its name as SPAM. Is that not a bit too broad?

6) There is no admin notification right now - so we will not know if posts are in the queue for moderation. I think that is fine for now, but we will need to remember to check daily once this is deployed until we have that. I am all for deploying this soon though, this SPAM is annoying.

review: Approve
432. By kaputtnik

removed akismet api

Revision history for this message
kaputtnik (franku) wrote :

Just short answers because i am in hurry:

> 1) Is it necessary that we ship the askimet client in our repo, i.e. can it
> not be installed wia pip_requirements.txt?

I don't why i am downloaded the complete zip file, whereas one could also download the py-file (containing all classes and functions) I have removed the complete zip file now in this branch. Regarding to pip_requirements: I actually don't know, sorry. But a single file shouldn't be a problem for our repo.

> 3) You should also check in the title of the post, not only the body. We had
> some spam that only had data in the title. You can do that cheaply by just
> concatenating: text = title + body and then working over text.

This is already in there. See the description of this merge proposal "Topic name filter: "

> 4) It might make sense to add a regular expression checking for international
> phone numbers - I do not think such a post can ever be legit.

Yes, but i am not a regular expression freak :-D Will take some time and tests for me to do something like that.

> 5) You are marking any post of a user with 'ji' in its name as SPAM. Is that
> not a bit too broad?

Yes, maybe change it into ' ji ' or ' ji'

> 6) There is no admin notification right now - so we will not know if posts are
> in the queue for moderation. I think that is fine for now, but we will need to
> remember to check daily once this is deployed until we have that. I am all for
> deploying this soon though, this SPAM is annoying.

Yes, if we add admin notification, we should also prevent notification of normal users (who has 'Inform me on new topics" activated).

Thanks for the changes :-)

Revision history for this message
kaputtnik (franku) wrote :

There is a akismet Pypi package, so we could easily add this to pip_requirements.txt.

> > 5) You are marking any post of a user with 'ji' in its name as SPAM. Is that
> > not a bit too broad?
>
> Yes, maybe change it into ' ji ' or ' ji'

Correcting: 'ji' is only used for topic names (subject). And i think it is better to have a difference between topic name and post text.

Looking at my gathered spam posts, we should also add ' molvi' to the keywords. Both in topic name and post text.

I will make the following changes tomorrow:
- Add ' molvi' to keywords
- Using of keyword lists in local_settings.py (instead of hardcoded in pybb/views.py)
- merge into trunk and deploy

For me this is quite an interesting problem and i would be interested to make an own 'anti_spam' app of it (at least because there is a lot to learn). But since akismet makes his/here job, for widelands it would be better to use this service, because it is maybe more effective in a shorter time. But if we use this we may need additional views and buttons to submit ham or spam to akismet.

433. By kaputtnik

moved list of keywords to local_settings.py

Revision history for this message
kaputtnik (franku) wrote :

Merged and deployed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'pybb/admin.py'
--- pybb/admin.py 2012-03-18 21:06:49 +0000
+++ pybb/admin.py 2016-10-09 11:17:41 +0000
@@ -39,7 +39,7 @@
39 ),39 ),
40 (_('Additional options'), {40 (_('Additional options'), {
41 'classes': ('collapse',),41 'classes': ('collapse',),
42 'fields': (('views',), ('sticky', 'closed'), 'subscribers')42 'fields': (('views',), ('sticky', 'closed', 'hidden'), 'subscribers')
43 }43 }
44 ),44 ),
45 )45 )
@@ -57,7 +57,7 @@
57 ),57 ),
58 (_('Additional options'), {58 (_('Additional options'), {
59 'classes': ('collapse',),59 'classes': ('collapse',),
60 'fields' : (('created', 'updated'), 'user_ip')60 'fields' : (('created', 'updated'), 'user_ip', 'hidden')
61 }61 }
62 ),62 ),
63 (_('Message'), {63 (_('Message'), {
6464
=== modified file 'pybb/forms.py'
--- pybb/forms.py 2016-06-22 21:02:53 +0000
+++ pybb/forms.py 2016-10-09 11:17:41 +0000
@@ -66,7 +66,6 @@
66 post = Post(topic=topic, user=self.user, user_ip=self.ip,66 post = Post(topic=topic, user=self.user, user_ip=self.ip,
67 markup=self.cleaned_data['markup'],67 markup=self.cleaned_data['markup'],
68 body=self.cleaned_data['body'])68 body=self.cleaned_data['body'])
69
70 post.save(*args, **kwargs)69 post.save(*args, **kwargs)
7170
72 if pybb_settings.ATTACHMENT_ENABLE:71 if pybb_settings.ATTACHMENT_ENABLE:
7372
=== added file 'pybb/migrations/0002_auto_20161001_2046.py'
--- pybb/migrations/0002_auto_20161001_2046.py 1970-01-01 00:00:00 +0000
+++ pybb/migrations/0002_auto_20161001_2046.py 2016-10-09 11:17:41 +0000
@@ -0,0 +1,19 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import models, migrations
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ('pybb', '0001_initial'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='post',
16 name='hidden',
17 field=models.BooleanField(default=False, verbose_name='Hidden'),
18 ),
19 ]
020
=== modified file 'pybb/models.py'
--- pybb/models.py 2016-06-22 21:02:53 +0000
+++ pybb/models.py 2016-10-09 11:17:41 +0000
@@ -95,6 +95,13 @@
95 except IndexError:95 except IndexError:
96 return None96 return None
9797
98 @property
99 def last_nonhidden_post(self):
100 posts = self.posts.order_by('-created').filter(hidden=False).select_related()
101 try:
102 return posts[0]
103 except IndexError:
104 return None
98105
99class Topic(models.Model):106class Topic(models.Model):
100 forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum'))107 forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum'))
@@ -132,6 +139,22 @@
132 return self.posts.all().order_by('-created').select_related()[0]139 return self.posts.all().order_by('-created').select_related()[0]
133140
134 @property141 @property
142 def last_nonhidden_post(self):
143 try:
144 return self.posts.all().order_by('-created').filter(hidden=False).select_related()[0]
145 except IndexError:
146 return self.posts.all().order_by('-created').select_related()[0]
147
148 # If the first post of this topic is hidden, the topic is hidden
149 @property
150 def is_hidden(self):
151 try:
152 p = self.posts.all().order_by('created').filter(hidden=False).select_related()[0]
153 except IndexError:
154 return True
155 return False
156
157 @property
135 def post_count(self):158 def post_count(self):
136 return Post.objects.filter(topic=self).count()159 return Post.objects.filter(topic=self).count()
137160
@@ -193,6 +216,7 @@
193 body_html = models.TextField(_('HTML version'))216 body_html = models.TextField(_('HTML version'))
194 body_text = models.TextField(_('Text version'))217 body_text = models.TextField(_('Text version'))
195 user_ip = models.GenericIPAddressField(_('User IP'), default='')218 user_ip = models.GenericIPAddressField(_('User IP'), default='')
219 hidden = models.BooleanField(_('Hidden'), blank=True, default=False)
196220
197 # Django sphinx221 # Django sphinx
198 if settings.USE_SPHINX:222 if settings.USE_SPHINX:
199223
=== modified file 'pybb/urls.py'
--- pybb/urls.py 2016-06-04 14:17:40 +0000
+++ pybb/urls.py 2016-10-09 11:17:41 +0000
@@ -30,7 +30,8 @@
30 url('^post/(?P<post_id>\d+)/$', views.show_post, name='pybb_post'),30 url('^post/(?P<post_id>\d+)/$', views.show_post, name='pybb_post'),
31 url('^post/(?P<post_id>\d+)/edit/$', views.edit_post, name='pybb_edit_post'),31 url('^post/(?P<post_id>\d+)/edit/$', views.edit_post, name='pybb_edit_post'),
32 url('^post/(?P<post_id>\d+)/delete/$', views.delete_post, name='pybb_delete_post'),32 url('^post/(?P<post_id>\d+)/delete/$', views.delete_post, name='pybb_delete_post'),
3333 url('pybb_moderate_info/$', views.pybb_moderate_info),
34
34 # Attachment35 # Attachment
35 url('^attachment/(?P<hash>\w+)/$', views.show_attachment, name='pybb_attachment'),36 url('^attachment/(?P<hash>\w+)/$', views.show_attachment, name='pybb_attachment'),
3637
3738
=== modified file 'pybb/views.py'
--- pybb/views.py 2016-06-15 19:20:24 +0000
+++ pybb/views.py 2016-10-09 11:17:41 +0000
@@ -1,6 +1,6 @@
1import math1import math
2from mainpage.templatetags.wl_markdown import do_wl_markdown2from mainpage.templatetags.wl_markdown import do_wl_markdown
3from pybb.markups import mypostmarkup 3from pybb.markups import mypostmarkup
44
5from django.shortcuts import get_object_or_4045from django.shortcuts import get_object_or_404
6from django.http import HttpResponseRedirect, HttpResponse, HttpResponseNotFound, Http4046from django.http import HttpResponseRedirect, HttpResponse, HttpResponseNotFound, Http404
@@ -10,13 +10,15 @@
10from django.core.urlresolvers import reverse10from django.core.urlresolvers import reverse
11from django.db import connection11from django.db import connection
12from django.utils import translation12from django.utils import translation
13from django.shortcuts import render
1314
14from pybb.util import render_to, paged, build_form, quote_text, paginate, set_language, ajax, urlize15from pybb.util import render_to, paged, build_form, quote_text, paginate, set_language, ajax, urlize
15from pybb.models import Category, Forum, Topic, Post, PrivateMessage, Attachment,\16from pybb.models import Category, Forum, Topic, Post, PrivateMessage, Attachment,\
16 MARKUP_CHOICES17 MARKUP_CHOICES
17from pybb.forms import AddPostForm, EditPostForm, UserSearchForm 18from pybb.forms import AddPostForm, EditPostForm, UserSearchForm
18from pybb import settings as pybb_settings19from pybb import settings as pybb_settings
19from pybb.orm import load_related20from pybb.orm import load_related
21from django.conf import settings
2022
21try:23try:
22 from notification import models as notification24 from notification import models as notification
@@ -74,7 +76,7 @@
74 }76 }
75show_forum = render_to('pybb/forum.html')(show_forum_ctx)77show_forum = render_to('pybb/forum.html')(show_forum_ctx)
7678
77 79
78def show_topic_ctx(request, topic_id):80def show_topic_ctx(request, topic_id):
7981
80 try:82 try:
@@ -112,7 +114,7 @@
112 # profiles = Profile.objects.filter(user__pk__in=114 # profiles = Profile.objects.filter(user__pk__in=
113 # set(x.user.id for x in page.object_list))115 # set(x.user.id for x in page.object_list))
114 # profiles = dict((x.user_id, x) for x in profiles)116 # profiles = dict((x.user_id, x) for x in profiles)
115 117
116 # for post in page.object_list:118 # for post in page.object_list:
117 # post.user.pybb_profile = profiles[post.user.id]119 # post.user.pybb_profile = profiles[post.user.id]
118120
@@ -159,7 +161,29 @@
159 initial={'markup': "markdown", 'body': quote})161 initial={'markup': "markdown", 'body': quote})
160162
161 if form.is_valid():163 if form.is_valid():
162 post = form.save();164 # TODO: Add akismet check here
165 spam = False
166
167 # Check in post text.
168 text = form.cleaned_data['body']
169 if any(x in text.lower() for x in settings.ANTI_SPAM_BODY):
170 spam = True
171
172 # Check in topic subject ('name' is empty if this a post to an existing topic)
173 text = form.cleaned_data['name']
174 if text != '':
175 # This is a new topic
176 if any(x in text.lower() for x in settings.ANTI_SPAM_TOPIC):
177 spam = True
178
179 post = form.save()
180 if spam:
181 # Hide the post against normal users
182 post.hidden = True
183 post.save()
184 # Redirect to an info page to inform the user
185 return HttpResponseRedirect('pybb_moderate_info')
186
163 if not topic:187 if not topic:
164 post.topic.subscribers.add(request.user)188 post.topic.subscribers.add(request.user)
165 return HttpResponseRedirect(post.get_absolute_url())189 return HttpResponseRedirect(post.get_absolute_url())
@@ -353,3 +377,6 @@
353377
354 html = urlize(html)378 html = urlize(html)
355 return {'content': html}379 return {'content': html}
380
381def pybb_moderate_info(request):
382 return render(request, 'pybb/pybb_moderate_info.html')
356383
=== modified file 'templates/pybb/forum.html'
--- templates/pybb/forum.html 2016-04-29 07:28:16 +0000
+++ templates/pybb/forum.html 2016-10-09 11:17:41 +0000
@@ -40,7 +40,8 @@
40 </tr>40 </tr>
41 </thead>41 </thead>
42 <tbody>42 <tbody>
43 {% for topic in topics %}43 {% for topic in topics %}
44 {% if not topic.is_hidden %}
44 <tr class="{% cycle 'odd' 'even' %}">45 <tr class="{% cycle 'odd' 'even' %}">
45 <td class="forumIcon center">46 <td class="forumIcon center">
46 {% if topic|pybb_has_unreads:user %}47 {% if topic|pybb_has_unreads:user %}
@@ -60,13 +61,43 @@
60 Views: {{ topic.views }}61 Views: {{ topic.views }}
61 </td>62 </td>
62 <td class="lastPost">63 <td class="lastPost">
63 {%if topic.last_post %}64 {% if user.is_superuser %}
65 {% if topic.last_post %}
66 {{ topic.last_post.user|user_link }} <a href="{{ topic.last_post.get_absolute_url }}">&#187;</a><br />
67 <span class="small">on {{ topic.last_post.created|custom_date:user }}</span>
68 {% endif %}
69 {% else %}
70 {{ topic.last_nonhidden_post.user|user_link }} <a href="{{ topic.last_nonhidden_post.get_absolute_url }}">&#187;</a><br />
71 <span class="small">on {{ topic.last_nonhidden_post.created|custom_date:user }}</span>
72 {% endif %}
73 </td>
74 </tr>
75 {% elif user.is_superuser %}
76 <tr class="{% cycle 'odd' 'even' %}">
77 <td class="forumIcon center">
78 {% if topic|pybb_has_unreads:user %}
79 <img src="{{ MEDIA_URL }}forum/img/doc_big_work_star.png" style="margin: 0px;" alt="" class="middle" />
80 {% else %}
81 <img src="{{ MEDIA_URL }}forum/img/doc_big_work.png" style="margin: 0px;" alt="" class="middle" />
82 {% endif %}
83 </td>
84 <td class="forumTitle">
85 {% if topic.sticky %}<img src="{{ MEDIA_URL }}forum/img/sticky.png" alt="Sticky" title="Sticky" />{% endif %}
86 {% if topic.closed %}<img src="{{ MEDIA_URL }}forum/img/closed.png" alt="Closed" title="Closed" />{% endif %}
87 <a href="{{ topic.get_absolute_url }}">{{ topic.name }}</a><br />
88 <span class="small">Created by {{ topic.user|user_link }} on {{ topic.created|custom_date:user }}</span>
89 </td>
90 <td class="forumCount center small" style="width: 120px;">
91 Posts: {{ topic.post_count }}<br/>
92 Views: {{ topic.views }}
93 </td>
94 <td class="lastPost">
64 {{ topic.last_post.user|user_link }} <a href="{{ topic.last_post.get_absolute_url }}">&#187;</a><br />95 {{ topic.last_post.user|user_link }} <a href="{{ topic.last_post.get_absolute_url }}">&#187;</a><br />
65 <span class="small">on {{ topic.last_post.created|custom_date:user }}</span>96 <span class="small">on {{ topic.last_post.created|custom_date:user }}</span>
66 {% endif %}
67 </td>97 </td>
68 </tr>98 </tr>
69 {% endfor %}99 {% endif %} {# topic.is_hidden #}
100 {% endfor %} {# topic #}
70 </tbody>101 </tbody>
71 </table>102 </table>
72103
73104
=== modified file 'templates/pybb/inlines/display_category.html'
--- templates/pybb/inlines/display_category.html 2016-03-02 21:02:38 +0000
+++ templates/pybb/inlines/display_category.html 2016-10-09 11:17:41 +0000
@@ -29,13 +29,22 @@
29 Topics: {{ forum.topics.count }}<br/>29 Topics: {{ forum.topics.count }}<br/>
30 Posts: {{ forum.posts.count }}30 Posts: {{ forum.posts.count }}
31 </td>31 </td>
32 <td class="lastPost">32 {% if user.is_superuser %} {# Show all to superuser #}
33 {%if forum.last_post %}33 {% if forum.last_post %}
34 <a href="{{forum.last_post.get_absolute_url}}">{{ forum.last_post.topic.name }}</a><br />34 <td class="lastPost">
35 <span class="small">by {{ forum.last_post.user|user_link }}<br />35 <a href="{{forum.last_post.get_absolute_url}}">{{ forum.last_post.topic.name }}</a><br />
36 on {{ forum.last_post.created|custom_date:user}}</span>36 <span class="small">by {{ forum.last_post.user|user_link }}<br />
37 {% else %}37 on {{ forum.last_post.created|custom_date:user}}</span>
38 &nbsp;38 </td>
39 {% endif %}
40 {% else %} {# no super_user: Show only nonhidden posts#}
41 {% if forum.last_nonhidden_post %}
42 <td class="lastPost">
43 <a href="{{forum.last_nonhidden_post.get_absolute_url}}">{{ forum.last_nonhidden_post.topic.name }}</a><br />
44 <span class="small">by {{ forum.last_nonhidden_post.user|user_link }}<br />
45 on {{ forum.last_nonhidden_post.created|custom_date:user}}</span>
46 </td>
47 {% endif %}
39 {% endif %}48 {% endif %}
40 </td>49 </td>
41 </tr>50 </tr>
4251
=== modified file 'templates/pybb/last_posts.html'
--- templates/pybb/last_posts.html 2016-03-02 21:02:38 +0000
+++ templates/pybb/last_posts.html 2016-10-09 11:17:41 +0000
@@ -9,11 +9,13 @@
9 <div class="columnModuleBox">9 <div class="columnModuleBox">
10 <ul>10 <ul>
11 {% for post in posts %}11 {% for post in posts %}
12 <li>12 {% if not post.hidden %}
13 {{ post.topic.forum.name }}<br />13 <li>
14 <a href="{{ post.get_absolute_url }}" title="{{ post.topic.name }}">{{ post.topic.name|pybb_cut_string:30 }}</a><br />14 {{ post.topic.forum.name }}<br />
15 by <a href="{% url 'profile_view' post.user %}">{{post.user.username}}</a> {{ post.created|minutes }} ago15 <a href="{{ post.get_absolute_url }}" title="{{ post.topic.name }}">{{ post.topic.name|pybb_cut_string:30 }}</a><br />
16 </li>16 by <a href="{% url 'profile_view' post.user %}">{{post.user.username}}</a> {{ post.created|minutes }} ago
17 </li>
18 {% endif %}
17 {% endfor %}19 {% endfor %}
18 </ul>20 </ul>
19 </div>21 </div>
2022
=== added file 'templates/pybb/pybb_moderate_info.html'
--- templates/pybb/pybb_moderate_info.html 1970-01-01 00:00:00 +0000
+++ templates/pybb/pybb_moderate_info.html 2016-10-09 11:17:41 +0000
@@ -0,0 +1,12 @@
1{% extends 'pybb/base.html' %}
2
3{% block content %}
4
5<h1>All comments have to be moderated</h1>
6
7<div class="blogEntry">
8 <p>Your comment has been saved but hidden to normal users. A moderator
9 will take a look at it and review it as soon as possible.</p>
10</div>
11
12{% endblock %}
013
=== modified file 'templates/pybb/topic.html'
--- templates/pybb/topic.html 2016-07-31 08:44:48 +0000
+++ templates/pybb/topic.html 2016-10-09 11:17:41 +0000
@@ -157,6 +157,7 @@
157 <table class="forum">157 <table class="forum">
158 <tbody>158 <tbody>
159 {% for post in posts %}159 {% for post in posts %}
160 {% if not post.hidden or user.is_superuser %}
160 <tr class="{% cycle 'odd' 'even' %}">161 <tr class="{% cycle 'odd' 'even' %}">
161 <td class="author">162 <td class="author">
162 {{ post.user|user_link }}<br />163 {{ post.user|user_link }}<br />
@@ -228,6 +229,7 @@
228 {% endif %}229 {% endif %}
229 </td>230 </td>
230 </tr>231 </tr>
232 {% endif %}
231 <tr class="spacer">233 <tr class="spacer">
232 <td></td>234 <td></td>
233 <td></td>235 <td></td>

Subscribers

People subscribed via source and target branches