diff --git a/.env.example b/.env.example
index b9a00a7..d1a9246 100644
--- a/.env.example
+++ b/.env.example
@@ -6,4 +6,5 @@ SPOKEN_DB='13thJune'
SECRET_KEY=''
VIDEO_PATH=''
DEBUG=True
-TEMPLATE_DEBUG=True
\ No newline at end of file
+TEMPLATE_DEBUG=True
+SPAM_LOG_FILE=''
\ No newline at end of file
diff --git a/forums/logs/spam_detection.log b/forums/logs/spam_detection.log
new file mode 100644
index 0000000..e69de29
diff --git a/forums/settings.py b/forums/settings.py
index 6c549d2..0ee0882 100644
--- a/forums/settings.py
+++ b/forums/settings.py
@@ -17,6 +17,7 @@
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+SPAM_LOG_FILE = os.getenv("SPAM_LOG_FILE")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
@@ -267,29 +268,54 @@
except ImportError:
pass
-
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
+
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
+
+ 'formatters': {
+ 'verbose': {
+ 'format': '[{asctime}] {levelname} {name}: {message}',
+ 'style': '{',
+ },
+ },
+
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
- }
+ },
+
+ # New handler for spam detection
+ 'spam_file': {
+ 'level': 'INFO',
+ 'class': 'logging.handlers.RotatingFileHandler',
+ 'filename': SPAM_LOG_FILE, # create logs/ dir
+ 'maxBytes': 1024 * 1024 * 5, # 5 MB
+ 'backupCount': 5, # keep 5 old logs
+ 'formatter': 'verbose',
+ },
},
+
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
+
+ # New dedicated logger
+ 'spam_detection': {
+ 'handlers': ['spam_file'],
+ 'level': 'INFO',
+ 'propagate': False,
+ },
}
}
-
VIDEO_PATH = os.getenv("VIDEO_PATH")
\ No newline at end of file
diff --git a/seed_spam_rules.py b/seed_spam_rules.py
new file mode 100644
index 0000000..3e393b0
--- /dev/null
+++ b/seed_spam_rules.py
@@ -0,0 +1,116 @@
+# Script to seed the database with predefined spam rules
+import os
+import django
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "forums.settings")
+django.setup()
+
+from django.db.models import Q
+from website.models import SpamRule
+
+
+
+def seed_spam_rules():
+ rules = {
+ # Certification/Exam dump patterns
+ "Certification/Exam Spam": {
+ "score": 30,
+ "type": SpamRule.KEYWORD,
+ "patterns": [
+ r"exam\s+dumps?", r"braindumps?", r"practice\s+test",
+ r"certification\s+exam", r"test\s+preparation",
+ r"exam\s+questions?", r"study\s+guides?",
+ r"pdf\s+\+\s+testing\s+engine", r"testing\s+engine",
+ r"exam\s+prep", r"mock\s+exam", r"real\s+exam",
+ r"dumps\s+pdf", r"braindump"
+ ],
+ },
+
+ # Promotional spam
+ "Promotional Spam": {
+ "score": 25,
+ "type": SpamRule.KEYWORD,
+ "patterns": [
+ r"click\s+here", r"join\s+now", r"limited\s+time",
+ r"discount", r"coupon\s+code", r"20%\s+off",
+ r"free\s+download", r"get\s+certified",
+ r"unlock\s+your\s+career", r"master\s+the",
+ r"boost\s+your\s+career", r"cert20",
+ r"at\s+checkout", r"special\s+offer",
+ ],
+ },
+
+ # Suspicious domains
+ "Suspicious Domain": {
+ "score": 35,
+ "type": SpamRule.DOMAIN,
+ "patterns": [
+ r"dumpscafe\.com", r"certsout\.com", r"mycertshub\.com",
+ r"vmexam\.com", r"kissnutra\.com", r"dumps.*\.com",
+ r"cert.*\.com", r"exam.*\.com",
+ ],
+ },
+
+ # Generic business language
+ "Business/Career Spam": {
+ "score": 15,
+ "type": SpamRule.KEYWORD,
+ "patterns": [
+ r"attests\s+to\s+your\s+proficiency",
+ r"esteemed\s+(?:accreditation|certification|credential)",
+ r"valuable\s+asset\s+to\s+companies",
+ r"demonstrates\s+your\s+ability",
+ r"comprehensive\s+study\s+(?:tools|materials)",
+ r"interactive\s+practice\s+tests",
+ r"real\s+exam\s+questions",
+ r"actual\s+exam\s+questions",
+ r"validated\s+by\s+.*certification",
+ r"urgently\s+need\s+experts",
+ ],
+ },
+
+ # Gaming content
+ "Gaming Spam": {
+ "score": 20,
+ "type": SpamRule.KEYWORD,
+ "patterns": [
+ r"spacebar\s+clicker", r"clicker\s+game",
+ r"addictive\s+game", r"upgrades\s+available",
+ r"instant\s+rewards",
+ ],
+ },
+
+ # Health/Supplement spam
+ "Health Spam": {
+ "score": 22,
+ "type": SpamRule.KEYWORD,
+ "patterns": [
+ r"vitalit[äa]t", r"nahrungserg[äa]nzungsmittel",
+ r"libido", r"fruchtbarkeit", r"energie",
+ r"hormonelle\s+balance", r"perforan",
+ ],
+ },
+ }
+
+ inserted, skipped = 0, 0
+ for note, config in rules.items():
+ for pattern in config["patterns"]:
+ exists = SpamRule.objects.filter(
+ Q(pattern=pattern) & Q(type=config["type"])
+ ).exists()
+ if not exists:
+ SpamRule.objects.create(
+ type=config["type"],
+ pattern=pattern,
+ score=config["score"],
+ notes=note,
+ )
+ inserted += 1
+ else:
+ skipped += 1
+
+ print(f"✅ Inserted {inserted} new rules, skipped {skipped} existing ones.")
+
+
+# Run it
+seed_spam_rules()
diff --git a/static/website/templates/index.html b/static/website/templates/index.html
index 71554a1..89c7b0b 100755
--- a/static/website/templates/index.html
+++ b/static/website/templates/index.html
@@ -93,6 +93,7 @@
Answers
@@ -279,7 +280,46 @@
Answers
-
+
+
+
+
+ | FOSS |
+ Tutorial |
+ Min |
+ Sec |
+ Question |
+ Date |
+ User |
+ Actions |
+
+
+
+ {% for question in spam_questions %}
+
+ | {{ question.category|truncatechars:12 }} |
+ {{ question.tutorial|truncatechars:12 }} |
+ {{ question.minute_range }} |
+ {{ question.second_range }} |
+
+
+ {{ question.title|truncatechars:40 }}
+
+ |
+ {{ question.date_created|date:"d-m-y" }} |
+ {{ question.user|truncatechars:10 }} |
+
+
+ Approve
+ Reject
+ |
+
+ {% empty %}
+ | No spam questions pending approval. |
+ {% endfor %}
+
+
+
{% endblock %}
diff --git a/website/forms.py b/website/forms.py
index 4a41b7e..38211e5 100755
--- a/website/forms.py
+++ b/website/forms.py
@@ -69,4 +69,4 @@ def __init__(self, *args, **kwargs):
class AnswerQuesitionForm(forms.Form):
question = forms.IntegerField(widget=forms.HiddenInput())
- body = forms.CharField(widget=forms.Textarea())
+ body = forms.CharField(widget=forms.Textarea())
\ No newline at end of file
diff --git a/website/helpers.py b/website/helpers.py
index 5db0a53..ecc6064 100755
--- a/website/helpers.py
+++ b/website/helpers.py
@@ -1,10 +1,26 @@
import re
-from website.models import Question
-from nltk.corpus import stopwords
+import json
+import logging
+from datetime import datetime
+from typing import Dict, List, Tuple, Optional
+from website.models import Question, User
+from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
+from website.templatetags.permission_tags import can_edit, can_hide_delete
from sklearn.metrics.pairwise import cosine_similarity
+from django.conf import settings
+from django.utils import timezone
+from django.db.models import Q
+import re
+from .models import SpamRule, SpamLog # assuming app is `forum`
+
sw = stopwords.words('english')
+# Configure logging for spam detection
+import logging
+spam_logger = logging.getLogger('spam_detection')
+
+
def get_video_info(path):
"""Uses ffmpeg to determine information about a video. This has not been broadly
tested and your milage may vary"""
@@ -67,4 +83,159 @@ def get_similar_questions(user_ques,question):
if w in question: l2.append(1)
else: l2.append(0)
cs = cosine_similarity((l1,l2))
- return cs[0][1]
\ No newline at end of file
+ return cs[0][1]
+
+
+# helpers.py
+
+MULTIPLE_URL_WEIGHT = 20
+MULTIPLE_URL_THRESHOLD = 3
+
+class SpamQuestionDetector:
+ def __init__(self):
+ # load only active + not expired rules
+ now = timezone.now()
+ qs = SpamRule.objects.filter(active=True).filter(
+ Q(expires_at__isnull=True) | Q(expires_at__gt=now)
+ )
+ self._compiled = []
+ for r in qs:
+ try:
+ cre = re.compile(r.pattern, re.IGNORECASE)
+ except re.error:
+ spam_logger.warning(f"Invalid regex in SpamRule id={r.id}: {r.pattern}")
+ continue
+ self._compiled.append({
+ 'rule': r,
+ 'compiled': cre
+ })
+
+ def extract_urls(self, text: str):
+ return re.findall(r'https?://[^\s)<>"]+', text)
+
+ def detect_spam(self,user,question, title: str, content: str, category: str = "", tutorial: str = "") -> dict:
+ combined_text = " ".join(filter(None, [title, content, category, tutorial])).lower()
+ spam_score = 0
+ matches = []
+
+ for entry in self._compiled:
+ rule = entry['rule']
+ cre = entry['compiled']
+ if cre.search(combined_text):
+ spam_score += rule.score
+ matches.append({
+ 'id': rule.id,
+ 'pattern': rule.pattern,
+ 'score': rule.score,
+ 'type': rule.type,
+ 'notes': rule.notes
+ })
+
+ # detect multiple URLs (we keep this behaviour from original)
+ urls = self.extract_urls(combined_text)
+ if len(urls) >= MULTIPLE_URL_THRESHOLD:
+ spam_score += MULTIPLE_URL_WEIGHT
+ matches.append({
+ 'pattern': f'{len(urls)} URLs',
+ 'score': MULTIPLE_URL_WEIGHT,
+ 'type': 'urls'
+ })
+
+ # classification (same thresholds as earlier)
+ if spam_score >= 60:
+ confidence, action = 'HIGH', 'DELETE'
+ elif spam_score >= 30:
+ confidence, action = 'MEDIUM', 'REVIEW'
+ elif spam_score >= 15:
+ confidence, action = 'LOW', 'REVIEW'
+ else:
+ confidence, action = 'CLEAN', 'APPROVE'
+
+ result = {
+ 'spam_score': spam_score,
+ 'matches': matches,
+ 'confidence': confidence,
+ 'recommended_action': action,
+ 'url_count': len(urls)
+ }
+
+ # debug log
+ spam_logger.info(
+ "SpamDetect result: question_id=%s user_id=%s score=%s action=%s matches=%s",
+ question.id, user.id, spam_score, action, len(matches)
+ )
+ return result
+
+
+def handle_spam(question, user, delete_on_high=True, save_question_metadata_before_delete=True):
+ """
+ Runs detection on a saved Question instance and logs/takes action.
+ - question: saved Question instance (has .id)
+ - user: Django user instance who created the question (for logging)
+ - delete_on_high: if True, HIGH confidence -> delete from DB; otherwise hide it (status=0)
+ Returns a status string: 'AUTO_DELETE', 'FLAGGED', 'APPROVED', 'HIDDEN'
+ """
+ detector = SpamQuestionDetector()
+ result = detector.detect_spam(
+ user=user,
+ question=question,
+ title=getattr(question, 'title', '') or '',
+ content=getattr(question, 'body', '') or '',
+ category=getattr(question, 'category', '') or '',
+ tutorial=getattr(question, 'tutorial', '') or ''
+ )
+
+ spam_score = result['spam_score']
+ confidence = result['confidence']
+ action = result['recommended_action']
+ details = result['matches']
+
+ # prepare log payload
+ log_payload = {
+ 'question_id': question.id,
+ 'user_id': user.id if user else None,
+ 'category': getattr(question, 'category', '') or '',
+ 'title': getattr(question, 'title', '') or '',
+ 'content': getattr(question, 'body', '') or '',
+ 'action': None,
+ 'spam_score': spam_score,
+ 'confidence': confidence,
+ 'details': details
+ }
+
+ # TAKE ACTION
+ if action == 'DELETE' and confidence == 'HIGH':
+ log_payload['action'] = 'AUTO_DELETE'
+ SpamLog.objects.create(**log_payload)
+
+ if delete_on_high:
+ # delete after logging
+ spam_logger.info(f"MARK_INACTIVE: Question {question.id} by user {user.id} score={spam_score}")
+ question.status = 0
+ question.save(update_fields=["status"])
+ user.is_active = 0
+ user.save(update_fields=["is_active"])
+ return 'AUTO_DELETE'
+ else:
+ # hide instead of delete
+ question.spam = True
+ question.status = 0
+ question.save(update_fields=['spam', 'status'])
+ return 'HIDDEN'
+
+ elif action == 'REVIEW':
+ # flag for admin review
+ log_payload['action'] = 'FLAGGED'
+ SpamLog.objects.create(**log_payload)
+
+ question.approval_required = True
+ question.spam = False
+ question.save(update_fields=['approval_required', 'spam'])
+ return 'FLAGGED'
+
+ else:
+ # APPROVE / CLEAN
+ question.spam = False
+ question.approval_required = False
+ question.save(update_fields=['spam', 'approval_required'])
+ return 'APPROVED'
\ No newline at end of file
diff --git a/website/migrations/0001_initial.py b/website/migrations/0001_initial.py
new file mode 100644
index 0000000..8c484ae
--- /dev/null
+++ b/website/migrations/0001_initial.py
@@ -0,0 +1,102 @@
+# Generated by Django 2.2.6 on 2025-09-12 11:42
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Answer',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('body', models.TextField()),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_modified', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('pid', models.IntegerField()),
+ ('qid', models.IntegerField()),
+ ('aid', models.IntegerField(default=0)),
+ ('cid', models.IntegerField(default=0)),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Question',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('category', models.CharField(max_length=200)),
+ ('tutorial', models.CharField(max_length=200)),
+ ('minute_range', models.CharField(max_length=10)),
+ ('second_range', models.CharField(max_length=10)),
+ ('title', models.CharField(max_length=200)),
+ ('body', models.TextField()),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_modified', models.DateTimeField(auto_now=True)),
+ ('views', models.IntegerField(default=1)),
+ ('status', models.IntegerField(default=1)),
+ ('last_active', models.DateTimeField(null=True)),
+ ('last_post_by', models.IntegerField(null=True)),
+ ],
+ options={
+ 'get_latest_by': 'date_created',
+ },
+ ),
+ migrations.CreateModel(
+ name='QuestionVote',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='website.Question')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='QuestionComment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('body', models.TextField()),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_modified', models.DateTimeField(auto_now=True)),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='website.Question')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='AnswerVote',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='website.Answer')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='AnswerComment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uid', models.IntegerField()),
+ ('body', models.TextField()),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_modified', models.DateTimeField(auto_now=True)),
+ ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='website.Answer')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='answer',
+ name='question',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='website.Question'),
+ ),
+ ]
diff --git a/website/migrations/0002_auto_20250912_1713.py b/website/migrations/0002_auto_20250912_1713.py
new file mode 100644
index 0000000..9dd45e8
--- /dev/null
+++ b/website/migrations/0002_auto_20250912_1713.py
@@ -0,0 +1,65 @@
+# Generated by Django 2.2.6 on 2025-09-12 11:43
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('website', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SpamLog',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('question_id', models.IntegerField()),
+ ('user_id', models.IntegerField(blank=True, null=True)),
+ ('category', models.CharField(blank=True, max_length=200)),
+ ('title', models.CharField(blank=True, max_length=200)),
+ ('content', models.TextField(blank=True)),
+ ('action', models.CharField(choices=[('AUTO_DELETE', 'Auto Deleted'), ('FLAGGED', 'Flagged for Review'), ('APPROVED', 'Approved')], max_length=20)),
+ ('spam_score', models.IntegerField()),
+ ('confidence', models.CharField(max_length=20)),
+ ('details', models.TextField(blank=True, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='SpamRule',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('type', models.CharField(choices=[('keyword', 'Keyword'), ('domain', 'Domain / URL')], max_length=10)),
+ ('pattern', models.CharField(max_length=500)),
+ ('score', models.IntegerField(default=1)),
+ ('active', models.BooleanField(default=True)),
+ ('notes', models.CharField(blank=True, max_length=200)),
+ ('expires_at', models.DateTimeField(blank=True, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='approval_required',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='approved_by',
+ field=models.IntegerField(null=True),
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='date_approved',
+ field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='spam',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/website/migrations/__init__.py b/website/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/website/models.py b/website/models.py
index 9dbf6f0..44a8093 100755
--- a/website/models.py
+++ b/website/models.py
@@ -1,6 +1,6 @@
from django.db import models
from django.contrib.auth import get_user_model
-
+import json
User = get_user_model()
@@ -18,6 +18,11 @@ class Question(models.Model):
status = models.IntegerField(default=1)
last_active = models.DateTimeField(null=True)
last_post_by = models.IntegerField(null=True)
+ spam = models.BooleanField(default=False)
+ approval_required = models.BooleanField(default=False)
+ approved_by = models.IntegerField(null=True)
+ date_approved = models.DateTimeField(auto_now_add=True)
+
# votes = models.IntegerField(default=0)
def user(self):
@@ -88,3 +93,49 @@ def poster(self):
return user.username
# CDEEP database created using inspectdb arg of manage.py
+class SpamRule(models.Model):
+ KEYWORD = "keyword"
+ DOMAIN = "domain"
+ TYPES = [(KEYWORD, "Keyword"), (DOMAIN, "Domain / URL")]
+
+ type = models.CharField(max_length=10, choices=TYPES)
+ pattern = models.CharField(max_length=500)
+ score = models.IntegerField(default=1)
+ active = models.BooleanField(default=True)
+ notes = models.CharField(max_length=200, blank=True)
+ expires_at = models.DateTimeField(null=True, blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f"{self.type}: {self.pattern} ({self.score})"
+
+class SpamLog(models.Model):
+ ACTIONS = [
+ ("AUTO_DELETE", "Auto Deleted"),
+ ("FLAGGED", "Flagged for Review"),
+ ("APPROVED", "Approved"),
+ ]
+
+ question_id = models.IntegerField()
+ user_id = models.IntegerField(null=True, blank=True)
+ category = models.CharField(max_length=200, blank=True)
+ title = models.CharField(max_length=200, blank=True)
+ content = models.TextField(blank=True)
+ action = models.CharField(max_length=20, choices=ACTIONS)
+ spam_score = models.IntegerField()
+ confidence = models.CharField(max_length=20)
+ details = models.TextField(blank=True, null=True)
+
+ def set_details(self, data):
+ self.details = json.dumps(data)
+
+ def get_details(self):
+ try:
+ return json.loads(self.details)
+ except Exception:
+ return {}
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ return f"Q{self.question_id} - {self.action} ({self.spam_score})"
diff --git a/website/views.py b/website/views.py
index c40fce4..a07635d 100755
--- a/website/views.py
+++ b/website/views.py
@@ -8,16 +8,18 @@
from django.core.mail import EmailMultiAlternatives
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.auth import get_user_model
-
+from django.contrib.auth.models import Group
+from django.contrib import messages
from website.models import Question, Answer, Notification, AnswerComment
from spoken_auth.models import TutorialDetails, TutorialResources
from website.forms import NewQuestionForm, AnswerQuesitionForm
-from website.helpers import get_video_info, prettify, clean_user_data, get_similar_questions
+from website.helpers import get_video_info, prettify,clean_user_data, get_similar_questions, SpamQuestionDetector, handle_spam
from django.conf import settings
from website.templatetags.permission_tags import can_edit, can_hide_delete
from spoken_auth.models import FossCategory
from .sortable import SortableHeader, get_sorted_list, get_field_index
-
+from forums.views import user_logout
+from website.permissions import is_administrator
User = get_user_model()
categories = []
@@ -37,7 +39,14 @@ def home(request):
slider_questions = Question.objects.filter(
date_created=Subquery(subquery), status=1
).order_by('category')
-
+
+ # Add spam questions only for admin users
+ spam_questions = []
+ is_admin = False
+ if request.user.is_authenticated and is_administrator(request.user):
+ spam_questions = Question.objects.filter(status=2).order_by('-date_created') # status=2 for spam
+ is_admin = True
+
# Mapping of foss name as in spk db & its corresponding category name in forums db
category_fosses = {val.replace(" ", "-") : val for val in categories}
@@ -62,7 +71,9 @@ def home(request):
context = {
'questions': questions,
'active_questions':active_questions,
- 'category_question_map': category_question_map
+ 'category_question_map': category_question_map,
+ 'spam_questions': spam_questions,
+ 'is_admin': is_admin,
}
return render(request, "website/templates/index.html", context)
@@ -70,9 +81,9 @@ def home(request):
def questions(request):
questions = Question.objects.filter(status=1).order_by('category', 'tutorial')
questions = questions.annotate(total_answers=Count('answer'))
-
+
raw_get_data = request.GET.get('o', None)
-
+
header = {
1: SortableHeader('category', True, 'Foss'),
2: SortableHeader('tutorial', True, 'Tutorial Name'),
@@ -331,47 +342,58 @@ def new_question(request):
question.body = cleaned_data['body']
question.views = 1
question.save()
+ # Run spam detection
+ action = handle_spam(question, request.user)
+
+ if action == "AUTO_DELETE":
+ messages.error(request, " Your question is being marked as spam and your account has been deactivated.")
+ user_logout(request)
+ return HttpResponseRedirect('/')
+
+ elif action == "FLAGGED":
+ messages.warning(request, " Your question is pending moderator review.")
+ # Don’t send email for flagged content
+ return HttpResponseRedirect('/')
+
+ else: # APPROVED
+
+ subject = 'New Forum Question'
+ message = f"""
+ The following new question has been posted in the Spoken Tutorial Forum:
+ Title: {question.title}
+ Category: {question.category}
+ Tutorial: {question.tutorial}
+ Link:
+ http://forums.spoken-tutorial.org/question/{question.id}
+
+ Question: {question.body}
+ """
+ email = EmailMultiAlternatives(
+ subject, '', 'forums',
+ ['team@spoken-tutorial.org', 'team@fossee.in'],
+ headers={"Content-type": "text/html;charset=iso-8859-1"}
+ )
+ email.attach_alternative(message, "text/html")
+ email.send(fail_silently=True)
+ return HttpResponseRedirect('/')
- # Sending email when a new question is asked
- subject = 'New Forum Question'
- message = """
- The following new question has been posted in the Spoken Tutorial Forum:
- Title: {0}
- Category: {1}
- Tutorial: {2}
- Link: {3}
- Question: {4}
- """.format(
- question.title,
- question.category,
- question.tutorial,
- 'http://forums.spoken-tutorial.org/question/' + str(question.id),
- question.body
- )
- email = EmailMultiAlternatives(
- subject, '', 'forums',
- ['team@spoken-tutorial.org', 'team@fossee.in'],
- headers={"Content-type": "text/html;charset=iso-8859-1"}
- )
- email.attach_alternative(message, "text/html")
- email.send(fail_silently=True)
- # End of email send
+ # If form not valid → re-render with errors
+ context['form'] = form
+ return render(request, 'website/templates/new-question.html', context)
- return HttpResponseRedirect('/')
else:
- # get values from URL.
+ # GET request → render empty form
category = request.GET.get('category', None)
tutorial = request.GET.get('tutorial', None)
minute_range = request.GET.get('minute_range', None)
second_range = request.GET.get('second_range', None)
- # pass minute_range and second_range value to NewQuestionForm to populate on select
- form = NewQuestionForm(category=category, tutorial=tutorial,
- minute_range=minute_range, second_range=second_range)
+ form = NewQuestionForm(
+ category=category, tutorial=tutorial,
+ minute_range=minute_range, second_range=second_range
+ )
+ context['form'] = form
context['category'] = category
-
- context['form'] = form
- context.update(csrf(request))
- return render(request, 'website/templates/new-question.html', context)
+ return render(request, 'website/templates/new-question.html', context)
# Notification Section