diff --git a/.gitignore b/.gitignore index 48f6843..7c88ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ env.py # Django *.log db.sqlite3 +db.sqlite3-journal media/ staticfiles/ local_settings.py @@ -23,6 +24,14 @@ settings_local.py # VS Code .vscode/ +*.code-workspace + +# PyCharm +.idea/ + +# Sublime Text +*.sublime-project +*.sublime-workspace # Node.js / Frontend node_modules/ @@ -32,10 +41,18 @@ yarn-error.log* build/ dist/ *.tgz +package-lock.json +yarn.lock # OS files .DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db Thumbs.db +Desktop.ini # Coverage reports htmlcov/ @@ -46,25 +63,120 @@ nosetests.xml coverage.xml *.cover *.py,cover +.pytest_cache/ +.tox/ -# Migrations +# Migrations (be careful with this - usually want to commit migrations) +# Only ignore compiled Python in migrations +**/migrations/__pycache__/ **/migrations/*.pyc **/migrations/*.pyo **/migrations/*.pyd -**/migrations/__pycache__/ -**/migrations/*.sqlite3 -# Others +# Backup files +*.bak *.swp *.swo -*.csv +*~ -# Windows OS files -Thumbs.db -ehthumbs.db -Desktop.ini +# Celery +celerybeat-schedule +celerybeat.pid -# Python Windows environments -Scripts/ -.venv/ -env.py \ No newline at end of file +# SQLite database files +*.sqlite +*.sqlite3 +*.db + +# Media files (uploaded content) +media/ +*.jpg +*.jpeg +*.png +*.gif +*.pdf +*.mp4 +*.mov +*.avi +*.webm + +# Temporary files +*.tmp +*.temp + +# MacOS +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.TemporaryItems +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml + +# PEP 582 +__pypackages__/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Sentry +.sentryclirc + +# Webpack +webpack-stats.json + +# Redis +dump.rdb + +# Logs +logs/ +*.log + +# SSL certificates (if storing locally) +*.pem +*.key +*.crt +*.csr + +# Environment-specific files +.envrc +.env.local +.env.*.local \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index 20fd689..0a3bb39 100644 --- a/config/settings.py +++ b/config/settings.py @@ -2,6 +2,12 @@ import os import dj_database_url import sys +from dotenv import load_dotenv + + +# Load .env file +load_dotenv() + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -12,6 +18,7 @@ import env SECRET_KEY = os.environ.get("SECRET_KEY") +GOOGLE_MAPS_API_KEY = os.environ.get('GOOGLE_MAPS_API_KEY') DEBUG = True ALLOWED_HOSTS = [ "deanery-11c659a713eb.herokuapp.com", @@ -123,6 +130,10 @@ STATICFILES_DIRS = [BASE_DIR / 'staticfiles'] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +# Add MEDIA configuration +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' if sys.platform == "win32": @@ -138,11 +149,24 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' DEFAULT_FROM_EMAIL = 'noreply@ncd.com' -# For production, configure a real email backend, HERE: -# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -# EMAIL_HOST = 'smtp.yourprovider.com' # e.g., 'smtp.gmail.com' -# EMAIL_PORT = 587 # or 465 for SSL -# EMAIL_HOST_USER = 'your@email.com' -# EMAIL_HOST_PASSWORD = 'your-email-password' -# EMAIL_USE_TLS = True # True for port 587, False for 465 -# DEFAULT_FROM_EMAIL = 'your@email.com' +# For production - uncomment and configure: +if not DEBUG: + EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com') + EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587)) + EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') + EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') + EMAIL_USE_TLS = True + DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@ncd.com') + +# Security settings for production +if not DEBUG: + SECURE_SSL_REDIRECT = True + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True + SECURE_BROWSER_XSS_FILTER = True + SECURE_CONTENT_TYPE_NOSNIFF = True + X_FRAME_OPTIONS = 'DENY' + SECURE_HSTS_SECONDS = 31536000 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_HSTS_PRELOAD = True diff --git a/config/urls.py b/config/urls.py index 5bd5a55..2215cb7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,6 +18,7 @@ from django.urls import path, include from django.conf import settings from django.views.generic import TemplateView +from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), @@ -30,3 +31,5 @@ urlpatterns += [ path("__reload__/", include("django_browser_reload.urls")), ] +# media files serving in development + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/docs/OLDNorthCarnmarthDeaneryLocations.csv b/docs/OLDNorthCarnmarthDeaneryLocations.csv new file mode 100644 index 0000000..c007295 --- /dev/null +++ b/docs/OLDNorthCarnmarthDeaneryLocations.csv @@ -0,0 +1,19 @@ +Location,Name,Address,Postcode,Latitude,Longitude,Contact +Camborne,St Martins & Meriadoc's Church,TR14 7DF,50.21366877043699,-5.301824086944084,, +Tuckingmill,All Saints Church,TR14 8RQ,50.22018118627777,-5.28559548851654,, +Penponds,Holy Trinity Church,TR14 0QE,50.2069714041776,-5.315726981398124,, +Crowan,Crowan Church,TR14 9NB,50.1637994385378,-5.297747715781878,, +Treslothan,Treslothan Church,TR14 9LP,50.19979459265355,-5.296158532971124,, +St Day,Holy Trinity Church,33 Church St Redruth,TR16 5LD,50.2375012032268,-5.183848071600855,01209 822862, +Carharrack,St Pirans Mission Church,TR16 5RP,50.22932300343052,-5.181982517627701,, +Gwennap,St Wenappa Church,TR16 6BD,50.21784792863372,-5.170980531121604,, +Chacewater,St Pauls Church,TR4 8PZ,50.2538404644562,-5.1565885605074975,, +Stithians,St Stythians Church,TR3 7RN,50.19078249192517,-5.179857640256757,, +St Andrews,St Andrews Church,TR15 2LL,50.23179708057995,-5.2256908348185105,, +St Euny,St Euny Church,TR15 3BT,50.22613250475454,-5.238225500436913,, +Pencoys,St Andrews Church,TR16 6LT,50.19936321351329,-5.241719334819983,, +Lanner,Christ Church,TR16 6ER,50.21405614336509,-5.204804504135002,, +Treleigh,St Stephens Church,TR16 4AY,50.24735827059275,-5.221079258107016,, +Illogan,St Illogan Parish Church,TR16 4SR,50.24995225224803,-5.267790936114373,, +Trevenson,Trevenson Church (St Illogan),TR15 3PT,50.23090881484769,-5.273534963967737,, +Portreath,St Mary’s Church,TR16 4LW,50.26128781853588,-5.28668583111962,, diff --git a/home/management/commands/create_sample_events.py b/home/management/commands/create_sample_events.py new file mode 100644 index 0000000..53161b2 --- /dev/null +++ b/home/management/commands/create_sample_events.py @@ -0,0 +1,140 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from home.models import Event, Church +from django.contrib.auth.models import User +import datetime + + +class Command(BaseCommand): + help = 'Create sample events for Deanery Management System' + + def handle(self, *args, **options): + # Get a staff user or superuser to assign as event creator + try: + user = ( + User.objects.filter(is_staff=True).first() + or User.objects.filter(is_superuser=True).first() + ) + if not user: + self.stdout.write( + self.style.ERROR( + 'No staff or superuser found to assign event creator' + ) + ) + return + except User.DoesNotExist: + self.stdout.write( + self.style.ERROR( + 'No staff or superuser found to assign as event creator' + ) + ) + return + + # Get some churches for the events + churches = list(Church.objects.all()[:5]) + + # Current time for reference + now = timezone.now() + + # Sample events + events = [ + { + 'title': 'Christmas Midnight Mass', + 'description': ( + 'Join us for our annual Christmas Midnight Mass. The service will ' + 'feature traditional carols, scripture readings, and Holy Communion. ' + 'All are welcome to attend this special celebration.' + ), + 'church': churches[0] if churches else None, + 'location': 'St. Martin and St. Meriadoc Church' if not churches else '', + 'start_time': datetime.datetime(now.year if now.month < 12 else now.year + 1, 12, 24, 23, 30, tzinfo=timezone.get_current_timezone()), + 'end_time': datetime.datetime(now.year if now.month < 12 else now.year + 1, 12, 25, 1, 0, tzinfo=timezone.get_current_timezone()), + 'is_featured': True + }, + { + 'title': 'Easter Sunday Service', + 'description': ( + 'Celebrate the resurrection of Jesus Christ with our ' + 'Easter Sunday service, Egg hunt and special music. ' + 'Families are encouraged to attend together.' + ), + 'church': churches[1] if len(churches) > 1 else None, + 'location': 'Holy Trinity Church' if len(churches) <= 1 else '', + 'start_time': datetime.datetime( + now.year + 1, 4, 9, 10, 0, + tzinfo=timezone.get_current_timezone() + ), + 'end_time': datetime.datetime( + now.year + 1, 4, 9, 12, 0, + tzinfo=timezone.get_current_timezone() + ), + 'is_featured': True + }, + { + 'title': 'Halloween Graveyard Scavenger Hunt', + 'description': ( + 'A family-friendly event exploring the historic graveyard. Learn about local ' + 'history while solving puzzles and finding clues. Prizes for all participants. ' + 'Suitable for ages 7+. Please bring a torch/flashlight.' + ), + 'church': churches[2] if len(churches) > 2 else None, + 'location': 'St. Illogan Church Graveyard' if len(churches) <= 2 else '', + 'start_time': datetime.datetime( + now.year + 1, 10, 31, 18, 0, + tzinfo=timezone.get_current_timezone() + ), + 'end_time': datetime.datetime( + now.year + 1, 10, 31, 20, 0, + tzinfo=timezone.get_current_timezone() + ), + 'is_featured': False + }, + { + 'title': 'Community Coffee Morning', + 'description': ( + 'Join us for coffee, cake and conversation.' + 'Everyone welcome!' + + ), + 'church': churches[3] if len(churches) > 3 else None, + 'location': 'Church Hall' if len(churches) <= 3 else '', + 'start_time': now + datetime.timedelta(days=7), + 'end_time': now + datetime.timedelta(days=7, hours=2), + 'is_featured': False + }, + { + 'title': 'Choir Practice', + 'description': ( + 'Weekly practice for the church choir.' + 'No experience necessary!' + ), + 'church': churches[4] if len(churches) > 4 else None, + 'location': 'Church Choir Room' if len(churches) <= 4 else '', + 'start_time': now + datetime.timedelta(days=3, hours=18), + 'end_time': now + datetime.timedelta(days=3, hours=20), + 'is_featured': False + } + ] + + # Create events + count = 0 + for event_data in events: + Event.objects.get_or_create( + title=event_data['title'], + start_time=event_data['start_time'], + defaults={ + 'description': event_data['description'], + 'church': event_data['church'], + 'location': event_data['location'], + 'end_time': event_data['end_time'], + 'created_by': user, + 'is_featured': event_data['is_featured'] + } + ) + count += 1 + + self.stdout.write( + self.style.SUCCESS( + f'{count} sample events created successfully.' + ) + ) diff --git a/home/migrations/0006_event.py b/home/migrations/0006_event.py new file mode 100644 index 0000000..fc73618 --- /dev/null +++ b/home/migrations/0006_event.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.6 on 2025-10-21 16:52 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0005_church_contact_email_church_website'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('location', models.CharField(blank=True, max_length=255)), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('image', models.ImageField(blank=True, null=True, upload_to='events/')), + ('is_featured', models.BooleanField(default=False)), + ('church', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.church')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('likes', models.ManyToManyField(blank=True, related_name='liked_events', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['start_time'], + }, + ), + ] diff --git a/home/models.py b/home/models.py index 3b21b94..9fcfddf 100644 --- a/home/models.py +++ b/home/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth.models import User +from django.urls import reverse from django.utils import timezone import uuid @@ -62,11 +63,51 @@ class Church(models.Model): address = models.CharField(max_length=255, blank=True) postcode = models.CharField(max_length=20, blank=True) contact = models.CharField(max_length=100, blank=True) - contact = models.CharField(max_length=100, blank=True) - contact_email = models.EmailField(blank=True) # Add this + contact_email = models.EmailField(blank=True) website = models.URLField(blank=True) latitude = models.FloatField(null=True, blank=True) longitude = models.FloatField(null=True, blank=True) - + def __str__(self): return self.name + + +class Event(models.Model): + title = models.CharField(max_length=255) + description = models.TextField() + location = models.CharField(max_length=255, blank=True) + church = models.ForeignKey( + 'Church', + on_delete=models.CASCADE, + null=True, + blank=True + ) + start_time = models.DateTimeField() + end_time = models.DateTimeField() + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + image = models.ImageField(upload_to='events/', null=True, blank=True) + is_featured = models.BooleanField(default=False) + likes = models.ManyToManyField( + User, + related_name='liked_events', + blank=True + ) + + class Meta: + ordering = ['start_time'] + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('event_detail', kwargs={'pk': self.pk}) + + @property + def like_count(self): + return self.likes.count() + + @property + def is_past(self): + return self.end_time < timezone.now() diff --git a/home/templates/home/event_confirm_delete.html b/home/templates/home/event_confirm_delete.html new file mode 100644 index 0000000..9e059dd --- /dev/null +++ b/home/templates/home/event_confirm_delete.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} {% load static %} {% block content %} +
+
+

