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 @@

Volunteer

Sponsor Us

-

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 %}
diff --git a/templates/portal/sponsorship/create_sponsorship_profile.html b/templates/portal/sponsorship/create_sponsorship_profile.html new file mode 100644 index 0000000..8e73b97 --- /dev/null +++ b/templates/portal/sponsorship/create_sponsorship_profile.html @@ -0,0 +1,13 @@ +{% extends "portal/base.html" %} +{% load static %} + +{% block content %} +
+

Create Sponsorship Profile

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock %} diff --git a/templates/sponsorship/profile_form.html b/templates/sponsorship/profile_form.html new file mode 100644 index 0000000..638a337 --- /dev/null +++ b/templates/sponsorship/profile_form.html @@ -0,0 +1,29 @@ +{% extends "portal/base.html" %} +{% load static %} + +{% block content %} +
+

Create Sponsorship Profile

+ + {% if form.errors %} +
+
    + {% for field in form %} + {% for error in field.errors %} +
  • {{ field.label }}: {{ error }}
  • + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} + +
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock %} diff --git a/templates/sponsorship/success.html b/templates/sponsorship/success.html new file mode 100644 index 0000000..b2e7587 --- /dev/null +++ b/templates/sponsorship/success.html @@ -0,0 +1,9 @@ +{% extends "portal/base.html" %} + +{% block content %} +
+

🎉 Thank you for your sponsorship submission!

+

Your sponsorship profile has been submitted successfully. We will review it and get back to you soon.

