From 4c6a5b3cafdf7dd4aeb65414164d3d92916d1268 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 10 Feb 2026 19:21:45 +0000 Subject: [PATCH] Email campaign and CSV import --- config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5552 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1557 -> 1557 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 1823 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes .../context_processors.cpython-311.pyc | Bin 763 -> 763 bytes core/__pycache__/mail.cpython-311.pyc | Bin 0 -> 2367 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 4117 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 742 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 6584 bytes core/admin.py | 19 +- core/mail.py | 50 ++++ core/management/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 168 bytes core/management/commands/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 177 bytes .../__pycache__/seed_data.cpython-311.pyc | Bin 0 -> 4228 bytes core/management/commands/seed_data.py | 47 ++++ core/migrations/0001_initial.py | 59 +++++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 3227 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 51 +++- core/templates/base.html | 164 ++++++++++-- core/templates/core/campaign_list.html | 71 +++++ core/templates/core/guest_list.html | 86 ++++++ core/templates/core/import_guests.html | 71 +++++ core/templates/core/index.html | 249 ++++++++---------- core/urls.py | 11 +- core/views.py | 121 +++++++-- 31 files changed, 818 insertions(+), 181 deletions(-) create mode 100644 core/__pycache__/mail.cpython-311.pyc create mode 100644 core/mail.py create mode 100644 core/management/__init__.py create mode 100644 core/management/__pycache__/__init__.cpython-311.pyc create mode 100644 core/management/commands/__init__.py create mode 100644 core/management/commands/__pycache__/__init__.cpython-311.pyc create mode 100644 core/management/commands/__pycache__/seed_data.cpython-311.pyc create mode 100644 core/management/commands/seed_data.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/templates/core/campaign_list.html create mode 100644 core/templates/core/guest_list.html create mode 100644 core/templates/core/import_guests.html diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 423a6362b2322713e75da67a35e209e76169dbae..bca31c35b9d657c6cbb25a4487370f4013eb3acb 100644 GIT binary patch delta 19 ZcmbQwIG>SwIWI340}yakbx-7;0stv81Xch5 delta 19 ZcmbQwIG>SwIWI340}xbw%b&y+NCMIWI340}yakb#LTeDhdELtpySQ delta 20 acmdm>y+NCMIWI340}xbw%iqYoR1^R_83p11 diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94ece283a83ff1af1d71f1b265c943eb37a..a45d5691f565e8ad3dd67d1605dd09049dcf19b8 100644 GIT binary patch delta 20 acmbQrGnI#XIWI340}yakb#LV6Vgmpz3Ip^2 delta 20 acmbQrGnI#XIWI340}xbw%iqY&#RdQ}b_B!# diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09df194d2dbcad4868349c9177db4b15571..8324ded739eb1ef4b6c4f36f6bd95556388493d7 100644 GIT binary patch delta 20 acmZ3^x}24JIWI340}yakb#LUJ!vp{?+60&Y delta 20 acmZ3^x}24JIWI340}xbw%iqX7hY0{RMg?d9 diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 74b111269bd81aac528770a53e2f849291524d56..5960c981e711b891f26f5db2a9383276b4a325ae 100644 GIT binary patch delta 19 ZcmbQsIG2%oIWI340}yakbx-7;1OO=L1W*6~ delta 19 ZcmbQsIG2%oIWI340}xbw%b&>mG1t0(b diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392d6714413db63120e4233d2e96cbadb5de..a8524cd2f77c60f5d29f236602ca8e6fb2f8409b 100644 GIT binary patch literal 1823 zcmbVMzi$&s6rR~%Svxif5C=kmf`pJv1TNsDJBgfhLI@-v0?{qSYO*u2H|!5HJFB}~ z<${hT9sfZP;_sj*l8b06Bq};bp`zlw-K=FCBO+#YKfjrI`_22_yz$#+vreEr`2ThI z!64)_4yxuH8%L57@`iB2X+&b`QA)VM&B%yN&x|e4ifzx19nYb}AcAhsRRqG^gD>C1 zTQ-B-C)|2PxD8%zHM?hm54>gDgP-@+fOxejMr#J61B`R0W~@&!+A|nkV62^*aej)? znZZ~G#`#k-HhAM1X*a)M9@_CC!WEnXL1AfFmgpl>%iy)+h`d^1l}-Vy(a zAXlmE{F}-bS)T=>6Qojf3Z*yuI}*#X01J%lJiuVwKb^llc!XUpRbI12FT?V*XwTv% z2)A^-$X==1*7dDWLePcwTEr^T7BLGWZTEX=B4iWz+NQjSyRINy0@!HVasda62xk#6 z*}9ntCWD^86^e*w5+#)Jw$Qpz0p5cQ87K8^3?3aU{W*Ayz5EFMe?w#qYopR=cZ+&) z`&9wCK~DrP{4mkA(n@os8!A;S@{_RJQ(4=PtFW`Y0`N1T^Tly*g^?0+TIOZ^7y(mv zLgrJDdG7k)A@))xy3UsQ^8&64zm#cdTZJlu3eNJ)Z4oH#WQwUgg9(u+EMs{FqnbL9 zYdD918=WY)JCg;6UK{PEKEt?F?tmdm!)3RLy@f_kROBl=H4l<+TXGRJb0#_fsWkS(9Dw>9| PqIFcXKB;KJwJ68$UBZ$% literal 212 zcmZ3^%ge<81k-=yXW9el#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@K?*b( zZ?Pt(n;80F)6nZ2$lO diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4873bc56e3763e4ed09960c6e3f34140e1..420ad5b16a7521202586c0c621b0f22f39ac4d49 100644 GIT binary patch delta 20 ZcmeBS>0#ks&dbZi00dlB-5a?XnE)z&1H}LU delta 20 acmeBS>0#ks&dbZi00dRv@;7obG64WD>I9Vl diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf2234fb21a6b62efc5cec11af9512dd0c9616..b2a1276ad7b4cc59b50bf7fc988982dc77b44f86 100644 GIT binary patch delta 20 acmey(`kR$|IWI340}yakb#LVUzytt1`UR8# delta 20 acmey(`kR$|IWI340}xbw%iqZTfe8RYW(H&c diff --git a/core/__pycache__/mail.cpython-311.pyc b/core/__pycache__/mail.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a91762bc1fcee8bb4860992aa638b4924b7604b GIT binary patch literal 2367 zcmZuyOKcNI7@mE4cfENz2@sxJ2qK0k0f|E)36jYJ$Wv|71ChXTyc^@xug;E}V6BlW zaWD^ws;WdJR4J#FDy-?p?U2+EZ&9>jkY5c->T8ilJXua`i1jC7MK;MTat!Tr2|g(- z3XaYv#H4T0#~>CNXhmumGvrMMp)L5_vZ(07U9{-eMUbk_%%h<1C7qfLvbJm*DLtHw z#1ruT!1{qs24MO6n7}XV%;CLoXA~7?{)=!`fJ)VZ~x&Q}1 zMiyG3qfW`Q(DU5Ma|FU~6pP#IY@R1#V-BGezMgK^w>hfw?|S_N>4K#^LErFu^8zRN>L>ZGBVEiZca(>B z-FB5(MQhx3w8~hb(6wnyZ^afIXY4?PkVy1aIMRG{Hc!v6ddf0RC&TW+EoL?v zHB1vGv#?NEh6wAr?Af%rq#3gr!=#r-Q(7-?`v-5Olg40n3HD|OjWr{hC22gkn#P}+ znMl+ajHa*MAR} zrf=*9Zk7W#i=!2_?N9aiAL{Xq;q61a>PT4~DXAkc`snKYtM?`!P8KICYR~#dyXw*6 zOmXH-we3I+u`<|hh^iWRG<|=%)HAt#?WZ|tpg#M|ZFkkFvN}~#rz+~c^$+gN6~SEN z_XbK$04ubW5FB#4Yb+x-;0~@aJM{` z0Akp&I@-0U6`+HO%w-}+&LG$ z@nj}pXsbAq$rxB$PGgO}>PVDOJ?Ubwj@n$$tg)(%)~9FSSw~`qMqj&@6DOd#sO4B~ zIHz7xl=|#NWdaDj#d-E7fICGJw-<8Sdq8iTm~>52u=_C0mV<6gVPUtA&dn}_Mi*Rk zhZp8>8(q*&MF)tWX!EH^(!eTR?W>z|(28ja@gWcOy|0$Khkj;2K;4yw%hGU38m_c; z6|;?B{q+mopqSm@@mGDr=ock4!0m`fLP2gPD1aVpv3-=_;6K)G*H9eNECIk|o- z4JXrjHep=C{V+!1W8MW(;~0jipw@!(tDsAz|Em?GmG;&O>MQN7FXV&8{bl*!`iHx6 bZ&~gwh&6s6bFzj!aW=p}g8wV<8F1uZ78qIH literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640edbdcec3e5c1744466c916e7acdd2763b..eb53d8681d493985c3f9d2852ec9023cfc978697 100644 GIT binary patch literal 4117 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 TQ2}P|Ab7q&|Eoi8^11yV!8C!X literal 209 zcmZ3^%ge<81k-=yXIcX3#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@K?*b( zZ?Wa(r=;c-`)M-W;!Md(%uCPLOGzqX21>4E_zY6>OHV%|KQ~psG^sSNq*On(A~m_R zB)>?%JijQrxF9h(RX;huC{-U~j9x+IFAf_ZyEG@&u80Guoe_wOWr4&8W=2NF8w@fR Ku%RM0pb7xLi8T)Z diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659f6c6e0ae848e54157af197c543a09315f..4cd27f349cae4d3559656f2f6a1dda66b15fd57b 100644 GIT binary patch literal 742 zcmZvZziZn-6vywZlWj>+64F8&8tBk^a0%8_Fu?>2^;!=lix;E%u8v~s2X|7_dPyKl zpdGR`W2gQeGWO4qKoq03Q#M1kOnqlrh7_MpkN3Xs^N{X)Urkd-utsMeoD+=DUs)7O zTO>DsAo+wSqIis4+`|~r5>>_}SLrDjl@P;}Fy)98gH-)h{-PlC6W)BynF=s!q0s6| zMgyk2Wb~Dc0Ze7dtgU2BV5&=I{eNbt_B6Wj3e`8SB~-^53GDFj8Z3~7`qp8`B10KD z^j#*qp6#+pWf(CL3M>V3Kq0g;jpN~Xf512LZIke$3jo|%lwdMLW8nb=A z1;f%E@dLt4!5B}`PJDMy(Y^TYo|TcRoxV8VIzRZ>yL=OqT0&|msZB{0)a{E-djILS zR!o`+X{Mw(mt$@F@_G8Gan+AWCn24bbaH&>B1#`TyLudxc0$@IX)o|6pAXVn^Y=+i Vx(Vr~q&qXp8>Lf2K712?_aEw#!2$pP delta 244 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 literal 1364 zcmZ`(&1)M+6ra_{dS%J>I&SM!uH%hUh$T{~CE!96oTjQ1oHkCdQ$lozPf-Q6#oM~=1-7}AYwoe=&3h>36z}rW~~)fO5P4{-h1=r&HMPV zzfVm~BA|nRhVUX&zZ=1NEJDd#(kRMro?7~GlVKiARAFbK7y={g8`rq_)Wa;XDEltUWCmB zkq~sth&3ZeP{;A878u54V{Oaty2i>_vx<&kIwj51DaMXgGg(=)ND+pj!HI^Q9g`Br z#tzdA%!;PvWg3a1>()Z2BR!#4DWHiJ1dw>FOuS)~xge&2p-9tZ06jh%7)=`o)~k%rY>m)oo?Fy$ z*3WOp#5FJD)_FvD(?z&0py>SutcBjF^RHFyMAbU#a#vk`t*)G?D;+i6Rnx7Fo|by~ z_(Z$b)~@ZR_tUQyT0itB&pp5LYvy^Tl^e+D)6aYJ%l+g^CzEdC%omzVp>MZ5DOS%5y(&3}_\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 0000000000000000000000000000000000000000..fcc56947a8a18df69ee725496dc15eacc89e8e83 GIT binary patch literal 168 zcmZ3^%ge<81Usv{GePuY5CH>>P{wCAAY(d13PUi1CZpd?sKV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a842f496648a0be591d0e4deb6d5c148d613b646 GIT binary patch literal 177 zcmZ3^%ge<81Usv{GePuY5CH>>P{wCAAY(d13PUi1CZpdlIY~;;_lhPbtkwwJTx;8Va(um>)=dU}j`w{J;PsikN|70E00slK=n! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..597bfb90ba4e3ef53acad6bdd75f3914ff95c7a4 GIT binary patch literal 4228 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 - - - {% 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 %} +
+
+ +
+
+ © {% now "Y" %} Host Loyalty CRM. All rights reserved. +
+
+ + + {% 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.

+
+ +
+ +{% 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!

+ +
+
+ {% 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.

+
+ +
+ +
+
+ + + + + + + + + + + + {% 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.

+
+ +
+ +
+
+
+
+ {% 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.

+
+ +
+ + +
+
+
+
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" }} -

-
-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% 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