Delete Event

+ +

+ Are you sure you want to delete the event "{{ object.title }}"? +

+ +
+ {% csrf_token %} +
+ Cancel + +
+
+
+
+{% endblock %} diff --git a/home/templates/home/event_detail.html b/home/templates/home/event_detail.html new file mode 100644 index 0000000..095d090 --- /dev/null +++ b/home/templates/home/event_detail.html @@ -0,0 +1,172 @@ +{% extends "base.html" %} {% load static %} {% block content %} +
+
+ {% if event.image %} +
+ {% endif %} + +
+

{{ event.title }}

+ +
+
+

Date & Time

+

+ + {{ event.start_time|date:"D, d M Y" }} at {{ + event.start_time|time:"g:i A" }} {% if + event.end_time|date:"D, d M Y" == + event.start_time|date:"D, d M Y" %} - {{ + event.end_time|time:"g:i A" }} {% else %} to {{ + event.end_time|date:"D, d M Y" }} at {{ + event.end_time|time:"g:i A" }} {% endif %} +

+
+ +
+

Location

+

+ + {% if event.church %} {{ event.church.name }} {% if + event.church.address %}
{{ event.church.address + }}{% endif %} {% if event.church.postcode %}, {{ + event.church.postcode }}{% endif %} {% else %} {{ + event.location }} {% endif %} +

