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="
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 %} - - -