Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
cca40df
Add sponsorship profile and Tier models with admin and forms
lidokogi Mar 31, 2025
8ac7ae0
Merge branch 'main' of https://github.com/lidokogi/pyladiescon-portal
lidokogi Apr 1, 2025
14781a1
Finalize Sponsorship form and clean up error handling
lidokogi Apr 1, 2025
cc35880
Remove db.sqlite3 from repo and add it to .gitignore
lidokogi Apr 1, 2025
b9fbd07
Update .gitignore to ignore __pycache__ and Python bytecode files
lidokogi Apr 1, 2025
1003bf2
Update portal/settings.py
lidokogi Apr 1, 2025
c7b0dca
Remove __pycache__ and .pyc files from repo
lidokogi Apr 1, 2025
bf317ba
Merge branch 'main' of https://github.com/lidokogi/pyladiescon-portal
lidokogi Apr 1, 2025
193324b
Update Homepage to Link to Sponsorship Form
lidokogi Apr 1, 2025
94f60d5
Update portal/settings.py
lidokogi Apr 1, 2025
cb2fe7c
Update portal/settings.py
lidokogi Apr 1, 2025
eeef289
Update portal/settings.py
lidokogi Apr 1, 2025
649691d
Update portal/settings.py
lidokogi Apr 1, 2025
957b63a
Delete image files
lidokogi Apr 7, 2025
79e5554
Merge branch 'main' of https://github.com/lidokogi/pyladiescon-portal
lidokogi Apr 7, 2025
4ca02de
Make main_contact readonly and finalize sponsorship form behavior
lidokogi Apr 7, 2025
9feecb3
Temporarily remove additional_contacts field to simplify PR as suggested
lidokogi Apr 7, 2025
3891194
Refactor application_status to use Python StrEnum for improved clarity
lidokogi Apr 7, 2025
2d16a68
Add Pillow dependency for ImageField support
lidokogi Apr 7, 2025
8ee3350
Merge branch 'main' into main
lidokogi Apr 7, 2025
91ac57f
Implement Sponsorship App changes
lidokogi Jul 3, 2025
62eddea
Add python-dotenv to fix missing module error in CI
lidokogi Jul 4, 2025
c7ea9f0
Fix: Add python-dotenv to correct requirements file used by CI
lidokogi Jul 4, 2025
f00df82
Fix import order
lidokogi Jul 9, 2025
ecdbe07
Add complete tests for sponsorship profile
lidokogi Jul 9, 2025
fbdf682
Remove media files from repo and ignore media directory
lidokogi Jul 10, 2025
1a740bb
Revert ALLOWED_HOSTS to original env-based configuration
lidokogi Jul 10, 2025
0c59ff3
Revert default DB name and password to 'postgres' and 'password'
lidokogi Jul 10, 2025
c0b50fc
Move sponsorship URL include to portal/urls.py as requested
lidokogi Jul 10, 2025
f6c0df5
Squash sponsorship migrations into one initial migration
lidokogi Jul 10, 2025
5325bc3
Revert volunteer to migration 0006 and remove 0007 & 0008
lidokogi Jul 10, 2025
7945388
Update templates/portal/index.html
lidokogi Jul 10, 2025
b0d04c2
Update templates/portal/index.html
lidokogi Jul 10, 2025
9eeebef
chore: apply code formatting from make reformat
lidokogi Jul 11, 2025
c32dfae
Update sponsorship/templates/sponsorship/sponsorship_profile_form.html
lidokogi Jul 11, 2025
e2eef86
Update sponsorship/templates/sponsorship/sponsorship_profile_form.html
lidokogi Jul 11, 2025
9d7d374
Update sponsorship/templates/sponsorship/sponsorship_profile_form.html
lidokogi Jul 11, 2025
3b3b0e4
Fix linter errors: quotes, method case, endblock names
lidokogi Jul 11, 2025
bd388e2
Update sponsorship/templates/sponsorship/create_profile.html
lidokogi Jul 12, 2025
931f0dc
Update sponsorship/templates/sponsorship/sponsorship_profile_form.html
lidokogi Jul 12, 2025
cf3f557
Remove dotenv function from settings
lidokogi Jul 12, 2025
c79c6ab
fix errors
lidokogi Jul 14, 2025
d6723e3
Fix test logic with correct image function
lidokogi Jul 16, 2025
cd8f6e7
Implement sponsorship email notification logic
lidokogi Jul 25, 2025
39348d0
Resolved merge conflicts
lidokogi Jul 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ staticroot/