+
+
+ +
+ +
+ {{ event.description|linebreaks }} +
+ +
+ {% if user.is_authenticated %} + + {% endif %} + + + + {% if event.church %} + + Visit Church + + {% endif %} {% if user.is_staff or user.is_superuser %} +
+ + {% endif %} +
+
+
+ +
+ + Back to Events + +
+
+ + +{% endblock %} diff --git a/home/templates/home/event_form.html b/home/templates/home/event_form.html new file mode 100644 index 0000000..b6aaeea --- /dev/null +++ b/home/templates/home/event_form.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+

+ {% if form.instance.pk %}Edit{% else %}Create New{% endif %} Event +

+ +
+ {% csrf_token %} + +
+ + {{ form.title.errors }} + +
+ +
+ + {{ form.description.errors }} + +
+ +
+
+ + {{ form.start_time.errors }} + +
+ +
+ + {{ form.end_time.errors }} + +
+
+ +
+ + {{ form.church.errors }} + +
+ +
+ + {{ form.location.errors }} + +
+ +
+ + {{ form.image.errors }} + {% if form.instance.image %} +
+ Current event image +

Current image. Upload a new one to replace.

+
+ {% endif %} + +
+ +
+ +

Featured events will be highlighted on the homepage

+
+ +
+ Cancel + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/home/templates/home/events.html b/home/templates/home/events.html new file mode 100644 index 0000000..35d410c --- /dev/null +++ b/home/templates/home/events.html @@ -0,0 +1,234 @@ + +{% extends "base.html" %} {% load static %} {% block content %} +
+
+

