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 %} +
+ Are you sure you want to delete the event "{{ object.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.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 %} +
+| Event | +Date & Time | +Location | +Actions | +
|---|---|---|---|
|
+ {{ 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 %}
+
+ |
+
| {{ event.title }} | +{{ event.start_time|date:"D, d M Y" }} | ++ View + | +
+ Connecting our church communities across the region. + Join us for worship, fellowship, and service. +
+ ++ Explore our vibrant church communities across the deanery +
++ Subscribe to our newsletter for updates on events, services, and community news +
+ + + + ++ + {{ 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 %} +
+| Event | +Date & Time | +Location | +Actions | +
|---|---|---|---|
|
+ {{ 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 + + | +|||