+ Return to Home +
+{% endblock %} diff --git a/volunteer/migrations/0001_initial.py b/volunteer/migrations/0001_initial.py index 12e3caf..8585f4d 100644 --- a/volunteer/migrations/0001_initial.py +++ b/volunteer/migrations/0001_initial.py @@ -1,17 +1,10 @@ -# Generated by Django 5.1.7 on 2025-03-18 22:57 +# Generated by Django 5.1.7 on 2025-03-29 06:08 import django.db.models.deletion import portal.models import volunteer.constants from django.conf import settings from django.db import migrations, models -from volunteer.constants import RoleTypes - - -def create_initial_roles(apps, schema_editor): - Role = apps.get_model("volunteer", "Role") - for role_type in RoleTypes: - Role.objects.create(short_name=role_type, description=role_type) class Migration(migrations.Migration): @@ -19,307 +12,53 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("portal", "0001_initial"), + ('portal', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name="Role", + name='Role', fields=[ - ( - "basemodel_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="portal.basemodel", - ), - ), - ("short_name", models.CharField(max_length=40, verbose_name="name")), - ( - "description", - models.CharField(max_length=1000, verbose_name="description"), - ), + ('basemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='portal.basemodel')), + ('short_name', models.CharField(max_length=40, verbose_name='name')), + ('description', models.CharField(max_length=1000, verbose_name='description')), ], - bases=("portal.basemodel",), + bases=('portal.basemodel',), ), migrations.CreateModel( - name="Team", + name='Team', fields=[ - ( - "basemodel_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="portal.basemodel", - ), - ), - ("short_name", models.CharField(max_length=40, verbose_name="name")), - ( - "description", - models.CharField(max_length=1000, verbose_name="description"), - ), + ('basemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='portal.basemodel')), + ('short_name', models.CharField(max_length=40, verbose_name='name')), + ('description', models.CharField(max_length=1000, verbose_name='description')), ], - bases=("portal.basemodel",), + bases=('portal.basemodel',), ), migrations.CreateModel( - name="VolunteerProfile", + name='VolunteerProfile', fields=[ - ( - "basemodel_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="portal.basemodel", - ), - ), - ( - "application_status", - models.CharField( - choices=[ - ( - volunteer.constants.ApplicationStatus["PENDING"], - volunteer.constants.ApplicationStatus["PENDING"], - ), - ( - volunteer.constants.ApplicationStatus["APPROVED"], - volunteer.constants.ApplicationStatus["APPROVED"], - ), - ( - volunteer.constants.ApplicationStatus["REJECTED"], - volunteer.constants.ApplicationStatus["REJECTED"], - ), - ( - volunteer.constants.ApplicationStatus["CANCELLED"], - volunteer.constants.ApplicationStatus["CANCELLED"], - ), - ], - default=volunteer.constants.ApplicationStatus["PENDING"], - max_length=50, - ), - ), - ("coc_agreement", models.BooleanField(default=False)), - ( - "github_username", - models.CharField(blank=True, max_length=50, null=True), - ), - ( - "discord_username", - models.CharField(blank=True, max_length=50, null=True), - ), - ( - "instagram_username", - models.CharField(blank=True, max_length=50, null=True), - ), - ( - "bluesky_username", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "mastodon_url", - models.CharField(blank=True, max_length=100, null=True), - ), - ("x_username", models.CharField(blank=True, max_length=100, null=True)), - ( - "linkedin_url", - models.CharField(blank=True, max_length=100, null=True), - ), - ("pronouns", models.CharField(blank=True, max_length=100, null=True)), - ( - "languages_spoken", - portal.models.ChoiceArrayField( - base_field=models.CharField( - blank=True, - choices=[ - ("af", "Afrikaans"), - ("ar", "Arabic"), - ("ar-dz", "Algerian Arabic"), - ("ast", "Asturian"), - ("az", "Azerbaijani"), - ("bg", "Bulgarian"), - ("be", "Belarusian"), - ("bn", "Bengali"), - ("br", "Breton"), - ("bs", "Bosnian"), - ("ca", "Catalan"), - ("ckb", "Central Kurdish (Sorani)"), - ("cs", "Czech"), - ("cy", "Welsh"), - ("da", "Danish"), - ("de", "German"), - ("dsb", "Lower Sorbian"), - ("el", "Greek"), - ("en", "English"), - ("en-au", "Australian English"), - ("en-gb", "British English"), - ("eo", "Esperanto"), - ("es", "Spanish"), - ("es-ar", "Argentinian Spanish"), - ("es-co", "Colombian Spanish"), - ("es-mx", "Mexican Spanish"), - ("es-ni", "Nicaraguan Spanish"), - ("es-ve", "Venezuelan Spanish"), - ("et", "Estonian"), - ("eu", "Basque"), - ("fa", "Persian"), - ("fi", "Finnish"), - ("fr", "French"), - ("fy", "Frisian"), - ("ga", "Irish"), - ("gd", "Scottish Gaelic"), - ("gl", "Galician"), - ("he", "Hebrew"), - ("hi", "Hindi"), - ("hr", "Croatian"), - ("hsb", "Upper Sorbian"), - ("hu", "Hungarian"), - ("hy", "Armenian"), - ("ia", "Interlingua"), - ("id", "Indonesian"), - ("ig", "Igbo"), - ("io", "Ido"), - ("is", "Icelandic"), - ("it", "Italian"), - ("ja", "Japanese"), - ("ka", "Georgian"), - ("kab", "Kabyle"), - ("kk", "Kazakh"), - ("km", "Khmer"), - ("kn", "Kannada"), - ("ko", "Korean"), - ("ky", "Kyrgyz"), - ("lb", "Luxembourgish"), - ("lt", "Lithuanian"), - ("lv", "Latvian"), - ("mk", "Macedonian"), - ("ml", "Malayalam"), - ("mn", "Mongolian"), - ("mr", "Marathi"), - ("ms", "Malay"), - ("my", "Burmese"), - ("nb", "Norwegian Bokmål"), - ("ne", "Nepali"), - ("nl", "Dutch"), - ("nn", "Norwegian Nynorsk"), - ("os", "Ossetic"), - ("pa", "Punjabi"), - ("pl", "Polish"), - ("pt", "Portuguese"), - ("pt-br", "Brazilian Portuguese"), - ("ro", "Romanian"), - ("ru", "Russian"), - ("sk", "Slovak"), - ("sl", "Slovenian"), - ("sq", "Albanian"), - ("sr", "Serbian"), - ("sr-latn", "Serbian Latin"), - ("sv", "Swedish"), - ("sw", "Swahili"), - ("ta", "Tamil"), - ("te", "Telugu"), - ("tg", "Tajik"), - ("th", "Thai"), - ("tk", "Turkmen"), - ("tr", "Turkish"), - ("tt", "Tatar"), - ("udm", "Udmurt"), - ("ug", "Uyghur"), - ("uk", "Ukrainian"), - ("ur", "Urdu"), - ("uz", "Uzbek"), - ("vi", "Vietnamese"), - ("zh-hans", "Simplified Chinese"), - ("zh-hant", "Traditional Chinese"), - ], - max_length=32, - ), - size=None, - ), - ), - ( - "pyladies_chapter", - models.CharField(blank=True, max_length=50, null=True), - ), - ( - "timezone", - models.CharField( - choices=[ - ("UTC+14", "UTC+14"), - ("UTC+13", "UTC+13"), - ("UTC+12", "UTC+12"), - ("UTC+11", "UTC+11"), - ("UTC+10", "UTC+10"), - ("UTC+9", "UTC+9"), - ("UTC+8", "UTC+8"), - ("UTC+7", "UTC+7"), - ("UTC+6", "UTC+6"), - ("UTC+5", "UTC+5"), - ("UTC+4", "UTC+4"), - ("UTC+3", "UTC+3"), - ("UTC+2", "UTC+2"), - ("UTC+1", "UTC+1"), - ("UTC", "UTC"), - ("UTC-1", "UTC-1"), - ("UTC-2", "UTC-2"), - ("UTC-3", "UTC-3"), - ("UTC-4", "UTC-4"), - ("UTC-5", "UTC-5"), - ("UTC-6", "UTC-6"), - ("UTC-7", "UTC-7"), - ("UTC-8", "UTC-8"), - ("UTC-9", "UTC-9"), - ("UTC-10", "UTC-10"), - ("UTC-11", "UTC-11"), - ("UTC-12", "UTC-12"), - ], - max_length=6, - ), - ), - ( - "roles", - models.ManyToManyField( - blank=True, - related_name="roles", - to="volunteer.role", - verbose_name="Roles", - ), - ), - ( - "teams", - models.ManyToManyField( - blank=True, - related_name="team", - to="volunteer.team", - verbose_name="team", - ), - ), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), + ('basemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='portal.basemodel')), + ('application_status', models.CharField(choices=[(volunteer.constants.ApplicationStatus['PENDING'], volunteer.constants.ApplicationStatus['PENDING']), (volunteer.constants.ApplicationStatus['APPROVED'], volunteer.constants.ApplicationStatus['APPROVED']), (volunteer.constants.ApplicationStatus['REJECTED'], volunteer.constants.ApplicationStatus['REJECTED']), (volunteer.constants.ApplicationStatus['CANCELLED'], volunteer.constants.ApplicationStatus['CANCELLED'])], default=volunteer.constants.ApplicationStatus['PENDING'], max_length=50)), + ('github_username', models.CharField(blank=True, max_length=50, null=True)), + ('discord_username', models.CharField(blank=True, max_length=50, null=True)), + ('instagram_username', models.CharField(blank=True, max_length=50, null=True)), + ('bluesky_username', models.CharField(blank=True, max_length=100, null=True)), + ('mastodon_url', models.CharField(blank=True, max_length=100, null=True)), + ('x_username', models.CharField(blank=True, max_length=100, null=True)), + ('linkedin_url', models.CharField(blank=True, max_length=100, null=True)), + ('languages_spoken', portal.models.ChoiceArrayField(base_field=models.CharField(blank=True, choices=[('af', 'Afrikaans'), ('ar', 'Arabic'), ('ar-dz', 'Algerian Arabic'), ('ast', 'Asturian'), ('az', 'Azerbaijani'), ('bg', 'Bulgarian'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('br', 'Breton'), ('bs', 'Bosnian'), ('ca', 'Catalan'), ('ckb', 'Central Kurdish (Sorani)'), ('cs', 'Czech'), ('cy', 'Welsh'), ('da', 'Danish'), ('de', 'German'), ('dsb', 'Lower Sorbian'), ('el', 'Greek'), ('en', 'English'), ('en-au', 'Australian English'), ('en-gb', 'British English'), ('eo', 'Esperanto'), ('es', 'Spanish'), ('es-ar', 'Argentinian Spanish'), ('es-co', 'Colombian Spanish'), ('es-mx', 'Mexican Spanish'), ('es-ni', 'Nicaraguan Spanish'), ('es-ve', 'Venezuelan Spanish'), ('et', 'Estonian'), ('eu', 'Basque'), ('fa', 'Persian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Frisian'), ('ga', 'Irish'), ('gd', 'Scottish Gaelic'), ('gl', 'Galician'), ('he', 'Hebrew'), ('hi', 'Hindi'), ('hr', 'Croatian'), ('hsb', 'Upper Sorbian'), ('hu', 'Hungarian'), ('hy', 'Armenian'), ('ia', 'Interlingua'), ('id', 'Indonesian'), ('ig', 'Igbo'), ('io', 'Ido'), ('is', 'Icelandic'), ('it', 'Italian'), ('ja', 'Japanese'), ('ka', 'Georgian'), ('kab', 'Kabyle'), ('kk', 'Kazakh'), ('km', 'Khmer'), ('kn', 'Kannada'), ('ko', 'Korean'), ('ky', 'Kyrgyz'), ('lb', 'Luxembourgish'), ('lt', 'Lithuanian'), ('lv', 'Latvian'), ('mk', 'Macedonian'), ('ml', 'Malayalam'), ('mn', 'Mongolian'), ('mr', 'Marathi'), ('ms', 'Malay'), ('my', 'Burmese'), ('nb', 'Norwegian Bokmål'), ('ne', 'Nepali'), ('nl', 'Dutch'), ('nn', 'Norwegian Nynorsk'), ('os', 'Ossetic'), ('pa', 'Punjabi'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('pt-br', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('sq', 'Albanian'), ('sr', 'Serbian'), ('sr-latn', 'Serbian Latin'), ('sv', 'Swedish'), ('sw', 'Swahili'), ('ta', 'Tamil'), ('te', 'Telugu'), ('tg', 'Tajik'), ('th', 'Thai'), ('tk', 'Turkmen'), ('tr', 'Turkish'), ('tt', 'Tatar'), ('udm', 'Udmurt'), ('ug', 'Uyghur'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese')], max_length=32), size=None)), + ('pyladies_chapter', models.CharField(blank=True, max_length=50, null=True)), + ('timezone', models.CharField(choices=[('UTC+14', 'UTC+14'), ('UTC+13', 'UTC+13'), ('UTC+12', 'UTC+12'), ('UTC+11', 'UTC+11'), ('UTC+10', 'UTC+10'), ('UTC+9', 'UTC+9'), ('UTC+8', 'UTC+8'), ('UTC+7', 'UTC+7'), ('UTC+6', 'UTC+6'), ('UTC+5', 'UTC+5'), ('UTC+4', 'UTC+4'), ('UTC+3', 'UTC+3'), ('UTC+2', 'UTC+2'), ('UTC+1', 'UTC+1'), ('UTC', 'UTC'), ('UTC-1', 'UTC-1'), ('UTC-2', 'UTC-2'), ('UTC-3', 'UTC-3'), ('UTC-4', 'UTC-4'), ('UTC-5', 'UTC-5'), ('UTC-6', 'UTC-6'), ('UTC-7', 'UTC-7'), ('UTC-8', 'UTC-8'), ('UTC-9', 'UTC-9'), ('UTC-10', 'UTC-10'), ('UTC-11', 'UTC-11'), ('UTC-12', 'UTC-12')], max_length=6)), + ('roles', models.ManyToManyField(blank=True, related_name='roles', to='volunteer.role', verbose_name='Roles')), + ('teams', models.ManyToManyField(blank=True, related_name='team', to='volunteer.team', verbose_name='team')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], - bases=("portal.basemodel",), + bases=('portal.basemodel',), ), migrations.AddField( - model_name="team", - name="team_leads", - field=models.ManyToManyField( - related_name="team_leads", - to="volunteer.volunteerprofile", - verbose_name="team leads", - ), + model_name='team', + name='team_leads', + field=models.ManyToManyField(related_name='team_leads', to='volunteer.volunteerprofile', verbose_name='team leads'), ), - migrations.RunPython(create_initial_roles, migrations.RunPython.noop), ] diff --git a/volunteer/migrations/0002_remove_volunteerprofile_coc_agreement_and_more.py b/volunteer/migrations/0002_remove_volunteerprofile_coc_agreement_and_more.py deleted file mode 100644 index 2c26f98..0000000 --- a/volunteer/migrations/0002_remove_volunteerprofile_coc_agreement_and_more.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-23 20:38 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("volunteer", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="volunteerprofile", - name="coc_agreement", - ), - migrations.RemoveField( - model_name="volunteerprofile", - name="pronouns", - ), - ] diff --git a/volunteer/models.py b/volunteer/models.py index d2b7f9a..0d6f8ed 100644 --- a/volunteer/models.py +++ b/volunteer/models.py @@ -2,7 +2,7 @@ from django.conf.global_settings import LANGUAGES from django.contrib.auth.models import User -from portal.models import BaseModel, ChoiceArrayField +from portal.models import BaseModel from .constants import ApplicationStatus from django.urls import reverse @@ -85,8 +85,11 @@ class VolunteerProfile(BaseModel): mastodon_url = models.CharField(max_length=100, blank=True, null=True) x_username = models.CharField(max_length=100, blank=True, null=True) linkedin_url = models.CharField(max_length=100, blank=True, null=True) - languages_spoken = ChoiceArrayField( - models.CharField(max_length=32, blank=True, choices=LANGUAGES) + languages_spoken = models.CharField( + max_length=255, + blank=True, + null=True, + help_text="Comma-separated list of languages" ) teams = models.ManyToManyField( "volunteer.Team", verbose_name="team", related_name="team", blank=True diff --git a/volunteer/urls.py b/volunteer/urls.py index f9f3686..fa53886 100644 --- a/volunteer/urls.py +++ b/volunteer/urls.py @@ -1,6 +1,5 @@ from django.contrib.auth.decorators import login_required -from django.urls import path - +from django.urls import path, include from . import views app_name = "volunteer" diff --git a/volunteer/views.py b/volunteer/views.py index 7daed81..97621a7 100644 --- a/volunteer/views.py +++ b/volunteer/views.py @@ -4,6 +4,8 @@ from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.urls import reverse_lazy +from django.http import HttpResponse + from .models import VolunteerProfile from .forms import VolunteerProfileForm @@ -48,3 +50,6 @@ class VolunteerProfileUpdate(UpdateView): class VolunteerProfileDelete(DeleteView): model = VolunteerProfile success_url = reverse_lazy("volunteer:index") + +def sponsorship_success(request): + return HttpResponse("Thank you for submitting your sponsorship profile!")