Church Events

+ {% if user.is_staff or user.is_superuser %} + + Add New Event + + {% endif %} +
+ + + {% if featured_events %} +
+

Featured Events

+
+ {% for event in featured_events %} +
+ {% if event.image %} +
+ {{ event.title }} +
+ {% else %} +
+ +
+ {% endif %} +
+

{{ event.title }}

+

+ + {{ event.start_time|date:"D, d M Y" }} at {{ + event.start_time|time:"g:i A" }} +

+

+ + {% if event.church %}{{ event.church.name }}{% else %}{{ + event.location }}{% endif %} +

+ +
+
+ {% endfor %} +
+
+ {% endif %} + + +
+

Upcoming Events

+ {% if events %} +
+ + + + + + + + + + + {% for event in events %} + + + + + + + {% endfor %} + +
EventDate & TimeLocationActions
+
{{ event.title }}
+ {% if event.is_featured %} + Featured + {% endif %} +
+ {{ event.start_time|date:"D, d M Y" }}
+ + {{ event.start_time|time:"g:i A" }} - {{ + event.end_time|time:"g:i A" }} + +
+ {% if event.church %} {{ event.church.name }} {% + else %} {{ event.location }} {% endif %} + +
+ + Details + + {% if user.is_authenticated %} + + {% endif %} {% if user.is_staff or + user.is_superuser %} + Edit + Delete + {% endif %} +
+
+
+ {% else %} +
+ + No upcoming events scheduled. Check back soon! +
+ {% endif %} +
+ + + {% if past_events %} +
+

