From 9c5e218c2b9ba821e8121c5907cd43a8642feb07 Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 00:47:45 +0530 Subject: [PATCH 01/14] Add verbose name to visible field in DonationTier model --- src/donations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/donations/models.py b/src/donations/models.py index 3353112..b57ada8 100644 --- a/src/donations/models.py +++ b/src/donations/models.py @@ -21,7 +21,7 @@ class DonationTier(models.Model): name = models.CharField(max_length=256) description = models.CharField(max_length=1024) amount = models.PositiveIntegerField(validators=[MaxValueValidator(10000)]) - visible = models.BooleanField(default=False) + visible = models.BooleanField(verbose_name="Show on Donations Page ?", default=False) def __str__(self): return self.name From 1efbc7ddecb9b07f31f3292d71cafe50e9619027 Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 00:47:59 +0530 Subject: [PATCH 02/14] Filter donation tiers to show only visible ones --- src/donations/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/donations/views.py b/src/donations/views.py index 061eafa..721be3f 100644 --- a/src/donations/views.py +++ b/src/donations/views.py @@ -20,7 +20,7 @@ def donations(request): donation_config = DonationConfig.get_solo() - donation_tiers = DonationTier.objects.all() + donation_tiers = DonationTier.objects.filter(visible=True) context = {"donation_config": donation_config, "donation_tiers": donation_tiers} return render(request, "donations/donations.html", context=context) From 02fa4ccc2dfd979b0135dc361e480b492d57c5ce Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 00:48:54 +0530 Subject: [PATCH 03/14] Add payment and custom form support for event registration Enhance event registration system with payment integration and dynamic form fields. Add support for optional login, custom registration forms, and payment tracking. Update admin interface for better event management. --- .../0002_alter_donationtier_visible.py | 19 +++ src/haps/admin.py | 69 ++++++++--- ...equired_event_registration_fee_and_more.py | 109 ++++++++++++++++++ src/haps/models.py | 60 ++++++++-- 4 files changed, 234 insertions(+), 23 deletions(-) create mode 100644 src/donations/migrations/0002_alter_donationtier_visible.py create mode 100644 src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py diff --git a/src/donations/migrations/0002_alter_donationtier_visible.py b/src/donations/migrations/0002_alter_donationtier_visible.py new file mode 100644 index 0000000..81a3aa2 --- /dev/null +++ b/src/donations/migrations/0002_alter_donationtier_visible.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-12-27 19:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("donations", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="donationtier", + name="visible", + field=models.BooleanField( + default=False, verbose_name="Show on Donations Page ?" + ), + ), + ] diff --git a/src/haps/admin.py b/src/haps/admin.py index 70dc045..0fa5a66 100644 --- a/src/haps/admin.py +++ b/src/haps/admin.py @@ -1,5 +1,11 @@ from django.contrib import admin -from .models import EventRegistration, Event +from .models import EventRegistration, Event, EventFormField + + +class EventFormFieldInline(admin.TabularInline): + model = EventFormField + extra = 1 + ordering = ['order'] @admin.register(Event) @@ -11,24 +17,57 @@ class EventAdmin(admin.ModelAdmin): "start_time", "accept_reg", "show_on_home", + "login_required", + "registration_fee", "event_page", ] - list_filter = ["show_on_home", "accept_reg"] - fields = [ - "name", - "description", - "cover_image", - "venue", - "start_time", - "end_time", - "accept_reg", - "show_on_home", - "content", + list_filter = ["show_on_home", "accept_reg", "login_required"] + fieldsets = [ + (None, { + 'fields': [ + "name", + "description", + "cover_image", + "venue", + "start_time", + "end_time", + ] + }), + ('Registration Settings', { + 'fields': [ + "accept_reg", + "login_required", + "registration_fee", + ], + 'description': 'Configure how users can register for this event' + }), + ('Display Settings', { + 'fields': [ + "show_on_home", + "content", + ] + }) ] + inlines = [EventFormFieldInline] @admin.register(EventRegistration) class EventRegistrationAdmin(admin.ModelAdmin): - search_fields = ["event", "user"] - list_display = ["event", "user_name", "user_whatsapp", "user", "user_profile_link"] - list_filter = ["event__name"] + search_fields = ["event__name", "user__email", "order_id"] + list_display = [ + "event", + "user_name", + "user_whatsapp", + "datetime", + "amount", + "payment_status", + "order_id" + ] + list_filter = ["event__name", "payment_status"] + readonly_fields = ["datetime", "form_responses"] + + def has_add_permission(self, request): + return False # Registrations should only be created through the website + + def get_queryset(self, request): + return super().get_queryset(request).select_related('event', 'user') diff --git a/src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py b/src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py new file mode 100644 index 0000000..59a53cf --- /dev/null +++ b/src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py @@ -0,0 +1,109 @@ +# Generated by Django 4.2.1 on 2024-12-27 19:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("haps", "0004_event_content"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="login_required", + field=models.BooleanField( + default=True, verbose_name="Login required for registration?" + ), + ), + migrations.AddField( + model_name="event", + name="registration_fee", + field=models.PositiveIntegerField( + blank=True, help_text="Leave blank for free registration", null=True + ), + ), + migrations.AddField( + model_name="eventregistration", + name="amount", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="eventregistration", + name="form_responses", + field=models.JSONField(default=dict), + ), + migrations.AddField( + model_name="eventregistration", + name="order_id", + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name="eventregistration", + name="payment_status", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("success", "Success"), + ("failure", "Failure"), + ], + default="pending", + max_length=10, + ), + ), + migrations.AlterField( + model_name="eventregistration", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="EventFormField", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("field_label", models.CharField(max_length=100)), + ( + "field_type", + models.CharField( + choices=[ + ("text", "Text Input"), + ("number", "Number Input"), + ("email", "Email Input"), + ("textarea", "Text Area"), + ], + max_length=20, + ), + ), + ("required", models.BooleanField(default=True)), + ("order", models.PositiveIntegerField(default=0)), + ("help_text", models.CharField(blank=True, max_length=200)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="form_fields", + to="haps.event", + ), + ), + ], + options={ + "ordering": ["order"], + "unique_together": {("event", "field_label")}, + }, + ), + ] diff --git a/src/haps/models.py b/src/haps/models.py index 455c851..7f2da41 100644 --- a/src/haps/models.py +++ b/src/haps/models.py @@ -5,7 +5,6 @@ from utils.slugs import generate_unique_slug from ckeditor_uploader.fields import RichTextUploadingField from users.models import UserProfile -from django.utils.html import format_html User = get_user_model() @@ -21,6 +20,8 @@ class Event(models.Model): accept_reg = models.BooleanField(verbose_name="Accepting registrations ?") show_on_home = models.BooleanField(verbose_name="Show on Home Page ?") content = RichTextUploadingField(null=True, blank=True) + login_required = models.BooleanField(default=True, verbose_name="Login required for registration?") + registration_fee = models.PositiveIntegerField(null=True, blank=True, help_text="Leave blank for free registration") def __str__(self): return self.name + " (" + str(self.start_time) + ")" @@ -28,7 +29,7 @@ def __str__(self): def save(self, *args, **kwargs): if self.slug == "": self.slug = generate_unique_slug(self.name, Event) - super().save(args, kwargs) + super().save(*args, **kwargs) def get_absolute_url(self): return f"/events/{self.slug}" @@ -39,20 +40,63 @@ def event_page(self): ) +class EventFormField(models.Model): + FIELD_TYPES = [ + ('text', 'Text Input'), + ('number', 'Number Input'), + ('email', 'Email Input'), + ('textarea', 'Text Area'), + ] + + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='form_fields') + field_label = models.CharField(max_length=100) + field_type = models.CharField(max_length=20, choices=FIELD_TYPES) + required = models.BooleanField(default=True) + order = models.PositiveIntegerField(default=0) + help_text = models.CharField(max_length=200, blank=True) + + class Meta: + ordering = ['order'] + unique_together = ['event', 'field_label'] + + def __str__(self): + return f"{self.event.name} - {self.field_label}" + + class EventRegistration(models.Model): + PAYMENT_STATUS_CHOICES = [ + ('pending', 'Pending'), + ('success', 'Success'), + ('failure', 'Failure'), + ] + event = models.ForeignKey(Event, on_delete=models.CASCADE) - user = models.ForeignKey(User, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True) - # regno = models.CharField(unique=True) + form_responses = models.JSONField(default=dict) + amount = models.PositiveIntegerField(null=True, blank=True) + payment_status = models.CharField( + max_length=10, + choices=PAYMENT_STATUS_CHOICES, + default='pending' + ) + order_id = models.CharField(max_length=100, blank=True, null=True) def user_name(self): - return self.user.full_name() + if self.user: + return self.user.full_name() + return self.form_responses.get('name', 'Anonymous') def user_whatsapp(self): - return UserProfile.objects.get(user=self.user).whatsapp_number + if self.user: + return UserProfile.objects.get(user=self.user).whatsapp_number + return self.form_responses.get('whatsapp_number', '') def user_profile_link(self): - return self.user.profile_link() + if self.user: + return self.user.profile_link() + return None def __str__(self) -> str: - return self.user.__str__() + "%" + self.event.__str__() + user_str = str(self.user) if self.user else self.form_responses.get('name', 'Anonymous') + return f"{user_str} % {self.event}" From ceaf0cdb55d6a8cfd8f2a6fa9b0b66b69cb5322c Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 01:31:45 +0530 Subject: [PATCH 04/14] feat: add template tags for accessing dictionary items - Add get_item template filter to access form responses - Create templatetags package for haps app --- src/haps/templatetags/__init__.py | 1 + src/haps/templatetags/haps_extras.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 src/haps/templatetags/__init__.py create mode 100644 src/haps/templatetags/haps_extras.py diff --git a/src/haps/templatetags/__init__.py b/src/haps/templatetags/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/haps/templatetags/__init__.py @@ -0,0 +1 @@ + diff --git a/src/haps/templatetags/haps_extras.py b/src/haps/templatetags/haps_extras.py new file mode 100644 index 0000000..83bc6b3 --- /dev/null +++ b/src/haps/templatetags/haps_extras.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + +@register.filter +def get_item(dictionary, key): + """Get an item from a dictionary using template filter""" + return dictionary.get(key, '') From 1e29b75dc68b514077502df97786a17f1577666b Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 01:32:05 +0530 Subject: [PATCH 05/14] fix: handle missing event cover image gracefully - Add conditional check for cover image - Show placeholder with event icon when image missing - Improve alt text for accessibility --- src/haps/templates/haps/item.html | 165 ++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 40 deletions(-) diff --git a/src/haps/templates/haps/item.html b/src/haps/templates/haps/item.html index e742453..18ad412 100644 --- a/src/haps/templates/haps/item.html +++ b/src/haps/templates/haps/item.html @@ -7,8 +7,14 @@
- image description + {% if hap.cover_image %} + {{hap.name}} cover image + {% else %} +
+ event +
+ {% endif %}
@@ -17,62 +23,141 @@ {{hap.name}} -

{{hap.description}}

-
-
- - event - + event {{hap.start_time}}
- - location_on - - - {{hap.venue}} - + location_on + {{hap.venue}} +
+ + {% if hap.registration_fee %} +
+ payments + Registration Fee: ₹{{hap.registration_fee}}
+ {% endif %}
- - Register Now - - - - - - - Contact us - + {% if hap.accept_reg %} + {% if hap.login_required and not user.is_authenticated %} + + Login to Register + + + + + {% else %} + + {% endif %} + {% endif %}
- - - - -
-
-
- {{hap.content | safe}} -
+ {% if hap.content %} +
+ {{hap.content|safe}}
+ {% endif %} +
+ + +{% endblock content %} \ No newline at end of file From 69540ddfb134cdb560b18d27523ce6f1ff9e78eb Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 01:32:22 +0530 Subject: [PATCH 06/14] feat: enhance registration success page - Add registration number display - Load template tags for form responses - Update print button handler --- src/haps/templates/haps/register_success.html | 206 +++++++++++------- 1 file changed, 125 insertions(+), 81 deletions(-) diff --git a/src/haps/templates/haps/register_success.html b/src/haps/templates/haps/register_success.html index 2207e98..452f3f3 100644 --- a/src/haps/templates/haps/register_success.html +++ b/src/haps/templates/haps/register_success.html @@ -1,12 +1,12 @@ {% extends "commons.html" %} {% block title %} {{ event.name }} {% endblock title %} +{% load haps_extras %} {% block content %} - -
-
+
+
@@ -14,91 +14,135 @@

Registration Done!

-

Thank you for completing your registration for event

-

Looking forward to see you in the event!

- +

Thank you for completing your registration.

+

Registration #{{registration.id}}

+

Looking forward to seeing you at the event!

- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {# show registration date time #} - -
- Event - - {{event.name}} -
- Name - - {{user.first_name}} {{user.last_name}} -
- Email - - {{user.email}} -
- Date - - {{event.start_time}} -
- Venue - - {{event.venue}} -
-
- - - +
+ + + + + + + + + + + + + {% if registration.user %} + + + + + {% endif %} + + {% if not registration.user %} + + + + + {% endif %} + + + + + + + + + + + + {% if registration.datetime %} + + + + + {% endif %} + + {% if event.registration_fee %} + + + + + {% endif %} + + {% for field in event.form_fields.all %} + + + + + {% endfor %} + +
+ Event + + {{event.name}} +
+ Name + + {% if registration.user %} + {{registration.user.get_full_name}} + {% else %} + {{registration.form_responses.name}} + {% endif %} +
+ Email + + {{registration.user.email}} +
+ WhatsApp + + {{registration.form_responses.whatsapp_number}} +
+ Date + + {{event.start_time}} +
+ Venue + + {{event.venue}} +
+ Registration Time + + {{registration.datetime}} +
+ Registration Fee + + ₹{{event.registration_fee}} + + {{registration.payment_status|title}} + +
+ {{field.field_label}} + + {{registration.form_responses|get_item:field.field_label}} +
-
- {% endblock content %} \ No newline at end of file From 97ca91d9736aa2b42a8d0652330815c56bd2aed0 Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 01:32:39 +0530 Subject: [PATCH 07/14] feat: add registration number to admin interface - Add registration_number column to list display - Format registration ID with # prefix - Improve admin list readability --- src/haps/admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/haps/admin.py b/src/haps/admin.py index 0fa5a66..51b5f79 100644 --- a/src/haps/admin.py +++ b/src/haps/admin.py @@ -55,6 +55,7 @@ class EventAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin): search_fields = ["event__name", "user__email", "order_id"] list_display = [ + "registration_number", "event", "user_name", "user_whatsapp", @@ -71,3 +72,7 @@ def has_add_permission(self, request): def get_queryset(self, request): return super().get_queryset(request).select_related('event', 'user') + + def registration_number(self, obj): + return f"#{obj.id}" + registration_number.short_description = "Registration No." From 86eab50d3011c39310e63505fa9984cc579d0a72 Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 01:32:57 +0530 Subject: [PATCH 08/14] chore: update VS Code settings - Change HTML file association to django-html - Update Python language server settings --- .vscode/settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c659b65..adb3c07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "files.associations": { - "*.html": "jinja-html" - } + "*.html": "django-html" + }, + "python.languageServer": "None" } From fd2845b193cc2e7b777ae3a6a3be1454aee24803 Mon Sep 17 00:00:00 2001 From: aahnik Date: Sat, 28 Dec 2024 01:33:31 +0530 Subject: [PATCH 09/14] feat: enhance event registration view - Add form response collection for custom fields - Handle both logged-in and non-logged-in registrations - Add payment flow integration - Improve error handling and user feedback --- src/haps/views.py | 54 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/haps/views.py b/src/haps/views.py index 2a22bbf..561950e 100644 --- a/src/haps/views.py +++ b/src/haps/views.py @@ -1,8 +1,10 @@ -from django.shortcuts import render -from django.http import HttpRequest, Http404 -from .models import Event, EventRegistration +from django.shortcuts import render, redirect +from django.http import HttpRequest, HttpResponse from django.contrib.auth.decorators import login_required +from django.contrib import messages +from .models import Event, EventRegistration import logging +import json log = logging.getLogger(__name__) @@ -24,9 +26,45 @@ def haps_item(request: HttpRequest, slug: str): @login_required(login_url="/users/register") def register_for_event(request: HttpRequest, slug: str): event = Event.objects.get(slug=slug) - registration = EventRegistration.objects.get_or_create( - event=event, user=request.user - ) + + if not event.accept_reg: + messages.error(request, "Registration is closed for this event.") + return redirect('haps:item', slug=slug) + + if event.login_required and not request.user.is_authenticated: + messages.error(request, "Please login to register for this event.") + return redirect('users:register') - context = {"event": event, "user": request.user, "reg": registration} - return render(request, "haps/register_success.html", context=context) + if request.method == 'POST': + # Collect form responses + form_responses = {} + + # If not login required, collect basic info + if not event.login_required: + form_responses['name'] = request.POST.get('name') + form_responses['whatsapp_number'] = request.POST.get('whatsapp_number') + + # Collect custom field responses + for field in event.form_fields.all(): + field_id = f'field_{field.id}' + form_responses[field.field_label] = request.POST.get(field_id) + + # Create registration + registration = EventRegistration.objects.create( + event=event, + user=request.user if event.login_required else None, + form_responses=form_responses, + amount=event.registration_fee + ) + + # If payment required, redirect to payment page + if event.registration_fee: + # TODO: Implement payment flow using donations module + return redirect('donations:payment', registration_id=registration.id) + + messages.success(request, "Successfully registered for the event!") + return render(request, "haps/register_success.html", + context={"event": event, "registration": registration}) + + # If GET request with registration modal, return to event page + return redirect('haps:item', slug=slug) From 8783c3618e0e4955afca0a1bb07db29db6bc3746 Mon Sep 17 00:00:00 2001 From: aahnik Date: Mon, 30 Dec 2024 02:06:50 +0530 Subject: [PATCH 10/14] Move payment gateway to utils for reusability --- src/{donations => utils/payment}/upi_gateway.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) rename src/{donations => utils/payment}/upi_gateway.py (92%) diff --git a/src/donations/upi_gateway.py b/src/utils/payment/upi_gateway.py similarity index 92% rename from src/donations/upi_gateway.py rename to src/utils/payment/upi_gateway.py index 345c3e3..cba53ed 100644 --- a/src/donations/upi_gateway.py +++ b/src/utils/payment/upi_gateway.py @@ -40,6 +40,8 @@ def create_order( """ if not customer_email: customer_email = "example@example.com" + if not customer_mobile: + customer_mobile = "9999999999" payload_dict = { "key": str(PaymentGatewayConfig.API_KEY), "client_txn_id": client_txn_id, @@ -55,19 +57,25 @@ def create_order( payload_json_str = json.dumps(payload_dict) + log.info(f"making payment request for {amount=} and {client_txn_id=}") + log.debug(payload_dict) + response = requests.request( "POST", PaymentGatewayConfig.CREATE_ORDER, headers=PaymentGatewayConfig.REQUEST_HEADERS, data=payload_json_str, ) - + log.debug(response.text) if response.status_code == 200: rj = response.json() if rj["status"] is True: + log.warning(rj) return True, rj["data"] else: log.warning("Failed to create order \n%s", response.text) + log.warning("rj[data]", rj["data"]) + log.warning("rj", rj) return False, response else: log.warning( From 9e9ee67ed6f2b995796a6274cfc6be8ebd582c5e Mon Sep 17 00:00:00 2001 From: aahnik Date: Mon, 30 Dec 2024 02:07:04 +0530 Subject: [PATCH 11/14] Add critical logging for payment retries --- src/utils/logging.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/utils/logging.py diff --git a/src/utils/logging.py b/src/utils/logging.py new file mode 100644 index 0000000..f745009 --- /dev/null +++ b/src/utils/logging.py @@ -0,0 +1,34 @@ +import logging +import os +from datetime import datetime +from pathlib import Path + +def setup_critical_logger(): + """Setup logger for critical events that need to be preserved""" + + # Create logs directory if it doesn't exist + logs_dir = Path(__file__).parent.parent.parent / 'logs' + logs_dir.mkdir(exist_ok=True) + + # Create critical logger + critical_logger = logging.getLogger('critical') + critical_logger.setLevel(logging.CRITICAL) + + # Create file handler + log_file = logs_dir / 'critical.log' + handler = logging.FileHandler(str(log_file)) + handler.setLevel(logging.CRITICAL) + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + + # Add handler to logger + critical_logger.addHandler(handler) + + return critical_logger + +# Create the logger instance +critical_logger = setup_critical_logger() From 1834548fc35c3701f4fa0f2b16b68c3fd5102b60 Mon Sep 17 00:00:00 2001 From: aahnik Date: Mon, 30 Dec 2024 02:07:16 +0530 Subject: [PATCH 12/14] Improve payment flow in event registrations --- src/donations/views.py | 2 +- src/haps/admin.py | 86 +++++- ...ventregistration_client_txn_id_and_more.py | 32 +++ src/haps/models.py | 29 +- src/haps/templates/haps/register_failure.html | 137 ++++++++++ src/haps/urls.py | 9 +- src/haps/views.py | 248 ++++++++++++++++-- 7 files changed, 515 insertions(+), 28 deletions(-) create mode 100644 src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py create mode 100644 src/haps/templates/haps/register_failure.html diff --git a/src/donations/views.py b/src/donations/views.py index 721be3f..6c5325f 100644 --- a/src/donations/views.py +++ b/src/donations/views.py @@ -4,7 +4,7 @@ from .forms import DonationForm from .models import DonationConfig, DonationTier, DonationReceived from temple_web.myconfig import PaymentGatewayConfig -from .upi_gateway import create_order, check_order_status +from utils.payment.upi_gateway import create_order, check_order_status from uuid import uuid4 from datetime import date from utils.adirect import adirect diff --git a/src/haps/admin.py b/src/haps/admin.py index 51b5f79..8c96405 100644 --- a/src/haps/admin.py +++ b/src/haps/admin.py @@ -53,7 +53,7 @@ class EventAdmin(admin.ModelAdmin): @admin.register(EventRegistration) class EventRegistrationAdmin(admin.ModelAdmin): - search_fields = ["event__name", "user__email", "order_id"] + search_fields = ["event__name", "user__email", "order_id", "client_txn_id"] list_display = [ "registration_number", "event", @@ -62,17 +62,93 @@ class EventRegistrationAdmin(admin.ModelAdmin): "datetime", "amount", "payment_status", - "order_id" + "order_id", + "payment_date_time", ] list_filter = ["event__name", "payment_status"] - readonly_fields = ["datetime", "form_responses"] + readonly_fields = [ + "datetime", + "form_responses", + "payment_status", + "order_id", + "client_txn_id", + "payment_date_time", + "payment_data", + "get_payment_details" + ] + + fieldsets = [ + (None, { + 'fields': [ + "event", + "user", + "datetime", + "amount", + "form_responses", + ] + }), + ('Payment Information', { + 'fields': [ + "payment_status", + "order_id", + "client_txn_id", + "payment_date_time", + ], + 'classes': ['collapse'] + }), + ('Payment Diagnostic Data', { + 'fields': [ + "get_payment_details", + ], + 'classes': ['collapse'], + 'description': 'Detailed payment transaction data from UPI gateway' + }) + ] def has_add_permission(self, request): - return False # Registrations should only be created through the website + return False def get_queryset(self, request): return super().get_queryset(request).select_related('event', 'user') def registration_number(self, obj): - return f"#{obj.id}" + return obj.registration_number registration_number.short_description = "Registration No." + + def get_payment_details(self, obj): + if not obj.payment_data: + return "No payment data available" + + # Format payment data for display + details = [] + if obj.payment_data.get('customer_vpa'): + details.append(f"Customer UPI: {obj.payment_data['customer_vpa']}") + if obj.payment_data.get('upi_txn_id'): + details.append(f"UPI Transaction ID: {obj.payment_data['upi_txn_id']}") + if obj.payment_data.get('status'): + details.append(f"Status: {obj.payment_data['status']}") + if obj.payment_data.get('remark'): + details.append(f"Remark: {obj.payment_data['remark']}") + if obj.payment_data.get('txnAt'): + details.append(f"Transaction Time: {obj.payment_data['txnAt']}") + + # Merchant details + merchant = obj.payment_data.get('merchant', {}) + if merchant: + details.append("Merchant Details:") + if merchant.get('name'): + details.append(f" - Name: {merchant['name']}") + if merchant.get('upi_id'): + details.append(f" - UPI ID: {merchant['upi_id']}") + + # User defined fields + for i in range(1, 4): + udf = obj.payment_data.get(f'udf{i}') + if udf: + details.append(f"UDF{i}: {udf}") + + if obj.payment_data.get('createdAt'): + details.append(f"Created At: {obj.payment_data['createdAt']}") + + return "\n".join(details) + get_payment_details.short_description = "Payment Details" diff --git a/src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py b/src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py new file mode 100644 index 0000000..f3ae884 --- /dev/null +++ b/src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.1 on 2024-12-29 19:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("haps", "0005_event_login_required_event_registration_fee_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="eventregistration", + name="client_txn_id", + field=models.CharField( + blank=True, db_index=True, max_length=128, null=True, unique=True + ), + ), + migrations.AddField( + model_name="eventregistration", + name="payment_data", + field=models.JSONField( + default=dict, + help_text="\n Stores payment-related data from UPI gateway including:\n - customer_vpa: Customer's UPI ID\n - upi_txn_id: UPI transaction ID\n - status: Detailed payment status\n - remark: Payment remarks/failure reason\n - txnAt: Transaction timestamp\n - merchant: Merchant details (name, upi_id)\n - udf1, udf2, udf3: User defined fields\n - redirect_url: Payment redirect URL\n - createdAt: Order creation time\n ", + ), + ), + migrations.AddField( + model_name="eventregistration", + name="payment_date_time", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/haps/models.py b/src/haps/models.py index 7f2da41..442f509 100644 --- a/src/haps/models.py +++ b/src/haps/models.py @@ -75,12 +75,39 @@ class EventRegistration(models.Model): datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True) form_responses = models.JSONField(default=dict) amount = models.PositiveIntegerField(null=True, blank=True) + + # Payment related fields payment_status = models.CharField( max_length=10, choices=PAYMENT_STATUS_CHOICES, default='pending' ) order_id = models.CharField(max_length=100, blank=True, null=True) + client_txn_id = models.CharField(max_length=128, unique=True, db_index=True, null=True, blank=True) + payment_date_time = models.DateTimeField(null=True, blank=True) + + # Payment diagnostic data from UPI gateway + payment_data = models.JSONField(default=dict, help_text=""" + Stores payment-related data from UPI gateway including: + - customer_vpa: Customer's UPI ID + - upi_txn_id: UPI transaction ID + - status: Detailed payment status + - remark: Payment remarks/failure reason + - txnAt: Transaction timestamp + - merchant: Merchant details (name, upi_id) + - udf1, udf2, udf3: User defined fields + - redirect_url: Payment redirect URL + - createdAt: Order creation time + """) + + def __str__(self): + return f"Registration #{self.id} - {self.event.name}" + + @property + def registration_number(self): + """Returns a formatted registration number.""" + return f"#{self.id}" + def user_name(self): if self.user: @@ -99,4 +126,4 @@ def user_profile_link(self): def __str__(self) -> str: user_str = str(self.user) if self.user else self.form_responses.get('name', 'Anonymous') - return f"{user_str} % {self.event}" + return f"{user_str} % {self.event}" \ No newline at end of file diff --git a/src/haps/templates/haps/register_failure.html b/src/haps/templates/haps/register_failure.html new file mode 100644 index 0000000..319222c --- /dev/null +++ b/src/haps/templates/haps/register_failure.html @@ -0,0 +1,137 @@ +{% extends "commons.html" %} + +{% block title %} {{ event.name }} - Registration Failed {% endblock title %} +{% load haps_extras %} + +{% block content %} +
+
+
+ +
+ +
+

Registration Failed!

+

Your registration for the event is incomplete due to payment failure.

+

Registration #{{registration.id}}

+ {% if remark %} +

{{remark}}

+ {% endif %} +
+ +
+ + + + + + + + + + + + + {% if registration.user %} + + + + + {% endif %} + + {% if not registration.user %} + + + + + {% endif %} + + + + + + + + + + + + {% if registration.datetime %} + + + + + {% endif %} + + + + + + + {% for field in event.form_fields.all %} + + + + + {% endfor %} + +
+ Event + + {{event.name}} +
+ Name + + {% if registration.user %} + {{registration.user.get_full_name|default:"Anonymous User"}} + {% else %} + {{registration.form_responses.name|default:"Anonymous User"}} + {% endif %} +
+ Email + + {{registration.user.email}} +
+ WhatsApp + + {{registration.form_responses.whatsapp_number}} +
+ Date + + {{event.start_time}} +
+ Venue + + {{event.venue}} +
+ Registration Time + + {{registration.datetime}} +
+ Registration Fee + + ₹{{event.registration_fee}} + + Payment Failed + +
+ {{field.field_label}} + + {{registration.form_responses|get_item:field.field_label}} +
+
+ + +
+
+{% endblock content %} diff --git a/src/haps/urls.py b/src/haps/urls.py index f0ddcec..d3d9c01 100644 --- a/src/haps/urls.py +++ b/src/haps/urls.py @@ -6,6 +6,11 @@ urlpatterns = [ path("", views.haps_list, name="events"), - path("", views.haps_item, name="event_item"), - path("/register", views.register_for_event, name="event_register"), + path("", views.haps_item, name="event_item"), + path("/register", views.register_for_event, name="register"), + path("registration//failure", views.register_failure, name="register_failure"), + path("registration//success", views.register_success, name="register_success"), + path("registration//pay", views.initiate_payment, name="initiate_payment"), + path("registration//retry", views.retry_payment, name="retry_payment"), + path("payment/callback", views.payment_callback, name="payment_callback"), ] diff --git a/src/haps/views.py b/src/haps/views.py index 561950e..1ddb8bf 100644 --- a/src/haps/views.py +++ b/src/haps/views.py @@ -5,7 +5,11 @@ from .models import Event, EventRegistration import logging import json - +from utils.payment.upi_gateway import create_order, check_order_status +from django.urls import reverse +from uuid import uuid4 +from datetime import date +from utils.logging import critical_logger log = logging.getLogger(__name__) @@ -25,46 +29,252 @@ def haps_item(request: HttpRequest, slug: str): @login_required(login_url="/users/register") def register_for_event(request: HttpRequest, slug: str): - event = Event.objects.get(slug=slug) - + """Register for an event""" + try: + event = Event.objects.get(slug=slug) + except Event.DoesNotExist: + raise Http404("Event not found") + if not event.accept_reg: messages.error(request, "Registration is closed for this event.") - return redirect('haps:item', slug=slug) + return redirect('haps:event_item', slug=slug) if event.login_required and not request.user.is_authenticated: messages.error(request, "Please login to register for this event.") - return redirect('users:register') + return redirect('haps:event_item', slug=slug) if request.method == 'POST': # Collect form responses form_responses = {} - + # If not login required, collect basic info if not event.login_required: form_responses['name'] = request.POST.get('name') form_responses['whatsapp_number'] = request.POST.get('whatsapp_number') - + # Collect custom field responses for field in event.form_fields.all(): field_id = f'field_{field.id}' form_responses[field.field_label] = request.POST.get(field_id) - + # Create registration - registration = EventRegistration.objects.create( + registration = EventRegistration( event=event, - user=request.user if event.login_required else None, + user=request.user if request.user.is_authenticated else None, form_responses=form_responses, - amount=event.registration_fee + amount=event.registration_fee if event.registration_fee else None ) - + registration.save() + # If payment required, redirect to payment page if event.registration_fee: - # TODO: Implement payment flow using donations module - return redirect('donations:payment', registration_id=registration.id) - + return redirect('haps:initiate_payment', registration_id=registration.id) + messages.success(request, "Successfully registered for the event!") - return render(request, "haps/register_success.html", - context={"event": event, "registration": registration}) - + return render(request, "haps/register_success.html", + context={"registration": registration, "event": event}) + # If GET request with registration modal, return to event page - return redirect('haps:item', slug=slug) + return redirect('haps:event_item', slug=slug) + + +def register_failure(request: HttpRequest, registration_id: int): + """View for displaying registration failure page. + This is typically shown when payment fails.""" + + try: + registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + # For testing, you can pass a remark through URL query parameter + remark = request.GET.get('remark', 'Payment was not completed') + + context = { + "registration": registration, + "event": registration.event, + "remark": remark + } + return render(request, "haps/register_failure.html", context=context) + + +def initiate_payment(request: HttpRequest, registration_id: int): + """Initiate payment for event registration""" + try: + registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + # Validate amount + if not registration.amount: + log.error(f"Registration {registration.id} has no amount set") + messages.error(request, "Invalid registration amount") + return redirect('haps:register_failure', registration_id=registration.id) + + if registration.payment_status == 'success': + messages.info(request, "Payment already completed") + return redirect('haps:register_success', registration_id=registration.id) + + # Generate unique transaction ID if not exists + if not registration.client_txn_id: + registration.client_txn_id = f"TempleWebPay-event-{registration.id}-{uuid4().hex[:8]}" + registration.save(update_fields=['client_txn_id']) + + # Create callback URL + callback_url = request.build_absolute_uri( + reverse('haps:payment_callback') + ) + + # Get user details + name = None + if registration.user: + name = registration.user.get_full_name() + if not name: + name = registration.user.username + else: + name = registration.form_responses.get('name') + + if not name: + log.error(f"Registration {registration.id} has no customer name") + + # return redirect('haps:register_failure', registration_id=registration.id) + name = "Anonymous User" + + mobile = registration.form_responses.get('whatsapp_number', '') + email = registration.user.email if registration.user else '' + + try: + # Create payment order + status, api_resp = create_order( + client_txn_id=registration.client_txn_id, + redirect_url=callback_url, + amount=registration.amount, + product_info=f"Registration for {registration.event.name}", + customer_name=name, + # customer_email=email, + # customer_mobile=mobile + ) + + if not status: + log.error(f"Payment gateway error for registration {registration.id}: {api_resp.text}") + messages.error(request, "Payment gateway error. Please try again later.") + return redirect('haps:register_failure', + registration_id=registration.id, + remark=f"Payment gateway error: {api_resp.text}") + + # Save order ID and redirect to payment URL + registration.order_id = api_resp["order_id"] + registration.save(update_fields=['order_id']) + + return redirect(api_resp["payment_url"]) + + except Exception as e: + log.error(f"Payment initiation failed for registration {registration.id}: {str(e)}") + messages.error(request, "Failed to initiate payment. Please try again.") + return redirect('haps:register_failure', registration_id=registration.id) + + +def payment_callback(request: HttpRequest): + """Handle payment gateway callback""" + log.debug("Payment callback called") + client_txn_id = request.GET.get('client_txn_id') + if not client_txn_id: + messages.error(request, "Invalid payment callback") + return redirect('haps:events') + + try: + registration = EventRegistration.objects.get(client_txn_id=client_txn_id) + except EventRegistration.DoesNotExist: + messages.error(request, "Registration not found") + return redirect('haps:events') + + # Prevent duplicate processing + if registration.payment_status == 'success': + return redirect('haps:register_success', registration_id=registration.id) + + # Check payment status + try: + status_data = check_order_status(client_txn_id) + if not status_data: + raise ValueError("Empty response from payment gateway") + + # Store all payment diagnostic data + registration.payment_data = { + 'customer_vpa': status_data.get('customer_vpa'), + 'upi_txn_id': status_data.get('upi_txn_id'), + 'status': status_data.get('status'), + 'remark': status_data.get('remark'), + 'txnAt': status_data.get('txnAt'), + 'merchant': status_data.get('Merchant', {}), + 'udf1': status_data.get('udf1'), + 'udf2': status_data.get('udf2'), + 'udf3': status_data.get('udf3'), + 'redirect_url': status_data.get('redirect_url'), + 'createdAt': status_data.get('createdAt') + } + + # Update payment status + if status_data['status'] == 'success': + registration.payment_status = 'success' + registration.payment_date_time = status_data.get('txnAt') + registration.save(update_fields=['payment_status', 'payment_date_time', 'payment_data']) + messages.success(request, "Payment successful!") + return redirect('haps:register_success', registration_id=registration.id) + else: + registration.payment_status = 'failure' + registration.save(update_fields=['payment_status', 'payment_data']) + return redirect('haps:register_failure', + registration_id=registration.id, + remark=status_data.get('remark', 'Payment failed')) + + except Exception as e: + log.error(f"Payment verification failed for registration {registration.id}: {str(e)}") + registration.payment_data = {'error': str(e)} + registration.payment_status = 'failure' + registration.save(update_fields=['payment_status', 'payment_data']) + return redirect('haps:register_failure', + registration_id=registration.id, + remark="Payment verification failed") + + +def retry_payment(request: HttpRequest, registration_id: int): + """Retry failed payment""" + try: + registration = EventRegistration.objects.get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + if registration.payment_status == 'success': + messages.info(request, "Payment already completed") + return redirect('haps:register_success', registration_id=registration.id) + + # Log critical payment data before reset + critical_logger.critical( + f"Payment retry initiated for registration {registration.id}. " + f"Previous payment data: order_id={registration.order_id}, " + f"client_txn_id={registration.client_txn_id}, " + f"payment_status={registration.payment_status}, " + f"payment_data={registration.payment_data}" + ) + + # Reset payment fields for retry + registration.order_id = None + registration.client_txn_id = None + registration.payment_status = 'pending' + registration.save(update_fields=['order_id', 'client_txn_id', 'payment_status']) + + return redirect('haps:initiate_payment', registration_id=registration.id) + + +def register_success(request: HttpRequest, registration_id: int): + """View for displaying registration success page.""" + try: + registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + context = { + "registration": registration, + "event": registration.event, + } + return render(request, "haps/register_success.html", context=context) From dc53eb9da086ebd2f93a103fe95e8fe84b2f4f03 Mon Sep 17 00:00:00 2001 From: aahnik Date: Mon, 30 Dec 2024 02:15:03 +0530 Subject: [PATCH 13/14] Make event registrations admin readonly --- src/haps/admin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/haps/admin.py b/src/haps/admin.py index 8c96405..55c471b 100644 --- a/src/haps/admin.py +++ b/src/haps/admin.py @@ -105,6 +105,12 @@ class EventRegistrationAdmin(admin.ModelAdmin): }) ] + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + def has_add_permission(self, request): return False From aa136db8826a96f4968d0f5240d666263ab6d519 Mon Sep 17 00:00:00 2001 From: aahnik Date: Mon, 30 Dec 2024 02:25:06 +0530 Subject: [PATCH 14/14] Update styles.css --- src/static/styles.css | 268 +++++++++++++----------------------------- 1 file changed, 79 insertions(+), 189 deletions(-) diff --git a/src/static/styles.css b/src/static/styles.css index 8bfc291..2e4896c 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -1714,133 +1714,6 @@ input:checked + .toggle-bg { margin-bottom: 0; } -.format-lg { - font-size: 1.125rem; - line-height: 1.7777778; -} - -.format-lg :where(p):not(:where([class~="not-format"] *)) { - margin-top: 1.3333333em; - margin-bottom: 1.3333333em; -} - -.format-lg :where([class~="lead"]):not(:where([class~="not-format"] *)) { - font-size: 1.2222222em; - line-height: 1.4545455; - margin-top: 1.0909091em; - margin-bottom: 1.0909091em; -} - -.format-lg :where(blockquote):not(:where([class~="not-format"] *))::before { - margin-top: 1.6666667em; -} - -.format-lg :where(blockquote > p:first-child):not(:where([class~="not-format"] *)) { - margin-top: 0.5em; -} - -.format-lg :where(h1):not(:where([class~="not-format"] *)) { - font-size: 2.6666667em; - margin-top: 0; - margin-bottom: 0.8333333em; - line-height: 1; -} - -.format-lg :where(h2):not(:where([class~="not-format"] *)) { - font-size: 2em; - margin-top: 0; - margin-bottom: 0.6666667em; - line-height: 1.3333333; -} - -.format-lg :where(h3):not(:where([class~="not-format"] *)) { - font-size: 1.3333333em; - margin-top: 0; - margin-bottom: 0.6666667em; - line-height: 1.5; -} - -.format-lg :where(h4):not(:where([class~="not-format"] *)) { - margin-top: 0; - margin-bottom: 0.4444444em; - line-height: 1.5555556; -} - -.format-lg :where(img):not(:where([class~="not-format"] *)) { - margin-top: 1.7777778em; - margin-bottom: 1.7777778em; -} - -.format-lg :where(video):not(:where([class~="not-format"] *)) { - margin-top: 1.7777778em; - margin-bottom: 1.7777778em; -} - -.format-lg :where(figure):not(:where([class~="not-format"] *)) { - margin-top: 1.7777778em; - margin-bottom: 1.7777778em; -} - -.format-lg :where(figure > *):not(:where([class~="not-format"] *)) { - margin-top: 0; - margin-bottom: 0; -} - -.format-lg :where(figcaption):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; - line-height: 1.5; - margin-top: 1em; -} - -.format-lg :where(code):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; -} - -.format-lg :where(h2 code):not(:where([class~="not-format"] *)) { - font-size: 0.8666667em; -} - -.format-lg :where(h3 code):not(:where([class~="not-format"] *)) { - font-size: 0.875em; -} - -.format-lg :where(pre):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; - line-height: 1.75; - margin-top: 2em; - margin-bottom: 2em; - border-radius: 0.375rem; - padding-top: 1em; - padding-right: 1.5em; - padding-bottom: 1em; - padding-left: 1.5em; -} - -.format-lg :where(ol):not(:where([class~="not-format"] *)) { - margin-top: 1.3333333em; - margin-bottom: 1.3333333em; - padding-left: 1.5555556em; -} - -.format-lg :where(ul):not(:where([class~="not-format"] *)) { - margin-top: 1.3333333em; - margin-bottom: 1.3333333em; - padding-left: 1.5555556em; -} - -.format-lg :where(li):not(:where([class~="not-format"] *)) { - margin-top: 0.6666667em; - margin-bottom: 0.6666667em; -} - -.format-lg :where(ol > li):not(:where([class~="not-format"] *)) { - padding-left: 0.4444444em; -} - -.format-lg :where(ul > li):not(:where([class~="not-format"] *)) { - padding-left: 0.4444444em; -} - .format-lg :where(.format > ul > li p):not(:where([class~="not-format"] *)) { margin-top: 0.8888889em; margin-bottom: 0.8888889em; @@ -1862,58 +1735,6 @@ input:checked + .toggle-bg { margin-bottom: 1.3333333em; } -.format-lg :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-format"] *)) { - margin-top: 0.8888889em; - margin-bottom: 0.8888889em; -} - -.format-lg :where(hr):not(:where([class~="not-format"] *)) { - margin-top: 3.1111111em; - margin-bottom: 3.1111111em; -} - -.format-lg :where(hr + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(h2 + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(h3 + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(h4 + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(table):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; - line-height: 1.5; -} - -.format-lg :where(thead th):not(:where([class~="not-format"] *)) { - padding-right: 0.75em; - padding-bottom: 0.75em; - padding-left: 0.75em; -} - -.format-lg :where(thead th:last-child):not(:where([class~="not-format"] *)) { - padding-right: 0; -} - -.format-lg :where(tbody td, tfoot td):not(:where([class~="not-format"] *)) { - padding-top: 0.75em; - padding-right: 0.75em; - padding-bottom: 0.75em; - padding-left: 0.75em; -} - -.format-lg :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-format"] *)) { - padding-right: 0; -} - .format-lg :where(.format > :first-child):not(:where([class~="not-format"] *)) { margin-top: 0; } @@ -2036,6 +1857,10 @@ input:checked + .toggle-bg { grid-column: span 12 / span 12; } +.col-span-2 { + grid-column: span 2 / span 2; +} + .m-4 { margin: 1rem; } @@ -2168,6 +1993,11 @@ input:checked + .toggle-bg { margin-inline-start: 0.5rem; } +.ms-auto { + -webkit-margin-start: auto; + margin-inline-start: auto; +} + .mt-1 { margin-top: 0.25rem; } @@ -2268,6 +2098,10 @@ input:checked + .toggle-bg { height: 1.5rem; } +.h-64 { + height: 16rem; +} + .h-7 { height: 1.75rem; } @@ -2360,6 +2194,10 @@ input:checked + .toggle-bg { width: 2rem; } +.w-96 { + width: 24rem; +} + .w-\[52rem\] { width: 52rem; } @@ -2695,6 +2533,11 @@ input:checked + .toggle-bg { border-bottom-right-radius: 0.5rem; } +.rounded-t { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + .rounded-t-lg { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; @@ -2827,6 +2670,10 @@ input:checked + .toggle-bg { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } +.bg-gray-900\/50 { + background-color: rgb(17 24 39 / 0.5); +} + .bg-green-100 { --tw-bg-opacity: 1; background-color: rgb(222 247 236 / var(--tw-bg-opacity)); @@ -2867,6 +2714,10 @@ input:checked + .toggle-bg { background-color: rgb(253 242 242 / var(--tw-bg-opacity)); } +.bg-transparent { + background-color: transparent; +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -3103,6 +2954,11 @@ input:checked + .toggle-bg { line-height: 1; } +.text-6xl { + font-size: 3.75rem; + line-height: 1; +} + .text-7xl { font-size: 4.5rem; line-height: 1; @@ -3269,6 +3125,11 @@ input:checked + .toggle-bg { color: rgb(240 82 82 / var(--tw-text-opacity)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(224 36 36 / var(--tw-text-opacity)); +} + .text-red-800 { --tw-text-opacity: 1; color: rgb(155 28 28 / var(--tw-text-opacity)); @@ -3358,6 +3219,12 @@ input:checked + .toggle-bg { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.backdrop-blur-lg { + --tw-backdrop-blur: blur(16px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + .transition { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; @@ -3876,14 +3743,14 @@ input:checked + .toggle-bg { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } -.hover\:bg-primary-100:hover { +.hover\:bg-gray-200:hover { --tw-bg-opacity: 1; - background-color: rgb(254 243 199 / var(--tw-bg-opacity)); + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } -.hover\:bg-primary-600:hover { +.hover\:bg-primary-100:hover { --tw-bg-opacity: 1; - background-color: rgb(217 119 6 / var(--tw-bg-opacity)); + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); } .hover\:bg-primary-700:hover { @@ -3934,6 +3801,15 @@ input:checked + .toggle-bg { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.focus\:z-10:focus { + z-index: 10; +} + +.focus\:border-blue-500:focus { + --tw-border-opacity: 1; + border-color: rgb(63 131 248 / var(--tw-border-opacity)); +} + .focus\:border-primary-500:focus { --tw-border-opacity: 1; border-color: rgb(245 158 11 / var(--tw-border-opacity)); @@ -3966,9 +3842,9 @@ input:checked + .toggle-bg { --tw-ring-color: rgb(164 202 254 / var(--tw-ring-opacity)); } -.focus\:ring-gray-100:focus { +.focus\:ring-blue-500:focus { --tw-ring-opacity: 1; - --tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity)); + --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); } .focus\:ring-gray-200:focus { @@ -4154,6 +4030,11 @@ input:checked + .toggle-bg { color: rgb(107 114 128 / var(--tw-text-opacity)); } +:is(.dark .dark\:text-gray-600) { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + :is(.dark .dark\:text-gray-800) { --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity)); @@ -4253,19 +4134,24 @@ input:checked + .toggle-bg { color: rgb(255 255 255 / var(--tw-text-opacity)); } +:is(.dark .dark\:focus\:border-blue-500:focus) { + --tw-border-opacity: 1; + border-color: rgb(63 131 248 / var(--tw-border-opacity)); +} + :is(.dark .dark\:focus\:border-primary-500:focus) { --tw-border-opacity: 1; border-color: rgb(245 158 11 / var(--tw-border-opacity)); } -:is(.dark .dark\:focus\:ring-gray-600:focus) { +:is(.dark .dark\:focus\:ring-blue-500:focus) { --tw-ring-opacity: 1; - --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); + --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); } -:is(.dark .dark\:focus\:ring-gray-800:focus) { +:is(.dark .dark\:focus\:ring-gray-600:focus) { --tw-ring-opacity: 1; - --tw-ring-color: rgb(31 41 55 / var(--tw-ring-opacity)); + --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); } :is(.dark .dark\:focus\:ring-primary-500:focus) { @@ -4630,6 +4516,10 @@ input:checked + .toggle-bg { padding: 3rem; } + .md\:p-5 { + padding: 1.25rem; + } + .md\:px-12 { padding-left: 3rem; padding-right: 3rem;