site/
htmlcov/
/media/

16 changes: 13 additions & 3 deletions portal/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

import dj_database_url

# from dotenv import load_dotenv

# load_dotenv()


# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

Expand All @@ -28,9 +33,9 @@
# 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 = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost")
ALLOWED_HOSTS = ALLOWED_HOSTS.split(",")


# Application definition

Expand All @@ -49,9 +54,11 @@
"allauth.account",
"storages",
"portal",
'sponsorship',
"volunteer",
"portal_account",
"widget_tweaks",
"sponsorship",
]
DJANGO_TABLES2_TEMPLATE = "portal/base-tables-responsive.html"

Expand Down Expand Up @@ -110,6 +117,7 @@
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}



# Password validation
Expand Down Expand Up @@ -298,3 +306,5 @@
"LOCATION": "stats_cache_table",
}
}

DEBUG = True
1 change: 1 addition & 0 deletions portal/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
path("volunteer/", include("volunteer.urls", namespace="volunteer")),
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
path("sponsorship/", include("sponsorship.urls", namespace="sponsorship")),
path(
"portal_account/",
include("portal_account.urls", namespace="portal_account"),
Expand Down
3 changes: 3 additions & 0 deletions portal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ def index(request):
context["teams"] = teams

return render(request, "portal/index.html", context)

def sponsorship_success(request):
return render(request, "sponsorship/success.html")
2 changes: 1 addition & 1 deletion portal_account/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from . import views

app_name = "volunteer"
app_name = "portal_account"

urlpatterns = [
path("", views.index, name="index"),
Expand Down
1 change: 1 addition & 0 deletions requirements-app.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ boto3==1.38.5
django-storages==1.14.6
django-widget-tweaks==1.5.0
pillow==11.2.1
python-dotenv
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pytest-django==4.8.0
pytest==8.3.5
pytest-cov==6.1.1
coverage==7.7.0
python-dotenv
3 changes: 2 additions & 1 deletion requirements-docs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ mkdocs-awesome-nav==3.1.1
mkdocs-material[imaging]
mkdocs-rss-plugin
mkdocs-git-revision-date-localized-plugin
mkdocs-git-committers-plugin-2
mkdocs-git-committers-plugin-2
python-dotenv
13 changes: 13 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
-r requirements-docs.txt
python-dotenv
asgiref==3.8.1
Django==5.1.7
django-allauth==65.5.0
gunicorn==23.0.0
packaging==24.2
psycopg2-binary==2.9.10
sqlparse==0.5.3
django-bootstrap5==25.1
whitenoise==6.9.0
dj-database-url==2.3.0
Pillow==11.1.0

Empty file added sponsorship/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions sponsorship/admin.py
Original file line number Diff line number Diff line change
@@ -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')
8 changes: 8 additions & 0 deletions sponsorship/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig

class SponsorshipConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sponsorship'

def ready(self):
import sponsorship.signals # this registers the signals
47 changes: 47 additions & 0 deletions sponsorship/emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.conf import settings

def send_sponsorship_status_emails(profile):
user = profile.user

# Email to sponsor
sponsor_subject = "Your Sponsorship Profile Has Been Approved"
sponsor_message = render_to_string("sponsorship/email/sponsor_status_update.txt", {
"user": user,
"profile": profile
})
send_mail(
sponsor_subject,
sponsor_message,
settings.DEFAULT_FROM_EMAIL,
[user.email]
)

# Email to internal team (hardcoded for now)
team_subject = f"New Sponsorship Approved: {profile.organization_name}"
team_message = render_to_string("sponsorship/email/team_status_notification.txt", {
"user": user,
"profile": profile
})
send_mail(
team_subject,
team_message,
settings.DEFAULT_FROM_EMAIL,
["[email protected]"] # Replace with actual team emails later
)
def send_sponsorship_profile_email(user, profile, is_update=False):
subject = "Sponsorship Profile Submission Received"
message = render_to_string("sponsorship/email/sponsor_status_update.txt", {
"user": user,
"profile": profile,
"is_update": is_update
})

send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[user.email],
fail_silently=False,
)
37 changes: 37 additions & 0 deletions sponsorship/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django import forms

from .models import SponsorshipProfile


class SponsorshipProfileForm(forms.ModelForm):
class Meta:
model = SponsorshipProfile
fields = [
'main_contact',
'sponsor_organization_name',
'sponsorship_type',
'sponsorship_tier',
'logo',
'company_description',
'application_status',
]
widgets = {
'company_description': forms.Textarea(attrs={'rows': 4,}),
}

def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None) # Expecting current user from the view
super().__init__(*args, **kwargs)