Past Events

+
+ +
+ View past events ({{ past_events|length }}) +
+
+
+ + + {% for event in past_events %} + + + + + + {% endfor %} + +
{{ event.title }}{{ event.start_time|date:"D, d M Y" }} + View +
+
+
+
+
+ {% endif %} +
+ + +{% endblock %} diff --git a/home/templates/home/index.html b/home/templates/home/index.html new file mode 100644 index 0000000..4aeec6a --- /dev/null +++ b/home/templates/home/index.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + +
+
+
+

Welcome to Our Deanery

+

+ Connecting our church communities across the region. + Join us for worship, fellowship, and service. +

+ +
+
+
+ + +
+
+ {% include "home/partials/_events_calendar.html" with upcoming_events=upcoming_events featured_events=featured_events %} + + +
+
+ + +
+
+
+

Our Churches

+

+ Explore our vibrant church communities across the deanery +

+
+ + +
+
+ + +
+
+
+

Stay Connected

+

+ Subscribe to our newsletter for updates on events, services, and community news +

+ +
+ {% csrf_token %} + + + + + +
+ +
+
+
+
+ + +{% endblock %} diff --git a/home/templates/home/partials/_events_calendar.html b/home/templates/home/partials/_events_calendar.html new file mode 100644 index 0000000..bc2d1cd --- /dev/null +++ b/home/templates/home/partials/_events_calendar.html @@ -0,0 +1,248 @@ +
+

Upcoming Events

+ + + {% if featured_events %} + + {% endif %} + + +
+ + + + + + + + + + + {% for event in upcoming_events %} + + + + + + + {% empty %} + + + + {% endfor %} {% if user.is_staff or user.is_superuser %} + + + + {% endif %} + +
EventDate & TimeLocationActions
+
{{ event.title }}
+ {% if event.is_featured %} + Featured + {% endif %} +
+ {{ event.start_time|date:"D, d M Y" }}
+ + {{ event.start_time|time:"g:i A" }} - {{ + event.end_time|time:"g:i A" }} + +
+ {% if event.church %} {{ event.church.name }} {% else %} + {{ event.location }} {% endif %} + +
+ + + Details + + + + + {% if user.is_authenticated %} + + + {% endif %} {% if user.is_staff or user.is_superuser + %} + + + + + + + {% endif %} +
+
+
+ +

+ No upcoming events scheduled. Check back soon! +

