update
This commit is contained in:
0
legal/__init__.py
Normal file
0
legal/__init__.py
Normal file
47
legal/admin.py
Normal file
47
legal/admin.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Admin configuration for legal app.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from .models import ConsentRecord, DataRequest, SecurityEvent
|
||||
|
||||
|
||||
@admin.register(ConsentRecord)
|
||||
class ConsentRecordAdmin(admin.ModelAdmin):
|
||||
"""Consent record admin."""
|
||||
list_display = ('user', 'consent_type', 'consent_given', 'timestamp', 'version')
|
||||
list_filter = ('consent_type', 'consent_given', 'timestamp')
|
||||
search_fields = ('user__username', 'user__email')
|
||||
readonly_fields = ('timestamp',)
|
||||
date_hierarchy = 'timestamp'
|
||||
|
||||
|
||||
@admin.register(DataRequest)
|
||||
class DataRequestAdmin(admin.ModelAdmin):
|
||||
"""Data request admin."""
|
||||
list_display = ('user', 'request_type', 'status', 'requested_at', 'completed_at', 'handled_by')
|
||||
list_filter = ('request_type', 'status', 'requested_at')
|
||||
search_fields = ('user__username', 'user__email', 'description')
|
||||
readonly_fields = ('requested_at',)
|
||||
date_hierarchy = 'requested_at'
|
||||
|
||||
fieldsets = (
|
||||
('Request Information', {
|
||||
'fields': ('user', 'request_type', 'status', 'description')
|
||||
}),
|
||||
('Response', {
|
||||
'fields': ('response_data', 'response_file', 'notes')
|
||||
}),
|
||||
('Handling', {
|
||||
'fields': ('handled_by', 'requested_at', 'completed_at')
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(SecurityEvent)
|
||||
class SecurityEventAdmin(admin.ModelAdmin):
|
||||
"""Security event admin."""
|
||||
list_display = ('event_type', 'user', 'severity', 'ip_address', 'timestamp', 'resolved')
|
||||
list_filter = ('event_type', 'severity', 'resolved', 'timestamp')
|
||||
search_fields = ('user__username', 'ip_address')
|
||||
readonly_fields = ('timestamp',)
|
||||
date_hierarchy = 'timestamp'
|
||||
6
legal/apps.py
Normal file
6
legal/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LegalConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'legal'
|
||||
22
legal/forms.py
Normal file
22
legal/forms.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Forms for legal app.
|
||||
"""
|
||||
from django import forms
|
||||
from .models import DataRequest
|
||||
|
||||
|
||||
class DataRequestForm(forms.ModelForm):
|
||||
"""Form for GDPR data requests."""
|
||||
|
||||
class Meta:
|
||||
model = DataRequest
|
||||
fields = ['request_type', 'description']
|
||||
widgets = {
|
||||
'request_type': forms.Select(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 5,
|
||||
'placeholder': 'Additional details about your request...'
|
||||
}),
|
||||
}
|
||||
|
||||
83
legal/migrations/0001_initial.py
Normal file
83
legal/migrations/0001_initial.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# 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='ConsentRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('consent_type', models.CharField(choices=[('privacy_policy', 'Privacy Policy'), ('terms_of_service', 'Terms of Service'), ('data_processing', 'Data Processing'), ('marketing', 'Marketing Communications'), ('cookies', 'Cookie Consent')], max_length=50)),
|
||||
('consent_given', models.BooleanField(default=False)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('version', models.CharField(blank=True, help_text='Version of the policy/terms', max_length=20)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consents', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Consent Record',
|
||||
'verbose_name_plural': 'Consent Records',
|
||||
'db_table': 'legal_consentrecord',
|
||||
'ordering': ['-timestamp'],
|
||||
'indexes': [models.Index(fields=['user', 'consent_type'], name='legal_conse_user_id_c707fa_idx'), models.Index(fields=['consent_type', 'timestamp'], name='legal_conse_consent_216033_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DataRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request_type', models.CharField(choices=[('access', 'Data Access Request'), ('deletion', 'Data Deletion Request'), ('portability', 'Data Portability Request'), ('rectification', 'Data Rectification Request'), ('objection', 'Objection to Processing'), ('restriction', 'Restriction of Processing')], max_length=50)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('rejected', 'Rejected')], default='pending', max_length=20)),
|
||||
('description', models.TextField(blank=True, help_text='Additional details about the request')),
|
||||
('requested_at', models.DateTimeField(auto_now_add=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('response_data', models.JSONField(blank=True, default=dict, help_text='Response data (e.g., exported data)')),
|
||||
('response_file', models.FileField(blank=True, help_text='File containing requested data', null=True, upload_to='data_requests/')),
|
||||
('notes', models.TextField(blank=True, help_text='Internal notes about handling the request')),
|
||||
('handled_by', models.ForeignKey(blank=True, limit_choices_to={'role': 'admin'}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='handled_data_requests', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='data_requests', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Data Request',
|
||||
'verbose_name_plural': 'Data Requests',
|
||||
'db_table': 'legal_datarequest',
|
||||
'ordering': ['-requested_at'],
|
||||
'indexes': [models.Index(fields=['user', 'status'], name='legal_datar_user_id_da1063_idx'), models.Index(fields=['request_type', 'status'], name='legal_datar_request_ad226f_idx'), models.Index(fields=['status', 'requested_at'], name='legal_datar_status_392f15_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SecurityEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_type', models.CharField(choices=[('login_success', 'Successful Login'), ('login_failed', 'Failed Login'), ('password_change', 'Password Changed'), ('account_locked', 'Account Locked'), ('suspicious_activity', 'Suspicious Activity'), ('data_breach', 'Data Breach'), ('unauthorized_access', 'Unauthorized Access Attempt'), ('file_upload', 'File Upload'), ('data_export', 'Data Export'), ('admin_action', 'Admin Action')], max_length=50)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True)),
|
||||
('details', models.JSONField(blank=True, default=dict, help_text='Additional event details')),
|
||||
('severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], default='low', max_length=20)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('resolved', models.BooleanField(default=False)),
|
||||
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolved_by', models.ForeignKey(blank=True, limit_choices_to={'role': 'admin'}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_security_events', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='security_events', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Security Event',
|
||||
'verbose_name_plural': 'Security Events',
|
||||
'db_table': 'security_securityevent',
|
||||
'ordering': ['-timestamp'],
|
||||
'indexes': [models.Index(fields=['event_type', 'timestamp'], name='security_se_event_t_1a00f0_idx'), models.Index(fields=['severity', 'timestamp'], name='security_se_severit_5c25b4_idx'), models.Index(fields=['user', 'timestamp'], name='security_se_user_id_6ceb62_idx'), models.Index(fields=['resolved', 'timestamp'], name='security_se_resolve_dbd0de_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
legal/migrations/__init__.py
Normal file
0
legal/migrations/__init__.py
Normal file
210
legal/models.py
Normal file
210
legal/models.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Legal compliance and GDPR models.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ConsentRecord(models.Model):
|
||||
"""
|
||||
Track user consent for GDPR compliance.
|
||||
"""
|
||||
CONSENT_TYPE_CHOICES = [
|
||||
('privacy_policy', 'Privacy Policy'),
|
||||
('terms_of_service', 'Terms of Service'),
|
||||
('data_processing', 'Data Processing'),
|
||||
('marketing', 'Marketing Communications'),
|
||||
('cookies', 'Cookie Consent'),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='consents',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
consent_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=CONSENT_TYPE_CHOICES
|
||||
)
|
||||
consent_given = models.BooleanField(default=False)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
version = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
help_text='Version of the policy/terms'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'legal_consentrecord'
|
||||
verbose_name = 'Consent Record'
|
||||
verbose_name_plural = 'Consent Records'
|
||||
ordering = ['-timestamp']
|
||||
indexes = [
|
||||
models.Index(fields=['user', 'consent_type']),
|
||||
models.Index(fields=['consent_type', 'timestamp']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
status = "Given" if self.consent_given else "Not Given"
|
||||
return f"{self.get_consent_type_display()} - {status} - {self.timestamp}"
|
||||
|
||||
|
||||
class DataRequest(models.Model):
|
||||
"""
|
||||
GDPR data subject requests (access, deletion, portability).
|
||||
"""
|
||||
REQUEST_TYPE_CHOICES = [
|
||||
('access', 'Data Access Request'),
|
||||
('deletion', 'Data Deletion Request'),
|
||||
('portability', 'Data Portability Request'),
|
||||
('rectification', 'Data Rectification Request'),
|
||||
('objection', 'Objection to Processing'),
|
||||
('restriction', 'Restriction of Processing'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('rejected', 'Rejected'),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='data_requests'
|
||||
)
|
||||
request_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=REQUEST_TYPE_CHOICES
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='pending'
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
help_text='Additional details about the request'
|
||||
)
|
||||
requested_at = models.DateTimeField(auto_now_add=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
response_data = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text='Response data (e.g., exported data)'
|
||||
)
|
||||
response_file = models.FileField(
|
||||
upload_to='data_requests/',
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='File containing requested data'
|
||||
)
|
||||
handled_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='handled_data_requests',
|
||||
limit_choices_to={'role': 'admin'}
|
||||
)
|
||||
notes = models.TextField(
|
||||
blank=True,
|
||||
help_text='Internal notes about handling the request'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'legal_datarequest'
|
||||
verbose_name = 'Data Request'
|
||||
verbose_name_plural = 'Data Requests'
|
||||
ordering = ['-requested_at']
|
||||
indexes = [
|
||||
models.Index(fields=['user', 'status']),
|
||||
models.Index(fields=['request_type', 'status']),
|
||||
models.Index(fields=['status', 'requested_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_request_type_display()} by {self.user.username} - {self.get_status_display()}"
|
||||
|
||||
|
||||
class SecurityEvent(models.Model):
|
||||
"""
|
||||
Security event logging for compliance and monitoring.
|
||||
"""
|
||||
EVENT_TYPE_CHOICES = [
|
||||
('login_success', 'Successful Login'),
|
||||
('login_failed', 'Failed Login'),
|
||||
('password_change', 'Password Changed'),
|
||||
('account_locked', 'Account Locked'),
|
||||
('suspicious_activity', 'Suspicious Activity'),
|
||||
('data_breach', 'Data Breach'),
|
||||
('unauthorized_access', 'Unauthorized Access Attempt'),
|
||||
('file_upload', 'File Upload'),
|
||||
('data_export', 'Data Export'),
|
||||
('admin_action', 'Admin Action'),
|
||||
]
|
||||
|
||||
SEVERITY_CHOICES = [
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High'),
|
||||
('critical', 'Critical'),
|
||||
]
|
||||
|
||||
event_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=EVENT_TYPE_CHOICES
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='security_events'
|
||||
)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
details = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text='Additional event details'
|
||||
)
|
||||
severity = models.CharField(
|
||||
max_length=20,
|
||||
choices=SEVERITY_CHOICES,
|
||||
default='low'
|
||||
)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
resolved_at = models.DateTimeField(null=True, blank=True)
|
||||
resolved_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='resolved_security_events',
|
||||
limit_choices_to={'role': 'admin'}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'security_securityevent'
|
||||
verbose_name = 'Security Event'
|
||||
verbose_name_plural = 'Security Events'
|
||||
ordering = ['-timestamp']
|
||||
indexes = [
|
||||
models.Index(fields=['event_type', 'timestamp']),
|
||||
models.Index(fields=['severity', 'timestamp']),
|
||||
models.Index(fields=['user', 'timestamp']),
|
||||
models.Index(fields=['resolved', 'timestamp']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_event_type_display()} - {self.get_severity_display()} - {self.timestamp}"
|
||||
3
legal/tests.py
Normal file
3
legal/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
16
legal/urls.py
Normal file
16
legal/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
URL configuration for legal app.
|
||||
"""
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'legal'
|
||||
|
||||
urlpatterns = [
|
||||
path('privacy/', views.PrivacyPolicyView.as_view(), name='privacy'),
|
||||
path('terms/', views.TermsOfServiceView.as_view(), name='terms'),
|
||||
path('data-request/', views.DataRequestView.as_view(), name='data_request'),
|
||||
path('data-request/<int:pk>/', views.DataRequestDetailView.as_view(), name='data_request_detail'),
|
||||
path('cookie-consent/', views.cookie_consent_view, name='cookie_consent'),
|
||||
]
|
||||
|
||||
111
legal/views.py
Normal file
111
legal/views.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Views for legal app.
|
||||
"""
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import TemplateView, CreateView, DetailView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from .models import DataRequest, ConsentRecord
|
||||
from .forms import DataRequestForm
|
||||
|
||||
|
||||
class PrivacyPolicyView(TemplateView):
|
||||
"""Privacy policy page."""
|
||||
template_name = 'legal/privacy_policy.html'
|
||||
|
||||
|
||||
class TermsOfServiceView(TemplateView):
|
||||
"""Terms of service page."""
|
||||
template_name = 'legal/terms_of_service.html'
|
||||
|
||||
|
||||
class DataRequestView(LoginRequiredMixin, CreateView):
|
||||
"""GDPR data request form."""
|
||||
model = DataRequest
|
||||
form_class = DataRequestForm
|
||||
template_name = 'legal/data_request.html'
|
||||
success_url = reverse_lazy('legal:data_request_detail')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('legal:data_request_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class DataRequestDetailView(LoginRequiredMixin, DetailView):
|
||||
"""View data request status."""
|
||||
model = DataRequest
|
||||
template_name = 'legal/data_request_detail.html'
|
||||
context_object_name = 'data_request'
|
||||
|
||||
def get_queryset(self):
|
||||
return DataRequest.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def cookie_consent_view(request):
|
||||
"""
|
||||
Handle cookie consent submission.
|
||||
Stores consent in database and sets a cookie.
|
||||
"""
|
||||
import json
|
||||
from django.utils import timezone
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
consent_given = data.get('consent', False)
|
||||
|
||||
# Get client IP
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip_address = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip_address = request.META.get('REMOTE_ADDR')
|
||||
|
||||
# Create consent record
|
||||
ConsentRecord.objects.create(
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
consent_type='cookies',
|
||||
consent_given=consent_given,
|
||||
ip_address=ip_address,
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', ''),
|
||||
version='1.0'
|
||||
)
|
||||
|
||||
# Create response
|
||||
response = JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Cookie consent recorded successfully'
|
||||
})
|
||||
|
||||
# Set cookie (expires in 1 year)
|
||||
if consent_given:
|
||||
response.set_cookie(
|
||||
'cookie_consent',
|
||||
'accepted',
|
||||
max_age=31536000, # 1 year in seconds
|
||||
httponly=False,
|
||||
samesite='Lax',
|
||||
secure=request.is_secure()
|
||||
)
|
||||
else:
|
||||
response.set_cookie(
|
||||
'cookie_consent',
|
||||
'declined',
|
||||
max_age=31536000,
|
||||
httponly=False,
|
||||
samesite='Lax',
|
||||
secure=request.is_secure()
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
}, status=400)
|
||||
Reference in New Issue
Block a user