diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 423a636..bca31c3 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..3e2db19 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..a45d569 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09..8324ded 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 74b1112..5960c98 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..a8524cd 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..420ad5b 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..b2a1276 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/mail.cpython-311.pyc b/core/__pycache__/mail.cpython-311.pyc new file mode 100644 index 0000000..9a91762 Binary files /dev/null and b/core/__pycache__/mail.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..eb53d86 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..4cd27f3 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..ac564ec 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..890395b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,20 @@ from django.contrib import admin +from .models import Property, Guest, Stay, Campaign -# Register your models here. +@admin.register(Property) +class PropertyAdmin(admin.ModelAdmin): + list_display = ('name', 'address', 'created_at') + +@admin.register(Guest) +class GuestAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name', 'email', 'phone', 'created_at') + search_fields = ('first_name', 'last_name', 'email') + +@admin.register(Stay) +class StayAdmin(admin.ModelAdmin): + list_display = ('guest', 'property', 'check_in', 'check_out', 'total_nights') + list_filter = ('property', 'check_in') + +@admin.register(Campaign) +class CampaignAdmin(admin.ModelAdmin): + list_display = ('title', 'subject', 'status', 'sent_at') \ No newline at end of file diff --git a/core/mail.py b/core/mail.py new file mode 100644 index 0000000..bb09385 --- /dev/null +++ b/core/mail.py @@ -0,0 +1,50 @@ +import logging +from django.core.mail import send_mail, EmailMultiAlternatives +from django.conf import settings +from django.utils.html import strip_tags + +logger = logging.getLogger(__name__) + +def send_campaign_email(campaign, recipients): + """ + Sends a campaign email to a list of recipients. + """ + subject = campaign.subject + html_content = campaign.body + text_content = strip_tags(html_content) + from_email = settings.DEFAULT_FROM_EMAIL + + success_count = 0 + fail_count = 0 + + for recipient in recipients: + try: + msg = EmailMultiAlternatives(subject, text_content, from_email, [recipient.email]) + msg.attach_alternative(html_content, "text/html") + msg.send() + success_count += 1 + except Exception as e: + logger.error(f"Failed to send email to {recipient.email}: {e}") + fail_count += 1 + + return success_count, fail_count + +def send_contact_message(name, email, message): + """ + Simple wrapper for contact form emails. + """ + subject = f"New Contact Message from {name}" + body = f"From: {name} <{email}>\n\nMessage:\n{message}" + + try: + send_mail( + subject, + body, + settings.DEFAULT_FROM_EMAIL, + settings.CONTACT_EMAIL_TO, + fail_silently=False, + ) + return True + except Exception as e: + logger.error(f"Failed to send contact message: {e}") + return False diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..fcc5694 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a842f49 Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/seed_data.cpython-311.pyc b/core/management/commands/__pycache__/seed_data.cpython-311.pyc new file mode 100644 index 0000000..597bfb9 Binary files /dev/null and b/core/management/commands/__pycache__/seed_data.cpython-311.pyc differ diff --git a/core/management/commands/seed_data.py b/core/management/commands/seed_data.py new file mode 100644 index 0000000..c2ba18a --- /dev/null +++ b/core/management/commands/seed_data.py @@ -0,0 +1,47 @@ +from django.core.management.base import BaseCommand +from core.models import Property, Guest, Stay, Campaign +from django.utils import timezone +import datetime + +class Command(BaseCommand): + help = 'Seeds the database with sample data' + + def handle(self, *args, **kwargs): + # Create Properties + p1, _ = Property.objects.get_or_create(name="Ocean View Apartment", address="123 Beach Blvd, Miami") + p2, _ = Property.objects.get_or_create(name="Mountain Cabin", address="456 Pine Rd, Aspen") + p3, _ = Property.objects.get_or_create(name="City Loft", address="789 Main St, New York") + + # Create Guests + g1, _ = Guest.objects.get_or_create(first_name="John", last_name="Doe", email="john@example.com", phone="123-456-7890") + g2, _ = Guest.objects.get_or_create(first_name="Jane", last_name="Smith", email="jane@example.com", phone="987-654-3210") + g3, _ = Guest.objects.get_or_create(first_name="Alice", last_name="Johnson", email="alice@example.com") + + # Create Stays + Stay.objects.get_or_create(guest=g1, property=p1, check_in=datetime.date(2025, 1, 10), check_out=datetime.date(2025, 1, 15)) + Stay.objects.get_or_create(guest=g2, property=p2, check_in=datetime.date(2025, 2, 1), check_out=datetime.date(2025, 2, 10)) + Stay.objects.get_or_create(guest=g3, property=p3, check_in=datetime.date(2025, 2, 5), check_out=datetime.date(2025, 2, 12)) + Stay.objects.get_or_create(guest=g1, property=p3, check_in=datetime.date(2026, 1, 5), check_out=datetime.date(2026, 1, 10)) + + # Create Campaigns + Campaign.objects.get_or_create( + title="Spring Discount Offer", + subject="Special 20% Off Your Next Stay!", + body="