+
+
+ + Add New Event + +
+
+
+ + + diff --git a/home/urls.py b/home/urls.py index bf98718..63fb6a9 100644 --- a/home/urls.py +++ b/home/urls.py @@ -1,10 +1,9 @@ from . import views from django.urls import path -from django.shortcuts import redirect, render, get_object_or_404 -from django.http import JsonResponse urlpatterns = [ - path('home/', views.HomePage.as_view(), name='home'), + # Remove 'home/' - it's already in the include path + path('', views.HomePage.as_view(), name='home'), path('signup/', views.newsletter_signup, name='newsletter_signup'), path( 'confirm//', @@ -19,4 +18,23 @@ path('churches/', views.churches, name='churches'), path('contact/', views.ContactView.as_view(), name='contact'), path('about/', views.AboutPage.as_view(), name='about'), + # Events urls + path('events/', views.EventListView.as_view(), name='events'), + path( + 'events//', + views.EventDetailView.as_view(), + name='event_detail' + ), + path('events/new/', views.EventCreateView.as_view(), name='event_new'), + path( + 'events//edit/', + views.EventUpdateView.as_view(), + name='event_edit' + ), + path( + 'events//delete/', + views.EventDeleteView.as_view(), + name='event_delete' + ), + path('events//like/', views.event_like, name='event_like'), ] diff --git a/home/views.py b/home/views.py index f8873f4..c251c4e 100644 --- a/home/views.py +++ b/home/views.py @@ -7,10 +7,14 @@ from django.utils import timezone from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_protect -from django.urls import reverse -from django.views.generic import TemplateView, FormView +from django.urls import reverse, reverse_lazy +from django.views.generic import ( + TemplateView, FormView, ListView, DetailView, + CreateView, UpdateView, DeleteView +) +from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin # from requests import request -from .models import NewsletterSubscriber, Church +from .models import NewsletterSubscriber, Church, Event from .forms import NewsletterSignupForm, ContactForm @@ -18,11 +22,25 @@ class HomePage(TemplateView): """ Displays home page" """ - template_name = 'index.html' + template_name = 'home/index.html' + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Add upcoming events (next 3) + context['upcoming_events'] = Event.objects.filter( + start_time__gte=timezone.now() + ).order_by('start_time')[:3] -def get_client_ip(request): + # Add featured events + context['featured_events'] = Event.objects.filter( + is_featured=True, + start_time__gte=timezone.now() + )[:2] + + return context + +def get_client_ip(request): """Utility function to get client IP address from request""" x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: @@ -144,36 +162,6 @@ def newsletter_unsubscribe(request, token): }) -# class ChurchListView(TemplateView): -# """Display a list of churches""" -# # Placeholder implementation - replace with actual church data retrieval - -# template_name = 'home/churches.html' - -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# churches = [] -# csv_path = os.path.join( -# settings.BASE_DIR, -# 'docs', -# 'NorthCarnmarthDeaneryLocations.csv' -# ) -# with open(csv_path, 'r') as file: -# for line in file.readlines()[1:]: -# parts = line.strip().split(',') -# if len(parts) >= 5: -# churches.append({ -# 'location': parts[0], -# 'name': parts[1], -# 'postcode': parts[2], -# 'latitude': parts[3], -# 'longitude': parts[4], -# }) - -# context['churches'] = churches -# return context - - class ContactView(FormView): template_name = 'home/contact.html' form_class = ContactForm @@ -212,3 +200,79 @@ class AboutPage(TemplateView): def churches(request): churches = Church.objects.all() return render(request, "home/churches.html", {"churches": churches}) + + +# Event Views + +class StaffRequiredMixin(UserPassesTestMixin): + """Mixin to require staff or superuser access""" + def test_func(self): + return self.request.user.is_staff or self.request.user.is_superuser + + +class EventListView(ListView): + model = Event + template_name = 'home/events.html' + context_object_name = 'events' + + def get_queryset(self): + return Event.objects.filter(start_time__gte=timezone.now()).order_by('start_time') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['featured_events'] = Event.objects.filter(is_featured=True, start_time__gte=timezone.now()) + context['past_events'] = Event.objects.filter(end_time__lt=timezone.now()).order_by('-start_time')[:5] + return context + + +class EventDetailView(DetailView): + model = Event + template_name = 'home/event_detail.html' + + +class EventCreateView(LoginRequiredMixin, StaffRequiredMixin, CreateView): + model = Event + template_name = 'home/event_form.html' + fields = ['title', 'description', 'location', 'church', 'start_time', 'end_time', 'image', 'is_featured'] + success_url = reverse_lazy('events') + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + +class EventUpdateView(LoginRequiredMixin, StaffRequiredMixin, UpdateView): + model = Event + template_name = 'home/event_form.html' + fields = ['title', 'description', 'location', 'church', 'start_time', 'end_time', 'image', 'is_featured'] + success_url = reverse_lazy('events') + + +class EventDeleteView(LoginRequiredMixin, StaffRequiredMixin, DeleteView): + model = Event + template_name = 'home/event_confirm_delete.html' + success_url = reverse_lazy('events') + + +def event_like(request, pk): + """Toggle like status for an event""" + if not request.user.is_authenticated: + return JsonResponse( + {'status': 'error', 'message': 'Login required'}, + status=401 + ) + + event = get_object_or_404(Event, pk=pk) + + if request.user in event.likes.all(): + event.likes.remove(request.user) + liked = False + else: + event.likes.add(request.user) + liked = True + + return JsonResponse({ + 'status': 'success', + 'liked': liked, + 'likes': event.like_count + }) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2375f43..6963ed4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,9 +20,11 @@ MarkupSafe==3.0.2 mdurl==0.1.2 numpy==2.3.3 pandas==2.3.2 +pillow==12.0.0 psycopg2==2.9.10 Pygments==2.19.2 python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 python-slugify==8.0.4 pytz==2025.2 PyYAML==6.0.2 diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 39976ba..0000000 --- a/templates/index.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base.html" %} -{% load static %} -{% block content %} -
-
-{% endblock %} \ No newline at end of file