if user:
self.fields["main_contact"].initial = user
self.fields["main_contact"].disabled = True # Makes it read-only

def save(self, commit=True):
instance = super().save(commit=False)
instance.main_contact = self._user # Enforce value
instance.application_status = 'pending' # Set status manually
if commit:
instance.save()
self.save_m2m()
return instance
41 changes: 41 additions & 0 deletions sponsorship/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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')),
],
),
]
Empty file.
45 changes: 45 additions & 0 deletions sponsorship/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.db import models
from django.contrib.auth.models import User
from enum import StrEnum


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 ApplicationStatus(StrEnum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
CANCELLED = "cancelled"

class SponsorshipProfile(models.Model):
INDIVIDUAL = 'individual'
ORGANIZATION = 'organization'

SPONSORSHIP_TYPE_CHOICES = [
(INDIVIDUAL, 'Individual'),
(ORGANIZATION, 'Organization/Company'),
]

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=[(status.value, status.name.capitalize()) for status in ApplicationStatus],
default=ApplicationStatus.PENDING.value,
)


def __str__(self):
return self.sponsor_organization_name
59 changes: 59 additions & 0 deletions sponsorship/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import SponsorshipProfile
from django.conf import settings
from django.template.loader import render_to_string
from django.core.mail import EmailMultiAlternatives
from django.contrib.sites.models import Site


def _send_email(subject, recipient_list, *, html_template=None, text_template=None, context=None):
context = context or {}
context["current_site"] = Site.objects.get_current()

text_content = render_to_string(text_template, context)
html_content = render_to_string(html_template, context)

msg = EmailMultiAlternatives(
subject,
text_content,
settings.DEFAULT_FROM_EMAIL,
recipient_list,
)
msg.attach_alternative(html_content, "text/html")
msg.send()


@receiver(post_save, sender=SponsorshipProfile)
def sponsorship_profile_signal(sender, instance, created, **kwargs):
"""Send emails when sponsorship profile is submitted or approved."""
if created:
# Email on submission
subject = f"{settings.ACCOUNT_EMAIL_SUBJECT_PREFIX} Sponsorship Application Received"
_send_email(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is calling the internal _send_email() function. Should it call either the send_sponsorship_profile_email or send_sponsorship_status_emails?

subject,
[instance.user.email],
html_template="sponsorship/email/sponsor_status_update.html",
text_template="sponsorship/email/sponsor_status_update.txt",
context={"profile": instance},
)
elif instance.application_status == "approved":
# Email on approval
subject = f"{settings.ACCOUNT_EMAIL_SUBJECT_PREFIX} Sponsorship Profile Approved"
_send_email(
subject,
[instance.user.email],
html_template="sponsorship/email/sponsor_approved.html",
text_template="sponsorship/email/sponsor_approved.txt",
context={"profile": instance},
)

# Internal team notification
internal_subject = f"{settings.ACCOUNT_EMAIL_SUBJECT_PREFIX} New Sponsorship Approved: {instance.organization_name}"
_send_email(
internal_subject,
["[email protected]"], # Replace with real internal emails later
html_template="sponsorship/email/team_status_notification.html",
text_template="sponsorship/email/team_status_notification.txt",
context={"profile": instance},
)
13 changes: 13 additions & 0 deletions sponsorship/templates/sponsorship/create_profile.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<h2>
Create Sponsorship Profile
</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">
Submit
</button>
</form>
{% endblock content %}
7 changes: 7 additions & 0 deletions sponsorship/templates/sponsorship/email/sponsor_approved.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>Hi {{ profile.user.first_name }},</p>

<p>Congratulations! Your sponsorship application for {{ profile.organization_name }} has been approved 🎉</p>

<p>We're excited to have you as a sponsor for PyLadiesCon. A team member will reach out soon with next steps and onboarding information.</p>

<p>Best,<br>The PyLadiesCon Team</p>
Loading