Spring is here!

We'd love to have you back. Use code SPRING20 for 20% off your next booking at any of our properties.

", + status='draft' + ) + Campaign.objects.get_or_create( + title="Refer a Friend Program", + subject="Share the Love, Get a Reward", + body="

Refer a friend to our apartments and get $50 credit for your next stay!

", + status='draft' + ) + Campaign.objects.get_or_create( + title="Welcome Back", + subject="We missed you!", + body="

It's been a while since your last stay. Come visit us again soon!

", + status='sent', + sent_at=timezone.now() - datetime.timedelta(days=30) + ) + + self.stdout.write(self.style.SUCCESS('Successfully seeded sample data including campaigns')) \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..8aefb9c --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.7 on 2026-02-10 18:34 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Campaign', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('subject', models.CharField(max_length=255)), + ('body', models.TextField()), + ('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent')], default='draft', max_length=10)), + ('sent_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Guest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=100)), + ('last_name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254, unique=True)), + ('phone', models.CharField(blank=True, max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Property', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('address', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Stay', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('check_in', models.DateField()), + ('check_out', models.DateField()), + ('total_nights', models.IntegerField(default=0, editable=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('guest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stays', to='core.guest')), + ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stays', to='core.property')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..bc554bc Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 9c833c8..39b5ac2 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..70a53b9 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,52 @@ from django.db import models +from django.conf import settings -# Create your models here. +class Property(models.Model): + name = models.CharField(max_length=255) + address = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + +class Guest(models.Model): + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + email = models.EmailField(unique=True) + phone = models.CharField(max_length=20, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + +class Stay(models.Model): + guest = models.ForeignKey(Guest, on_delete=models.CASCADE, related_name='stays') + property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name='stays') + check_in = models.DateField() + check_out = models.DateField() + total_nights = models.IntegerField(editable=False, default=0) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if self.check_in and self.check_out: + delta = self.check_out - self.check_in + self.total_nights = delta.days + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.guest} at {self.property} ({self.check_in} to {self.check_out})" + +class Campaign(models.Model): + STATUS_CHOICES = [ + ('draft', 'Draft'), + ('sent', 'Sent'), + ] + title = models.CharField(max_length=255) + subject = models.CharField(max_length=255) + body = models.TextField() + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft') + sent_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.title \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..bafa7ea 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,151 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}{{ project_name }}{% endblock %} + + + + + + + + + + + + + + + + + + {% block extra_css %}{% endblock %} - - {% block content %}{% endblock %} - + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} - + + +
+
+ {% block content %}{% endblock %} +
+
+ + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/campaign_list.html b/core/templates/core/campaign_list.html new file mode 100644 index 0000000..a367e39 --- /dev/null +++ b/core/templates/core/campaign_list.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Email Campaigns

