From 615937cb65e8760cd797d51ddcca39c84730ccad Mon Sep 17 00:00:00 2001 From: Sijan Bhandari Date: Sat, 5 Jun 2021 17:27:17 +0200 Subject: [PATCH] Bug/spam snippet filtering (#272) * Add flag filtering in the snippet listing * Update flake8 linting --- cab/api/views.py | 4 +- cab/models.py | 60 +++++++++------- cab/views/snippets.py | 155 ++++++++++++++++++++++-------------------- setup.cfg | 2 +- 4 files changed, 118 insertions(+), 103 deletions(-) diff --git a/cab/api/views.py b/cab/api/views.py index e052f69c..746c61bf 100644 --- a/cab/api/views.py +++ b/cab/api/views.py @@ -5,10 +5,10 @@ class SnippetList(generics.ListCreateAPIView): - queryset = Snippet.objects.all() + queryset = Snippet.objects.active_snippet() serializer_class = SnippetSerializer class SnippetDetail(generics.RetrieveUpdateAPIView): - queryset = Snippet.objects.all() + queryset = Snippet.objects.active_snippet() serializer_class = SnippetSerializer diff --git a/cab/models.py b/cab/models.py index 022f2b68..ff9a24ff 100644 --- a/cab/models.py +++ b/cab/models.py @@ -13,12 +13,12 @@ from .listeners import start_listening from .utils import sanitize_markdown -VERSIONS = getattr(settings, 'CAB_VERSIONS', ()) +VERSIONS = getattr(settings, "CAB_VERSIONS", ()) class LanguageManager(models.Manager): def top_languages(self): - return self.annotate(score=Count('snippet')).order_by('-score') + return self.annotate(score=Count("snippet")).order_by("-score") class Language(models.Model): @@ -31,13 +31,13 @@ class Language(models.Model): objects = LanguageManager() class Meta: - ordering = ('name',) + ordering = ("name",) def __str__(self): return self.name def get_absolute_url(self): - return reverse('cab_language_detail', kwargs={'slug': self.slug}) + return reverse("cab_language_detail", kwargs={"slug": self.slug}) def get_lexer(self): return lexers.get_lexer_by_name(self.language_code) @@ -45,25 +45,29 @@ def get_lexer(self): class SnippetManager(models.Manager): def top_authors(self): - return User.objects.annotate( - score=Count('snippet')).order_by('-score', 'username') + return User.objects.annotate(score=Count("snippet")).order_by( + "-score", "username" + ) def top_tags(self): - return self.model.tags.most_common().order_by('-num_times', 'name') + return self.model.tags.most_common().order_by("-num_times", "name") def top_rated(self): # this is slow # return self.annotate(score=Sum('ratings__score')).order_by('-score') - return self.all().order_by('-rating_score', '-pub_date') + return self.all().order_by("-rating_score", "-pub_date") def most_bookmarked(self): # this is slow # self.annotate(score=Count('bookmarks')).order_by('-score') - return self.all().order_by('-bookmark_count', '-pub_date') + return self.all().order_by("-bookmark_count", "-pub_date") def matches_tag(self, tag): return self.filter(tags__in=[tag]) + def active_snippet(self): + return self.exclude(flags__flag=SnippetFlag.FLAG_SPAM) + class Snippet(models.Model): title = models.CharField(max_length=255) @@ -73,7 +77,7 @@ class Snippet(models.Model): description_html = models.TextField(editable=False) code = models.TextField() highlighted_code = models.TextField(editable=False) - version = models.CharField(max_length=5, choices=VERSIONS, default='0.0') + version = models.CharField(max_length=5, choices=VERSIONS, default="0.0") pub_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) bookmark_count = models.IntegerField(default=0) # denormalized count @@ -85,7 +89,7 @@ class Snippet(models.Model): objects = SnippetManager() class Meta: - ordering = ('-pub_date',) + ordering = ("-pub_date",) def __str__(self): return self.title @@ -96,15 +100,15 @@ def save(self, *args, **kwargs): super(Snippet, self).save(*args, **kwargs) def get_absolute_url(self): - return reverse('cab_snippet_detail', kwargs={'snippet_id': self.id}) + return reverse("cab_snippet_detail", kwargs={"snippet_id": self.id}) def highlight(self): - return highlight(self.code, - self.language.get_lexer(), - formatters.HtmlFormatter(linenos=True)) + return highlight( + self.code, self.language.get_lexer(), formatters.HtmlFormatter(linenos=True) + ) def get_tagstring(self): - return ", ".join([t.name for t in self.tags.order_by('name').all()]) + return ", ".join([t.name for t in self.tags.order_by("name").all()]) def get_version(self): return dict(VERSIONS)[self.version] @@ -119,12 +123,14 @@ def update_bookmark_count(self): def mark_as_inappropiate(self): snippet_flag = SnippetFlag( - snippet=self, user=self.author, flag=SnippetFlag.FLAG_INAPPROPRIATE) + snippet=self, user=self.author, flag=SnippetFlag.FLAG_INAPPROPRIATE + ) snippet_flag.save() def mark_as_spam(self): snippet_flag = SnippetFlag( - snippet=self, user=self.author, flag=SnippetFlag.FLAG_SPAM) + snippet=self, user=self.author, flag=SnippetFlag.FLAG_SPAM + ) snippet_flag.save() @@ -132,15 +138,15 @@ class SnippetFlag(models.Model): FLAG_SPAM = 1 FLAG_INAPPROPRIATE = 2 FLAG_CHOICES = ( - (FLAG_SPAM, 'Spam'), - (FLAG_INAPPROPRIATE, 'Inappropriate'), + (FLAG_SPAM, "Spam"), + (FLAG_INAPPROPRIATE, "Inappropriate"), ) - snippet = models.ForeignKey(Snippet, related_name='flags', on_delete=models.CASCADE) + snippet = models.ForeignKey(Snippet, related_name="flags", on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) flag = models.IntegerField(choices=FLAG_CHOICES) def __str__(self): - return '%s flagged as %s by %s' % ( + return "%s flagged as %s by %s" % ( self.snippet.title, self.get_flag_display(), self.user.username, @@ -155,12 +161,16 @@ def remove_and_ban(self): class Bookmark(models.Model): - snippet = models.ForeignKey(Snippet, related_name='bookmarks', on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name='cab_bookmarks', on_delete=models.CASCADE) + snippet = models.ForeignKey( + Snippet, related_name="bookmarks", on_delete=models.CASCADE + ) + user = models.ForeignKey( + User, related_name="cab_bookmarks", on_delete=models.CASCADE + ) date = models.DateTimeField(auto_now_add=True) class Meta: - ordering = ('-date',) + ordering = ("-date",) def __str__(self): return "%s bookmarked by %s" % (self.snippet, self.user) diff --git a/cab/views/snippets.py b/cab/views/snippets.py index 03210d0e..e9ccc687 100644 --- a/cab/views/snippets.py +++ b/cab/views/snippets.py @@ -18,14 +18,9 @@ def snippet_list(request, queryset=None, **kwargs): if queryset is None: - queryset = Snippet.objects.all() + queryset = Snippet.objects.active_snippet() - return month_object_list( - request, - queryset=queryset, - paginate_by=20, - **kwargs - ) + return month_object_list(request, queryset=queryset, paginate_by=20, **kwargs) def snippet_detail(request, snippet_id): @@ -33,41 +28,42 @@ def snippet_detail(request, snippet_id): request, queryset=Snippet.objects.all(), object_id=snippet_id, - extra_context={'flag_form': SnippetFlagForm()}, + extra_context={"flag_form": SnippetFlagForm()}, ) def download_snippet(request, snippet_id): snippet = get_object_or_404(Snippet, pk=snippet_id) - response = HttpResponse(snippet.code, content_type='text/plain') - response['Content-Disposition'] = 'attachment; filename=%s.%s' % \ - (snippet.id, snippet.language.file_extension) - response['Content-Type'] = snippet.language.mime_type + response = HttpResponse(snippet.code, content_type="text/plain") + response["Content-Disposition"] = "attachment; filename=%s.%s" % ( + snippet.id, + snippet.language.file_extension, + ) + response["Content-Type"] = snippet.language.mime_type return response def raw_snippet(request, snippet_id): snippet = get_object_or_404(Snippet, pk=snippet_id) - response = HttpResponse(snippet.code, content_type='text/plain') - response['Content-Disposition'] = 'inline' + response = HttpResponse(snippet.code, content_type="text/plain") + response["Content-Disposition"] = "inline" return response @login_required def rate_snippet(request, snippet_id): snippet = get_object_or_404(Snippet, pk=snippet_id) - score = request.GET.get('score') - if score and score in ['up', 'down']: - score = {'up': 1, 'down': -1}[score] + score = request.GET.get("score") + if score and score in ["up", "down"]: + score = {"up": 1, "down": -1}[score] snippet.ratings.rate(user=request.user, score=score) - elif score == 'reset': + elif score == "reset": snippet.ratings.unrate(request.user) return redirect(snippet) @login_required -def edit_snippet(request, snippet_id=None, - template_name='cab/edit_snippet.html'): +def edit_snippet(request, snippet_id=None, template_name="cab/edit_snippet.html"): if not request.user.is_active: return HttpResponseForbidden() @@ -76,55 +72,53 @@ def edit_snippet(request, snippet_id=None, if request.user.id != snippet.author.id: return HttpResponseForbidden() else: - template_name = 'cab/add_snippet.html' - snippet = Snippet(author=request.user, - language=Language.objects.get(name='Python')) + template_name = "cab/add_snippet.html" + snippet = Snippet( + author=request.user, language=Language.objects.get(name="Python") + ) - if request.method == 'POST': + if request.method == "POST": form = SnippetForm(instance=snippet, data=request.POST) if form.is_valid(): snippet = form.save() - messages.info(request, 'Your snippet has been saved') + messages.info(request, "Your snippet has been saved") return redirect(snippet) else: form = SnippetForm(instance=snippet) - return render(request, template_name, {'form': form}) + return render(request, template_name, {"form": form}) @login_required -def flag_snippet(request, snippet_id, template_name='cab/flag_snippet.html'): +def flag_snippet(request, snippet_id, template_name="cab/flag_snippet.html"): snippet = get_object_or_404(Snippet, id=snippet_id) snippet_flag = SnippetFlag(snippet=snippet, user=request.user) - if request.method == 'POST': + if request.method == "POST": form = SnippetFlagForm(request.POST, instance=snippet_flag) if form.is_valid(): snippet_flag = form.save() admin_link = request.build_absolute_uri( - reverse('admin:cab_snippetflag_changelist') + reverse("admin:cab_snippetflag_changelist") ) mail_admins( 'Snippet flagged: "%s"' % (snippet.title), - '%s\n\nAdmin link: %s' % (snippet_flag, admin_link), + "%s\n\nAdmin link: %s" % (snippet_flag, admin_link), fail_silently=True, ) - messages.info(request, 'Thank you for helping improve the site!') + messages.info(request, "Thank you for helping improve the site!") return redirect(snippet) else: if request.is_ajax(): return redirect(snippet) - messages.error(request, 'Invalid form submission') + messages.error(request, "Invalid form submission") else: form = SnippetFlagForm(instance=snippet_flag) - return render(request, template_name, { - 'form': form, - 'snippet': snippet, - }) + return render(request, template_name, {"form": form, "snippet": snippet}) def author_snippets(request, username): @@ -133,8 +127,8 @@ def author_snippets(request, username): return snippet_list( request, snippet_qs, - template_name='cab/user_detail.html', - extra_context={'author': user}, + template_name="cab/user_detail.html", + extra_context={"author": user}, ) @@ -144,64 +138,67 @@ def matches_tag(request, slug): return snippet_list( request, queryset=snippet_qs, - template_name='cab/tag_detail.html', - extra_context={'tag': tag}, + template_name="cab/tag_detail.html", + extra_context={"tag": tag}, ) def search(request): - query = request.GET.get('q') + query = request.GET.get("q") snippet_qs = Snippet.objects.none() if query: - snippet_qs = Snippet.objects.filter( - Q(title__icontains=query) | - Q(tags__in=[query]) | - Q(author__username__iexact=query) - ).distinct().order_by('-rating_score', '-pub_date') + snippet_qs = ( + Snippet.objects.filter( + Q(title__icontains=query) + | Q(tags__in=[query]) + | Q(author__username__iexact=query) + ) + .distinct() + .order_by("-rating_score", "-pub_date") + ) return snippet_list( request, queryset=snippet_qs, - template_name='search/search.html', - extra_context={'query': query}, + template_name="search/search.html", + extra_context={"query": query}, ) def autocomplete(request): - q = request.GET.get('q', '') + q = request.GET.get("q", "") results = [] if len(q) > 2: - result_set = Snippet.objects.annotate(search=SearchVector('title')).filter(search=q)[:10] + result_set = Snippet.objects.annotate(search=SearchVector("title")).filter( + search=q + )[:10] for obj in result_set: url = obj.get_absolute_url() - results.append({ - 'title': obj.title, - 'author': obj.author.username, - 'url': url - }) - return HttpResponse(json.dumps(results), content_type='application/json') + results.append( + {"title": obj.title, "author": obj.author.username, "url": url} + ) + return HttpResponse(json.dumps(results), content_type="application/json") def tag_hint(request): - q = request.GET.get('q', '') + q = request.GET.get("q", "") results = [] if len(q) > 2: tag_qs = Tag.objects.filter(slug__startswith=q) - annotated_qs = tag_qs.annotate(count=Count('taggit_taggeditem_items__id')) + annotated_qs = tag_qs.annotate(count=Count("taggit_taggeditem_items__id")) - for obj in annotated_qs.order_by('-count', 'slug')[:10]: - results.append({ - 'tag': obj.slug, - 'count': obj.count, - }) + for obj in annotated_qs.order_by("-count", "slug")[:10]: + results.append({"tag": obj.slug, "count": obj.count}) - return HttpResponse(json.dumps(results), content_type='application/json') + return HttpResponse(json.dumps(results), content_type="application/json") def basic_search(request): - q = request.GET.get('q') - snippet_qs = Snippet.objects.annotate(search=SearchVector('title', 'description', 'author__username')) + q = request.GET.get("q") + snippet_qs = Snippet.objects.annotate( + search=SearchVector("title", "description", "author__username") + ) form = AdvancedSearchForm(request.GET) if form.is_valid(): @@ -210,17 +207,25 @@ def basic_search(request): return snippet_list( request, queryset=snippet_qs, - template_name='search/search.html', - extra_context={ - 'query': q, 'form': form - },) + template_name="search/search.html", + extra_context={"query": q, "form": form}, + ) def advanced_search(request): - snippet_qs = Snippet.objects.annotate(search=SearchVector('title', 'description', 'language__name', - 'version', 'pub_date', - 'bookmark_count', 'rating_score', 'author__username')) + snippet_qs = Snippet.objects.annotate( + search=SearchVector( + "title", + "description", + "language__name", + "version", + "pub_date", + "bookmark_count", + "rating_score", + "author__username", + ) + ) form = AdvancedSearchForm(request.GET) if form.is_valid(): snippet_qs = form.search(snippet_qs) @@ -228,6 +233,6 @@ def advanced_search(request): return snippet_list( request, queryset=snippet_qs, - template_name='search/advanced_search.html', - extra_context={'form': form}, + template_name="search/advanced_search.html", + extra_context={"form": form}, ) diff --git a/setup.cfg b/setup.cfg index c4c4a79e..b1be45b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [flake8] max-line-length=119 -ignore=F405,W504 +ignore=F405,W504,W503 [isort] combine_as_imports = true