455 lines
20 KiB
Python
455 lines
20 KiB
Python
"""
|
||
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})
|