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

- +
+ + + + + + + + + + + + + + + {% for question in spam_questions %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
FOSSTutorialMinSecQuestionDateUserActions
{{ 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 +
No spam questions pending approval.
+
{% 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