diff --git a/.gitignore b/.gitignore index 055125d..f51d438 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/portal/settings.py b/portal/settings.py index 6e85203..5a60b54 100644 --- a/portal/settings.py +++ b/portal/settings.py @@ -9,6 +9,9 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ +from dotenv import load_dotenv +load_dotenv() + import os from pathlib import Path @@ -28,9 +31,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = bool(os.environ.get("DEBUG", default=0)) -ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS") -if ALLOWED_HOSTS: - ALLOWED_HOSTS = ALLOWED_HOSTS.split(",") +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # Application definition @@ -46,6 +47,7 @@ "allauth", "allauth.account", "portal", + 'sponsorship', "volunteer", "portal_account", ] @@ -104,6 +106,7 @@ "PORT": os.environ.get("SQL_PORT", "5432"), } } + # Password validation diff --git a/portal/urls.py b/portal/urls.py index d4d5b01..4931a53 100644 --- a/portal/urls.py +++ b/portal/urls.py @@ -26,4 +26,6 @@ path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), path("portal_account/", include("portal_account.urls", namespace="portal_account")), + path("sponsorship/", include("sponsorship.urls", namespace="sponsorship")), + ] diff --git a/portal/views.py b/portal/views.py index 9ffa45e..1303374 100644 --- a/portal/views.py +++ b/portal/views.py @@ -1,3 +1,4 @@ +from allauth.account.models import EmailAddress from django.shortcuts import render, redirect @@ -12,4 +13,10 @@ def index(request): and not PortalProfile.objects.filter(user=request.user).exists() ): return redirect("portal_account:portal_profile_new") + if request.user.is_authenticated: + context["email_verified"] = EmailAddress.objects.filter(user=request.user, verified=True).exists() + return render(request, "portal/index.html", context) + +def sponsorship_success(request): + return render(request, "sponsorship/success.html") \ No newline at end of file diff --git a/sponsorship/__init__.py b/sponsorship/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsorship/admin.py b/sponsorship/admin.py new file mode 100644 index 0000000..355a080 --- /dev/null +++ b/sponsorship/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from .models import SponsorshipProfile, SponsorshipTier + +# Register your models here. +@admin.register(SponsorshipTier) +class SponsorshipTierAdmin(admin.ModelAdmin): + list_display = ('name', 'amount') + search_fields = ('name',) + ordering = ('amount',) + +@admin.register(SponsorshipProfile) +class SponsorshipProfileAdmin(admin.ModelAdmin): + list_display = ('sponsor_organization_name', 'main_contact','sponsorship_type', 'application_status') + list_filter = ('sponsorship_type', 'application_status', 'sponsorship_tier') + search_fields = ('sponsor_organization_name', 'main_contact__username') \ No newline at end of file diff --git a/sponsorship/apps.py b/sponsorship/apps.py new file mode 100644 index 0000000..db38c61 --- /dev/null +++ b/sponsorship/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SponsorshipConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'sponsorship' diff --git a/sponsorship/forms.py b/sponsorship/forms.py new file mode 100644 index 0000000..d07b3db --- /dev/null +++ b/sponsorship/forms.py @@ -0,0 +1,21 @@ +from django import forms +from .models import SponsorshipProfile + +class SponsorshipProfileForm(forms.ModelForm): + class Meta: + model = SponsorshipProfile + exclude = ['user', 'application_status'] + fields = [ + 'main_contact', + 'additional_contacts', + 'sponsor_organization_name', + 'sponsorship_type', + 'sponsorship_tier', + 'logo', + 'company_description', + 'application_status', + ] + widgets = { + 'additional_contacts': forms.CheckboxSelectMultiple(attrs={'class': 'form-control'}), + 'company_description': forms.Textarea(attrs={'rows': 4,}), + } \ No newline at end of file diff --git a/sponsorship/migrations/0001_initial.py b/sponsorship/migrations/0001_initial.py new file mode 100644 index 0000000..699ddf5 --- /dev/null +++ b/sponsorship/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.7 on 2025-03-29 05:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='SponsorshipTier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('name', models.CharField(max_length=100)), + ('description', models.TextField()), + ], + ), + migrations.CreateModel( + name='SponsorshipProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sponsor_organization_name', models.CharField(max_length=255)), + ('sponsorship_type', models.CharField(choices=[('individual', 'Individual'), ('organization', 'Organization/Company')], max_length=20)), + ('logo', models.ImageField(upload_to='sponsor_logos/')), + ('company_description', models.TextField()), + ('application_status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('cancelled', 'Cancelled')], default='pending', max_length=20)), + ('additional_contacts', models.ManyToManyField(blank=True, related_name='additional_sponsorship_contacts', to=settings.AUTH_USER_MODEL)), + ('main_contact', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='main_contact_for', to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sponsorship_profile', to=settings.AUTH_USER_MODEL)), + ('sponsorship_tier', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='sponsorship.sponsorshiptier')), + ], + ), + ] diff --git a/sponsorship/migrations/__init__.py b/sponsorship/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsorship/models.py b/sponsorship/models.py new file mode 100644 index 0000000..99c810a --- /dev/null +++ b/sponsorship/models.py @@ -0,0 +1,45 @@ +from django.db import models +from django.contrib.auth.models import User + +class SponsorshipTier(models.Model): + amount = models.DecimalField(max_digits=10, decimal_places=2) + name = models.CharField(max_length=100) + description = models.TextField() + + def __str__(self): + return self.name + + +class SponsorshipProfile(models.Model): + INDIVIDUAL = 'individual' + ORGANIZATION = 'organization' + + SPONSORSHIP_TYPE_CHOICES = [ + (INDIVIDUAL, 'Individual'), + (ORGANIZATION, 'Organization/Company'), + ] + + APPLICATION_PENDING = 'pending' + APPLICATION_APPROVED = 'approved' + APPLICATION_REJECTED = 'rejected' + APPLICATION_CANCELLED = 'cancelled' + + APPLICATION_STATUS_CHOICES = [ + (APPLICATION_PENDING, 'Pending'), + (APPLICATION_APPROVED, 'Approved'), + (APPLICATION_REJECTED, 'Rejected'), + (APPLICATION_CANCELLED, 'Cancelled'), + ] + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='sponsorship_profile') + main_contact = models.OneToOneField(User, on_delete=models.CASCADE, related_name='main_contact_for') + additional_contacts = models.ManyToManyField(User, blank=True, related_name='additional_sponsorship_contacts') + sponsor_organization_name = models.CharField(max_length=255) + sponsorship_type = models.CharField(max_length=20, choices=SPONSORSHIP_TYPE_CHOICES) + sponsorship_tier = models.ForeignKey(SponsorshipTier, on_delete=models.SET_NULL, null=True) + logo = models.ImageField(upload_to='sponsor_logos/') + company_description = models.TextField() + application_status = models.CharField(max_length=20, choices=APPLICATION_STATUS_CHOICES, default=APPLICATION_PENDING) + + def __str__(self): + return self.sponsor_organization_name diff --git a/sponsorship/tests.py b/sponsorship/tests.py new file mode 100644 index 0000000..8e1c179 --- /dev/null +++ b/sponsorship/tests.py @@ -0,0 +1,55 @@ +from django.test import TestCase + +# Create your tests here. + +from django.test import TestCase +from django.contrib.auth.models import User +from sponsorship.models import SponsorshipProfile, SponsorshipTier +from volunteer.models import VolunteerProfile # if it's in a separate app +from django.core.files.uploadedfile import SimpleUploadedFile + +class SponsorshipProfileTests(TestCase): + + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpass') + self.tier = SponsorshipTier.objects.create( + name="Gold", + amount=5000.00, + description="Gold Tier Benefits" + ) + + def test_application_status_is_pending_on_creation(self): + logo = SimpleUploadedFile("logo.png", b"file_content", content_type="image/png") + profile = SponsorshipProfile.objects.create( + user=self.user, + main_contact=self.user, + sponsor_organization_name="Test Org", + sponsorship_type=SponsorshipProfile.ORGANIZATION, + sponsorship_tier=self.tier, + logo=logo, + company_description="We support PyLadiesCon!" + ) + self.assertEqual(profile.application_status, SponsorshipProfile.APPLICATION_PENDING) + + def test_user_can_have_both_volunteer_and_sponsor_profiles(self): + # Create a VolunteerProfile for the user + VolunteerProfile.objects.create( + user=self.user, + timezone="UTC", # required field + languages_spoken="{English}" + ) + + # Create a SponsorshipProfile for the same user + profile = SponsorshipProfile.objects.create( + user=self.user, + main_contact=self.user, + sponsor_organization_name="Both Roles Org", + sponsorship_type=SponsorshipProfile.INDIVIDUAL, + sponsorship_tier=self.tier, + logo="test_logo.png", + company_description="Has both roles", + ) + + self.assertEqual(profile.user, self.user) + self.assertTrue(VolunteerProfile.objects.filter(user=self.user).exists()) + diff --git a/sponsorship/urls.py b/sponsorship/urls.py new file mode 100644 index 0000000..ef4c53d --- /dev/null +++ b/sponsorship/urls.py @@ -0,0 +1,11 @@ +from . import views +from django.urls import path, include + + +app_name = 'sponsorship' + +urlpatterns = [ + path('profile/new/', views.create_sponsorship_profile, name='create'), + path('profile/success/', views.sponsorship_success, name='success'), + +] diff --git a/sponsorship/views.py b/sponsorship/views.py new file mode 100644 index 0000000..0b125f2 --- /dev/null +++ b/sponsorship/views.py @@ -0,0 +1,28 @@ +from django.shortcuts import render, redirect +from .forms import SponsorshipProfileForm +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from .models import SponsorshipProfile + +# Create your views here. + +@login_required +def create_sponsorship_profile(request): + if request.method == 'POST': + form = SponsorshipProfileForm(request.POST, request.FILES) + if form.is_valid(): + sponsorship_profile = form.save(commit=False) + sponsorship_profile.user = request.user # Assuming the user is logged in + sponsorship_profile.save() + sponsorship_profile.application_status = SponsorshipProfile.APPLICATION_PENDING + form.save_m2m() # Save many-to-many relationships + return redirect('sponsorship:success') # Redirect to a success page or profile page + else: + form = SponsorshipProfileForm() + return render(request, 'portal/sponsorship/create_sponsorship_profile.html', {'form': form}) + +def success(request): + return HttpResponse("Sponsorship profile created successfully!") + +def sponsorship_success(request): + return render(request, "sponsorship/success.html") \ No newline at end of file diff --git a/templates/portal/index.html b/templates/portal/index.html index 472d7a0..abdc345 100644 --- a/templates/portal/index.html +++ b/templates/portal/index.html @@ -25,11 +25,17 @@
Sponsorship package is coming soon!
-{# #} -{# Call to action#} -{# #} -{# #} +Want to become a sponsor? Fill out your sponsorship profile and we'll follow up with details!
+ {% if email_verified %} + + Become a sponsor! + + + {% else %} ++ Please verify your email address to access the sponsorship form. +
+ {% endif %}Your sponsorship profile has been submitted successfully. We will review it and get back to you soon.
+ Return to Home +