+

Re-engage your guests with personalized offers and updates.

+
+
+ + Create Campaign +
+
+ +{% if messages %} +
+
+ {% for message in messages %} + + {% endfor %} +
+
+{% endif %} + +
+ {% for campaign in campaigns %} +
+
+
+
+
{{ campaign.title }}
+ Subject: {{ campaign.subject }} +
+ {% if campaign.status == 'sent' %} + Sent + {% else %} + Draft + {% endif %} +
+

{{ campaign.body|striptags }}

+
+ + {% if campaign.status == 'sent' %} + Sent on {{ campaign.sent_at|date:"M d, Y" }} + {% else %} + Created {{ campaign.created_at|date:"M d, Y" }} + {% endif %} + +
+ Edit + {% if campaign.status == 'draft' %} + Send Now + {% endif %} +
+
+
+
+ {% empty %} +
+
+

No campaigns yet. Launch your first loyalty offer today!

+
+ + Create First Campaign +
+
+
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/guest_list.html b/core/templates/core/guest_list.html new file mode 100644 index 0000000..9e78292 --- /dev/null +++ b/core/templates/core/guest_list.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Guests

+

Manage your guest relationships and view their stay history.

+
+
+ + Import CSV + + + New Guest + +
+
+ +
+
+ + + + + + + + + + + + {% for guest in guests %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Guest DetailsEmailStaysJoinedActions
+
{{ guest }}
+ {{ guest.phone|default:"No phone" }} +
{{ guest.email }} + + {{ guest.stay_count }} stay{{ guest.stay_count|pluralize }} + + {{ guest.created_at|date:"M d, Y" }} + +
+
No guests found.
+ Import your first guests +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/import_guests.html b/core/templates/core/import_guests.html new file mode 100644 index 0000000..2b0d415 --- /dev/null +++ b/core/templates/core/import_guests.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Import Guests

+

Upload a CSV file to bulk add or update your guest list.

+
+
+ Back to List +
+
+ +
+
+
+
+ {% csrf_token %} +
+ + +
Maximum file size: 5MB
+
+ + +
+
+
+ +
+
+
CSV Format Requirements
+

Your CSV file should include a header row with the following column names:

+
    +
  • + first_name (Required) +
  • +
  • + last_name (Required) +
  • +
  • + email (Required, used for matching) +
  • +
  • + phone (Optional) +
  • +
+ +
+

Example Content:

+ first_name,last_name,email,phone + Jane,Doe,jane@example.com,123-456-7890 + John,Smith,john@example.com, +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..d41755c 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,120 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} +{% block content %} +
+
+

Welcome back, Host

+

Here's what's happening with your properties and loyalty program.

+
+
+ + Add New Guest +
+
+ + +
+
+
+
Total Guests
+

{{ total_guests }}

+
+
+
+
+
Properties
+

{{ total_properties }}

+
+
+
+
+
Total Stays
+

{{ total_stays }}

+
+
+
+ +
+ +
+
+
+

Recent Stays

+ View All +
+
+ + + + + + + + + + + {% for stay in recent_stays %} + + + + + + + {% empty %} + + + + {% endfor %} + +
GuestPropertyCheck-inNights
+
{{ stay.guest }}
+ {{ stay.guest.email }} +
{{ stay.property.name }}{{ stay.check_in|date:"M d, Y" }}{{ stay.total_nights }}
No stays recorded yet.
+
+
+
+ + +
+
+

Newest Guests

+
    + {% for guest in recent_guests %} +
  • +
    {{ guest.first_name|first }}{{ guest.last_name|first }}
    +
    +
    {{ guest }}
    + Joined {{ guest.created_at|date:"M d" }} +
    +
  • + {% empty %} +
  • No guests yet.
  • + {% endfor %} +
+
+ +
+
Launch a Campaign
+

