This commit is contained in:
Iliyan Angelov
2025-11-26 22:32:20 +02:00
commit ed94dd22dd
150 changed files with 14058 additions and 0 deletions

0
reports/__init__.py Normal file
View File

247
reports/admin.py Normal file
View 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
View 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
View 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
View 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': 'Ако имате документи, снимки или друга информация, която подкрепя вашата заявка, моля опишете я тук.',
}

View 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'),
),
]

View 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'),
),
]

View 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',
},
),
]

View File

@@ -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),
),
]

View 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')],
},
),
]

View File

@@ -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)'),
),
]

View File

411
reports/models.py Normal file
View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

33
reports/urls.py Normal file
View 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
View 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})