Merge branch '28-prepraviti-payment-page-da-podrzava-kodove' into 'master'
Resolve "Prepraviti payment page da podrzava kodove." Closes #28 See merge request kbr4/riskletpy!32
This commit was merged in pull request #81.
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Document, DocumentSegment, Organization, Risk, Control, DocumentTemplate, DocumentRiskControl
|
from .models import Document, DocumentSegment, Organization, Risk, Control, DocumentTemplate, DocumentRiskControl, PaymentCode
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
from .utils import generate_payment_code
|
||||||
|
from django.urls import path
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from .forms import GenerateCodesForm
|
||||||
|
|
||||||
class DocumentSegmentInline(admin.StackedInline):
|
class DocumentSegmentInline(admin.StackedInline):
|
||||||
model = DocumentSegment
|
model = DocumentSegment
|
||||||
@@ -41,6 +44,37 @@ class ControlAdmin(admin.ModelAdmin):
|
|||||||
class DocumentRiskControlAdmin(admin.ModelAdmin):
|
class DocumentRiskControlAdmin(admin.ModelAdmin):
|
||||||
list_display = ('document', 'risk', 'control', 'weight','likelihood')
|
list_display = ('document', 'risk', 'control', 'weight','likelihood')
|
||||||
|
|
||||||
|
class PaymentCodeAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ('code', 'created_at', 'used', 'company', 'used_at')
|
||||||
|
change_list_template = "admin/paymentcode_changelist.html"
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('generate-codes/', self.admin_site.admin_view(self.generate_codes_view), name='generate-codes'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def generate_codes_view(self, request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = GenerateCodesForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
count = form.cleaned_data['count']
|
||||||
|
created = 0
|
||||||
|
for _ in range(count):
|
||||||
|
while True:
|
||||||
|
code = generate_payment_code()
|
||||||
|
if not PaymentCode.objects.filter(code=code).exists():
|
||||||
|
PaymentCode.objects.create(code=code)
|
||||||
|
created += 1
|
||||||
|
break
|
||||||
|
self.message_user(request, f"{created} codes generated.")
|
||||||
|
return redirect('..')
|
||||||
|
else:
|
||||||
|
form = GenerateCodesForm()
|
||||||
|
return render(request, 'admin/generate_codes.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Document, DocumentAdmin)
|
admin.site.register(Document, DocumentAdmin)
|
||||||
admin.site.register(Organization, OrganizationAdmin)
|
admin.site.register(Organization, OrganizationAdmin)
|
||||||
@@ -48,3 +82,4 @@ admin.site.register(Risk ,RiskAdmin)
|
|||||||
admin.site.register(Control, ControlAdmin)
|
admin.site.register(Control, ControlAdmin)
|
||||||
admin.site.register(DocumentTemplate, DocumentTemplateAdmin)
|
admin.site.register(DocumentTemplate, DocumentTemplateAdmin)
|
||||||
admin.site.register(DocumentRiskControl, DocumentRiskControlAdmin)
|
admin.site.register(DocumentRiskControl, DocumentRiskControlAdmin)
|
||||||
|
admin.site.register(PaymentCode, PaymentCodeAdmin)
|
||||||
|
|||||||
@@ -47,3 +47,7 @@ class OrganizationForm(forms.ModelForm):
|
|||||||
cleaned_data['sensitive_data_types'] = types
|
cleaned_data['sensitive_data_types'] = types
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateCodesForm(forms.Form):
|
||||||
|
count = forms.IntegerField(label="How many codes to generate?", min_value=1, max_value=1000)
|
||||||
|
|||||||
25
backend/core/migrations/0016_paymentcode.py
Normal file
25
backend/core/migrations/0016_paymentcode.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-06-19 18:42
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0015_control_description_control_safeguard_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PaymentCode',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=10, unique=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('used', models.BooleanField(default=False)),
|
||||||
|
('used_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.organization')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -172,3 +172,14 @@ class DocumentRiskControl(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('document', 'risk', 'control')
|
unique_together = ('document', 'risk', 'control')
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentCode(models.Model):
|
||||||
|
code = models.CharField(max_length=10, unique=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
used = models.BooleanField(default=False)
|
||||||
|
used_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
company = models.ForeignKey(Organization, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (f"{self.code} - {'Used' if self.used else 'Available'}")
|
||||||
|
|||||||
11
backend/core/templates/admin/generate_codes.html
Normal file
11
backend/core/templates/admin/generate_codes.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="content-main">
|
||||||
|
<h2>Generate Payment Codes</h2>
|
||||||
|
<form method="post">{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input type="submit" name="apply" value="Generate" class="default">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
14
backend/core/templates/admin/paymentcode_changelist.html
Normal file
14
backend/core/templates/admin/paymentcode_changelist.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
{% block object-tools %}
|
||||||
|
<div style="padding: 12px 0; display: flex; gap: 8px;">
|
||||||
|
<div style="padding: 12px 0;">
|
||||||
|
<a href="{% url 'admin:generate-codes' %}" class="button">Generate Payment Codes</a>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 12px 0;">
|
||||||
|
<a href="{% url 'core:payment_codes_pdf' %}?filter_by=all" class="button">All (PDF)</a>
|
||||||
|
<a href="{% url 'core:payment_codes_pdf' %}?filter_by=used" class="button">Used (PDF)</a>
|
||||||
|
<a href="{% url 'core:payment_codes_pdf' %}?filter_by=available" class="button">Unused (PDF)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<main class="flex-grow">
|
<main class="flex-grow">
|
||||||
|
|
||||||
<!-- WhitepaperSection -->
|
<!-- WhitepaperSection -->
|
||||||
<section class="bg-gradient-to-br from-primary to-teal-700 text-light-text py-20 md:py-32 relative">
|
<section class="bg-gradient-to-br from-primary to-teal-700 text-light-text py-20 md:py-32 relative">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="py-16 bg-secondary sm:py-24 p-body-full">
|
<section class="py-24 bg-gradient-to-br from-teal-100 to-primary flex items-center justify-center">
|
||||||
<div class="max-w-lg w-full mx-auto text-center shadow-lg border border-success rounded-xl p-8 bg-white">
|
<div class="max-w-md w-full mx-auto text-center shadow-2xl border border-accent rounded-2xl p-4 bg-white/90 backdrop-blur">
|
||||||
<h2 class="text-3xl font-extrabold mb-4 text-success">Payment</h2>
|
<h2 class="text-3xl font-extrabold mb-6 text-accent">Payment</h2>
|
||||||
<p class="mb-8 text-gray-700 text-lg">
|
<p class="text-lg text-gray-700 mb-6">Please enter your payment code to proceed to document.</p>
|
||||||
Click the button below to securely pay and access your document.
|
|
||||||
</p>
|
{% if success %}
|
||||||
<form method="post">
|
<p class="text-green-600 font-semibold mb-4">{{ success }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" class="space-y-6">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="nav-link-desktop bg-transparent border-2 border-accent text-accent hover:bg-accent hover:text-primary font-semibold py-3 px-8 rounded-lg text-lg transition-all duration-300 ease-in-out">
|
<input
|
||||||
Pay & Check Your document
|
type="text"
|
||||||
|
name="code"
|
||||||
|
maxlength="10"
|
||||||
|
class="w-full px-4 py-3 border-2 border-accent rounded-lg focus:outline-none focus:ring-2 focus:ring-accent text-lg tracking-widest text-center font-mono mb-2"
|
||||||
|
placeholder="Enter your code"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-accent text-primary hover:bg-yellow-400 font-bold py-3 px-8 rounded-lg shadow-lg text-lg transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||||
|
>
|
||||||
|
Enter Code
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% if error %}
|
||||||
|
<p class="text-red-600 mt-6 font-semibold text-lg">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
39
backend/core/templates/payment_code_report.html
Normal file
39
backend/core/templates/payment_code_report.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PDF payment report</title>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/document.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Payment Codes Report</h1>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Used</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th>Used At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for code in codes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ code.code }}</td>
|
||||||
|
<td>{{ code.created_at }}</td>
|
||||||
|
<td>{{ code.used|yesno:"Yes,No" }}</td>
|
||||||
|
{% if code.company %}
|
||||||
|
<td>{{ code.company.name }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td>-</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>{{ code.used_at|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -17,4 +17,7 @@ urlpatterns = [
|
|||||||
path('no_confidential_data/', v.no_confidential_data, name='no_confidential_data'),
|
path('no_confidential_data/', v.no_confidential_data, name='no_confidential_data'),
|
||||||
path('downloads/risklet_example_document.pdf', v.download_example_pdf, name='download_example_pdf'),
|
path('downloads/risklet_example_document.pdf', v.download_example_pdf, name='download_example_pdf'),
|
||||||
|
|
||||||
|
#admin urls
|
||||||
|
path('admin/payment-codes-pdf/', v.payment_codes_pdf_view, name='payment_codes_pdf'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import matplotlib.pyplot as plt
|
|||||||
from django.contrib.staticfiles.finders import find
|
from django.contrib.staticfiles.finders import find
|
||||||
import matplotlib.image as mpimg
|
import matplotlib.image as mpimg
|
||||||
site_domain = settings.SITE_DOMAIN
|
site_domain = settings.SITE_DOMAIN
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -342,3 +342,7 @@ def generate_residual_risk_graph(risks_with_controls):
|
|||||||
plt.close()
|
plt.close()
|
||||||
|
|
||||||
return base64.b64encode(image_png).decode("utf-8")
|
return base64.b64encode(image_png).decode("utf-8")
|
||||||
|
|
||||||
|
def generate_payment_code(length=6):
|
||||||
|
chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||||
|
return ''.join(random.choices(chars, k=length))
|
||||||
@@ -3,7 +3,7 @@ import yaml
|
|||||||
|
|
||||||
from django.shortcuts import render, redirect , get_object_or_404
|
from django.shortcuts import render, redirect , get_object_or_404
|
||||||
from .forms import OrganizationForm
|
from .forms import OrganizationForm
|
||||||
from .models import Organization,Document, DocumentTemplate
|
from .models import Organization,Document, DocumentTemplate, PaymentCode
|
||||||
from backend.accounts.utils import send_confirmation_email, send_document_email
|
from backend.accounts.utils import send_confirmation_email, send_document_email
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from .utils import generate_pdf, generate_risk_graph, generate_residual_risk_graph
|
from .utils import generate_pdf, generate_risk_graph, generate_residual_risk_graph
|
||||||
@@ -11,11 +11,13 @@ from .tables import risk_matrix_table ,get_risk_table, get_safeguard_summary_tab
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
site_domain = settings.SITE_DOMAIN
|
site_domain = settings.SITE_DOMAIN
|
||||||
from .processors import render_template
|
from .processors import render_template
|
||||||
from django.http import JsonResponse, FileResponse, Http404
|
from django.http import JsonResponse, FileResponse, Http404, HttpResponse
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
import os
|
import os
|
||||||
|
from django.utils import timezone
|
||||||
|
from weasyprint import HTML
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
|
||||||
# @login_required
|
# @login_required
|
||||||
@@ -112,16 +114,28 @@ def pdf_view(request, document_id):
|
|||||||
return generate_pdf(document)
|
return generate_pdf(document)
|
||||||
|
|
||||||
def payment_page(request):
|
def payment_page(request):
|
||||||
email = request.GET.get("email")
|
error = None
|
||||||
organization = get_object_or_404(Organization, email=email)
|
email = request.GET.get('email')
|
||||||
document = get_object_or_404(Document, organization=organization)
|
if request.method == 'POST':
|
||||||
|
import re
|
||||||
if request.method == "POST":
|
code = re.sub(r'\s+', '', request.POST.get('code', '')).upper()[:10]
|
||||||
pdf_url = f"{site_domain}/pdf/{document.id}/"
|
try:
|
||||||
send_document_email(email, pdf_url, document)
|
payment_code = PaymentCode.objects.get(code=code)
|
||||||
return redirect(pdf_url)
|
if payment_code.used:
|
||||||
|
error = "CODE INVALID"
|
||||||
return render(request, "payment.html", {"email": email})
|
else:
|
||||||
|
org = Organization.objects.filter(email__iexact=email).first()
|
||||||
|
payment_code.used = True
|
||||||
|
payment_code.used_at = timezone.now()
|
||||||
|
payment_code.company = org
|
||||||
|
payment_code.save()
|
||||||
|
document = Document.objects.get(organization = org)
|
||||||
|
url = f"{site_domain}/pdf/{document.id}/"
|
||||||
|
send_document_email(email, url, document)
|
||||||
|
return redirect(url)
|
||||||
|
except PaymentCode.DoesNotExist:
|
||||||
|
error = "CODE INVALID"
|
||||||
|
return render(request, 'payment.html', {'error': error})
|
||||||
|
|
||||||
def no_confidential_data(request):
|
def no_confidential_data(request):
|
||||||
return render(request, "no_confidential_data.html")
|
return render(request, "no_confidential_data.html")
|
||||||
@@ -132,3 +146,19 @@ def download_example_pdf(request):
|
|||||||
if not os.path.exists(pdf_path):
|
if not os.path.exists(pdf_path):
|
||||||
raise Http404("File not found.")
|
raise Http404("File not found.")
|
||||||
return FileResponse(open(pdf_path, 'rb'), as_attachment=True, filename='risklet_example_document.pdf')
|
return FileResponse(open(pdf_path, 'rb'), as_attachment=True, filename='risklet_example_document.pdf')
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def payment_codes_pdf_view(request):
|
||||||
|
filter_by = request.GET.get('filter_by', 'all')
|
||||||
|
if filter_by == 'used':
|
||||||
|
codes = PaymentCode.objects.filter(used=True)
|
||||||
|
elif filter_by == 'available':
|
||||||
|
codes = PaymentCode.objects.filter(used=False)
|
||||||
|
else:
|
||||||
|
codes = PaymentCode.objects.all()
|
||||||
|
html_string = render_to_string('payment_code_report.html', {'codes': codes})
|
||||||
|
pdf_content = HTML(string=html_string, base_url=request.build_absolute_uri('/')).write_pdf()
|
||||||
|
response = HttpResponse(pdf_content, content_type='application/pdf')
|
||||||
|
response['Content-Disposition'] = f'inline; filename=payment_codes_{timezone.now().strftime("%Y%m%d_%H%M%S")}.pdf'
|
||||||
|
return response
|
||||||
Reference in New Issue
Block a user