update
This commit is contained in:
0
reports/__init__.py
Normal file
0
reports/__init__.py
Normal file
247
reports/admin.py
Normal file
247
reports/admin.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Admin configuration for reports app.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django import forms
|
||||
from .models import ScamTag, ScamReport, ScamVerification, SiteSettings, TakedownRequest
|
||||
|
||||
|
||||
@admin.register(ScamTag)
|
||||
class ScamTagAdmin(admin.ModelAdmin):
|
||||
"""Scam tag admin."""
|
||||
list_display = ('name', 'slug', 'color')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
@admin.register(ScamReport)
|
||||
class ScamReportAdmin(admin.ModelAdmin):
|
||||
"""Scam report admin."""
|
||||
list_display = ('title', 'reporter', 'scam_type', 'status', 'verification_score', 'created_at')
|
||||
list_filter = ('status', 'scam_type', 'is_public', 'created_at')
|
||||
search_fields = ('title', 'description', 'reported_url', 'reported_email', 'reported_phone')
|
||||
readonly_fields = ('created_at', 'updated_at', 'verified_at', 'reporter_ip')
|
||||
filter_horizontal = ('tags',)
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
fieldsets = (
|
||||
('Report Information', {
|
||||
'fields': ('title', 'description', 'scam_type', 'tags')
|
||||
}),
|
||||
('Reported Entities', {
|
||||
'fields': ('reported_url', 'reported_email', 'reported_phone', 'reported_company')
|
||||
}),
|
||||
('Reporter', {
|
||||
'fields': ('reporter', 'is_anonymous', 'reporter_ip')
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('status', 'verification_score', 'is_public', 'verified_at')
|
||||
}),
|
||||
('Evidence', {
|
||||
'fields': ('evidence_files',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at')
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(ScamVerification)
|
||||
class ScamVerificationAdmin(admin.ModelAdmin):
|
||||
"""Scam verification admin."""
|
||||
list_display = ('report', 'verification_method', 'confidence_score', 'verified_by', 'created_at')
|
||||
list_filter = ('verification_method', 'created_at')
|
||||
search_fields = ('report__title', 'notes')
|
||||
readonly_fields = ('created_at',)
|
||||
|
||||
|
||||
@admin.register(SiteSettings)
|
||||
class SiteSettingsAdmin(admin.ModelAdmin):
|
||||
"""Site settings admin - singleton pattern."""
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Only allow one instance
|
||||
return not SiteSettings.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Prevent deletion
|
||||
return False
|
||||
|
||||
fieldsets = (
|
||||
('Контактна Информация', {
|
||||
'fields': ('contact_email', 'contact_phone', 'contact_address'),
|
||||
'description': 'Тези настройки се използват навсякъде в сайта - в подножието, страницата за контакти, структурираните данни и др.'
|
||||
}),
|
||||
('Настройки на Имейл Сървър', {
|
||||
'fields': (
|
||||
'email_backend',
|
||||
'email_host',
|
||||
'email_port',
|
||||
'email_use_tls',
|
||||
'email_use_ssl',
|
||||
'email_host_user',
|
||||
'email_host_password',
|
||||
'default_from_email',
|
||||
'email_timeout',
|
||||
),
|
||||
'description': 'Настройки за SMTP сървър. Използват се за всички имейли в платформата - контактни форми, нулиране на пароли, уведомления и др. Паролата се криптира автоматично.'
|
||||
}),
|
||||
('Информация', {
|
||||
'fields': ('updated_at',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ('updated_at',)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Customize form to handle password field."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Make password field a password input
|
||||
form.base_fields['email_host_password'].widget = forms.PasswordInput(attrs={
|
||||
'class': 'vTextField',
|
||||
'autocomplete': 'new-password'
|
||||
})
|
||||
|
||||
# Add help text
|
||||
form.base_fields['email_host_password'].help_text = 'Въведете нова парола или оставете празно, за да запазите текущата.'
|
||||
|
||||
return form
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Handle password encryption and clear cache."""
|
||||
# If password field is empty and we're editing, keep the old password
|
||||
if change and not form.cleaned_data.get('email_host_password'):
|
||||
old_obj = self.model.objects.get(pk=obj.pk)
|
||||
obj.email_host_password = old_obj.email_host_password
|
||||
|
||||
# Validate TLS/SSL are mutually exclusive
|
||||
if obj.email_use_tls and obj.email_use_ssl:
|
||||
from django.contrib import messages
|
||||
messages.warning(request, 'TLS и SSL не могат да бъдат активирани едновременно. SSL е деактивиран, използва се TLS.')
|
||||
obj.email_use_ssl = False
|
||||
|
||||
# Save will encrypt the password if it's provided
|
||||
super().save_model(request, obj, form, change)
|
||||
# Clear email backend cache to reload settings
|
||||
from django.core.cache import cache
|
||||
cache.delete('site_settings')
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
# Redirect to the single instance if it exists
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
if SiteSettings.objects.exists():
|
||||
obj = SiteSettings.objects.get(pk=1)
|
||||
url = reverse('admin:reports_sitesettings_change', args=[str(obj.pk)])
|
||||
return redirect(url)
|
||||
return super().changelist_view(request, extra_context)
|
||||
|
||||
def response_change(self, request, obj):
|
||||
"""Handle test email button."""
|
||||
if "_test_email" in request.POST:
|
||||
try:
|
||||
from django.core.mail import send_mail
|
||||
from django.contrib import messages
|
||||
|
||||
test_email = request.POST.get('test_email_address', request.user.email)
|
||||
if not test_email:
|
||||
messages.error(request, 'Моля, въведете имейл адрес за тест.')
|
||||
return super().response_change(request, obj)
|
||||
|
||||
# Check if SMTP is configured
|
||||
if obj.email_backend == 'django.core.mail.backends.smtp.EmailBackend' and not obj.email_host:
|
||||
messages.warning(request, 'SMTP сървърът не е конфигуриран. Моля, въведете Email Host преди изпращане на тестов имейл.')
|
||||
return super().response_change(request, obj)
|
||||
|
||||
# Get the connection to check backend type
|
||||
from django.core.mail import get_connection, EmailMessage
|
||||
connection = get_connection()
|
||||
backend_name = connection.__class__.__name__
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Using email backend: {backend_name}")
|
||||
|
||||
# Check underlying backend
|
||||
if hasattr(connection, '_backend') and connection._backend:
|
||||
underlying_backend = connection._backend.__class__.__name__
|
||||
logger.info(f"Underlying backend: {underlying_backend}")
|
||||
|
||||
# Send email using EmailMessage for better error handling
|
||||
email = EmailMessage(
|
||||
subject='Тестов Имейл от Портал за Докладване на Измами',
|
||||
body='Това е тестов имейл за проверка на настройките на имейл сървъра. Ако получавате този имейл, настройките са правилни.',
|
||||
from_email=obj.default_from_email,
|
||||
to=[test_email],
|
||||
connection=connection,
|
||||
)
|
||||
|
||||
result = email.send(fail_silently=False)
|
||||
|
||||
logger.info(f"Email send result: {result} (1 = success, 0 = failed)")
|
||||
|
||||
# Check which backend was actually used
|
||||
if 'Console' in backend_name or 'console' in str(connection.__class__.__module__):
|
||||
messages.warning(request, f'Имейлът е изпратен чрез конзолен backend (за разработка). За реално изпращане, конфигурирайте SMTP настройките. Backend: {backend_name}')
|
||||
else:
|
||||
messages.success(request, f'Тестов имейл изпратен успешно до {test_email}! Използван backend: {backend_name}')
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception("Error sending test email")
|
||||
error_msg = str(e)
|
||||
if 'authentication failed' in error_msg.lower():
|
||||
messages.error(request, f'Грешка при удостоверяване: Проверете потребителското име и паролата.')
|
||||
elif 'connection' in error_msg.lower() or 'timeout' in error_msg.lower():
|
||||
messages.error(request, f'Грешка при свързване: Проверете SMTP сървъра и порта.')
|
||||
else:
|
||||
messages.error(request, f'Грешка при изпращане на тестов имейл: {error_msg}')
|
||||
|
||||
return super().response_change(request, obj)
|
||||
|
||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
if object_id:
|
||||
obj = self.get_object(request, object_id)
|
||||
if obj:
|
||||
extra_context['show_test_email'] = True
|
||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||
|
||||
|
||||
@admin.register(TakedownRequest)
|
||||
class TakedownRequestAdmin(admin.ModelAdmin):
|
||||
"""Takedown request admin."""
|
||||
list_display = ('report', 'requester_name', 'requester_email', 'status', 'created_at', 'reviewed_by')
|
||||
list_filter = ('status', 'created_at', 'reviewed_at')
|
||||
search_fields = ('requester_name', 'requester_email', 'report__title', 'reason')
|
||||
readonly_fields = ('created_at', 'updated_at', 'ip_address', 'user_agent')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
fieldsets = (
|
||||
('Информация за Доклада', {
|
||||
'fields': ('report',)
|
||||
}),
|
||||
('Информация за Заявителя', {
|
||||
'fields': ('requester_name', 'requester_email', 'requester_phone')
|
||||
}),
|
||||
('Детайли на Заявката', {
|
||||
'fields': ('reason', 'evidence')
|
||||
}),
|
||||
('Статус и Преглед', {
|
||||
'fields': ('status', 'reviewed_by', 'review_notes', 'reviewed_at')
|
||||
}),
|
||||
('Техническа Информация', {
|
||||
'fields': ('ip_address', 'user_agent', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if change and 'status' in form.changed_data and obj.status in ['approved', 'rejected']:
|
||||
from django.utils import timezone
|
||||
obj.reviewed_by = request.user
|
||||
obj.reviewed_at = timezone.now()
|
||||
super().save_model(request, obj, form, change)
|
||||
15
reports/apps.py
Normal file
15
reports/apps.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
App configuration for reports app.
|
||||
"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ReportsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'reports'
|
||||
|
||||
def ready(self):
|
||||
"""Set up signals and configure email settings."""
|
||||
# Note: Email settings are loaded dynamically via the email backend
|
||||
# No need to access database here to avoid warnings
|
||||
pass
|
||||
123
reports/email_backend.py
Normal file
123
reports/email_backend.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Custom email backend that uses SiteSettings for configuration.
|
||||
"""
|
||||
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
|
||||
from django.core.mail.backends.console import EmailBackend as ConsoleEmailBackend
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.conf import settings
|
||||
from .models import SiteSettings
|
||||
|
||||
|
||||
class SiteSettingsEmailBackend(BaseEmailBackend):
|
||||
"""
|
||||
Email backend that dynamically loads settings from SiteSettings model.
|
||||
Falls back to Django settings if SiteSettings are not configured.
|
||||
"""
|
||||
|
||||
def __init__(self, fail_silently=False, **kwargs):
|
||||
super().__init__(fail_silently=fail_silently)
|
||||
self._backend = None
|
||||
self._backend_instance = None
|
||||
self._load_backend()
|
||||
|
||||
def _load_backend(self):
|
||||
"""Load the appropriate email backend based on SiteSettings."""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
site_settings = SiteSettings.get_settings()
|
||||
backend_class = site_settings.email_backend
|
||||
logger.info(f"Loading email backend: {backend_class}")
|
||||
|
||||
# If using SMTP, configure it with SiteSettings
|
||||
if backend_class == 'django.core.mail.backends.smtp.EmailBackend':
|
||||
# Get decrypted password
|
||||
email_password = site_settings.get_email_password() if hasattr(site_settings, 'get_email_password') else site_settings.email_host_password
|
||||
|
||||
# Check if SMTP is properly configured
|
||||
email_host = site_settings.email_host or getattr(settings, 'EMAIL_HOST', '')
|
||||
|
||||
# If no host is configured, fall back to console backend
|
||||
if not email_host:
|
||||
logger.warning("Email host not configured, using console backend")
|
||||
self._backend = ConsoleEmailBackend(fail_silently=self.fail_silently)
|
||||
else:
|
||||
# Ensure TLS and SSL are mutually exclusive
|
||||
use_tls = site_settings.email_use_tls
|
||||
use_ssl = site_settings.email_use_ssl
|
||||
|
||||
# If both are True, prioritize TLS (common case)
|
||||
if use_tls and use_ssl:
|
||||
use_ssl = False
|
||||
logger.warning("Both TLS and SSL were enabled. Disabling SSL and using TLS only.")
|
||||
|
||||
logger.info(f"Configuring SMTP: host={email_host}, port={site_settings.email_port}, user={site_settings.email_host_user}, tls={use_tls}, ssl={use_ssl}")
|
||||
|
||||
self._backend = SMTPEmailBackend(
|
||||
host=email_host,
|
||||
port=site_settings.email_port or getattr(settings, 'EMAIL_PORT', 587),
|
||||
username=site_settings.email_host_user or getattr(settings, 'EMAIL_HOST_USER', ''),
|
||||
password=email_password or getattr(settings, 'EMAIL_HOST_PASSWORD', ''),
|
||||
use_tls=use_tls,
|
||||
use_ssl=use_ssl,
|
||||
timeout=site_settings.email_timeout or getattr(settings, 'EMAIL_TIMEOUT', 10),
|
||||
fail_silently=self.fail_silently,
|
||||
)
|
||||
logger.info("SMTP backend configured successfully")
|
||||
elif backend_class == 'django.core.mail.backends.console.EmailBackend':
|
||||
logger.info("Using console email backend")
|
||||
self._backend = ConsoleEmailBackend(fail_silently=self.fail_silently)
|
||||
else:
|
||||
# For other backends, try to import and instantiate
|
||||
from django.utils.module_loading import import_string
|
||||
backend_class_obj = import_string(backend_class)
|
||||
self._backend = backend_class_obj(fail_silently=self.fail_silently)
|
||||
logger.info(f"Loaded custom backend: {backend_class}")
|
||||
except Exception as e:
|
||||
# Fallback to console backend if there's an error
|
||||
logger.exception(f"Error loading email backend from SiteSettings: {e}. Using console backend.")
|
||||
self._backend = ConsoleEmailBackend(fail_silently=self.fail_silently)
|
||||
|
||||
def open(self):
|
||||
"""Open a network connection."""
|
||||
if self._backend:
|
||||
return self._backend.open()
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Close the network connection."""
|
||||
if self._backend:
|
||||
return self._backend.close()
|
||||
|
||||
def send_messages(self, email_messages):
|
||||
"""Send one or more EmailMessage objects and return the number sent."""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Reload backend before sending to get latest settings
|
||||
# This ensures settings changes take effect immediately
|
||||
self._load_backend()
|
||||
if self._backend:
|
||||
try:
|
||||
# Log email details for debugging
|
||||
for msg in email_messages:
|
||||
logger.info(f"Sending email: To={msg.to}, Subject={msg.subject}, From={msg.from_email}")
|
||||
|
||||
result = self._backend.send_messages(email_messages)
|
||||
logger.info(f"Successfully sent {result} email message(s)")
|
||||
return result
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.exception(f"Error sending email messages: {error_msg}")
|
||||
|
||||
# Log more details about the error
|
||||
if hasattr(self._backend, 'host'):
|
||||
logger.error(f"SMTP Host: {self._backend.host}, Port: {getattr(self._backend, 'port', 'N/A')}")
|
||||
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return 0
|
||||
logger.warning("No email backend available, cannot send messages")
|
||||
return 0
|
||||
|
||||
124
reports/forms.py
Normal file
124
reports/forms.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Forms for reports app.
|
||||
"""
|
||||
from django import forms
|
||||
from accounts.form_mixins import BotProtectionMixin, BrowserFingerprintMixin, RateLimitMixin
|
||||
from .models import ScamReport, TakedownRequest
|
||||
|
||||
|
||||
class ScamReportForm(RateLimitMixin, forms.ModelForm):
|
||||
"""Form for creating/editing scam reports."""
|
||||
class Meta:
|
||||
model = ScamReport
|
||||
fields = [
|
||||
'title', 'description', 'scam_type',
|
||||
'reported_url', 'reported_email', 'reported_phone', 'reported_company',
|
||||
'tags', 'is_anonymous'
|
||||
]
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 8}),
|
||||
'scam_type': forms.Select(attrs={'class': 'form-control'}),
|
||||
'reported_url': forms.URLInput(attrs={'class': 'form-control'}),
|
||||
'reported_email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
'reported_phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'reported_company': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'tags': forms.SelectMultiple(attrs={'class': 'form-control'}),
|
||||
'is_anonymous': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
|
||||
class ContactForm(BotProtectionMixin, BrowserFingerprintMixin, forms.Form):
|
||||
"""Contact form for users to reach out."""
|
||||
name = forms.CharField(
|
||||
max_length=200,
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Вашето име'
|
||||
}),
|
||||
label='Име *'
|
||||
)
|
||||
email = forms.EmailField(
|
||||
required=True,
|
||||
widget=forms.EmailInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'your.email@example.com'
|
||||
}),
|
||||
label='Имейл *'
|
||||
)
|
||||
subject = forms.CharField(
|
||||
max_length=200,
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Тема на съобщението'
|
||||
}),
|
||||
label='Тема *'
|
||||
)
|
||||
message = forms.CharField(
|
||||
required=True,
|
||||
widget=forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 8,
|
||||
'placeholder': 'Вашето съобщение...'
|
||||
}),
|
||||
label='Съобщение *'
|
||||
)
|
||||
inquiry_type = forms.ChoiceField(
|
||||
choices=[
|
||||
('general', 'Общ въпрос'),
|
||||
('report_issue', 'Проблем с доклад'),
|
||||
('technical', 'Техническа поддръжка'),
|
||||
('feedback', 'Обратна връзка'),
|
||||
('other', 'Друго'),
|
||||
],
|
||||
required=True,
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
label='Тип заявка *'
|
||||
)
|
||||
|
||||
|
||||
class TakedownRequestForm(BotProtectionMixin, BrowserFingerprintMixin, forms.ModelForm):
|
||||
"""Form for requesting takedown of a scam report."""
|
||||
|
||||
class Meta:
|
||||
model = TakedownRequest
|
||||
fields = ['requester_name', 'requester_email', 'requester_phone', 'reason', 'evidence']
|
||||
widgets = {
|
||||
'requester_name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Вашето име'
|
||||
}),
|
||||
'requester_email': forms.EmailInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'your.email@example.com'
|
||||
}),
|
||||
'requester_phone': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': '+359 XXX XXX XXX (незадължително)'
|
||||
}),
|
||||
'reason': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 6,
|
||||
'placeholder': 'Обяснете защо смятате, че докладът трябва да бъде премахнат...'
|
||||
}),
|
||||
'evidence': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 6,
|
||||
'placeholder': 'Предоставете доказателства или допълнителна информация (незадължително)...'
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'requester_name': 'Име *',
|
||||
'requester_email': 'Имейл *',
|
||||
'requester_phone': 'Телефон',
|
||||
'reason': 'Причина за заявката *',
|
||||
'evidence': 'Доказателства / Допълнителна информация',
|
||||
}
|
||||
help_texts = {
|
||||
'reason': 'Моля, обяснете подробно защо смятате, че информацията в доклада е невярна или несправедлива.',
|
||||
'evidence': 'Ако имате документи, снимки или друга информация, която подкрепя вашата заявка, моля опишете я тук.',
|
||||
}
|
||||
103
reports/migrations/0001_initial.py
Normal file
103
reports/migrations/0001_initial.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 13:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ScamTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(blank=True, max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('color', models.CharField(default='#007bff', help_text='Hex color code for display', max_length=7)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Scam Tag',
|
||||
'verbose_name_plural': 'Scam Tags',
|
||||
'db_table': 'reports_scamtag',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScamReport',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_anonymous', models.BooleanField(default=False, help_text='Report submitted anonymously')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('scam_type', models.CharField(choices=[('phishing', 'Phishing'), ('fake_website', 'Fake Website'), ('romance_scam', 'Romance Scam'), ('investment_scam', 'Investment Scam'), ('tech_support_scam', 'Tech Support Scam'), ('identity_theft', 'Identity Theft'), ('fake_product', 'Fake Product'), ('advance_fee', 'Advance Fee Fraud'), ('other', 'Other')], default='other', max_length=50)),
|
||||
('reported_url', models.URLField(blank=True, max_length=500, null=True)),
|
||||
('reported_email', models.EmailField(blank=True, max_length=254, null=True)),
|
||||
('reported_phone', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('reported_company', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('evidence_files', models.JSONField(blank=True, default=list, help_text='List of file paths for evidence')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending Review'), ('under_review', 'Under Review'), ('verified', 'Verified'), ('rejected', 'Rejected'), ('archived', 'Archived')], default='pending', max_length=20)),
|
||||
('verification_score', models.IntegerField(default=0, help_text='OSINT verification confidence score (0-100)')),
|
||||
('is_public', models.BooleanField(default=True, help_text='Visible in public database')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('verified_at', models.DateTimeField(blank=True, null=True)),
|
||||
('reporter_ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('reporter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reports', to=settings.AUTH_USER_MODEL)),
|
||||
('tags', models.ManyToManyField(blank=True, related_name='reports', to='reports.scamtag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Scam Report',
|
||||
'verbose_name_plural': 'Scam Reports',
|
||||
'db_table': 'reports_scamreport',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScamVerification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('verification_method', models.CharField(choices=[('whois', 'WHOIS Lookup'), ('dns', 'DNS Records'), ('ssl', 'SSL Certificate'), ('archive', 'Wayback Machine'), ('email_check', 'Email Validation'), ('phone_check', 'Phone Validation'), ('business_registry', 'Business Registry'), ('social_media', 'Social Media'), ('manual', 'Manual Review')], max_length=50)),
|
||||
('verification_data', models.JSONField(default=dict, help_text='Raw verification data')),
|
||||
('confidence_score', models.IntegerField(default=0, help_text='Confidence score for this verification (0-100)')),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='verifications', to='reports.scamreport')),
|
||||
('verified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='verifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Scam Verification',
|
||||
'verbose_name_plural': 'Scam Verifications',
|
||||
'db_table': 'reports_scamverification',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scamreport',
|
||||
index=models.Index(fields=['status', 'created_at'], name='reports_sca_status_91c8ad_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scamreport',
|
||||
index=models.Index(fields=['scam_type', 'status'], name='reports_sca_scam_ty_fd12f9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scamreport',
|
||||
index=models.Index(fields=['reported_url'], name='reports_sca_reporte_ebc596_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scamreport',
|
||||
index=models.Index(fields=['reported_email'], name='reports_sca_reporte_c31241_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scamreport',
|
||||
index=models.Index(fields=['reported_phone'], name='reports_sca_reporte_33869d_idx'),
|
||||
),
|
||||
]
|
||||
18
reports/migrations/0002_scamreport_is_auto_discovered.py
Normal file
18
reports/migrations/0002_scamreport_is_auto_discovered.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 18:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scamreport',
|
||||
name='is_auto_discovered',
|
||||
field=models.BooleanField(default=False, help_text='Automatically discovered by OSINT system'),
|
||||
),
|
||||
]
|
||||
29
reports/migrations/0003_sitesettings.py
Normal file
29
reports/migrations/0003_sitesettings.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated manually
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0002_scamreport_is_auto_discovered'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('contact_email', models.EmailField(default='support@fraudplatform.bg', help_text='Основен имейл за контакти и поддръжка', max_length=254)),
|
||||
('contact_phone', models.CharField(default='+359 2 XXX XXXX', help_text='Телефонен номер за контакти', max_length=50)),
|
||||
('contact_address', models.CharField(blank=True, default='София, България', help_text='Адрес за контакти', max_length=200)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Настройки на Сайта',
|
||||
'verbose_name_plural': 'Настройки на Сайта',
|
||||
'db_table': 'reports_sitesettings',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 19:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0003_sitesettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sitesettings',
|
||||
name='contact_address',
|
||||
field=models.CharField(blank=True, default='', help_text='Адрес за контакти (незадължително)', max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sitesettings',
|
||||
name='contact_phone',
|
||||
field=models.CharField(blank=True, default='', help_text='Телефонен номер за контакти (незадължително)', max_length=50),
|
||||
),
|
||||
]
|
||||
43
reports/migrations/0005_takedownrequest.py
Normal file
43
reports/migrations/0005_takedownrequest.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 19:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0004_alter_sitesettings_contact_address_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TakedownRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('requester_name', models.CharField(help_text='Име на заявителя', max_length=200)),
|
||||
('requester_email', models.EmailField(help_text='Имейл на заявителя', max_length=254)),
|
||||
('requester_phone', models.CharField(blank=True, help_text='Телефон на заявителя (незадължително)', max_length=50)),
|
||||
('reason', models.TextField(help_text='Причина за заявката за премахване')),
|
||||
('evidence', models.TextField(blank=True, help_text='Доказателства или допълнителна информация')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending Review'), ('under_review', 'Under Review'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20)),
|
||||
('review_notes', models.TextField(blank=True, help_text='Бележки от модератора')),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('reviewed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='takedown_requests', to='reports.scamreport')),
|
||||
('reviewed_by', models.ForeignKey(blank=True, limit_choices_to={'role__in': ['moderator', 'admin']}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_takedown_requests', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Заявка за Премахване',
|
||||
'verbose_name_plural': 'Заявки за Премахване',
|
||||
'db_table': 'reports_takedownrequest',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['report', 'status'], name='reports_tak_report__a40ed0_idx'), models.Index(fields=['status', 'created_at'], name='reports_tak_status_049c16_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 19:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0005_takedownrequest'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='default_from_email',
|
||||
field=models.EmailField(default='noreply@fraudplatform.bg', help_text='Имейл адрес по подразбиране за изпращане', max_length=254),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_backend',
|
||||
field=models.CharField(choices=[('django.core.mail.backends.smtp.EmailBackend', 'SMTP'), ('django.core.mail.backends.console.EmailBackend', 'Console (Development)'), ('django.core.mail.backends.filebased.EmailBackend', 'File Based')], default='django.core.mail.backends.smtp.EmailBackend', help_text='Тип на имейл сървъра', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_host',
|
||||
field=models.CharField(blank=True, default='', help_text='SMTP сървър (напр. smtp.gmail.com)', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_host_password',
|
||||
field=models.CharField(blank=True, default='', help_text='SMTP парола (ще бъде криптирана)', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_host_user',
|
||||
field=models.CharField(blank=True, default='', help_text='SMTP потребителско име / имейл', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_port',
|
||||
field=models.IntegerField(default=587, help_text='SMTP порт (обикновено 587 за TLS или 465 за SSL)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_timeout',
|
||||
field=models.IntegerField(default=10, help_text='Таймаут за имейл връзка (секунди)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_use_ssl',
|
||||
field=models.BooleanField(default=False, help_text='Използване на SSL (за порт 465)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='email_use_tls',
|
||||
field=models.BooleanField(default=True, help_text='Използване на TLS (за порт 587)'),
|
||||
),
|
||||
]
|
||||
0
reports/migrations/__init__.py
Normal file
0
reports/migrations/__init__.py
Normal file
411
reports/models.py
Normal file
411
reports/models.py
Normal file
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
Scam and fraud report models.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.core.cache import cache
|
||||
from accounts.security import DataEncryption
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SiteSettings(models.Model):
|
||||
"""
|
||||
Site-wide settings that can be managed from admin.
|
||||
Uses singleton pattern - only one instance should exist.
|
||||
"""
|
||||
contact_email = models.EmailField(
|
||||
default='support@fraudplatform.bg',
|
||||
help_text='Основен имейл за контакти и поддръжка'
|
||||
)
|
||||
contact_phone = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='Телефонен номер за контакти (незадължително)'
|
||||
)
|
||||
contact_address = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='Адрес за контакти (незадължително)'
|
||||
)
|
||||
|
||||
# Email Server Settings
|
||||
email_backend = models.CharField(
|
||||
max_length=100,
|
||||
default='django.core.mail.backends.smtp.EmailBackend',
|
||||
choices=[
|
||||
('django.core.mail.backends.smtp.EmailBackend', 'SMTP'),
|
||||
('django.core.mail.backends.console.EmailBackend', 'Console (Development)'),
|
||||
('django.core.mail.backends.filebased.EmailBackend', 'File Based'),
|
||||
],
|
||||
help_text='Тип на имейл сървъра'
|
||||
)
|
||||
email_host = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='SMTP сървър (напр. smtp.gmail.com)'
|
||||
)
|
||||
email_port = models.IntegerField(
|
||||
default=587,
|
||||
help_text='SMTP порт (обикновено 587 за TLS или 465 за SSL)'
|
||||
)
|
||||
email_use_tls = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Използване на TLS (за порт 587)'
|
||||
)
|
||||
email_use_ssl = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Използване на SSL (за порт 465)'
|
||||
)
|
||||
email_host_user = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='SMTP потребителско име / имейл'
|
||||
)
|
||||
email_host_password = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default='',
|
||||
help_text='SMTP парола (ще бъде криптирана)'
|
||||
)
|
||||
default_from_email = models.EmailField(
|
||||
default='noreply@fraudplatform.bg',
|
||||
help_text='Имейл адрес по подразбиране за изпращане'
|
||||
)
|
||||
email_timeout = models.IntegerField(
|
||||
default=10,
|
||||
help_text='Таймаут за имейл връзка (секунди)'
|
||||
)
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Настройки на Сайта'
|
||||
verbose_name_plural = 'Настройки на Сайта'
|
||||
db_table = 'reports_sitesettings'
|
||||
|
||||
def __str__(self):
|
||||
return 'Настройки на Сайта'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Ensure only one instance exists
|
||||
self.pk = 1
|
||||
|
||||
# Encrypt email password if it's provided and not already encrypted
|
||||
if self.email_host_password:
|
||||
# Check if it's already encrypted by trying to decrypt it
|
||||
# If decryption succeeds, it's already encrypted, so keep original
|
||||
# If decryption fails, it's plain text, so encrypt it
|
||||
is_encrypted = False
|
||||
try:
|
||||
# Try to decrypt - if it succeeds, it's already encrypted
|
||||
DataEncryption.decrypt(self.email_host_password)
|
||||
is_encrypted = True
|
||||
except (Exception, ValueError, TypeError):
|
||||
# Decryption failed, so it's plain text
|
||||
is_encrypted = False
|
||||
|
||||
# Only encrypt if it's not already encrypted
|
||||
if not is_encrypted:
|
||||
self.email_host_password = DataEncryption.encrypt(self.email_host_password)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
# Clear cache when settings are updated
|
||||
cache.delete('site_settings')
|
||||
|
||||
def get_email_password(self):
|
||||
"""Get decrypted email password."""
|
||||
if not self.email_host_password:
|
||||
return ''
|
||||
try:
|
||||
return DataEncryption.decrypt(self.email_host_password)
|
||||
except:
|
||||
# If decryption fails, return as-is (might be plain text from migration)
|
||||
return self.email_host_password
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Prevent deletion - settings should always exist
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_settings(cls):
|
||||
"""Get site settings with caching."""
|
||||
settings = cache.get('site_settings')
|
||||
if settings is None:
|
||||
settings, created = cls.objects.get_or_create(pk=1)
|
||||
cache.set('site_settings', settings, 3600) # Cache for 1 hour
|
||||
return settings
|
||||
|
||||
|
||||
class ScamTag(models.Model):
|
||||
"""
|
||||
Tags for categorizing scam reports.
|
||||
"""
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
slug = models.SlugField(max_length=100, unique=True, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
color = models.CharField(
|
||||
max_length=7,
|
||||
default='#007bff',
|
||||
help_text='Hex color code for display'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'reports_scamtag'
|
||||
verbose_name = 'Scam Tag'
|
||||
verbose_name_plural = 'Scam Tags'
|
||||
ordering = ['name']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ScamReport(models.Model):
|
||||
"""
|
||||
Main scam/fraud report model.
|
||||
"""
|
||||
SCAM_TYPE_CHOICES = [
|
||||
('phishing', 'Phishing'),
|
||||
('fake_website', 'Fake Website'),
|
||||
('romance_scam', 'Romance Scam'),
|
||||
('investment_scam', 'Investment Scam'),
|
||||
('tech_support_scam', 'Tech Support Scam'),
|
||||
('identity_theft', 'Identity Theft'),
|
||||
('fake_product', 'Fake Product'),
|
||||
('advance_fee', 'Advance Fee Fraud'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending Review'),
|
||||
('under_review', 'Under Review'),
|
||||
('verified', 'Verified'),
|
||||
('rejected', 'Rejected'),
|
||||
('archived', 'Archived'),
|
||||
]
|
||||
|
||||
# Reporter information
|
||||
reporter = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='reports'
|
||||
)
|
||||
is_anonymous = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Report submitted anonymously'
|
||||
)
|
||||
|
||||
# Report details
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
scam_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=SCAM_TYPE_CHOICES,
|
||||
default='other'
|
||||
)
|
||||
|
||||
# Reported entities
|
||||
reported_url = models.URLField(blank=True, null=True, max_length=500)
|
||||
reported_email = models.EmailField(blank=True, null=True)
|
||||
reported_phone = models.CharField(max_length=20, blank=True, null=True)
|
||||
reported_company = models.CharField(max_length=200, blank=True, null=True)
|
||||
|
||||
# Evidence
|
||||
evidence_files = models.JSONField(
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text='List of file paths for evidence'
|
||||
)
|
||||
|
||||
# Status and verification
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='pending'
|
||||
)
|
||||
verification_score = models.IntegerField(
|
||||
default=0,
|
||||
help_text='OSINT verification confidence score (0-100)'
|
||||
)
|
||||
|
||||
# Visibility
|
||||
is_public = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Visible in public database'
|
||||
)
|
||||
is_auto_discovered = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Automatically discovered by OSINT system'
|
||||
)
|
||||
|
||||
# Metadata
|
||||
tags = models.ManyToManyField(ScamTag, blank=True, related_name='reports')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
verified_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# IP tracking for anonymous reports
|
||||
reporter_ip = models.GenericIPAddressField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'reports_scamreport'
|
||||
verbose_name = 'Scam Report'
|
||||
verbose_name_plural = 'Scam Reports'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'created_at']),
|
||||
models.Index(fields=['scam_type', 'status']),
|
||||
models.Index(fields=['reported_url']),
|
||||
models.Index(fields=['reported_email']),
|
||||
models.Index(fields=['reported_phone']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} - {self.get_status_display()}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('reports:detail', kwargs={'pk': self.pk})
|
||||
|
||||
def get_reporter_display(self):
|
||||
if self.is_anonymous:
|
||||
return "Anonymous"
|
||||
return self.reporter.username if self.reporter else "Unknown"
|
||||
|
||||
|
||||
class ScamVerification(models.Model):
|
||||
"""
|
||||
OSINT verification data for scam reports.
|
||||
"""
|
||||
VERIFICATION_METHOD_CHOICES = [
|
||||
('whois', 'WHOIS Lookup'),
|
||||
('dns', 'DNS Records'),
|
||||
('ssl', 'SSL Certificate'),
|
||||
('archive', 'Wayback Machine'),
|
||||
('email_check', 'Email Validation'),
|
||||
('phone_check', 'Phone Validation'),
|
||||
('business_registry', 'Business Registry'),
|
||||
('social_media', 'Social Media'),
|
||||
('manual', 'Manual Review'),
|
||||
]
|
||||
|
||||
report = models.ForeignKey(
|
||||
ScamReport,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='verifications'
|
||||
)
|
||||
verification_method = models.CharField(
|
||||
max_length=50,
|
||||
choices=VERIFICATION_METHOD_CHOICES
|
||||
)
|
||||
verification_data = models.JSONField(
|
||||
default=dict,
|
||||
help_text='Raw verification data'
|
||||
)
|
||||
confidence_score = models.IntegerField(
|
||||
default=0,
|
||||
help_text='Confidence score for this verification (0-100)'
|
||||
)
|
||||
verified_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='verifications'
|
||||
)
|
||||
notes = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'reports_scamverification'
|
||||
verbose_name = 'Scam Verification'
|
||||
verbose_name_plural = 'Scam Verifications'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"Verification for {self.report.title} via {self.get_verification_method_display()}"
|
||||
|
||||
|
||||
class TakedownRequest(models.Model):
|
||||
"""
|
||||
Request to take down a scam report by the accused party.
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending Review'),
|
||||
('under_review', 'Under Review'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected'),
|
||||
]
|
||||
|
||||
report = models.ForeignKey(
|
||||
ScamReport,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='takedown_requests'
|
||||
)
|
||||
requester_name = models.CharField(
|
||||
max_length=200,
|
||||
help_text='Име на заявителя'
|
||||
)
|
||||
requester_email = models.EmailField(
|
||||
help_text='Имейл на заявителя'
|
||||
)
|
||||
requester_phone = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text='Телефон на заявителя (незадължително)'
|
||||
)
|
||||
reason = models.TextField(
|
||||
help_text='Причина за заявката за премахване'
|
||||
)
|
||||
evidence = models.TextField(
|
||||
blank=True,
|
||||
help_text='Доказателства или допълнителна информация'
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='pending'
|
||||
)
|
||||
reviewed_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='reviewed_takedown_requests',
|
||||
limit_choices_to={'role__in': ['moderator', 'admin']}
|
||||
)
|
||||
review_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text='Бележки от модератора'
|
||||
)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
reviewed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'reports_takedownrequest'
|
||||
verbose_name = 'Заявка за Премахване'
|
||||
verbose_name_plural = 'Заявки за Премахване'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['report', 'status']),
|
||||
models.Index(fields=['status', 'created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Takedown request for {self.report.title} by {self.requester_name}"
|
||||
3
reports/tests.py
Normal file
3
reports/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
33
reports/urls.py
Normal file
33
reports/urls.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
URL configuration for reports app.
|
||||
"""
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
from . import views
|
||||
|
||||
app_name = 'reports'
|
||||
|
||||
urlpatterns = [
|
||||
# Home page
|
||||
path('', views.HomeView.as_view(), name='home'),
|
||||
|
||||
# Public views
|
||||
path('reports/', views.ReportListView.as_view(), name='list'),
|
||||
path('reports/<int:pk>/', views.ReportDetailView.as_view(), name='detail'),
|
||||
path('create/', views.ReportCreateView.as_view(), name='create'),
|
||||
|
||||
# User views
|
||||
path('my-reports/', views.MyReportsView.as_view(), name='my_reports'),
|
||||
path('reports/<int:pk>/edit/', views.ReportEditView.as_view(), name='edit'),
|
||||
path('reports/<int:pk>/delete/', views.ReportDeleteView.as_view(), name='delete'),
|
||||
|
||||
# Search
|
||||
path('search/', views.ReportSearchView.as_view(), name='search'),
|
||||
|
||||
# Contact
|
||||
path('contact/', views.ContactView.as_view(), name='contact'),
|
||||
|
||||
# Takedown request
|
||||
path('reports/<int:report_pk>/takedown/', views.TakedownRequestView.as_view(), name='takedown_request'),
|
||||
]
|
||||
|
||||
454
reports/views.py
Normal file
454
reports/views.py
Normal file
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
Views for reports app.
|
||||
"""
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views.generic import TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView, FormView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Q, Count
|
||||
from django.core.exceptions import ValidationError
|
||||
from accounts.security import InputSanitizer
|
||||
from .models import ScamReport, ScamTag, TakedownRequest
|
||||
from .forms import ScamReportForm, ContactForm, TakedownRequestForm
|
||||
from django.contrib import messages
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
|
||||
# Bulgarian translations for scam types
|
||||
SCAM_TYPE_BG = {
|
||||
'phishing': 'Фишинг',
|
||||
'fake_website': 'Фалшив Уебсайт',
|
||||
'romance_scam': 'Романтична Измама',
|
||||
'investment_scam': 'Инвестиционна Измама',
|
||||
'tech_support_scam': 'Техническа Поддръжка Измама',
|
||||
'identity_theft': 'Кражба на Личност',
|
||||
'fake_product': 'Фалшив Продукт',
|
||||
'advance_fee': 'Авансово Плащане',
|
||||
'other': 'Друго',
|
||||
}
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
"""Home page view."""
|
||||
template_name = 'reports/home.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['total_reports'] = ScamReport.objects.filter(status='verified').count()
|
||||
context['recent_reports'] = ScamReport.objects.filter(
|
||||
is_public=True,
|
||||
status='verified'
|
||||
).order_by('-created_at')[:5]
|
||||
|
||||
# Get scam types with display names - ALL types, not just top 5
|
||||
scam_types_data = ScamReport.objects.filter(
|
||||
status='verified'
|
||||
).values('scam_type').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')
|
||||
|
||||
# Add display names with Bulgarian translations
|
||||
scam_types_list = []
|
||||
total_verified = context['total_reports'] or 1 # Avoid division by zero
|
||||
for item in scam_types_data:
|
||||
scam_type_key = item['scam_type']
|
||||
display_name = SCAM_TYPE_BG.get(scam_type_key, dict(ScamReport.SCAM_TYPE_CHOICES).get(scam_type_key, scam_type_key))
|
||||
percentage = (item['count'] / total_verified * 100) if total_verified > 0 else 0
|
||||
scam_types_list.append({
|
||||
'scam_type': scam_type_key,
|
||||
'display_name': display_name,
|
||||
'count': item['count'],
|
||||
'percentage': round(percentage, 1)
|
||||
})
|
||||
|
||||
context['scam_types'] = scam_types_list
|
||||
|
||||
# SEO metadata
|
||||
self.request.seo_title = 'Портал за Докладване на Измами - България'
|
||||
self.request.seo_description = f'Портал за докладване на измами. Над {context["total_reports"]} верифицирани доклада. Защита на гражданите от онлайн измами, фишинг, фалшиви уебсайтове и киберпрестъпления.'
|
||||
self.request.seo_keywords = 'измами, докладване измами, киберпрестъпления, фишинг, фалшив уебсайт, защита потребители, България, портал за докладване на измами, анти-измами'
|
||||
self.request.canonical_url = self.request.build_absolute_uri('/')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ReportListView(ListView):
|
||||
"""List all public verified reports."""
|
||||
model = ScamReport
|
||||
template_name = 'reports/list.html'
|
||||
context_object_name = 'reports'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return ScamReport.objects.filter(
|
||||
is_public=True,
|
||||
status='verified'
|
||||
).select_related('reporter').prefetch_related('tags')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# SEO metadata
|
||||
self.request.seo_title = 'Всички Доклади за Измами - Портал за Докладване на Измами'
|
||||
self.request.seo_description = 'Прегледайте всички верифицирани доклади за измами в България. Търсете по вид измама, дата и ключови думи.'
|
||||
self.request.seo_keywords = 'доклади измами, списък измами, верифицирани доклади, измами България'
|
||||
self.request.canonical_url = self.request.build_absolute_uri('/reports/')
|
||||
return context
|
||||
|
||||
|
||||
class ReportDetailView(DetailView):
|
||||
"""View a single report."""
|
||||
model = ScamReport
|
||||
template_name = 'reports/detail.html'
|
||||
context_object_name = 'report'
|
||||
|
||||
def get_queryset(self):
|
||||
# Allow viewing if public or user is owner/moderator
|
||||
queryset = ScamReport.objects.all()
|
||||
if not self.request.user.is_authenticated:
|
||||
queryset = queryset.filter(is_public=True, status='verified')
|
||||
elif not self.request.user.is_moderator():
|
||||
queryset = queryset.filter(
|
||||
Q(is_public=True, status='verified') | Q(reporter=self.request.user)
|
||||
)
|
||||
return queryset.select_related('reporter').prefetch_related('tags', 'verifications', 'moderation_actions')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
report = context['report']
|
||||
|
||||
# SEO metadata
|
||||
scam_type_display = SCAM_TYPE_BG.get(report.scam_type, report.get_scam_type_display())
|
||||
self.request.seo_title = f'{report.title} - Доклад за Измама'
|
||||
self.request.seo_description = f'Доклад за {scam_type_display.lower()}: {report.description[:150]}...' if len(report.description) > 150 else report.description
|
||||
self.request.seo_keywords = f'измама, {scam_type_display.lower()}, доклад, {", ".join([tag.name for tag in report.tags.all()[:5]])}'
|
||||
self.request.seo_type = 'article'
|
||||
self.request.canonical_url = self.request.build_absolute_uri(report.get_absolute_url())
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ReportCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
"""Create a new scam report."""
|
||||
model = ScamReport
|
||||
form_class = ScamReportForm
|
||||
template_name = 'reports/create.html'
|
||||
success_url = reverse_lazy('reports:my_reports')
|
||||
success_message = "Report submitted successfully! It will be reviewed by moderators."
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# SEO metadata
|
||||
self.request.seo_title = 'Докладване на Измама - Портал за Докладване на Измами'
|
||||
self.request.seo_description = 'Докладвайте измама. Помогнете да защитим другите граждани от онлайн измами и киберпрестъпления.'
|
||||
self.request.seo_keywords = 'докладване измама, сигнализиране измама, докладване онлайн измама'
|
||||
self.request.canonical_url = self.request.build_absolute_uri('/create/')
|
||||
self.request.meta_robots = 'noindex, nofollow' # Don't index form pages
|
||||
return context
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass request to form for rate limiting."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
# Sanitize user input
|
||||
if form.cleaned_data.get('title'):
|
||||
form.cleaned_data['title'] = InputSanitizer.sanitize_html(form.cleaned_data['title'])
|
||||
if form.cleaned_data.get('description'):
|
||||
form.cleaned_data['description'] = InputSanitizer.sanitize_html(form.cleaned_data['description'])
|
||||
|
||||
# Validate URLs
|
||||
if form.cleaned_data.get('reported_url'):
|
||||
if not InputSanitizer.validate_url(form.cleaned_data['reported_url']):
|
||||
form.add_error('reported_url', 'Invalid URL format')
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Validate email
|
||||
if form.cleaned_data.get('reported_email'):
|
||||
if not InputSanitizer.validate_email(form.cleaned_data['reported_email']):
|
||||
form.add_error('reported_email', 'Invalid email format')
|
||||
return self.form_invalid(form)
|
||||
|
||||
form.instance.reporter = self.request.user
|
||||
form.instance.reporter_ip = self.get_client_ip()
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
def get_client_ip(self):
|
||||
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = self.request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
|
||||
class MyReportsView(LoginRequiredMixin, ListView):
|
||||
"""List user's own reports."""
|
||||
model = ScamReport
|
||||
template_name = 'reports/my_reports.html'
|
||||
context_object_name = 'reports'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return ScamReport.objects.filter(
|
||||
reporter=self.request.user
|
||||
).prefetch_related('moderation_actions').order_by('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Get the first rejection action for each report
|
||||
for report in context['reports']:
|
||||
rejection_action = report.moderation_actions.filter(action_type='reject').first()
|
||||
report.rejection_reason = rejection_action.reason if rejection_action else None
|
||||
report.rejection_action = rejection_action
|
||||
return context
|
||||
|
||||
|
||||
class ReportEditView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
"""Edit own report (only if pending or rejected)."""
|
||||
model = ScamReport
|
||||
form_class = ScamReportForm
|
||||
template_name = 'reports/edit.html'
|
||||
success_url = reverse_lazy('reports:my_reports')
|
||||
success_message = "Report updated successfully!"
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass request to form for rate limiting."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def get_queryset(self):
|
||||
# Allow editing pending or rejected reports
|
||||
return ScamReport.objects.filter(
|
||||
reporter=self.request.user,
|
||||
status__in=['pending', 'rejected']
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
# If editing a rejected report, change status back to pending for re-review
|
||||
if form.instance.status == 'rejected':
|
||||
form.instance.status = 'pending'
|
||||
self.success_message = "Report updated and resubmitted for review!"
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ReportDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
"""Delete own report (only if pending or rejected)."""
|
||||
model = ScamReport
|
||||
template_name = 'reports/delete.html'
|
||||
success_url = reverse_lazy('reports:my_reports')
|
||||
success_message = "Report deleted successfully!"
|
||||
|
||||
def get_queryset(self):
|
||||
# Allow deleting pending or rejected reports
|
||||
return ScamReport.objects.filter(
|
||||
reporter=self.request.user,
|
||||
status__in=['pending', 'rejected']
|
||||
)
|
||||
|
||||
|
||||
class ReportSearchView(ListView):
|
||||
"""Search reports."""
|
||||
model = ScamReport
|
||||
template_name = 'reports/search.html'
|
||||
context_object_name = 'reports'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
query = self.request.GET.get('q', '')
|
||||
scam_type = self.request.GET.get('type', '')
|
||||
|
||||
queryset = ScamReport.objects.filter(
|
||||
is_public=True,
|
||||
status='verified'
|
||||
)
|
||||
|
||||
if query:
|
||||
queryset = queryset.filter(
|
||||
Q(title__icontains=query) |
|
||||
Q(description__icontains=query) |
|
||||
Q(reported_url__icontains=query) |
|
||||
Q(reported_email__icontains=query)
|
||||
)
|
||||
|
||||
if scam_type:
|
||||
queryset = queryset.filter(scam_type=scam_type)
|
||||
|
||||
return queryset.select_related('reporter').prefetch_related('tags')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['scam_type_choices'] = ScamReport.SCAM_TYPE_CHOICES
|
||||
|
||||
# SEO metadata
|
||||
query = self.request.GET.get('q', '')
|
||||
if query:
|
||||
self.request.seo_title = f'Търсене: {query} - Доклади за Измами'
|
||||
self.request.seo_description = f'Резултати от търсенето за "{query}" в базата данни с доклади за измами.'
|
||||
else:
|
||||
self.request.seo_title = 'Търсене на Доклади - Официален Портал'
|
||||
self.request.seo_description = 'Търсете в базата данни с верифицирани доклади за измами в България.'
|
||||
self.request.seo_keywords = 'търсене измами, доклади, база данни измами'
|
||||
self.request.canonical_url = self.request.build_absolute_uri('/search/')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ContactView(SuccessMessageMixin, FormView):
|
||||
"""Contact us page."""
|
||||
form_class = ContactForm
|
||||
template_name = 'reports/contact.html'
|
||||
success_url = reverse_lazy('reports:contact')
|
||||
success_message = "Благодарим ви! Вашето съобщение е изпратено успешно. Ще се свържем с вас скоро."
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass request to form for rate limiting."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
# Send email notification (if email is configured)
|
||||
try:
|
||||
subject = f"[Контакт] {form.cleaned_data['subject']}"
|
||||
message = f"""
|
||||
Име: {form.cleaned_data['name']}
|
||||
Имейл: {form.cleaned_data['email']}
|
||||
Тип заявка: {form.cleaned_data['inquiry_type']}
|
||||
|
||||
Съобщение:
|
||||
{form.cleaned_data['message']}
|
||||
"""
|
||||
from .models import SiteSettings
|
||||
site_settings = SiteSettings.get_settings()
|
||||
from_email = site_settings.default_from_email
|
||||
recipient_list = [site_settings.contact_email]
|
||||
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
from_email,
|
||||
recipient_list,
|
||||
fail_silently=True, # Don't fail if email is not configured
|
||||
)
|
||||
except Exception:
|
||||
pass # Email sending is optional
|
||||
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Add contact information from SiteSettings (already in context via context processor)
|
||||
# But we can add it explicitly if needed for backward compatibility
|
||||
from .models import SiteSettings
|
||||
site_settings = SiteSettings.get_settings()
|
||||
context['contact_email'] = site_settings.contact_email
|
||||
context['contact_phone'] = site_settings.contact_phone
|
||||
context['contact_address'] = site_settings.contact_address
|
||||
|
||||
# SEO metadata
|
||||
self.request.seo_title = 'Контакти - Официален Портал за Докладване на Измами'
|
||||
self.request.seo_description = 'Свържете се с нас за въпроси, обратна връзка или техническа поддръжка. Портал за докладване на измами в България.'
|
||||
self.request.seo_keywords = 'контакти, поддръжка, обратна връзка, свържете се, официален портал'
|
||||
self.request.canonical_url = self.request.build_absolute_uri('/contact/')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class TakedownRequestView(SuccessMessageMixin, FormView):
|
||||
"""View for requesting takedown of a scam report."""
|
||||
form_class = TakedownRequestForm
|
||||
template_name = 'reports/takedown_request.html'
|
||||
success_message = "Вашата заявка за премахване е изпратена успешно. Ще бъде прегледана от нашия екип в рамките на 2-5 работни дни."
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Get the report
|
||||
self.report = get_object_or_404(
|
||||
ScamReport.objects.filter(is_public=True, status='verified'),
|
||||
pk=kwargs['report_pk']
|
||||
)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass request to form for rate limiting."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['report'] = self.report
|
||||
|
||||
# SEO metadata
|
||||
self.request.seo_title = f'Заявка за Премахване - {self.report.title}'
|
||||
self.request.seo_description = 'Заявка за премахване на доклад за измама'
|
||||
self.request.canonical_url = self.request.build_absolute_uri()
|
||||
self.request.meta_robots = 'noindex, nofollow'
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
# Get client IP
|
||||
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip_address = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip_address = self.request.META.get('REMOTE_ADDR')
|
||||
|
||||
# Create takedown request
|
||||
takedown_request = form.save(commit=False)
|
||||
takedown_request.report = self.report
|
||||
takedown_request.ip_address = ip_address
|
||||
takedown_request.user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
takedown_request.save()
|
||||
|
||||
# Send email notification (if email is configured)
|
||||
try:
|
||||
from .models import SiteSettings
|
||||
|
||||
site_settings = SiteSettings.get_settings()
|
||||
subject = f"[Заявка за Премахване] Доклад: {self.report.title}"
|
||||
message = f"""
|
||||
Нова заявка за премахване на доклад:
|
||||
|
||||
Доклад: {self.report.title} (ID: {self.report.pk})
|
||||
URL: {self.request.build_absolute_uri(self.report.get_absolute_url())}
|
||||
|
||||
Заявител:
|
||||
Име: {form.cleaned_data['requester_name']}
|
||||
Имейл: {form.cleaned_data['requester_email']}
|
||||
Телефон: {form.cleaned_data.get('requester_phone', 'Не е предоставен')}
|
||||
|
||||
Причина:
|
||||
{form.cleaned_data['reason']}
|
||||
|
||||
Доказателства:
|
||||
{form.cleaned_data.get('evidence', 'Не са предоставени')}
|
||||
|
||||
---
|
||||
IP адрес: {ip_address}
|
||||
Дата: {takedown_request.created_at}
|
||||
"""
|
||||
from_email = site_settings.default_from_email
|
||||
recipient_list = [site_settings.contact_email]
|
||||
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
from_email,
|
||||
recipient_list,
|
||||
fail_silently=True,
|
||||
)
|
||||
except Exception:
|
||||
pass # Email sending is optional
|
||||
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('reports:detail', kwargs={'pk': self.report.pk})
|
||||
Reference in New Issue
Block a user