Ready to reach out? Send a special offer to your past guests to boost repeat bookings.

+ Create Campaign +
+
+
-{% block head %} - - - {% endblock %} - -{% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… -
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..bd5c3f5 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.home, name='home'), + path('guests/', views.guest_list, name='guest_list'), + path('guests/import/', views.import_guests, name='import_guests'), + path('campaigns/', views.campaign_list, name='campaign_list'), + path('campaigns/send//', views.send_campaign, name='send_campaign'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..be1123a 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,108 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render +import csv +import io +from django.shortcuts import render, redirect, get_object_or_404 +from django.db.models import Count, Sum from django.utils import timezone - +from django.contrib import messages +from .models import Property, Guest, Stay, Campaign +from .mail import send_campaign_email def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() - + """Host Dashboard""" + total_guests = Guest.objects.count() + total_properties = Property.objects.count() + total_stays = Stay.objects.count() + + recent_stays = Stay.objects.select_related('guest', 'property').order_by('-check_in')[:5] + recent_guests = Guest.objects.order_by('-created_at')[:5] + context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'total_guests': total_guests, + 'total_properties': total_properties, + 'total_stays': total_stays, + 'recent_stays': recent_stays, + 'recent_guests': recent_guests, + 'project_name': 'Host Loyalty CRM' } return render(request, "core/index.html", context) + +def guest_list(request): + """List of all guests""" + guests = Guest.objects.annotate(stay_count=Count('stays')).order_by('-created_at') + return render(request, "core/guest_list.html", {'guests': guests}) + +def import_guests(request): + """Import guests from a CSV file""" + if request.method == 'POST' and request.FILES.get('csv_file'): + csv_file = request.FILES['csv_file'] + + if not csv_file.name.endswith('.csv'): + messages.error(request, 'Please upload a CSV file.') + return redirect('import_guests') + + try: + decoded_file = csv_file.read().decode('utf-8') + io_string = io.StringIO(decoded_file) + reader = csv.DictReader(io_string) + + # Simple column mapping/validation + required_cols = {'first_name', 'last_name', 'email'} + if not required_cols.issubset(set(reader.fieldnames or [])): + messages.error(request, f'CSV must contain columns: {", ".join(required_cols)}') + return redirect('import_guests') + + created_count = 0 + updated_count = 0 + for row in reader: + guest, created = Guest.objects.update_or_create( + email=row['email'].strip().lower(), + defaults={ + 'first_name': row['first_name'].strip(), + 'last_name': row['last_name'].strip(), + 'phone': row.get('phone', '').strip(), + } + ) + if created: + created_count += 1 + else: + updated_count += 1 + + messages.success(request, f'Successfully imported {created_count} new guests and updated {updated_count}.') + return redirect('guest_list') + + except Exception as e: + messages.error(request, f'Error processing file: {str(e)}') + return redirect('import_guests') + + return render(request, "core/import_guests.html") + +def campaign_list(request): + """List of email campaigns""" + campaigns = Campaign.objects.all().order_by('-created_at') + return render(request, "core/campaign_list.html", {'campaigns': campaigns}) + +def send_campaign(request, pk): + """Send a campaign to all guests""" + campaign = get_object_or_404(Campaign, pk=pk) + + if campaign.status == 'sent': + messages.warning(request, "This campaign has already been sent.") + return redirect('campaign_list') + + guests = Guest.objects.all() + if not guests: + messages.error(request, "No guests found to send the campaign to.") + return redirect('campaign_list') + + success_count, fail_count = send_campaign_email(campaign, guests) + + campaign.status = 'sent' + campaign.sent_at = timezone.now() + campaign.save() + + if success_count > 0: + messages.success(request, f"Campaign sent successfully to {success_count} guests.") + if fail_count > 0: + messages.error(request, f"Failed to send to {fail_count} guests.") + + return redirect('campaign_list') \ No newline at end of file