├── common ├── __init__.py ├── send_emails.py ├── mixins.py └── pretix_wrapper.py ├── portal ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── makesuperuser.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_basemodel_creation_date_and_more.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── portal_extras.py ├── static │ ├── favicon.png │ └── styles.css ├── asgi.py ├── wsgi.py ├── validators.py ├── models.py ├── forms.py ├── views.py └── urls.py ├── tests ├── __init__.py ├── attendee │ └── __init__.py ├── portal │ ├── __init__.py │ └── test_portal_extras.py ├── volunteer │ └── __init__.py ├── webhooks │ └── __init__.py ├── portal_account │ ├── __init__.py │ └── test_models.py ├── sponsorship │ ├── __init__.py │ ├── test_img.png │ └── test_admin.py └── common │ └── __init__.py ├── attendee ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── fetch_pretix_orders.py ├── migrations │ ├── __init__.py │ └── 0003_alter_attendeeprofile_participated_in_previous_event.py └── admin.py ├── sponsorship ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0008_sponsorshipprofile_github_issue_url.py │ ├── 0006_sponsorshipprofile_po_number.py │ ├── 0005_alter_sponsorshipprofile_progress_status.py │ ├── 0004_sponsorshipprofile_organization_address_and_more.py │ ├── 0007_individualdonation.py │ └── 0002_sponsorshiptier_and_more.py ├── constants.py ├── templates │ └── sponsorship │ │ ├── email │ │ ├── team_status_notification.txt │ │ ├── sponsor_status_update.txt │ │ ├── sponsor_approved.txt │ │ ├── team_status_notification.html │ │ ├── sponsor_status_update.html │ │ ├── sponsor_approved.html │ │ └── psf_invoice_request.md │ │ ├── create_profile.html │ │ ├── sponsorship_charts.html │ │ └── sponsorshipprofile_list.html ├── apps.py ├── urls.py ├── emails.py └── forms.py ├── volunteer ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── unmigrate_volunteer_language.py │ │ └── migrate_volunteer_language.py ├── migrations │ ├── __init__.py │ ├── 0009_remove_volunteerprofile_pyladies_chapter.py │ ├── 0012_pyladieschapter_logo.py │ ├── 0002_remove_volunteerprofile_coc_agreement_and_more.py │ ├── 0011_populate_languages.py │ ├── 0006_alter_volunteerprofile_teams.py │ ├── 0003_alter_volunteerprofile_discord_username.py │ ├── 0007_team_open_to_new_members_and_more.py │ └── 0008_pyladieschapter_volunteerprofile_chapter.py ├── apps.py ├── constants.py └── urls.py ├── webhooks ├── __init__.py └── urls.py ├── portal_account ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_portalprofile_tos_agreement.py │ ├── 0002_portalprofile_profile_picture.py │ └── 0001_initial.py ├── apps.py ├── admin.py ├── urls.py ├── models.py ├── forms.py └── views.py ├── storage_backend ├── __init__.py └── custom_storage.py ├── templates ├── allauth │ ├── elements │ │ ├── hr.html │ │ ├── fields.html │ │ ├── th.html │ │ ├── tr.html │ │ ├── alert.html │ │ ├── table.html │ │ ├── tbody.html │ │ ├── thead.html │ │ ├── button_group.html │ │ ├── p.html │ │ ├── provider_list.html │ │ ├── h1.html │ │ ├── h2.html │ │ ├── provider.html │ │ ├── img.html │ │ ├── td.html │ │ ├── badge.html │ │ ├── form.html │ │ ├── panel.html │ │ ├── button.html │ │ └── field.html │ └── layouts │ │ ├── entrance.html │ │ └── manage.html ├── mfa │ ├── base_manage.html │ ├── base_entrance.html │ ├── recovery_codes │ │ ├── download.txt │ │ ├── base.html │ │ ├── generate.html │ │ └── index.html │ ├── messages │ │ ├── webauthn_added.txt │ │ ├── webauthn_removed.txt │ │ ├── totp_activated.txt │ │ ├── totp_deactivated.txt │ │ └── recovery_codes_generated.txt │ ├── email │ │ ├── webauthn_added_subject.txt │ │ ├── webauthn_removed_subject.txt │ │ ├── totp_activated_subject.txt │ │ ├── totp_deactivated_subject.txt │ │ ├── recovery_codes_generated_subject.txt │ │ ├── totp_activated_message.txt │ │ ├── totp_deactivated_message.txt │ │ ├── webauthn_added_message.txt │ │ ├── webauthn_removed_message.txt │ │ └── recovery_codes_generated_message.txt │ ├── totp │ │ ├── base.html │ │ ├── deactivate_form.html │ │ └── activate_form.html │ ├── webauthn │ │ ├── base.html │ │ ├── snippets │ │ │ ├── scripts.html │ │ │ └── login_script.html │ │ ├── edit_form.html │ │ ├── authenticator_confirm_delete.html │ │ ├── add_form.html │ │ ├── reauthenticate.html │ │ ├── signup_form.html │ │ └── authenticator_list.html │ └── reauthenticate.html ├── account │ ├── base_manage.html │ ├── base_entrance.html │ ├── base_manage_email.html │ ├── base_manage_password.html │ ├── base_manage_phone.html │ ├── messages │ │ ├── logged_out.txt │ │ ├── password_set.txt │ │ ├── email_confirmed.txt │ │ ├── email_deleted.txt │ │ ├── password_changed.txt │ │ ├── primary_email_set.txt │ │ ├── phone_verified.txt │ │ ├── email_confirmation_sent.txt │ │ ├── login_code_sent.txt │ │ ├── unverified_primary_email.txt │ │ ├── cannot_delete_primary_email.txt │ │ ├── logged_in.txt │ │ └── email_confirmation_failed.txt │ ├── email │ │ ├── email_confirmation_signup_message.txt │ │ ├── email_confirmation_signup_subject.txt │ │ ├── login_code_subject.txt │ │ ├── email_changed_subject.txt │ │ ├── email_deleted_subject.txt │ │ ├── password_reset_subject.txt │ │ ├── password_set_subject.txt │ │ ├── email_confirm_subject.txt │ │ ├── password_changed_subject.txt │ │ ├── unknown_account_subject.txt │ │ ├── password_reset_code_subject.txt │ │ ├── password_reset_key_subject.txt │ │ ├── account_already_exists_subject.txt │ │ ├── email_confirmation_subject.txt │ │ ├── password_set_message.txt │ │ ├── email_confirm_message.txt │ │ ├── password_reset_message.txt │ │ ├── password_changed_message.txt │ │ ├── email_changed_message.txt │ │ ├── email_deleted_message.txt │ │ ├── base_message.txt │ │ ├── login_code_message.txt │ │ ├── password_reset_code_message.txt │ │ ├── account_already_exists_message.txt │ │ ├── password_reset_key_message.txt │ │ ├── base_notification.txt │ │ ├── unknown_account_message.txt │ │ └── email_confirmation_message.txt │ ├── snippets │ │ ├── warn_no_email.html │ │ └── already_logged_in.html │ ├── account_inactive.html │ ├── password_reset_from_key_done.html │ ├── signup_closed.html │ ├── confirm_phone_verification_code.html │ ├── confirm_password_reset_code.html │ ├── confirm_email_verification_code.html │ ├── password_reset_done.html │ ├── confirm_login_code.html │ ├── verification_sent.html │ ├── logout.html │ ├── password_set.html │ ├── reauthenticate.html │ ├── password_change.html │ ├── base_reauthenticate.html │ ├── request_login_code.html │ ├── verified_email_required.html │ ├── password_reset.html │ ├── signup_by_passkey.html │ ├── email_confirm.html │ ├── password_reset_from_key.html │ ├── phone_change.html │ ├── base_confirm_code.html │ ├── login.html │ └── email_change.html ├── socialaccount │ ├── base_manage.html │ ├── base_entrance.html │ ├── snippets │ │ ├── login_extra.html │ │ ├── login.html │ │ └── provider_list.html │ ├── messages │ │ ├── account_connected_updated.txt │ │ ├── account_connected.txt │ │ ├── account_disconnected.txt │ │ └── account_connected_other.txt │ ├── email │ │ ├── account_connected_subject.txt │ │ ├── account_disconnected_subject.txt │ │ ├── account_connected_message.txt │ │ └── account_disconnected_message.txt │ ├── login_redirect.html │ ├── authentication_error.html │ ├── login_cancelled.html │ ├── signup.html │ ├── login.html │ └── connections.html ├── emails │ ├── sponsorship │ │ ├── internal_sponsor_onboarding.md │ │ ├── internal_sponsor_updated.md │ │ └── sponsor_information_partial.md │ ├── base_email.md │ └── volunteer │ │ ├── internal_volunteer_profile_email_notification.md │ │ ├── volunteer_cancellation_confirmation.md │ │ ├── team_is_now_closed.md │ │ ├── volunteer_profile_email_notification.md │ │ └── team_lead_cancellation_notification.md ├── portal │ ├── base-tables-responsive.html │ ├── registration_callout.html │ ├── sponsor_donate_callout.html │ ├── historical_comparison_charts.html │ └── dashboard_gallery.html ├── volunteer │ ├── volunteerprofile_list.html │ ├── volunteer_charts.html │ └── volunteer_stats.html ├── portal_account │ ├── portalprofile_edit.html │ └── index.html ├── pyladies_chapter │ └── index.html └── team │ └── index.html ├── requirements.txt ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── docs ├── assets │ ├── logo.png │ └── favicon.ico ├── user │ ├── sponsor.md │ ├── core_team.md │ ├── get_started.md │ └── volunteer.md ├── policies │ └── terms_of_use.md ├── about │ ├── team.md │ └── index.md ├── developer │ ├── deployment.md │ └── contributing.md └── index.md ├── locale └── pt_BR │ └── LC_MESSAGES │ └── django.mo ├── netlify.toml ├── Procfile ├── setup.cfg ├── requirements-docs.txt ├── requirements-dev.txt ├── CONTRIBUTING.md ├── config └── gunicorn.conf.py ├── .gitignore ├── pyproject.toml ├── requirements-app.txt ├── manage.py ├── LICENSE ├── Dockerfile ├── compose.yml └── conftest.py /common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /attendee/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sponsorship/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /volunteer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webhooks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal_account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/attendee/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/portal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/volunteer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/webhooks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /attendee/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /attendee/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/portal_account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sponsorship/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /volunteer/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /volunteer/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal_account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sponsorship/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /attendee/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/allauth/elements/hr.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Common tests 2 | -------------------------------------------------------------------------------- /volunteer/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements-docs.txt 2 | -------------------------------------------------------------------------------- /templates/allauth/elements/fields.html: -------------------------------------------------------------------------------- 1 | {{ attrs.form.as_p }} 2 | -------------------------------------------------------------------------------- /templates/mfa/base_manage.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/manage.html" %} 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://2025.conference.pyladies.com/en/donate/ 2 | -------------------------------------------------------------------------------- /templates/account/base_manage.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/manage.html" %} 2 | -------------------------------------------------------------------------------- /templates/mfa/base_entrance.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/entrance.html" %} 2 | -------------------------------------------------------------------------------- /templates/account/base_entrance.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/entrance.html" %} 2 | -------------------------------------------------------------------------------- /templates/account/base_manage_email.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage.html" %} 2 | -------------------------------------------------------------------------------- /templates/account/base_manage_password.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage.html" %} 2 | -------------------------------------------------------------------------------- /templates/account/base_manage_phone.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage.html" %} 2 | -------------------------------------------------------------------------------- /templates/socialaccount/base_manage.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/manage.html" %} 2 | -------------------------------------------------------------------------------- /templates/socialaccount/base_entrance.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/entrance.html" %} 2 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyladies/pyladiescon-portal/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /templates/mfa/recovery_codes/download.txt: -------------------------------------------------------------------------------- 1 | {% for code in unused_codes %}{{ code }} 2 | {% endfor %} 3 | -------------------------------------------------------------------------------- /templates/socialaccount/snippets/login_extra.html: -------------------------------------------------------------------------------- 1 | {% load socialaccount %} 2 | {% providers_media_js %} 3 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyladies/pyladiescon-portal/HEAD/docs/assets/favicon.ico -------------------------------------------------------------------------------- /portal/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyladies/pyladiescon-portal/HEAD/portal/static/favicon.png -------------------------------------------------------------------------------- /templates/allauth/elements/th.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 | 6 | -------------------------------------------------------------------------------- /templates/allauth/elements/tr.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 | 6 | -------------------------------------------------------------------------------- /templates/account/messages/logged_out.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have signed out.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/allauth/elements/alert.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 |

3 | {% slot message %} 4 | {% endslot %} 5 |

6 | -------------------------------------------------------------------------------- /templates/allauth/elements/table.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 |
6 | -------------------------------------------------------------------------------- /templates/allauth/elements/tbody.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 | 6 | -------------------------------------------------------------------------------- /templates/allauth/elements/thead.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 | 6 | -------------------------------------------------------------------------------- /templates/mfa/messages/webauthn_added.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Security key added.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /tests/sponsorship/test_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyladies/pyladiescon-portal/HEAD/tests/sponsorship/test_img.png -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_signup_message.txt: -------------------------------------------------------------------------------- 1 | {% include "account/email/email_confirmation_message.txt" %} 2 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_signup_subject.txt: -------------------------------------------------------------------------------- 1 | {% include "account/email/email_confirmation_subject.txt" %} 2 | -------------------------------------------------------------------------------- /templates/allauth/elements/button_group.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 |
3 | {% slot %} 4 | {% endslot %} 5 |
6 | -------------------------------------------------------------------------------- /templates/allauth/elements/p.html: -------------------------------------------------------------------------------- 1 | {% comment %} djlint:off {% endcomment %}{% load allauth %}

{% slot %}{% endslot %}

2 | -------------------------------------------------------------------------------- /templates/allauth/elements/provider_list.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 6 | -------------------------------------------------------------------------------- /templates/allauth/layouts/entrance.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% block content %} 3 | {% endblock content %} 4 | -------------------------------------------------------------------------------- /templates/allauth/layouts/manage.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% block content %} 3 | {% endblock content %} 4 | -------------------------------------------------------------------------------- /templates/mfa/messages/webauthn_removed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Security key removed.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/socialaccount/messages/account_connected_updated.txt: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/messages/account_connected.txt" %} 2 | -------------------------------------------------------------------------------- /locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyladies/pyladiescon-portal/HEAD/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sponsorship/constants.py: -------------------------------------------------------------------------------- 1 | PSF_ACCOUNTING_EMAIL = "accounting@python.org" 2 | SPONSORSHIP_COMMITTEE_EMAIL = "sponsors@pyladies.com" 3 | -------------------------------------------------------------------------------- /templates/account/messages/password_set.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Password successfully set.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/allauth/elements/h1.html: -------------------------------------------------------------------------------- 1 | {% comment %} djlint:off {% endcomment %}{% load allauth %}

{% slot %}{% endslot %}

2 | -------------------------------------------------------------------------------- /templates/allauth/elements/h2.html: -------------------------------------------------------------------------------- 1 | {% comment %} djlint:off {% endcomment %}{% load allauth %}

{% slot %}{% endslot %}

2 | -------------------------------------------------------------------------------- /templates/mfa/messages/totp_activated.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Authenticator app activated.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/mfa/messages/totp_deactivated.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Authenticator app deactivated.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/email_confirmed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have confirmed {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/email_deleted.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Removed email address {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/password_changed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Password successfully changed.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/primary_email_set.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Primary email address set.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # netlify.toml 2 | [build] 3 | command = "mkdocs build" 4 | publish = "site" 5 | environment = { PYTHON_VERSION = "3.14"} -------------------------------------------------------------------------------- /templates/allauth/elements/provider.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{ attrs.name }} 3 |
  • 4 | -------------------------------------------------------------------------------- /templates/account/messages/phone_verified.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have verified phone number {{phone}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/email_confirmation_sent.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Confirmation email sent to {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/login_code_sent.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}A sign-in code has been sent to {{recipient}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/mfa/messages/recovery_codes_generated.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}A new set of recovery codes has been generated.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/socialaccount/messages/account_connected.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}The third-party account has been connected.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/unverified_primary_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Your primary email address must be verified.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/allauth/elements/img.html: -------------------------------------------------------------------------------- 1 | {# djlint:off #}{# djlint:off #} 3 | -------------------------------------------------------------------------------- /templates/socialaccount/messages/account_disconnected.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}The third-party account has been disconnected.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py createcachetable && python manage.py migrate 2 | web: gunicorn -c config/gunicorn.conf.py portal.wsgi:application --log-file - 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = migrations,.*env,venv*,venv_local 3 | max-line-length = 100 4 | extend-ignore = 5 | E501 6 | 7 | [isort] 8 | profile = black 9 | -------------------------------------------------------------------------------- /templates/account/email/login_code_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Sign-In Code{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/email_changed_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Email Changed{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/email_deleted_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Email Removed{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Password Reset{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_set_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Password Set{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/allauth/elements/td.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 | 6 | -------------------------------------------------------------------------------- /templates/mfa/email/webauthn_added_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Security Key Added{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/email_confirm_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Email Confirmation{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_changed_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Password Changed{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/unknown_account_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Unknown Account{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/messages/cannot_delete_primary_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You cannot remove your primary email address ({{email}}).{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/mfa/email/webauthn_removed_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Security Key Removed{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_code_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Password Reset Code{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_key_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Password Reset Email{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/allauth/elements/badge.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | 3 | {% slot %} 4 | {% endslot %} 5 | 6 | -------------------------------------------------------------------------------- /templates/mfa/email/totp_activated_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Authenticator App Activated{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/mfa/email/totp_deactivated_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Authenticator App Deactivated{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/mfa/totp/base.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/base_manage.html" %} 2 | {% load i18n %} 3 | {% block head_title %} 4 | {% trans "Authenticator App" %} 5 | {% endblock head_title %} 6 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/base.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/base_manage.html" %} 2 | {% load i18n %} 3 | {% block head_title %} 4 | {% trans "Security Keys" %} 5 | {% endblock head_title %} 6 | -------------------------------------------------------------------------------- /templates/socialaccount/messages/account_connected_other.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}The third-party account is already connected to a different account.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/email/account_already_exists_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Account Already Exists{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/mfa/email/recovery_codes_generated_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}New Recovery Codes Generated{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/mfa/recovery_codes/base.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/base_manage.html" %} 2 | {% load i18n %} 3 | {% block head_title %} 4 | {% trans "Recovery Codes" %} 5 | {% endblock head_title %} 6 | -------------------------------------------------------------------------------- /volunteer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VolunteerConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "volunteer" 7 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.6.0 2 | mkdocs-awesome-nav==3.1.1 3 | mkdocs-material[imaging] 4 | mkdocs-rss-plugin 5 | mkdocs-git-revision-date-localized-plugin 6 | mkdocs-git-committers-plugin-2 -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Please Confirm Your Email Address{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/messages/logged_in.txt: -------------------------------------------------------------------------------- 1 | {% load account %} 2 | {% load i18n %} 3 | {% user_display user as name %} 4 | {% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %} 5 | -------------------------------------------------------------------------------- /templates/socialaccount/email/account_connected_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Third-Party Account Connected{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/messages/email_confirmation_failed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Unable to confirm {{email}} because it is already confirmed by a different account.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/socialaccount/email/account_disconnected_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Third-Party Account Disconnected{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /portal_account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PortalAccountConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "portal_account" 7 | -------------------------------------------------------------------------------- /templates/allauth/elements/form.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 |
    3 | {% slot body %} 4 | {% endslot %} 5 | {% slot actions %} 6 | {% endslot %} 7 |
    8 | -------------------------------------------------------------------------------- /storage_backend/custom_storage.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class MediaStorage(S3Boto3Storage): 5 | """Media Storage files""" 6 | 7 | location = "media" 8 | file_overwrite = False 9 | -------------------------------------------------------------------------------- /templates/account/email/password_set_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Your password has been set.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/mfa/email/totp_activated_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Authenticator app activated.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/account/email/email_confirm_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Your email has been confirmed.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Your password has been reset.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/mfa/email/totp_deactivated_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Authenticator app deactivated.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/mfa/email/webauthn_added_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}A new security key has been added.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/mfa/email/webauthn_removed_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}A security key has been removed.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_changed_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Your password has been changed.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /webhooks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "webhooks" 6 | 7 | urlpatterns = [ 8 | path( 9 | "pretix/", 10 | views.pretix_webhook, 11 | name="pretix_webhook", 12 | ), 13 | ] 14 | -------------------------------------------------------------------------------- /docs/user/sponsor.md: -------------------------------------------------------------------------------- 1 | # Sponsors 2 | 3 | Thank you for your interest in sponsoring PyLadiesCon. 4 | 5 | First of all, please review all the available [sponsorship tiers](https://2025.conference.pyladies.com/en/sponsors/). 6 | 7 | The rest is TBD. 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/team_status_notification.txt: -------------------------------------------------------------------------------- 1 | Hello team, 2 | 3 | The sponsorship profile for {{ profile.organization_name }} has been approved. 4 | 5 | Please review the profile and begin the sponsor onboarding process. 6 | 7 | Thanks, 8 | The System 9 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-app.txt 2 | black==25.1.0 3 | coverage==7.7.0 4 | django-debug-toolbar==5.0.1 5 | djlint==1.36.4 6 | flake8==7.2.0 7 | isort==6.0.1 8 | pip-audit==2.8.0 9 | pytest-django==4.8.0 10 | pytest==8.3.5 11 | pytest-cov==6.1.1 12 | coverage==7.7.0 13 | -------------------------------------------------------------------------------- /templates/account/email/email_changed_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Your email has been changed from {{ from_email }} to {{ to_email }}.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/account/email/email_deleted_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}Email address {{ deleted_email }} has been removed from your account.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /sponsorship/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SponsorshipConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "sponsorship" 7 | 8 | def ready(self): 9 | import sponsorship.signals # noqa: this registers the signals 10 | -------------------------------------------------------------------------------- /templates/mfa/email/recovery_codes_generated_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}A new set of Two-Factor Authentication recovery codes has been generated.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/account/snippets/warn_no_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n allauth %} 2 | {% element p %} 3 | {% trans "Warning:" %} {% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %} 4 | {% endelement %} 5 | -------------------------------------------------------------------------------- /templates/socialaccount/email/account_connected_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}A third-party account from {{ provider }} has been connected to your account.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /templates/socialaccount/email/account_disconnected_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_notification.txt" %} 2 | {% load i18n %} 3 | 4 | {% block notification_message %}{% blocktrans %}A third-party account from {{ provider }} has been disconnected from your account.{% endblocktrans %}{% endblock notification_message %} 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | All forms of contributions are welcome and appreciated. We have many contribution opportunities, including code, testing, and documentations. 4 | 5 | See the [Contributing section](https://pyladiescon-portal-docs.netlify.app/developer/contributing/) of the Developer Guide for more specifics. 6 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/snippets/scripts.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/sponsor_status_update.txt: -------------------------------------------------------------------------------- 1 | Hi {{ profile.user.first_name }}, 2 | 3 | Thank you for submitting your sponsorship application for {{ profile.organization_name }}! 4 | 5 | We’ve received your profile and our team will review it shortly. You will receive a follow-up email once a decision has been made. 6 | 7 | Best, 8 | The PyLadiesCon Team 9 | -------------------------------------------------------------------------------- /templates/emails/sponsorship/internal_sponsor_onboarding.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | Hello, {{ recipient_name }}. We're writing to let you know that a **new Sponsorship has been added** to the system. 6 | 7 | {% include "emails/sponsorship/sponsor_information_partial.md" with profile=profile %} 8 | 9 | {% endblock content %} -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/sponsor_approved.txt: -------------------------------------------------------------------------------- 1 | Hi {{ profile.user.first_name }}, 2 | 3 | Congratulations! Your sponsorship application for {{ profile.organization_name }} has been approved 🎉 4 | 5 | We're excited to have you as a sponsor for PyLadiesCon. A team member will reach out soon with next steps and onboarding information. 6 | 7 | Best, 8 | The PyLadiesCon Team 9 | -------------------------------------------------------------------------------- /config/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | bind = "unix:/var/run/cabotage/cabotage.sock" 2 | backlog = 2048 3 | preload_app = True 4 | max_requests = 2048 5 | max_requests_jitter = 128 6 | 7 | worker_connections = 1000 8 | timeout = 60 9 | keepalive = 2 10 | 11 | errorlog = "-" 12 | loglevel = "info" 13 | accesslog = "-" 14 | 15 | 16 | def when_ready(server): 17 | open("/tmp/app-initialized", "w").close() 18 | -------------------------------------------------------------------------------- /templates/emails/sponsorship/internal_sponsor_updated.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | There has been an **update to the sponsorship profile** for {{ profile.organization_name }}. 6 | 7 | Please review the changes. 8 | 9 | {% include "emails/sponsorship/sponsor_information_partial.md" with profile=profile %} 10 | 11 | {% endblock content %} -------------------------------------------------------------------------------- /templates/account/email/base_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name %}Hello from {{ site_name }}!{% endblocktrans %} 2 | 3 | {% block content %}{% endblock content %} 4 | 5 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you for using {{ site_name }}! 6 | {{ site_domain }}{% endblocktrans %} 7 | {% endautoescape %} 8 | -------------------------------------------------------------------------------- /templates/account/snippets/already_logged_in.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load account %} 3 | {% load allauth %} 4 | {% user_display user as user_display %} 5 | {% element alert %} 6 | {% slot message %} 7 | {% blocktranslate %}Note{% endblocktranslate %}: {% blocktranslate %}You are already logged in as {{ user_display }}.{% endblocktranslate %} 8 | {% endslot %} 9 | {% endelement %} 10 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/create_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

    4 | Create Sponsorship Profile 5 |

    6 |
    7 | {% csrf_token %} 8 | {{ form.as_p }} 9 | 12 |
    13 | {% endblock content %} 14 | -------------------------------------------------------------------------------- /docs/user/core_team.md: -------------------------------------------------------------------------------- 1 | # Core Team 2 | 3 | As part of PyLadiesCon Core team, you'll have access to additional features and the django admin area. 4 | 5 | ## Onboarding 6 | 7 | 1. Create an account and verify it. 8 | 2. Notify the tech lead or other existing core team members so that they can add you as a superuser 9 | 10 | ## Core team features 11 | 12 | - Access to the Django admin area 13 | - TBD 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/allauth/elements/panel.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 |
    3 |

    4 | {% slot title %} 5 | {% endslot %} 6 |

    7 | {% slot body %} 8 | {% endslot %} 9 | {% if slots.actions %} 10 | 17 | {% endif %} 18 |
    19 | -------------------------------------------------------------------------------- /templates/portal/base-tables-responsive.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base-tables.html" %} 2 | {% block table-wrapper %} 3 |
    4 | {% block table %} 5 | {{ block.super }} 6 | {% endblock table %} 7 | {% block pagination %} 8 | {{ block.super }} 9 | {% endblock pagination %} 10 |
    11 | {% endblock table-wrapper %} 12 | -------------------------------------------------------------------------------- /docs/policies/terms_of_use.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Terms of Service 3 | --- 4 | # PyLadiesCon Portal Terms of Service 5 | 6 | Thank you for using PyLadiesCon Portal! 7 | 8 | PyLadies is a fiscal sponsoree of The PSF, and PyladiesCon Portal is hosted on The PSF Infrastructure. 9 | 10 | We follow the same [policies](https://policies.python.org/) of The PSF. 11 | 12 | Read up on our [Acceptable Use Policy](acceptable_use_policy.md). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache 2 | __pycache__/ 3 | *.py[cod] 4 | .cache 5 | 6 | # Virtual envs 7 | .venv/ 8 | venv/ 9 | env/ 10 | 11 | # Env files 12 | .envrc 13 | .env.dev 14 | .env.prod 15 | .env.prod.db 16 | .env 17 | 18 | # Coverage and state 19 | .coverage 20 | .state 21 | 22 | # Django static build 23 | staticroot/ 24 | 25 | # Editor files 26 | .vscode/ 27 | 28 | # macOS file 29 | .DS_Store 30 | 31 | site/ 32 | htmlcov/ 33 | /media/ 34 | 35 | -------------------------------------------------------------------------------- /templates/emails/base_email.md: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name %}Hello from {{ site_name }}!{% endblocktrans %} 2 | 3 | {% block content %}{% endblock content %} 4 | 5 | --- 6 | 7 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you for using **{{ site_name }}**! 8 | 9 | [{{ site_domain }}](https://{{ site_domain }}){% endblocktrans %} 10 | {% endautoescape %} -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/team_status_notification.html: -------------------------------------------------------------------------------- 1 |

    2 | Hello team, 3 |

    4 |

    5 | The sponsorship profile for {{ profile.organization_name }} has been approved. 6 |

    7 |

    8 | Please review the profile and begin the sponsor onboarding process. 9 |

    10 |

    11 | View profile: (Add internal link if available) 12 |

    13 |

    14 | Thanks, 15 |
    16 | The System 17 |

    18 | -------------------------------------------------------------------------------- /templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "allauth/layouts/entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% translate "Account Inactive" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% translate "Account Inactive" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% translate "This account is inactive." %} 13 | {% endelement %} 14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Change Password" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Change Password" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% trans "Your password is now changed." %} 13 | {% endelement %} 14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Sign Up Closed" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Sign Up Closed" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% trans "We are sorry, but the sign up is currently closed." %} 13 | {% endelement %} 14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /portal/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for portal project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portal.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /portal/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for portal project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portal.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/sponsor_status_update.html: -------------------------------------------------------------------------------- 1 |

    2 | Hi {{ profile.user.first_name }}, 3 |

    4 |

    5 | Thank you for submitting your sponsorship application for {{ profile.organization_name }}! 6 |

    7 |

    8 | We’ve received your profile and our team will review it shortly. You will receive a follow-up email once a decision has been made. 9 |

    10 |

    11 | Best, 12 |
    13 | The PyLadiesCon Team 14 |

    15 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/sponsor_approved.html: -------------------------------------------------------------------------------- 1 |

    2 | Hi {{ profile.user.first_name }}, 3 |

    4 |

    5 | Congratulations! Your sponsorship application for {{ profile.organization_name }} has been approved 🎉 6 |

    7 |

    8 | We're excited to have you as a sponsor for PyLadiesCon. A team member will reach out soon with next steps and onboarding information. 9 |

    10 |

    11 | Best, 12 |
    13 | The PyLadiesCon Team 14 |

    15 | -------------------------------------------------------------------------------- /templates/account/email/login_code_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load account %} 3 | {% load i18n %} 4 | 5 | {% block content %}{% autoescape off %}{% blocktranslate %}Your sign-in code is listed below. Please enter it in your open browser window.{% endblocktranslate %}{% endautoescape %} 6 | 7 | {{ code }} 8 | 9 | {% blocktranslate %}This mail can be safely ignored if you did not initiate this action.{% endblocktranslate %}{% endblock content %} 10 | -------------------------------------------------------------------------------- /templates/socialaccount/login_redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load i18n allauth %} 4 | 5 | {% translate "Sign In" %} | {{ provider }} 6 | 7 | 8 | 9 | 10 | {% element p %} 11 | {% translate "Continue" %} 12 | {% endelement %} 13 | 14 | 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.djlint] 2 | # https://www.djlint.com/docs/linter/ 3 | # H030 - Consider adding a meta description. 4 | # H031 - Consider adding meta keywords. 5 | ignore="H030,H031" 6 | profile = "django" 7 | custom_blocks="switch" 8 | line_break_after_multiline_tag=true 9 | use_gitignore = true 10 | 11 | [tool.pytest.ini_options] 12 | DJANGO_SETTINGS_MODULE = "portal.settings" 13 | testpaths = [ "tests" ] 14 | pythonpath = [ "." ] 15 | 16 | [tool.coverage.run] 17 | omit = ["portal/settings.py"] 18 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_code_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load account %} 3 | {% load i18n %} 4 | 5 | {% block content %}{% autoescape off %}{% blocktranslate %}Your password reset code is listed below. Please enter it in your open browser window.{% endblocktranslate %}{% endautoescape %} 6 | 7 | {{ code }} 8 | 9 | {% blocktranslate %}This mail can be safely ignored if you did not initiate this action.{% endblocktranslate %}{% endblock content %} 10 | -------------------------------------------------------------------------------- /docs/about/team.md: -------------------------------------------------------------------------------- 1 | 2 | # PyLadiesCon Team 3 | 4 | - Tech lead: [@Mariatta](https://github.com/mariatta) 5 | - GSoC Mentors: [@Mariatta](https://github.com/mariatta), [@cmaureir](https://github.com/cmaureir) 6 | - Conference core organizers: [@mjmolina](https://github.com/mjmolina), [@georgically](https://github.com/georgically), [@DennyPerez18](https://github.com/DennyPerez18), [@Mariatta](https://github.com/mariatta), [@mesrenyamedogbe](https://github.com/mesrenyamedogbe), [@cmaureir](https://github.com/cmaureir) 7 | -------------------------------------------------------------------------------- /volunteer/migrations/0009_remove_volunteerprofile_pyladies_chapter.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-10-28 04:21 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("volunteer", "0008_pyladieschapter_volunteerprofile_chapter"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="volunteerprofile", 15 | name="pyladies_chapter", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /requirements-app.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | Django==5.2.8 3 | django-allauth==65.11.2 4 | django-filter==25.1 5 | django-tables2==2.7.5 6 | gunicorn==23.0.0 7 | packaging==24.2 8 | psycopg2-binary==2.9.10 9 | sqlparse==0.5.3 10 | django-bootstrap5==25.1 11 | whitenoise==6.9.0 12 | dj-database-url==2.3.0 13 | boto3==1.38.5 14 | django-storages==1.14.6 15 | django-widget-tweaks==1.5.0 16 | pillow==11.3.0 17 | django-import-export[all]==4.3.12 18 | markdown==3.7 19 | bleach==6.3.0 20 | sentry-sdk[django]==2.43.0 21 | requests==2.32.5 -------------------------------------------------------------------------------- /templates/socialaccount/authentication_error.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Third-Party Login Failure" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Third-Party Login Failure" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% trans "An error occurred while attempting to login via your third-party account." %} 13 | {% endelement %} 14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /tests/portal/test_portal_extras.py: -------------------------------------------------------------------------------- 1 | from portal.templatetags.portal_extras import as_currency, get_item 2 | 3 | 4 | def test_get_item_template_filter(): 5 | 6 | d = {"a": 1, "b": 2} 7 | assert get_item(d, "a") == 1 8 | assert get_item(d, "b") == 2 9 | assert get_item(d, "c") is None 10 | 11 | 12 | def test_as_currency(): 13 | assert as_currency(1000) == "$1,000" 14 | assert as_currency(1234567.89) == "$1,234,568" 15 | assert as_currency("invalid") == "" 16 | assert as_currency(None) == "" 17 | -------------------------------------------------------------------------------- /portal/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def validate_linked_in_pattern(value): 5 | """Validate that the passed in value matches the LinkedIn URL pattern. 6 | 7 | Support personal, school, and company urls. 8 | For now, just check if it starts with the linkedin url. 9 | 10 | Returns True if it starts with the LinkedIn URL. 11 | Returns False otherwise. 12 | """ 13 | 14 | linkedin_pattern = r"^(https?://)?(www\.)?linkedin\.com/(in|company|school)/" 15 | return re.match(linkedin_pattern, value) 16 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/snippets/login_script.html: -------------------------------------------------------------------------------- 1 | {% include "mfa/webauthn/snippets/scripts.html" %} 2 |
    3 | {% csrf_token %} 4 | {{ redirect_field }} 5 | 6 |
    7 | 14 | -------------------------------------------------------------------------------- /docs/developer/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployment 3 | description: Deployment Information for PyLadiesCon Portal 4 | --- 5 | 6 | # Deployment 7 | 8 | ## Web app deployment 9 | 10 | The web app is deployed to [cabotage](https://cabotage.us-east-2.psfhosted.computer/) 11 | automatically whenever the PR is merged. 12 | 13 | ## Documentation deployment 14 | 15 | The documentation is deployed to Netlify automatically whenever the PR is merged. 16 | 17 | There is also a preview of the documentation for each PR, also generated by Netlify. 18 | 19 | -------------------------------------------------------------------------------- /portal_account/migrations/0003_portalprofile_tos_agreement.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.8 on 2025-05-08 17:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("portal_account", "0002_portalprofile_profile_picture"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="portalprofile", 15 | name="tos_agreement", 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /volunteer/migrations/0012_pyladieschapter_logo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-11-04 19:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("volunteer", "0011_populate_languages"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="pyladieschapter", 15 | name="logo", 16 | field=models.ImageField(blank=True, null=True, upload_to="chapter_logos/"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /templates/socialaccount/snippets/login.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load allauth %} 3 | {% load socialaccount %} 4 | {% get_providers as socialaccount_providers %} 5 | {% if socialaccount_providers %} 6 | {% if not SOCIALACCOUNT_ONLY %} 7 | {% element hr %} 8 | {% endelement %} 9 | {% element h2 %} 10 | {% translate "Or use a third-party" %} 11 | {% endelement %} 12 | {% endif %} 13 | {% include "socialaccount/snippets/provider_list.html" with process="login" %} 14 | {% include "socialaccount/snippets/login_extra.html" %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /templates/account/email/account_already_exists_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load i18n %} 3 | 4 | {% block content %}{% autoescape off %}{% blocktrans %}You are receiving this email because you or someone else tried to signup for an 5 | account using email address: 6 | 7 | {{ email }} 8 | 9 | However, an account using that email address already exists. In case you have 10 | forgotten about this, please use the password forgotten procedure to recover 11 | your account: 12 | 13 | {{ password_reset_url }}{% endblocktrans %}{% endautoescape %}{% endblock content %} 14 | -------------------------------------------------------------------------------- /portal_account/migrations/0002_portalprofile_profile_picture.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-03-29 00:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("portal_account", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="portalprofile", 15 | name="profile_picture", 16 | field=models.ImageField( 17 | blank=True, null=True, upload_to="profile_pictures" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /volunteer/migrations/0002_remove_volunteerprofile_coc_agreement_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-03-23 20:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("volunteer", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="volunteerprofile", 15 | name="coc_agreement", 16 | ), 17 | migrations.RemoveField( 18 | model_name="volunteerprofile", 19 | name="pronouns", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_key_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load i18n %} 3 | 4 | {% block content %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account. 5 | It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %} 6 | 7 | {{ password_reset_url }}{% if username %} 8 | 9 | {% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock content %} 10 | -------------------------------------------------------------------------------- /portal/templatetags/portal_extras.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter 7 | def get_item(dictionary, key): 8 | """ 9 | Template filter to get a value from a dictionary by key, 10 | returning None if not found. 11 | """ 12 | return dictionary.get(key) 13 | 14 | 15 | @register.filter 16 | def as_currency(value): 17 | """ 18 | Template filter to format a number as currency. 19 | """ 20 | try: 21 | amount = float(value) 22 | return f"${amount:,.0f}" 23 | except (TypeError, ValueError): 24 | return "" 25 | -------------------------------------------------------------------------------- /portal_account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import PortalProfile 4 | 5 | 6 | class PortalProfileAdmin(admin.ModelAdmin): 7 | list_display = ( 8 | "user", 9 | "user__first_name", 10 | "user__last_name", 11 | "pronouns", 12 | "coc_agreement", 13 | "tos_agreement", 14 | ) 15 | search_fields = ("user__email", "user__first_name", "user__last_name") 16 | list_filter = ("pronouns", "coc_agreement", "tos_agreement") 17 | readonly_fields = ("coc_agreement", "tos_agreement") 18 | 19 | 20 | admin.site.register(PortalProfile, PortalProfileAdmin) 21 | -------------------------------------------------------------------------------- /volunteer/migrations/0011_populate_languages.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-11-04 02:58 2 | 3 | from django.db import migrations 4 | 5 | from volunteer.management.commands.migrate_volunteer_language import ( 6 | populate_language_choices, 7 | ) 8 | 9 | 10 | def populate_languages(apps, schema_editor): 11 | populate_language_choices() 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ("volunteer", "0010_language_alter_volunteerprofile_languages_spoken_and_more"), 18 | ] 19 | 20 | operations = [migrations.RunPython(populate_languages, migrations.RunPython.noop)] 21 | -------------------------------------------------------------------------------- /templates/account/confirm_phone_verification_code.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_confirm_code.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block head_title %} 5 | {% translate "Phone Verification" %} 6 | {% endblock head_title %} 7 | {% block title %} 8 | {% translate "Enter Phone Verification Code" %} 9 | {% endblock title %} 10 | {% block recipient %} 11 | {{ phone }} 12 | {% endblock recipient %} 13 | {% block action_url %} 14 | {% url 'account_verify_phone' %} 15 | {% endblock action_url %} 16 | {% block extra_tags %} 17 | phone,verification 18 | {% endblock extra_tags %} 19 | -------------------------------------------------------------------------------- /templates/account/confirm_password_reset_code.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_confirm_code.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block head_title %} 5 | {% translate "Password Reset" %} 6 | {% endblock head_title %} 7 | {% block title %} 8 | {% translate "Enter Password Reset Code" %} 9 | {% endblock title %} 10 | {% block recipient %} 11 | {{ email }} 12 | {% endblock recipient %} 13 | {% block action_url %} 14 | {% url "account_confirm_password_reset_code" %} 15 | {% endblock action_url %} 16 | {% block extra_tags %} 17 | email,verification 18 | {% endblock extra_tags %} 19 | -------------------------------------------------------------------------------- /templates/socialaccount/login_cancelled.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Login Cancelled" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Login Cancelled" %} 10 | {% endelement %} 11 | {% url 'account_login' as login_url %} 12 | {% element p %} 13 | {% blocktrans %}You decided to cancel logging in to our site using one of your existing accounts. If this was a mistake, please proceed to sign in.{% endblocktrans %} 14 | {% endelement %} 15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /templates/account/confirm_email_verification_code.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_confirm_code.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block head_title %} 5 | {% translate "Email Verification" %} 6 | {% endblock head_title %} 7 | {% block title %} 8 | {% translate "Enter Email Verification Code" %} 9 | {% endblock title %} 10 | {% block recipient %} 11 | {{ email }} 12 | {% endblock recipient %} 13 | {% block action_url %} 14 | {% url "account_email_verification_sent" %} 15 | {% endblock action_url %} 16 | {% block extra_tags %} 17 | email,verification 18 | {% endblock extra_tags %} 19 | -------------------------------------------------------------------------------- /templates/account/email/base_notification.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load account %} 3 | {% load i18n %} 4 | 5 | {% block content %}{% autoescape off %}{% blocktrans %}You are receiving this mail because the following change was made to your account:{% endblocktrans %} 6 | 7 | {% block notification_message %} 8 | {% endblock notification_message%} 9 | 10 | {% blocktrans %}If you do not recognize this change then please take proper security precautions immediately. The change to your account originates from: 11 | 12 | - IP address: {{ip}} 13 | - Browser: {{user_agent}} 14 | - Date: {{timestamp}}{% endblocktrans %}{% endautoescape %}{% endblock %} 15 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/edit_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/webauthn/base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load allauth %} 5 | {% block content %} 6 | {% element h1 %} 7 | {% trans "Edit Security Key" %} 8 | {% endelement %} 9 | {% url "mfa_edit_webauthn" as action_url %} 10 | {% element form form=form method="post" action=action_url %} 11 | {% slot body %} 12 | {% csrf_token %} 13 | {% element fields form=form %} 14 | {% endelement %} 15 | {% endslot %} 16 | {% slot actions %} 17 | {% element button id="mfa_webauthn_edit" type="submit" %} 18 | {% trans "Save" %} 19 | {% endelement %} 20 | {% endslot %} 21 | {% endelement %} 22 | {% endblock content %} 23 | -------------------------------------------------------------------------------- /templates/account/email/unknown_account_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load i18n %} 3 | 4 | {% block content %}{% autoescape off %}{% blocktranslate %}You are receiving this email because you, or someone else, tried to access an account with email {{ email }}. However, we do not have any record of such an account in our database.{% endblocktranslate %} 5 | 6 | {% blocktranslate %}This mail can be safely ignored if you did not initiate this action.{% endblocktranslate %} 7 | 8 | {% blocktranslate %}If it was you, you can sign up for an account using the link below.{% endblocktranslate %} 9 | 10 | {{ signup_url }}{% endautoescape %}{% endblock content %} 11 | -------------------------------------------------------------------------------- /tests/portal_account/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | from portal_account.models import PortalProfile 5 | 6 | 7 | @pytest.mark.django_db 8 | class TestPortalProfileModel: 9 | 10 | def test_profile_url(self, portal_user): 11 | profile = PortalProfile(user=portal_user) 12 | profile.save() 13 | 14 | assert profile.get_absolute_url() == reverse( 15 | "portal_account:portal_profile_edit", kwargs={"pk": profile.pk} 16 | ) 17 | 18 | def test_profile_str_representation(self, portal_user): 19 | profile = PortalProfile(user=portal_user) 20 | 21 | assert str(profile) == portal_user.username 22 | -------------------------------------------------------------------------------- /sponsorship/migrations/0008_sponsorshipprofile_github_issue_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.8 on 2025-12-05 00:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sponsorship", "0007_individualdonation"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="sponsorshipprofile", 15 | name="github_issue_url", 16 | field=models.URLField( 17 | blank=True, 18 | help_text="Link to the GitHub issue tracking this sponsorship", 19 | null=True, 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% load account %} 5 | {% block head_title %} 6 | {% trans "Password Reset" %} 7 | {% endblock head_title %} 8 | {% block content %} 9 | {% element h1 %} 10 | {% trans "Password Reset" %} 11 | {% endelement %} 12 | {% if user.is_authenticated %} 13 | {% include "account/snippets/already_logged_in.html" %} 14 | {% endif %} 15 | {% element p %} 16 | {% blocktrans %}We have sent you an email. If you have not received it please check your spam folder. Otherwise contact us if you do not receive it in a few minutes.{% endblocktrans %} 17 | {% endelement %} 18 | {% endblock content %} 19 | -------------------------------------------------------------------------------- /volunteer/migrations/0006_alter_volunteerprofile_teams.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.2 on 2025-06-09 23:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("volunteer", "0005_remove_volunteerprofile_timezone_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="volunteerprofile", 15 | name="teams", 16 | field=models.ManyToManyField( 17 | blank=True, 18 | related_name="members", 19 | to="volunteer.team", 20 | verbose_name="members", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/sponsorship_charts.html: -------------------------------------------------------------------------------- 1 | {% for breakdown in stats.sponsorship_breakdown %} 2 | var data = new google.visualization.DataTable(); 3 | {% for column in breakdown.columns %} 4 | data.addColumn('{{ column.0 }}', '{{ column.1 }}'); 5 | {% endfor %} 6 | data.addRows([ 7 | {% for data in breakdown.data %} 8 | ['{{ data.0 }}', {{ data.1 }}], 9 | {% endfor %} 10 | ]); 11 | var options = {'title':'{{ breakdown.title }}', 12 | 'width':400, 13 | 'height':300, 14 | is3D: true, 15 | }; 16 | var chart = new google.visualization.PieChart(document.getElementById('{{ breakdown.chart_id }}')); 17 | chart.draw(data, options); 18 | {% endfor %} 19 | -------------------------------------------------------------------------------- /templates/account/confirm_login_code.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_confirm_code.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block head_title %} 5 | {% translate "Sign In" %} 6 | {% endblock head_title %} 7 | {% block title %} 8 | {% translate "Enter Sign-In Code" %} 9 | {% endblock title %} 10 | {% block recipient %} 11 | {% if email %} 12 | {{ email }} 13 | {% else %} 14 | {{ phone }} 15 | {% endif %} 16 | {% endblock recipient %} 17 | {% block action_url %} 18 | {% url "account_confirm_login_code" %} 19 | {% endblock action_url %} 20 | {% block extra_tags %} 21 | login 22 | {% endblock extra_tags %} 23 | -------------------------------------------------------------------------------- /templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Verify Your Email Address" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Verify Your Email Address" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% blocktrans %}We have sent an email to you for verification. Follow the link provided to finalize the signup process. If you do not see the verification email in your main inbox, check your spam folder. Please contact us if you do not receive the verification email within a few minutes.{% endblocktrans %} 13 | {% endelement %} 14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /sponsorship/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "sponsorship" 6 | 7 | urlpatterns = [ 8 | path( 9 | "new", 10 | views.SponsorshipProfileCreate.as_view(), 11 | name="sponsorship_profile_new", 12 | ), 13 | path( 14 | "list", 15 | views.SponsorshipProfileList.as_view(), 16 | name="sponsorship_list", 17 | ), 18 | path( 19 | "/", 20 | views.SponsorshipProfileDetail.as_view(), 21 | name="sponsorship_profile_detail", 22 | ), 23 | path( 24 | "/send-invoice/", 25 | views.SponsorshipProfileSendInvoice.as_view(), 26 | name="sponsorship_send_invoice", 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /portal_account/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.urls import path 3 | 4 | from . import views 5 | 6 | app_name = "portal_account" 7 | 8 | urlpatterns = [ 9 | path("", views.index, name="index"), 10 | path( 11 | "profile/view//", 12 | login_required(views.PortalProfileView.as_view()), 13 | name="portal_profile_detail", 14 | ), 15 | path( 16 | "profile/new", 17 | login_required(views.PortalProfileCreate.as_view()), 18 | name="portal_profile_new", 19 | ), 20 | path( 21 | "profile/edit/", 22 | login_required(views.PortalProfileUpdate.as_view()), 23 | name="portal_profile_edit", 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/authenticator_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/webauthn/base.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block content %} 5 | {% element h1 %} 6 | {% trans "Remove Security Key" %} 7 | {% endelement %} 8 | {% element p %} 9 | {% blocktranslate %}Are you sure you want to remove this security key?{% endblocktranslate %} 10 | {% endelement %} 11 | {% url 'mfa_remove_webauthn' pk=authenticator.pk as action_url %} 12 | {% element form method="post" action=action_url no_visible_fields=True %} 13 | {% slot actions %} 14 | {% csrf_token %} 15 | {% element button tags="danger" type="submit" %} 16 | {% translate "Remove" %} 17 | {% endelement %} 18 | {% endslot %} 19 | {% endelement %} 20 | {% endblock content %} 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | repository_dispatch: 8 | types: [automated-update-trigger] 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [ "3.14", "3.15" ] 15 | fail-fast: 16 | false 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: docker/setup-buildx-action@v3 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: "${{ matrix.python-version }}" 23 | check-latest: true 24 | allow-prereleases: true 25 | - name: Lint 26 | run: make lint 27 | - name: Tests 28 | run: make test 29 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portal.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /sponsorship/migrations/0006_sponsorshipprofile_po_number.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.8 on 2025-11-14 03:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sponsorship", "0005_alter_sponsorshipprofile_progress_status"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="sponsorshipprofile", 15 | name="po_number", 16 | field=models.CharField( 17 | blank=True, 18 | help_text="Purchase Order number for the sponsorship contract and invoice", 19 | max_length=100, 20 | null=True, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage.html" %} 2 | {% load allauth i18n %} 3 | {% block head_title %} 4 | {% trans "Sign Out" %} 5 | {% endblock head_title %} 6 | {% block content %} 7 | {% element h1 %} 8 | {% trans "Sign Out" %} 9 | {% endelement %} 10 | {% element p %} 11 | {% trans "Are you sure you want to sign out?" %} 12 | {% endelement %} 13 | {% url "account_logout" as action_url %} 14 | {% element form method="post" action=action_url no_visible_fields=True %} 15 | {% slot body %} 16 | {% csrf_token %} 17 | {{ redirect_field }} 18 | {% endslot %} 19 | {% slot actions %} 20 | {% element button type="submit" %} 21 | {% trans "Sign Out" %} 22 | {% endelement %} 23 | {% endslot %} 24 | {% endelement %} 25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage_password.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Set Password" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Set Password" %} 10 | {% endelement %} 11 | {% url "account_set_password" as action_url %} 12 | {% element form method="post" action=action_url %} 13 | {% slot body %} 14 | {% csrf_token %} 15 | {{ redirect_field }} 16 | {% element fields form=form %} 17 | {% endelement %} 18 | {% endslot %} 19 | {% slot actions %} 20 | {% element button type="submit" name="action" %} 21 | {% trans "Set Password" %} 22 | {% endelement %} 23 | {% endslot %} 24 | {% endelement %} 25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /portal/migrations/0002_alter_basemodel_creation_date_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.2 on 2025-06-07 06:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("portal", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="basemodel", 15 | name="creation_date", 16 | field=models.DateTimeField(auto_now_add=True, verbose_name="creation_date"), 17 | ), 18 | migrations.AlterField( 19 | model_name="basemodel", 20 | name="modified_date", 21 | field=models.DateTimeField(auto_now=True, verbose_name="modified_date"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /templates/mfa/reauthenticate.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_reauthenticate.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block reauthenticate_content %} 5 | {% element p %} 6 | {% blocktranslate %}Enter an authenticator code:{% endblocktranslate %} 7 | {% endelement %} 8 | {% url "mfa_reauthenticate" as action_url %} 9 | {% element form form=form method="post" action=action_url %} 10 | {% slot body %} 11 | {% csrf_token %} 12 | {% element fields form=form unlabeled=True %} 13 | {% endelement %} 14 | {{ redirect_field }} 15 | {% endslot %} 16 | {% slot actions %} 17 | {% element button type="submit" tags="primary,mfa,login" %} 18 | {% trans "Confirm" %} 19 | {% endelement %} 20 | {% endslot %} 21 | {% endelement %} 22 | {% endblock reauthenticate_content %} 23 | -------------------------------------------------------------------------------- /templates/account/reauthenticate.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_reauthenticate.html" %} 2 | {% load allauth %} 3 | {% load i18n %} 4 | {% block reauthenticate_content %} 5 | {% element p %} 6 | {% blocktranslate %}Enter your password:{% endblocktranslate %} 7 | {% endelement %} 8 | {% url "account_reauthenticate" as action_url %} 9 | {% element form form=form method="post" action=action_url %} 10 | {% slot body %} 11 | {% csrf_token %} 12 | {% element fields form=form unlabeled=True %} 13 | {% endelement %} 14 | {{ redirect_field }} 15 | {% endslot %} 16 | {% slot actions %} 17 | {% element button type="submit" tags="primary,reauthenticate" %} 18 | {% trans "Confirm" %} 19 | {% endelement %} 20 | {% endslot %} 21 | {% endelement %} 22 | {% endblock reauthenticate_content %} 23 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_message.txt: -------------------------------------------------------------------------------- 1 | {% extends "account/email/base_message.txt" %} 2 | {% load account %} 3 | {% load i18n %} 4 | 5 | {% block content %}{% autoescape off %}{% user_display user as user_display %}{% blocktranslate with site_name=current_site.name site_domain=current_site.domain %}You're receiving this email because user {{ user_display }} has given your email address to register an account on {{ site_domain }}.{% endblocktranslate %} 6 | 7 | {% if code %}{% blocktranslate %}Your email verification code is listed below. Please enter it in your open browser window.{% endblocktranslate %} 8 | 9 | {{ code }}{% else %}{% blocktranslate %}To confirm this is correct, go to {{ activate_url }}{% endblocktranslate %}{% endif %}{% endautoescape %}{% endblock content %} 10 | -------------------------------------------------------------------------------- /volunteer/migrations/0003_alter_volunteerprofile_discord_username.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.8 on 2025-05-15 20:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("volunteer", "0002_remove_volunteerprofile_coc_agreement_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="volunteerprofile", 15 | name="discord_username", 16 | field=models.CharField( 17 | default="", 18 | help_text="Required - Your Discord username for team communication", 19 | max_length=50, 20 | verbose_name="Discord username (required)", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /portal_account/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from django.urls import reverse 4 | 5 | from portal.models import BaseModel 6 | 7 | 8 | class PortalProfile(BaseModel): 9 | 10 | user = models.OneToOneField(User, on_delete=models.CASCADE) 11 | pronouns = models.CharField(max_length=100, blank=True, null=True) 12 | coc_agreement = models.BooleanField(default=False) 13 | tos_agreement = models.BooleanField(default=False) 14 | profile_picture = models.ImageField( 15 | upload_to="profile_pictures", blank=True, null=True 16 | ) 17 | 18 | def get_absolute_url(self): 19 | return reverse("portal_account:portal_profile_edit", kwargs={"pk": self.pk}) 20 | 21 | def __str__(self): 22 | return self.user.username 23 | -------------------------------------------------------------------------------- /templates/volunteer/volunteerprofile_list.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% load render_table from django_tables2 %} 5 | {% block body %} 6 | {% endblock body %} 7 | {% block content %} 8 |

    9 | Manage Volunteers 10 |

    11 | {% if filter %} 12 |
    13 | {% bootstrap_form filter.form %} 14 | {% bootstrap_button 'Search' %} 15 | {% if filter.form.is_bound %} 16 | Reset Filters 18 | {% endif %} 19 |
    20 | {% endif %} 21 | {% render_table table %} 22 | {% endblock content %} 23 | -------------------------------------------------------------------------------- /volunteer/constants.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | 3 | 4 | class RoleTypes(StrEnum): 5 | """Role types for the volunteer.""" 6 | 7 | ADMIN = "Admin" 8 | STAFF = "Staff" 9 | VENDOR = "Vendor" 10 | VOLUNTEER = "Volunteer" 11 | 12 | 13 | class ApplicationStatus(StrEnum): 14 | """Application status for the volunteer.""" 15 | 16 | PENDING = "Pending Review" 17 | APPROVED = "Approved" 18 | REJECTED = "Rejected" 19 | CANCELLED = "Cancelled" 20 | WAITLISTED = "Waitlisted" 21 | 22 | 23 | class Region(StrEnum): 24 | """Region where the volunteer usually reside.""" 25 | 26 | NO_REGION = "" 27 | ASIA = "Asia" 28 | EUROPE = "Europe" 29 | NORTH_AMERICA = "North America" 30 | SOUTH_AMERICA = "South America" 31 | AFRICA = "Africa" 32 | OCEANIA = "Oceania" 33 | -------------------------------------------------------------------------------- /templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage_password.html" %} 2 | {% load allauth i18n %} 3 | {% block head_title %} 4 | {% trans "Change Password" %} 5 | {% endblock head_title %} 6 | {% block content %} 7 | {% element h1 %} 8 | {% trans "Change Password" %} 9 | {% endelement %} 10 | {% url 'account_change_password' as action_url %} 11 | {% element form form=form method="post" action=action_url %} 12 | {% slot body %} 13 | {% csrf_token %} 14 | {{ redirect_field }} 15 | {% element fields form=form %} 16 | {% endelement %} 17 | {% endslot %} 18 | {% slot actions %} 19 | {% element button type="submit" %} 20 | {% trans "Change Password" %} 21 | {% endelement %} 22 | {% trans "Forgot Password?" %} 23 | {% endslot %} 24 | {% endelement %} 25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /common/send_emails.py: -------------------------------------------------------------------------------- 1 | # Import the Markdown email system 2 | from .markdown_emails import send_markdown_email 3 | 4 | 5 | def send_email( 6 | subject, 7 | recipient_list, 8 | *, 9 | markdown_template, 10 | context=None, 11 | ): 12 | """Send an email using a Markdown template. 13 | 14 | This function only supports Markdown templates going forward. 15 | All email templates should be written in Markdown format. 16 | 17 | Args: 18 | subject: Email subject line 19 | recipient_list: List of recipient email addresses 20 | markdown_template: Path to Markdown template (required) 21 | context: Template context dictionary 22 | """ 23 | return send_markdown_email( 24 | subject, 25 | recipient_list, 26 | markdown_template=markdown_template, 27 | context=context, 28 | ) 29 | -------------------------------------------------------------------------------- /portal/static/styles.css: -------------------------------------------------------------------------------- 1 | .bi { 2 | display: inline-block; 3 | width: 1rem; 4 | height: 1rem; 5 | } 6 | 7 | /* 8 | * Sidebar 9 | */ 10 | 11 | @media (min-width: 768px) { 12 | .sidebar .offcanvas-lg { 13 | position: -webkit-sticky; 14 | position: sticky; 15 | top: 48px; 16 | } 17 | .navbar-search { 18 | display: block; 19 | } 20 | } 21 | 22 | .sidebar .nav-link { 23 | font-size: .875rem; 24 | font-weight: 500; 25 | } 26 | 27 | .sidebar .nav-link.active { 28 | color: #2470dc; 29 | } 30 | 31 | .sidebar-heading { 32 | font-size: .75rem; 33 | } 34 | 35 | /* 36 | * Navbar 37 | */ 38 | 39 | .navbar-brand { 40 | padding-top: .75rem; 41 | padding-bottom: .75rem; 42 | background-color: rgba(0, 0, 0, .25); 43 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); 44 | } 45 | 46 | .navbar .form-control { 47 | padding: .75rem 1rem; 48 | } 49 | -------------------------------------------------------------------------------- /templates/socialaccount/snippets/provider_list.html: -------------------------------------------------------------------------------- 1 | {% load allauth socialaccount %} 2 | {% get_providers as socialaccount_providers %} 3 | {% if socialaccount_providers %} 4 | {% element provider_list %} 5 | {% for provider in socialaccount_providers %} 6 | {% if provider.id == "openid" %} 7 | {% for brand in provider.get_brands %} 8 | {% provider_login_url provider openid=brand.openid_url process=process as href %} 9 | {% element provider name=brand.name provider_id=provider.id href=href %} 10 | {% endelement %} 11 | {% endfor %} 12 | {% endif %} 13 | {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} 14 | {% element provider name=provider.name provider_id=provider.id href=href %} 15 | {% endelement %} 16 | {% endfor %} 17 | {% endelement %} 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /portal/management/commands/makesuperuser.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.management.base import BaseCommand 3 | 4 | 5 | class Command(BaseCommand): 6 | help = "Make the user a superuser" 7 | 8 | def add_arguments(self, parser): 9 | parser.add_argument("username", help="Username to be made into superuser") 10 | 11 | def handle(self, *args, **options): 12 | 13 | users = User.objects.filter(username=options["username"]) 14 | if len(users) > 0: 15 | user = users[0] 16 | user.is_staff = True 17 | user.is_superuser = True 18 | user.save() 19 | self.stdout.write( 20 | self.style.SUCCESS(f"User {user.username} is now a superuser") 21 | ) 22 | else: 23 | self.stdout.write(self.style.ERROR(f"No such user {options['username']}")) 24 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/email/psf_invoice_request.md: -------------------------------------------------------------------------------- 1 | Dear PSF Accounting Team, 2 | 3 | We have an approved sponsorship from **{{ profile.organization_name }}** for PyLadiesCon. 4 | 5 | Please prepare the sponsorship contract and invoice with the following information: 6 | 7 | ## Sponsorship Details 8 | 9 | - **Company Name:** {{ profile.organization_name }} 10 | - **Company Address:** {{ profile.organization_address }} 11 | - **Sponsorship Tier:** {{ profile.sponsorship_tier.name }} 12 | - **Sponsorship Amount:** ${{ profile.sponsorship_tier.amount }}{% if profile.sponsorship_override_amount %} (Override Amount: ${{ profile.sponsorship_override_amount }}){% endif %} 13 | - **Contact Name:** {{ profile.sponsor_contact_name }} 14 | - **Contact Email:** {{ profile.sponsors_contact_email }} 15 | 16 | Please let us know if you need any additional information. 17 | 18 | Best regards, 19 | PyLadiesCon Team -------------------------------------------------------------------------------- /volunteer/management/commands/unmigrate_volunteer_language.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from volunteer.models import VolunteerProfile 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Reverse Volunteer languages_spoken field to the Language model." 8 | 9 | def handle(self, *args, **options): 10 | for volunteer in VolunteerProfile.objects.all(): 11 | for lang in volunteer.language.all(): 12 | languages_to_add = volunteer.languages_spoken or [] 13 | if ( 14 | not volunteer.languages_spoken 15 | or lang.code not in volunteer.languages_spoken 16 | ): 17 | volunteer.languages_spoken = languages_to_add 18 | 19 | if volunteer.language.count() == 0: 20 | volunteer.languages_spoken = ["en"] 21 | volunteer.save() 22 | -------------------------------------------------------------------------------- /templates/mfa/totp/deactivate_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/totp/base.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Deactivate Authenticator App" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Deactivate Authenticator App" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% blocktranslate %}You are about to deactivate authenticator app based authentication. Are you sure?{% endblocktranslate %} 13 | {% endelement %} 14 | {% url 'mfa_deactivate_totp' as action_url %} 15 | {% element form form=form method="post" action=action_url no_visible_fields=True %} 16 | {% slot body %} 17 | {% csrf_token %} 18 | {% element fields form=form %} 19 | {{ form.as_p }} 20 | {% endelement %} 21 | {% endslot %} 22 | {% slot actions %} 23 | {% element button tags="danger,delete" type="submit" %} 24 | {% trans "Deactivate" %} 25 | {% endelement %} 26 | {% endslot %} 27 | {% endelement %} 28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /templates/account/base_reauthenticate.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load allauth %} 3 | {% load i18n %} 4 | {% block head_title %} 5 | {% trans "Confirm Access" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Confirm Access" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% blocktranslate %}Please reauthenticate to safeguard your account.{% endblocktranslate %} 13 | {% endelement %} 14 | {% block reauthenticate_content %} 15 | {% endblock reauthenticate_content %} 16 | {% if reauthentication_alternatives %} 17 | {% element hr %} 18 | {% endelement %} 19 | {% element h2 %} 20 | {% translate "Alternative options" %} 21 | {% endelement %} 22 | {% element button_group %} 23 | {% for alt in reauthentication_alternatives %} 24 | {% element button href=alt.url tags="primary,outline" %} 25 | {{ alt.description }} 26 | {% endelement %} 27 | {% endfor %} 28 | {% endelement %} 29 | {% endif %} 30 | {% endblock content %} 31 | -------------------------------------------------------------------------------- /templates/allauth/elements/button.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | {% comment %} djlint:off {% endcomment %} 3 | <{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %} 4 | {% if attrs.form %}form="{{ attrs.form }}"{% endif %} 5 | {% if attrs.id %}id="{{ attrs.id }}"{% endif %} 6 | {% if attrs.name %}name="{{ attrs.name }}"{% endif %} 7 | {% if attrs.value %}value="{{ attrs.value }}"{% endif %} 8 | {% if attrs.type %}type="{{ attrs.type }}"{% endif %} 9 | 10 | class="{% block class %} 11 | btn 12 | {% if "link" in attrs.tags %}btn-link 13 | {% else %} 14 | {% if "prominent" in attrs.tags %}btn-lg{% elif "minor" in attrs.tags %}btn-sm{% endif %} 15 | btn-{% if 'outline' in attrs.tags %}outline-{% endif %}{% if "danger" in attrs.tags %}danger{% elif "secondary" in attrs.tags %}secondary{% elif "warning" in attrs.tags %}warning{% else %}primary{% endif %} 16 | {% endif %}{% endblock %}"> 17 | 18 | {% slot %} 19 | {% endslot %} 20 | 21 | -------------------------------------------------------------------------------- /templates/emails/sponsorship/sponsor_information_partial.md: -------------------------------------------------------------------------------- 1 | ## Sponsorship Information 2 | 3 | - **Sponsor Organization Name:** {{ profile.organization_name }} 4 | - **Sponsorship Tier:** {{ profile.sponsorship_tier }} 5 | - **Company Description:** {{ profile.company_description }} 6 | - **Sponsor Progress Status:** {{ profile.get_progress_status_display }} 7 | - **Sponsor Contact Name:** {{ profile.sponsor_contact_name }} 8 | - **Sponsor Contact Email:** {{ profile.sponsors_contact_email }} 9 | - **Sponsor Address:** {{ profile.organization_address }} 10 | - **Sponsorship Amount:** {% if profile.sponsorship_override_amount %}Custom Amount: {{ profile.sponsorship_override_amount }}{% else %}As per tier: {{ profile.sponsorship_tier.amount }}{% endif %} 11 | - **PO Number:** {% if profile.po_number %}{{ profile.po_number }}{% else %}N/A{% endif %} 12 | - **Internal Contact User:** {{ profile.main_contact_user.get_full_name }} 13 | - **GitHub Issue Url**: {% if profile.github_issue_url %}{{ profile.github_issue_url }}{% else %}N/A{% endif %} 14 | -------------------------------------------------------------------------------- /templates/socialaccount/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Signup" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Sign Up" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your {{provider_name}} account to login to 13 | {{site_name}}. As a final step, please complete the following form:{% endblocktrans %} 14 | {% endelement %} 15 | {% url 'socialaccount_signup' as action_url %} 16 | {% element form form=form method="post" action=action_url %} 17 | {% slot body %} 18 | {% csrf_token %} 19 | {% element fields form=form unlabeled=True %} 20 | {% endelement %} 21 | {{ redirect_field }} 22 | {% endslot %} 23 | {% slot actions %} 24 | {% element button type="submit" %} 25 | {% trans "Sign Up" %} 26 | {% endelement %} 27 | {% endslot %} 28 | {% endelement %} 29 | {% endblock content %} 30 | -------------------------------------------------------------------------------- /volunteer/management/commands/migrate_volunteer_language.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from volunteer.languages import LANGUAGES 4 | from volunteer.models import Language, VolunteerProfile 5 | 6 | 7 | def populate_language_choices(): 8 | """Populate the languages""" 9 | for code, name in LANGUAGES: 10 | if not Language.objects.filter(code=code).exists(): 11 | Language.objects.create(code=code, name=name) 12 | 13 | 14 | class Command(BaseCommand): 15 | help = "Migrate Volunteer languages_spoken field to the Language model." 16 | 17 | def handle(self, *args, **options): 18 | populate_language_choices() 19 | for volunteer in VolunteerProfile.objects.filter( 20 | languages_spoken__isnull=False 21 | ): 22 | for lang_code in volunteer.languages_spoken: 23 | language = Language.objects.filter(code=lang_code).first() 24 | if language and not volunteer.language.filter(code=lang_code).exists(): 25 | volunteer.language.add(language) 26 | -------------------------------------------------------------------------------- /sponsorship/migrations/0005_alter_sponsorshipprofile_progress_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-11-03 18:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sponsorship", "0004_sponsorshipprofile_organization_address_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="sponsorshipprofile", 15 | name="progress_status", 16 | field=models.IntegerField( 17 | choices=[ 18 | (1, "Not Contacted"), 19 | (2, "Awaiting Response"), 20 | (3, "Rejected"), 21 | (4, "Accepted"), 22 | (5, "Approved"), 23 | (6, "Agreement Sent"), 24 | (7, "Agreement Signed"), 25 | (8, "Invoiced"), 26 | (9, "Paid"), 27 | (10, "Cancelled"), 28 | ], 29 | default=1, 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/webauthn/base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load allauth %} 5 | {% block content %} 6 | {% element h1 %} 7 | {% trans "Add Security Key" %} 8 | {% endelement %} 9 | {% url "mfa_add_webauthn" as action_url %} 10 | {% element form form=form method="post" action=action_url %} 11 | {% slot body %} 12 | {% csrf_token %} 13 | {% element fields form=form %} 14 | {% endelement %} 15 | {% endslot %} 16 | {% slot actions %} 17 | {% element button id="mfa_webauthn_add" type="button" %} 18 | {% trans "Add" %} 19 | {% endelement %} 20 | {% endslot %} 21 | {% endelement %} 22 | {% include "mfa/webauthn/snippets/scripts.html" %} 23 | {{ js_data|json_script:"js_data" }} 24 | 33 | {% endblock content %} 34 | -------------------------------------------------------------------------------- /sponsorship/templates/sponsorship/sponsorshipprofile_list.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% load render_table from django_tables2 %} 5 | {% block body %} 6 | {% endblock body %} 7 | {% block content %} 8 |

    9 | {% if request.user.is_superuser %} 10 | Manage Sponsors Add New Sponsor 12 | {% else %} 13 | View Sponsors 14 | {% endif %} 15 |

    16 | {% include "sponsorship/sponsorship_stats.html" %} 17 | {% if filter %} 18 |
    19 | {% bootstrap_form filter.form %} 20 | {% bootstrap_button 'Search' %} 21 | {% if filter.form.is_bound %} 22 | Reset Filters 24 | {% endif %} 25 |
    26 | {% endif %} 27 | {% render_table table %} 28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /templates/mfa/recovery_codes/generate.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/recovery_codes/base.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block content %} 5 | {% element h1 %} 6 | {% translate "Recovery Codes" %} 7 | {% endelement %} 8 | {% element p %} 9 | {% blocktranslate %}You are about to generate a new set of recovery codes for your account.{% endblocktranslate %} 10 | {% if unused_code_count %} 11 | {% blocktranslate %}This action will invalidate your existing codes.{% endblocktranslate %} 12 | {% endif %} 13 | {% blocktranslate %}Are you sure?{% endblocktranslate %} 14 | {% endelement %} 15 | {% url 'mfa_generate_recovery_codes' as action_url %} 16 | {% element form method="post" action=action_url no_visible_fields=True %} 17 | {% slot body %} 18 | {% csrf_token %} 19 | {{ form.as_p }} 20 | {% endslot %} 21 | {% slot actions %} 22 | {% setvar tags %} 23 | {% if unused_code_count %} 24 | danger 25 | {% else %} 26 | {% endif %} 27 | {% endsetvar %} 28 | {% element button type="submit" tags=tags %} 29 | {% trans "Generate" %} 30 | {% endelement %} 31 | {% endslot %} 32 | {% endelement %} 33 | {% endblock content %} 34 | -------------------------------------------------------------------------------- /templates/volunteer/volunteer_charts.html: -------------------------------------------------------------------------------- 1 | {% for breakdown in stats.volunteer_breakdown %} 2 | var data = google.visualization.arrayToDataTable([ 3 | ['{{ breakdown.columns.0 }}', '{{ breakdown.columns.1 }}'], 4 | {% for data in breakdown.data %} 5 | ['{{ data.0 }}', {{ data.1 }},], 6 | {% endfor %} 7 | ]); 8 | var paddingHeight = 120; 9 | var rowHeight = 40; 10 | var chartHeight = Math.max(500, (data.getNumberOfRows() * rowHeight) + paddingHeight); 11 | document.getElementById('{{ breakdown.chart_id }}').style.height = chartHeight + 'px'; 12 | var options = { 13 | chart: { 14 | 'title':'{{ breakdown.title }}' 15 | }, 16 | hAxis: {title: '{{ breakdown.columns.1 }}', minValue: 0, format:'0'}, 17 | vAxis: {title: '{{ breakdown.columns.0 }}'}, 18 | height: chartHeight, 19 | chartArea: { 20 | width: '75%', 21 | height: '85%' 22 | }, 23 | bars: 'horizontal' 24 | }; 25 | var chart = new google.charts.Bar(document.getElementById('{{ breakdown.chart_id }}')); 26 | chart.draw(data, google.charts.Bar.convertOptions(options)); 27 | {% endfor %} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PyLadies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/reauthenticate.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_reauthenticate.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block reauthenticate_content %} 5 | {% url 'mfa_reauthenticate_webauthn' as action_url %} 6 | {% element form form=form method="post" action=action_url %} 7 | {% slot body %} 8 | {% csrf_token %} 9 | {% element fields form=form unlabeled=True %} 10 | {% endelement %} 11 | {{ redirect_field }} 12 | {% endslot %} 13 | {% slot actions %} 14 | {% element button id="mfa_webauthn_reauthenticate" type="submit" tags="primary,mfa,login" %} 15 | {% trans "Use a security key" %} 16 | {% endelement %} 17 | {% endslot %} 18 | {% endelement %} 19 | {{ js_data|json_script:"js_data" }} 20 | {% include "mfa/webauthn/snippets/scripts.html" %} 21 | 29 | {% endblock reauthenticate_content %} 30 | -------------------------------------------------------------------------------- /templates/account/request_login_code.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block head_title %} 5 | {% translate "Sign In" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% translate "Send me a sign-in code" %} 10 | {% endelement %} 11 | {% element p %} 12 | {% blocktranslate %}You will receive a special code for a password-free sign-in.{% endblocktranslate %} 13 | {% endelement %} 14 | {% url 'account_request_login_code' as login_url %} 15 | {% element form form=form method="post" action=login_url tags="entrance,login" %} 16 | {% slot body %} 17 | {% csrf_token %} 18 | {% element fields form=form unlabeled=True %} 19 | {% endelement %} 20 | {{ redirect_field }} 21 | {% endslot %} 22 | {% slot actions %} 23 | {% element button type="submit" tags="prominent,login" %} 24 | {% translate "Request Code" %} 25 | {% endelement %} 26 | {% endslot %} 27 | {% endelement %} 28 | {% url 'account_login' as login_url %} 29 | {% element button href=login_url tags="link" %} 30 | {% translate "Other sign-in options" %} 31 | {% endelement %} 32 | {% endblock content %} 33 | -------------------------------------------------------------------------------- /templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Verify Your Email Address" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Verify Your Email Address" %} 10 | {% endelement %} 11 | {% url 'account_email' as email_url %} 12 | {% element p %} 13 | {% blocktrans %}This part of the site requires us to verify that 14 | you are who you claim to be. For this purpose, we require that you 15 | verify ownership of your email address. {% endblocktrans %} 16 | {% endelement %} 17 | {% element p %} 18 | {% blocktrans %}We have sent an email to you for 19 | verification. Please click on the link inside that email. If you do not see the verification email in your main inbox, check your spam folder. Otherwise 20 | contact us if you do not receive it within a few minutes.{% endblocktrans %} 21 | {% endelement %} 22 | {% element p %} 23 | {% blocktrans %}Note: you can still change your email address.{% endblocktrans %} 24 | {% endelement %} 25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /attendee/migrations/0003_alter_attendeeprofile_participated_in_previous_event.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.8 on 2025-12-05 00:11 2 | 3 | from django.db import migrations, models 4 | 5 | import portal.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("attendee", "0002_attendeeprofile"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="attendeeprofile", 17 | name="participated_in_previous_event", 18 | field=portal.models.ChoiceArrayField( 19 | base_field=models.CharField( 20 | blank=True, 21 | choices=[ 22 | ("PyLadiesCon 2024", "PyLadiesCon 2024"), 23 | ("PyLadiesCon 2023", "PyLadiesCon 2023"), 24 | ("No this is my first one", "No this is my first one"), 25 | ], 26 | help_text="Have you participated in a previous event?", 27 | null=True, 28 | ), 29 | default=list, 30 | size=None, 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n allauth account %} 3 | {% block head_title %} 4 | {% trans "Password Reset" %} 5 | {% endblock head_title %} 6 | {% block content %} 7 | {% element h1 %} 8 | {% trans "Password Reset" %} 9 | {% endelement %} 10 | {% if user.is_authenticated %} 11 | {% include "account/snippets/already_logged_in.html" %} 12 | {% endif %} 13 | {% element p %} 14 | {% trans "Forgotten your password? Enter your email address below, and we'll send you an email allowing you to reset it." %} 15 | {% endelement %} 16 | {% url 'account_reset_password' as reset_url %} 17 | {% element form form=form method="post" action=reset_url %} 18 | {% slot body %} 19 | {% csrf_token %} 20 | {% element fields form=form %} 21 | {% endelement %} 22 | {{ redirect_field }} 23 | {% endslot %} 24 | {% slot actions %} 25 | {% element button type="submit" %} 26 | {% trans "Reset My Password" %} 27 | {% endelement %} 28 | {% endslot %} 29 | {% endelement %} 30 | {% element p %} 31 | {% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %} 32 | {% endelement %} 33 | {% endblock content %} 34 | -------------------------------------------------------------------------------- /templates/portal/registration_callout.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |

    5 | Register for PyLadiesCon now! 6 |

    7 |

    8 | 5th-7th December | Multi-language | Multi-timezone 9 |

    10 |

    11 | View Schedule 13 | Get Your Ticket 15 |

    16 |
    17 |
    18 |
    19 |
    20 | Join 21 |
    22 |

    23 | {{ stats.attendee_count }} 24 |

    25 | other attendees for three days of learning, inspiration, and connection. 26 |
    27 |
    28 |
    29 | -------------------------------------------------------------------------------- /templates/socialaccount/login.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Sign In" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% if process == "connect" %} 9 | {% element h1 %} 10 | {% blocktrans with provider.name as provider %}Connect {{ provider }}{% endblocktrans %} 11 | {% endelement %} 12 | {% element p %} 13 | {% blocktrans with provider.name as provider %}You are about to connect a new third-party account from {{ provider }}.{% endblocktrans %} 14 | {% endelement %} 15 | {% else %} 16 | {% element h1 %} 17 | {% blocktrans with provider.name as provider %}Sign In Via {{ provider }}{% endblocktrans %} 18 | {% endelement %} 19 | {% element p %} 20 | {% blocktrans with provider.name as provider %}You are about to sign in using a third-party account from {{ provider }}.{% endblocktrans %} 21 | {% endelement %} 22 | {% endif %} 23 | {% element form method="post" no_visible_fields=True %} 24 | {% slot actions %} 25 | {% csrf_token %} 26 | {% element button type="submit" %} 27 | {% trans "Continue" %} 28 | {% endelement %} 29 | {% endslot %} 30 | {% endelement %} 31 | {% endblock content %} 32 | -------------------------------------------------------------------------------- /common/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import UserPassesTestMixin 2 | 3 | from volunteer.models import VolunteerProfile 4 | 5 | 6 | class AdminRequiredMixin(UserPassesTestMixin): 7 | """Mixin for views that require administrative permission. 8 | 9 | Currently it requires the user to be a superuser or staff member. 10 | This can be extended to include more complex permission checks in the future. 11 | """ 12 | 13 | def test_func(self): 14 | return self.request.user.is_superuser or self.request.user.is_staff 15 | 16 | 17 | class VolunteerOrAdminRequiredMixin(UserPassesTestMixin): 18 | """Mixin for views that require the user to be the owner of the object or an admin. 19 | 20 | This is useful for views where users can only access their own data unless they have 21 | administrative privileges. 22 | """ 23 | 24 | def test_func(self): 25 | pk = self.kwargs.get("pk") 26 | instance = VolunteerProfile.objects.filter(pk=pk).first() 27 | is_owner = False 28 | if instance and instance.user == self.request.user: 29 | is_owner = True 30 | is_admin = self.request.user.is_superuser or self.request.user.is_staff 31 | return is_owner or is_admin 32 | -------------------------------------------------------------------------------- /templates/account/signup_by_passkey.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load allauth i18n %} 3 | {% block head_title %} 4 | {% trans "Signup" %} 5 | {% endblock head_title %} 6 | {% block content %} 7 | {% element h1 %} 8 | {% trans "Passkey Sign Up" %} 9 | {% endelement %} 10 | {% setvar link %} 11 | 12 | {% endsetvar %} 13 | {% setvar end_link %} 14 | 15 | {% endsetvar %} 16 | {% element p %} 17 | {% blocktranslate %}Already have an account? Then please {{ link }}sign in{{ end_link }}.{% endblocktranslate %} 18 | {% endelement %} 19 | {% url 'account_signup_by_passkey' as action_url %} 20 | {% element form form=form method="post" action=action_url tags="entrance,signup" %} 21 | {% slot body %} 22 | {% csrf_token %} 23 | {% element fields form=form unlabeled=True %} 24 | {% endelement %} 25 | {{ redirect_field }} 26 | {% endslot %} 27 | {% slot actions %} 28 | {% element button tags="prominent,signup" type="submit" %} 29 | {% trans "Sign Up" %} 30 | {% endelement %} 31 | {% endslot %} 32 | {% endelement %} 33 | {% element hr %} 34 | {% endelement %} 35 | {% element button href=signup_url tags="prominent,signup,outline,primary" %} 36 | {% trans "Other options" %} 37 | {% endelement %} 38 | {% endblock content %} 39 | -------------------------------------------------------------------------------- /templates/portal/sponsor_donate_callout.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    5 |

    6 | Support PyLadiesCon! 7 |

    8 |

    9 | Help us make PyLadiesCon possible by becoming a sponsor or making a donation. 10 | Your support enables us to create an inclusive and accessible conference for the global PyLadies 11 | community. 12 |

    13 |
    14 | 28 |
    29 |
    30 | -------------------------------------------------------------------------------- /templates/mfa/recovery_codes/index.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/recovery_codes/base.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block content %} 5 | {% element h1 %} 6 | {% translate "Recovery Codes" %} 7 | {% endelement %} 8 | {% element p %} 9 | {% blocktranslate count unused_count=unused_codes|length %}There is {{ unused_count }} out of {{ total_count }} recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes available.{% endblocktranslate %} 10 | {% endelement %} 11 | {% element field id="recovery_codes" type="textarea" disabled=True rows=unused_codes|length readonly=True %} 12 | {% slot label %} 13 | {% translate "Unused codes" %} 14 | {% endslot %} 15 | {% comment %} djlint:off {% endcomment %} 16 | {% slot value %}{% for code in unused_codes %}{% if forloop.counter0 %} 17 | {% endif %}{{ code }}{% endfor %}{% endslot %} 18 | {% comment %} djlint:on {% endcomment %} 19 | {% endelement %} 20 | {% if unused_codes %} 21 | {% url 'mfa_download_recovery_codes' as download_url %} 22 | {% element button href=download_url %} 23 | {% translate "Download codes" %} 24 | {% endelement %} 25 | {% endif %} 26 | {% url 'mfa_generate_recovery_codes' as generate_url %} 27 | {% element button href=generate_url %} 28 | {% translate "Generate new codes" %} 29 | {% endelement %} 30 | {% endblock content %} 31 | -------------------------------------------------------------------------------- /docs/user/get_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with PyLadiesCon 2 | 3 | ## Who needs an account? 4 | 5 | If you're interested in volunteering or sponsoring the conference, you'll need to create an account. 6 | 7 | If you're part of the PyLadiesCon Core team, you'll also need to create an account. This will allow you to access the Django admin area and other features. 8 | 9 | If you're a speaker, at this time you don't need to create an account yet. (TBD) 10 | 11 | If you're a conference attendee, you don't need to create an account. (TBD) 12 | 13 | ## Sign up for an account 14 | 15 | In order to volunteer with PyLadiesCon, you'll first need to create an account. 16 | 17 | 1. Go to the [Sign up page](https://portal.pyladies.com/accounts/signup/) 18 | 2. Choose a username, enter your email address, and your password. 19 | 20 | ## Verify your account 21 | 22 | After signing up, you'll receive an email with a verification code. Enter this code on the sign up page to verify your 23 | account. 24 | 25 | ## Changing your email address 26 | 27 | You can add more than one email address to your account. We will be sending emails to your primary account. 28 | 29 | To change an email address, add a new one to your account, and then set it as the primary email address. 30 | 31 | We will only send email to your secondary email if we've been unable to contact you in your primary email address. 32 | 33 | -------------------------------------------------------------------------------- /docs/developer/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing 3 | description: Contributing to PyLadiesCon Portal 4 | --- 5 | 6 | # Contributing Guide 7 | 8 | ## Where and How to get started 9 | 10 | Find an issue in our [repo](https://github.com/pyladies/pyladiescon-portal) to get started. You can also create a new issue or [start a discussion item](https://github.com/pyladies/pyladiescon-portal/discussions) on GitHub. 11 | 12 | ## Setup 13 | 14 | To start working with the code, refer to our [setup guide](/developer/setup/). 15 | 16 | Ask questions in the **[#portal_dev](https://discord.gg/X6fcufjb)** channel of our Discord comunity. 17 | 18 | ## Running Tests 19 | 20 | Tests are run using [pytest](https://docs.pytest.org/en/latest/). 21 | 22 | To run the tests: 23 | 24 | ```bash 25 | make test 26 | ``` 27 | 28 | ## Test coverage 29 | 30 | We aim for 100% test coverage. The coverage report is generated after running the tests, and can be viewed in the `htmlcov` directory. 31 | 32 | ## Code style and linting 33 | 34 | Run ``make reformat`` and ``make check`` prior to committing your code. 35 | There is a CI that checks for code style and linting issues. 36 | 37 | PRs will not be merged if there is any CI failures. 38 | 39 | ## Documentation Preview on Pull Requests 40 | 41 | A documentation preview is generated for each pull request using Netlify. This allows us to preview 42 | the docs changes before merging the PR. 43 | -------------------------------------------------------------------------------- /portal/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-03-18 22:57 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="BaseModel", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ( 27 | "creation_date", 28 | models.DateTimeField( 29 | default=django.utils.timezone.now, 30 | editable=False, 31 | verbose_name="creation_date", 32 | ), 33 | ), 34 | ( 35 | "modified_date", 36 | models.DateTimeField( 37 | default=django.utils.timezone.now, 38 | editable=False, 39 | verbose_name="modified_date", 40 | ), 41 | ), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /portal/models.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.admin.widgets import FilteredSelectMultiple 3 | from django.contrib.postgres.fields import ArrayField 4 | from django.db import models 5 | from django.utils.timezone import now 6 | 7 | 8 | class ChoiceArrayField(ArrayField): 9 | 10 | def formfield(self, **kwargs): # pragma: no cover 11 | # Currently this is only used the languages_spoken field but the field is being deprecated 12 | defaults = { 13 | "form_class": forms.TypedMultipleChoiceField, 14 | "choices": self.base_field.choices, 15 | "coerce": self.base_field.to_python, 16 | "widget": FilteredSelectMultiple(self.verbose_name, False), 17 | } 18 | defaults.update(kwargs) 19 | return super(ArrayField, self).formfield(**defaults) 20 | 21 | 22 | class BaseModel(models.Model): 23 | creation_date = models.DateTimeField( 24 | "creation_date", editable=False, auto_now_add=True 25 | ) 26 | modified_date = models.DateTimeField("modified_date", editable=False, auto_now=True) 27 | 28 | def save(self, *args, **kwargs): 29 | self.modified_date = now() 30 | if ( 31 | "update_fields" in kwargs and "modified_date" not in kwargs["update_fields"] 32 | ): # pragma: no cover 33 | kwargs["update_fields"].append("modified_date") 34 | super().save(*args, **kwargs) 35 | -------------------------------------------------------------------------------- /docs/about/index.md: -------------------------------------------------------------------------------- 1 | ## Why this project exists 2 | 3 | Being an online, multi-language, multi-timezone conference, we face unique and different challenges from other types of events and conferences. 4 | Our organizers are all volunteers from different part of the world. 5 | We have many communications and coordinations with our team of volunteers ahead of the conference, and less during the conference itself. 6 | 7 | ## Challenges in managing our online conference 8 | 9 | 🤕 Our organizing team collaborate with each other to manage our volunteers, our speakers, and our sponsors. 10 | Each team also collaborate with our design and media team to produce promotional assets. 11 | For the last 2 years, we manage a lot of our assets and information using spreadsheets and Google Forms. 12 | However, managing and sharing data with various volunteers using spreadsheets have been challenging, and causing frustrations and confusions among our team of volunteers. 13 | 14 | ## What we're building: PyLadiesCon Web Portal 15 | 16 | 💻 This year, we are developing an online web portal for us to manage the behind the scenes work of our conference. 17 | Instead of spreadsheets, we will be accepting volunteer sign ups and sponsorship sign ups through our web portal. 18 | Our team leads will be assigning task and tracking team progress through the web portal. 19 | We also want to build a conference dashboard to provide overview and statistics about our conference. 20 | -------------------------------------------------------------------------------- /templates/mfa/totp/activate_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/totp/base.html" %} 2 | {% load allauth i18n %} 3 | {% block head_title %} 4 | {% translate "Activate Authenticator App" %} 5 | {% endblock head_title %} 6 | {% block content %} 7 | {% element h1 %} 8 | {% translate "Activate Authenticator App" %} 9 | {% endelement %} 10 | {% element p %} 11 | {% blocktranslate %}To protect your account with two-factor authentication, scan the QR code below with your authenticator app. Then, input the verification code generated by the app below.{% endblocktranslate %} 12 | {% endelement %} 13 | {% url 'mfa_activate_totp' as action_url %} 14 | {% element form form=form method="post" action=action_url %} 15 | {% slot body %} 16 | {% element img src=totp_svg_data_uri alt=form.secret tags="mfa,totp,qr" %} 17 | {% endelement %} 18 | {% csrf_token %} 19 | {% element field id="authenticator_secret" type="text" value=form.secret disabled=True %} 20 | {% slot label %} 21 | {% translate "Authenticator secret" %} 22 | {% endslot %} 23 | {% slot help_text %} 24 | {% translate "You can store this secret and use it to reinstall your authenticator app at a later time." %} 25 | {% endslot %} 26 | {% endelement %} 27 | {% element fields form=form %} 28 | {% endelement %} 29 | {% endslot %} 30 | {% slot actions %} 31 | {% element button type="submit" %} 32 | {% trans "Activate" %} 33 | {% endelement %} 34 | {% endslot %} 35 | {% endelement %} 36 | {% endblock content %} 37 | -------------------------------------------------------------------------------- /templates/portal_account/portalprofile_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% block body %} 5 | {% endblock body %} 6 | {% block content %} 7 |
    8 |

    9 | {% trans "Edit your Portal Profile" %} 10 |

    11 |

    12 | {% trans "Update your portal profile information below." %} 13 |

    14 |
    15 | {% bootstrap_form_errors form %} 16 |
    19 | {% csrf_token %} 20 | {% bootstrap_field form.username %} 21 | {% bootstrap_field form.first_name %} 22 | {% bootstrap_field form.last_name %} 23 | {% bootstrap_field form.email %} 24 | {% bootstrap_field form.pronouns %} 25 | {% bootstrap_field form.coc_agreement %} 26 |
    27 | 28 | {% trans "Cancel" %} 30 |
    31 |
    32 |
    33 |
    34 | {% endblock content %} 35 | -------------------------------------------------------------------------------- /templates/emails/volunteer/internal_volunteer_profile_email_notification.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | Hello, {{ recipient_name }}. 6 | 7 | We're writing to let you know that a new volunteer has just signed up. Be sure to review their application in a timely manner. 8 | 9 | **Volunteer Application Status:** {{ profile.application_status }}. 10 | 11 | ## Volunteer Information 12 | 13 | **Name:** {{ profile.user.first_name }} {{ profile.user.last_name }} 14 | 15 | ### Details 16 | 17 | - **Availability:** {{ profile.availability_hours_per_week }} hours per week 18 | - **GitHub username:** {{ profile.github_username }} 19 | - **Discord username:** {{ profile.discord_username }} 20 | - **Instagram username:** {{ profile.instagram_username }} 21 | - **Bluesky username:** {{ profile.bluesky_username }} 22 | - **Mastodon URL:** {{ profile.mastodon_url }} 23 | - **X username:** {{ profile.x_username }} 24 | - **LinkedIn URL:** {{ profile.linkedin_url }} 25 | - **PyLadies Chapter:** {{ profile.chapter }} 26 | - **Region:** {{ profile.region }} 27 | - **Additional Comments:** {{ profile.additional_comments }} 28 | 29 | To approve this volunteer and assign them to your teams, click [here](https://{{ current_site.domain }}{% url 'volunteer:volunteer_profile_manage' profile.id %}). 30 | 31 | To manage other volunteers, visit the [Volunteer Management Portal](https://{{ current_site.domain }}{% url 'volunteer:volunteer_profile_list' %}). 32 | 33 | {% endblock content %} -------------------------------------------------------------------------------- /templates/emails/volunteer/volunteer_cancellation_confirmation.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | Dear {{ profile.user.first_name|default:profile.user.username }}, 6 | 7 | This email confirms that your Volunteer application for PyLadiesCon has been cancelled. 8 | 9 | ## Changes Made 10 | 11 | Your Volunteer Profile status has been set to **"Cancelled"** 12 | 13 | {% if teams_removed %} 14 | You have been removed from the following teams: 15 | 16 | {% for team in teams_removed %}- {{ team.short_name }} {% endfor %} 17 | 18 | {% endif %} 19 | 20 | {% if roles_removed %} 21 | You have been removed from the following roles: 22 | 23 | {% for role in roles_removed %}- {{ role.short_name }} {% endfor %} 24 | 25 | {% endif %} 26 | 27 | Team leads have been notified of your departure. 28 | 29 | Your access to volunteer resources will be revoked within the next 24 hours. 30 | 31 | We understand that circumstances can change, and we appreciate the time you were willing to dedicate to PyLadiesCon. 32 | 33 | If you change your mind in the future and would like to volunteer again, 34 | you're welcome to submit a new volunteer application through our portal. 35 | 36 | If this was a mistake, or you do not wish to cancel your application, please contact us so that we 37 | can rectify the situation. 38 | 39 | Thank you for your interest in PyLadiesCon, and we hope to see you as a participant or volunteer in future events! 40 | 41 | Best regards. 42 | 43 | {% endblock content %} -------------------------------------------------------------------------------- /templates/portal/historical_comparison_charts.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Historical comparison charts for PyLadiesCon stats across years 3 | Uses Google Charts to display bar charts comparing metrics from 2023, 2024, and 2025 4 | {% endcomment %} 5 | {% for comparison in stats.historical_comparison %} 6 | var data_{{ comparison.chart_id }} = new google.visualization.DataTable(); 7 | {% for column in comparison.columns %} 8 | data_{{ comparison.chart_id }}.addColumn('{{ column.0 }}', '{{ column.1 }}'); 9 | {% endfor %} 10 | data_{{ comparison.chart_id }}.addRows([ 11 | {% for row in comparison.data %} 12 | ['{{ row.0 }}', {{ row.1 }}] 13 | {% if not forloop.last %} 14 | , 15 | {% endif %} 16 | {% endfor %} 17 | ]); 18 | var options_{{ comparison.chart_id }} = { 19 | title: '{{ comparison.title }}', 20 | chartArea: { width: '70%' }, 21 | hAxis: { 22 | title: 'Year', 23 | minValue: 0 24 | }, 25 | vAxis: { 26 | title: '{{ comparison.columns.1.1 }}' 27 | }, 28 | colors: ['#7C5BC8'], 29 | legend: { position: 'none' }, 30 | animation: { 31 | startup: true, 32 | duration: 1000, 33 | easing: 'out' 34 | } 35 | }; 36 | var chart_{{ comparison.chart_id }} = new google.visualization.ColumnChart( 37 | document.getElementById('{{ comparison.chart_id }}') 38 | ); 39 | chart_{{ comparison.chart_id }}.draw(data_{{ comparison.chart_id }}, options_{{ comparison.chart_id }}); 40 | {% endfor %} 41 | -------------------------------------------------------------------------------- /sponsorship/migrations/0004_sponsorshipprofile_organization_address_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-10-26 04:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sponsorship", "0003_remove_sponsorshipprofile_application_status_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="sponsorshipprofile", 15 | name="organization_address", 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="sponsorshipprofile", 20 | name="sponsor_contact_name", 21 | field=models.CharField(blank=True, max_length=255, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name="sponsorshipprofile", 25 | name="sponsors_contact_email", 26 | field=models.EmailField(blank=True, max_length=254, null=True), 27 | ), 28 | migrations.AddField( 29 | model_name="sponsorshipprofile", 30 | name="sponsorship_override_amount", 31 | field=models.DecimalField( 32 | blank=True, decimal_places=2, max_digits=10, null=True 33 | ), 34 | ), 35 | migrations.AlterField( 36 | model_name="sponsorshipprofile", 37 | name="company_description", 38 | field=models.TextField(blank=True, null=True), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /templates/emails/volunteer/team_is_now_closed.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | {{ profile.user.first_name }}, thank you for applying to volunteer with us. 6 | 7 | {% if updated %} 8 | Your volunteer profile has recently been updated. 9 | {% endif %} 10 | 11 | ## Application Status 12 | 13 | We received many volunteer applications, and we're not able to accept everyone. At this time, the team you've applied for is **at full capacity**. 14 | 15 | We are placing you on a **waitlist** in case the volunteer opportunity opens up in the future. 16 | 17 | Thank you for your understanding and for your interest in volunteering with us! 18 | 19 | ## Your Information 20 | 21 | - **Availability:** {{ profile.availability_hours_per_week }} hours per week 22 | - **GitHub username:** {{ profile.github_username }} 23 | - **Discord username:** {{ profile.discord_username }} 24 | - **Instagram username:** {{ profile.instagram_username }} 25 | - **Bluesky username:** {{ profile.bluesky_username }} 26 | - **Mastodon URL:** {{ profile.mastodon_url }} 27 | - **X username:** {{ profile.x_username }} 28 | - **LinkedIn URL:** {{ profile.linkedin_url }} 29 | - **PyLadies Chapter:** {{ profile.chapter }} 30 | - **Region:** {{ profile.region }} 31 | - **Additional Comments:** {{ profile.additional_comments }} 32 | 33 | If you would like to review or update your application at any time, go to your [Volunteer Dashboard](https://{{ current_site.domain }}{% url 'volunteer:index' %}). 34 | 35 | {% endblock content %} -------------------------------------------------------------------------------- /portal/forms.py: -------------------------------------------------------------------------------- 1 | from allauth.account.forms import SignupForm 2 | from django import forms 3 | 4 | 5 | class CustomSignupForm(SignupForm): 6 | first_name = forms.CharField( 7 | max_length=200, 8 | label="First Name", 9 | widget=forms.TextInput(attrs={"placeholder": "First Name"}), 10 | ) 11 | last_name = forms.CharField( 12 | max_length=200, 13 | label="Last Name", 14 | widget=forms.TextInput(attrs={"placeholder": "Last Name"}), 15 | ) 16 | coc_agreement = forms.BooleanField( 17 | required=True, 18 | label="I agree to the Code of Conduct", 19 | help_text="You must agree to our Code of Conduct to use this site.", 20 | ) 21 | tos_agreement = forms.BooleanField( 22 | required=True, 23 | label="I agree to the Terms of Service", 24 | help_text="You must agree to our Terms of Service to use this site.", 25 | ) 26 | 27 | def save(self, request): 28 | user = super().save(request) 29 | user.first_name = self.cleaned_data["first_name"] 30 | user.last_name = self.cleaned_data["last_name"] 31 | user.save() 32 | 33 | from portal_account.models import PortalProfile 34 | 35 | portal_profile, created = PortalProfile.objects.get_or_create(user=user) 36 | portal_profile.coc_agreement = self.cleaned_data.get("coc_agreement", False) 37 | portal_profile.tos_agreement = self.cleaned_data.get("tos_agreement", False) 38 | portal_profile.save() 39 | 40 | return user 41 | -------------------------------------------------------------------------------- /templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load account %} 4 | {% load allauth %} 5 | {% block head_title %} 6 | {% trans "Confirm Email Address" %} 7 | {% endblock head_title %} 8 | {% block content %} 9 | {% element h1 %} 10 | {% trans "Confirm Email Address" %} 11 | {% endelement %} 12 | {% if confirmation %} 13 | {% user_display confirmation.email_address.user as user_display %} 14 | {% if can_confirm %} 15 | {% element p %} 16 | {% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an email address for user {{ user_display }}.{% endblocktrans %} 17 | {% endelement %} 18 | {% url "account_confirm_email" confirmation.key as action_url %} 19 | {% element form method="post" action=action_url %} 20 | {% slot actions %} 21 | {% csrf_token %} 22 | {{ redirect_field }} 23 | {% element button type="submit" %} 24 | {% trans "Confirm" %} 25 | {% endelement %} 26 | {% endslot %} 27 | {% endelement %} 28 | {% else %} 29 | {% element p %} 30 | {% blocktrans %}Unable to confirm {{ email }} because it is already confirmed by a different account.{% endblocktrans %} 31 | {% endelement %} 32 | {% endif %} 33 | {% else %} 34 | {% url "account_email" as email_url %} 35 | {% element p %} 36 | {% blocktrans %}This email confirmation link expired or is invalid. Please issue a new email confirmation request.{% endblocktrans %} 37 | {% endelement %} 38 | {% endif %} 39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /templates/emails/volunteer/volunteer_profile_email_notification.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | {{ profile.user.first_name }}, thank you for applying to volunteer with us. 6 | 7 | {% if updated %} 8 | Your volunteer profile has recently been updated. 9 | {% endif %} 10 | 11 | ## Application Status 12 | 13 | Your current volunteer application status: **{{ profile.application_status }}**. 14 | 15 | ## Your Information 16 | 17 | - **Availability:** {{ profile.availability_hours_per_week }} hours per week 18 | - **GitHub username:** {{ profile.github_username }} 19 | - **Discord username:** {{ profile.discord_username }} 20 | - **Instagram username:** {{ profile.instagram_username }} 21 | - **Bluesky username:** {{ profile.bluesky_username }} 22 | - **Mastodon URL:** {{ profile.mastodon_url }} 23 | - **X username:** {{ profile.x_username }} 24 | - **LinkedIn URL:** {{ profile.linkedin_url }} 25 | - **PyLadies Chapter:** {{ profile.chapter }} 26 | - **Region:** {{ profile.region }} 27 | - **Additional Comments:** {{ profile.additional_comments }} 28 | 29 | If you would like to review or update your application at any time, go to your [Volunteer Dashboard](https://{{ current_site.domain }}{% url 'volunteer:index' %}) at https://{{ current_site.domain }}{% url 'volunteer:index' %}. 30 | 31 | Be sure to join our [Discord server](https://discord.com/invite/2fUN4ddVfP) at https://discord.com/invite/2fUN4ddVfP. All of our volunteers are required to join the server to communicate and collaborate with the rest of the team. 32 | 33 | {% endblock content %} -------------------------------------------------------------------------------- /templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Change Password" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% if token_fail %} 10 | {% trans "Bad Token" %} 11 | {% else %} 12 | {% trans "Change Password" %} 13 | {% endif %} 14 | {% endelement %} 15 | {% if token_fail %} 16 | {% url 'account_reset_password' as passwd_reset_url %} 17 | {% element p %} 18 | {% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %} 19 | {% endelement %} 20 | {% else %} 21 | {% element form method="post" action=action_url %} 22 | {% slot body %} 23 | {% csrf_token %} 24 | {{ redirect_field }} 25 | {% element fields form=form %} 26 | {% endelement %} 27 | {% endslot %} 28 | {% slot actions %} 29 | {% element button type="submit" name="action" %} 30 | {% trans "Change Password" %} 31 | {% endelement %} 32 | {% element button type="submit" form="logout-from-stage" tags="link,cancel" %} 33 | {% translate "Cancel" %} 34 | {% endelement %} 35 | {% endslot %} 36 | {% endelement %} 37 | {% endif %} 38 | {% if not cancel_url %} 39 |
    42 | 43 | {% csrf_token %} 44 |
    45 | {% endif %} 46 | {% endblock content %} 47 | -------------------------------------------------------------------------------- /portal_account/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-03-23 21:45 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("portal", "0001_initial"), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name="PortalProfile", 20 | fields=[ 21 | ( 22 | "basemodel_ptr", 23 | models.OneToOneField( 24 | auto_created=True, 25 | on_delete=django.db.models.deletion.CASCADE, 26 | parent_link=True, 27 | primary_key=True, 28 | serialize=False, 29 | to="portal.basemodel", 30 | ), 31 | ), 32 | ( 33 | "pronouns", 34 | models.CharField(blank=True, max_length=100, null=True), 35 | ), 36 | ("coc_agreement", models.BooleanField(default=False)), 37 | ( 38 | "user", 39 | models.OneToOneField( 40 | on_delete=django.db.models.deletion.CASCADE, 41 | to=settings.AUTH_USER_MODEL, 42 | ), 43 | ), 44 | ], 45 | bases=("portal.basemodel",), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /templates/portal_account/index.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% block body %} 5 | {% endblock body %} 6 | {% block content %} 7 |

    8 | {% trans "Account Management" %} 9 |

    10 | {% trans "Manage Emails" %} 13 | {% trans "Change Password" %} 16 | {% trans "Sign out" %} 19 |
    20 |

    21 | {% trans "Your Portal Profile" %} 22 |

    23 | {% if profile_id %} 24 | {% trans "View my profile" %} 27 | Edit profile 30 | {% else %} 31 |

    32 | {% blocktranslate %}Your profile is not complete. Please complete your profile before proceeding. {% endblocktranslate %} 33 |

    34 | {% trans "Create my profile" %} 37 | {% endif %} 38 | {% endblock content %} 39 | -------------------------------------------------------------------------------- /docs/user/volunteer.md: -------------------------------------------------------------------------------- 1 | # Volunteers 2 | 3 | Thank you for your interest in volunteering with PyLadiesCon. Please review prerequisites prior to signing up. 4 | 5 | ## Prerequisites 6 | 7 | Ensure you have created an account for PyLadiesCon. Information on how to do that is on the [getting started page](/user/get_started/). 8 | 9 | Communication for PyLadiesCon is done via Discord. Go to [Discord](https://discord.com/register) to register for an account. 10 | 11 | Review available volunteer roles, responsibilities, and required commitment for the roles in our [main conference site](https://conference.pyladies.com/docs/). Keep the teams in mind for your signup. 12 | 13 | ## Signup 14 | 15 | Once you're ready to volunteer, please follow these steps: 16 | 17 | 1. Go to the [portal website](https://portal.pyladies.com) and sign in 18 | 2. Go to Volunteer 19 | 3. Click create your volunteer profile 20 | 4. Fill in required information (hours available, Discord username, language selection, team selection, etc.). Feel free to fill in additional information about what skills you have so we can determine what team best fits your skillset or interests. 21 | 22 | You'll receive an email from `pyladiescon@pyladies.com` stating that your volunteer application was received. 23 | 24 | ## Review 25 | 26 | After you've filled out your volunteer profile, our team leads will review your application and assign you to the appropriate roles and begin the onboarding process. 27 | 28 | Accepted applications will be given access to necessary resources, e.g. Discord roles, GDrive permissions etc. All communications will be done via the email that you signed up with. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/signup_form.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load allauth %} 5 | {% block content %} 6 | {% element h1 %} 7 | {% trans "Create Passkey" %} 8 | {% endelement %} 9 | {% element p %} 10 | {% blocktranslate %}You are about to create a passkey for your account. As you can add additional keys later on, you can use a descriptive name to tell the keys apart.{% endblocktranslate %} 11 | {% endelement %} 12 | {% url "mfa_signup_webauthn" as action_url %} 13 | {% element form form=form method="post" action=action_url %} 14 | {% slot body %} 15 | {% csrf_token %} 16 | {% element fields form=form %} 17 | {% endelement %} 18 | {% endslot %} 19 | {% slot actions %} 20 | {% element button id="mfa_webauthn_signup" type="button" %} 21 | {% trans "Create" %} 22 | {% endelement %} 23 | {% endslot %} 24 | {% endelement %} 25 | {% element button type="submit" form="logout-from-stage" tags="link,cancel" %} 26 | {% translate "Cancel" %} 27 | {% endelement %} 28 |
    31 | 32 | {% csrf_token %} 33 |
    34 | {% include "mfa/webauthn/snippets/scripts.html" %} 35 | {{ js_data|json_script:"js_data" }} 36 | 44 | {% endblock content %} 45 | -------------------------------------------------------------------------------- /sponsorship/migrations/0007_individualdonation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.8 on 2025-11-14 03:49 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("portal", "0002_alter_basemodel_creation_date_and_more"), 11 | ("sponsorship", "0006_sponsorshipprofile_po_number"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="IndividualDonation", 17 | fields=[ 18 | ( 19 | "basemodel_ptr", 20 | models.OneToOneField( 21 | auto_created=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | parent_link=True, 24 | primary_key=True, 25 | serialize=False, 26 | to="portal.basemodel", 27 | ), 28 | ), 29 | ("transaction_id", models.CharField(max_length=100, unique=True)), 30 | ("donor_name", models.CharField(blank=True, max_length=255, null=True)), 31 | ("transaction_date", models.DateTimeField(blank=True, null=True)), 32 | ( 33 | "donation_amount", 34 | models.DecimalField(decimal_places=2, max_digits=10), 35 | ), 36 | ("donor_email", models.EmailField(max_length=254)), 37 | ("is_anonymous", models.BooleanField(default=False)), 38 | ], 39 | bases=("portal.basemodel",), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /templates/emails/volunteer/team_lead_cancellation_notification.md: -------------------------------------------------------------------------------- 1 | {% extends "emails/base_email.md" %} 2 | {% load i18n %} 3 | {% block content %} 4 | 5 | Dear Team Lead, 6 | 7 | This email is to inform you that a member of your team has been canceled from volunteering. 8 | 9 | Please review the following details, 10 | and take the necessary steps to offboard the volunteer from your team and revoke their access to any resources they may have had. 11 | 12 | ## Volunteer Information 13 | 14 | - **Name**: {{ profile.user.first_name }} {{ profile.user.last_name }} 15 | - **Username**: {{ profile.user.username }} 16 | - **Email**: {{ profile.user.email }} 17 | - **Team**: {{ team.short_name }} 18 | {% if profile.discord_username %}- **Discord**: {{ profile.discord_username }}{% endif %} 19 | {% if profile.discord_username %}- **GitHub**: {{ profile.discord_username }}{% endif %} 20 | 21 | 22 | ## Action Items 23 | 24 | As the team lead, you are responsible for completing the following offboarding tasks: 25 | 26 | - **Remove the Volunteer role on Discord.** Their Discord username is: **{{ profile.discord_username }}**. 27 | - **Remove access to PyLadiesCon GDrive** (if applicable). 28 | - **Remove access to Jelly** (if applicable). 29 | - **Remove access to the PyLadiesCon Regular Meeting Calendar** (if applicable). 30 | - **Remove access to the PyLadiesCon 1Password account** (if applicable). 31 | - **Remove access to GitHub repositories** (if applicable). 32 | 33 | **Please complete the above tasks within 24 hours.** 34 | 35 | If you have any questions or need assistance with the offboarding process, please reach out to the admin team. 36 | 37 | Best regards. 38 | 39 | {% endblock content %} -------------------------------------------------------------------------------- /volunteer/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.urls import path 3 | 4 | from . import views 5 | 6 | app_name = "volunteer" 7 | 8 | urlpatterns = [ 9 | path("", views.index, name="index"), 10 | path( 11 | "list", 12 | login_required(views.VolunteerProfileList.as_view()), 13 | name="volunteer_profile_list", 14 | ), 15 | path( 16 | "view//", 17 | login_required(views.VolunteerProfileView.as_view()), 18 | name="volunteer_profile_detail", 19 | ), 20 | path( 21 | "new", 22 | login_required(views.VolunteerProfileCreate.as_view()), 23 | name="volunteer_profile_new", 24 | ), 25 | path( 26 | "edit/", 27 | login_required(views.VolunteerProfileUpdate.as_view()), 28 | name="volunteer_profile_edit", 29 | ), 30 | path( 31 | "delete/", 32 | views.VolunteerProfileDelete.as_view(), 33 | name="volunteer_profile_delete", 34 | ), 35 | path( 36 | "list", 37 | login_required(views.VolunteerProfileList.as_view()), 38 | name="volunteers_list", 39 | ), 40 | path( 41 | "volunteer_profile_manage//", 42 | views.ManageVolunteerProfile.as_view(), 43 | name="volunteer_profile_manage", 44 | ), 45 | path( 46 | "resend_onboarding_email//", 47 | views.ResendOnboardingEmailView.as_view(), 48 | name="resend_onboarding_email", 49 | ), 50 | path( 51 | "cancel//", 52 | login_required(views.CancelVolunteeringView.as_view()), 53 | name="cancel_volunteering", 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /tests/sponsorship/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.core import mail 2 | from tablib import Dataset 3 | 4 | from sponsorship.admin import SponsorshipProfileResource 5 | from sponsorship.models import ( 6 | SponsorshipProgressStatus, 7 | ) 8 | 9 | 10 | class TestSponsorshipImportExport: 11 | def test_import_sponsorship_does_not_trigger_email(self, admin_user): 12 | dataset = Dataset() 13 | dataset.headers = [ 14 | "id", 15 | "organization_name", 16 | "sponsor_contact_name", 17 | "sponsors_contact_email", 18 | "sponsorship_tier", 19 | "progress_status", 20 | "sponsorship_override_amount", 21 | "main_contact_user", 22 | ] 23 | dataset.append( 24 | [ 25 | "", 26 | "Test 1", 27 | "", 28 | "", 29 | "", 30 | SponsorshipProgressStatus.AWAITING_RESPONSE.value, 31 | "", 32 | str(admin_user.id), 33 | ] 34 | ) 35 | 36 | dataset.append( 37 | [ 38 | "", 39 | "Test 2", 40 | "", 41 | "", 42 | "", 43 | SponsorshipProgressStatus.PAID.value, 44 | "", 45 | str(admin_user.id), 46 | ] 47 | ) 48 | 49 | resource = SponsorshipProfileResource() 50 | mail.outbox.clear() 51 | resource.import_data(dataset, dry_run=False) 52 | assert len(mail.outbox) == 0 # no email 53 | resource.import_data(dataset, dry_run=True) 54 | assert len(mail.outbox) == 0 # no email 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.14-bookworm AS base 2 | ENV PYTHONUNBUFFERED=1 3 | ENV PYTHONDONTWRITEBYTECODE=1 4 | RUN mkdir /code 5 | 6 | WORKDIR /code 7 | 8 | RUN pip --no-cache-dir --disable-pip-version-check install --upgrade pip setuptools wheel 9 | 10 | COPY requirements-app.txt /code/ 11 | RUN --mount=type=cache,target=/root/.cache/pip \ 12 | pip install -r requirements-app.txt 13 | 14 | RUN apt-get update && apt-get install -y gettext 15 | 16 | 17 | ############################################################################### 18 | # Build our development container 19 | ############################################################################### 20 | FROM base AS dev 21 | 22 | ARG USER_ID 23 | ARG GROUP_ID 24 | 25 | RUN groupadd -o -g $GROUP_ID -r usergrp 26 | RUN useradd -o -m -u $USER_ID -g $GROUP_ID user 27 | RUN chown user /code 28 | 29 | COPY requirements-dev.txt /code/ 30 | RUN --mount=type=cache,target=/root/.cache/pip \ 31 | pip install -r requirements-dev.txt 32 | 33 | RUN chown -R user /usr/local/lib/python3.14/site-packages 34 | 35 | USER user 36 | ENV PATH="${PATH}:/home/user/.local/bin" 37 | 38 | 39 | ############################################################################### 40 | # Build our production container 41 | ############################################################################### 42 | FROM base 43 | 44 | RUN chown -R nobody /usr/local/lib/python3.14/site-packages 45 | 46 | COPY . /code/ 47 | 48 | RUN \ 49 | DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,[::1] \ 50 | DJANGO_SECRET_KEY=deadbeefcafe \ 51 | DATABASE_URL=postgres://localhost:5432/db \ 52 | DJANGO_SETTINGS_MODULE=portal.settings \ 53 | python manage.py collectstatic --noinput --clear 54 | 55 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PyLadiesCon Web Portal Documentation 2 | 3 | Documentation for PyLadiesCon Web Portal. 4 | 5 | PyLadiesCon online conference management tool. 6 | 7 | ## About PyLadiesCon 8 | 9 | ✨ [PyLadiesCon](https://conference.pyladies.com) is an online conference for the global [PyLadies](https://pyladies.com) community. 10 | Our conference began in 2023. During our conference, we host 24 hours of online engagement, talks, keynotes, panels, and workshops for our community members. 11 | We strive of inclusivity and accessibility, providing talks in multiple-languages, and we take extra care in subtitling and translating our talks. 12 | Our conference is free to attend, and attendees can optionally donate to our conference. 13 | 14 | ## Support PyLadiesCon 15 | 16 | 🫶 Support us by contributing to our project, [donating](https://2025.conference.pyladies.com/en/donate/), or by sponsoring the [conference](https://conference.pyladies.com). 17 | 18 | We have many contribution opportunities, including code, testing, and documentations. All forms of contributions are welcome and appreciated. 19 | 20 | Check the Local Dev Setup section below to learn how to set this up on your local development environment and get started. 21 | 22 | - Repo: 23 | - Issues: 24 | - Project: 25 | - Milestone with due dates and goals: 26 | - Discord: 27 | 28 | 29 | Deploys by Netlify 30 | 31 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:16 4 | restart: always 5 | ports: 6 | - "5433:5432" 7 | environment: 8 | POSTGRES_USER: pyladiescon 9 | POSTGRES_PASSWORD: pyladiescon 10 | POSTGRES_DB: pyladiescon 11 | POSTGRES_HOST_AUTH_METHOD: trust # never do this in production! 12 | POSTGRES_FSYNC: null 13 | healthcheck: 14 | test: ["CMD", "pg_isready", "-U", "pyladiescon", "-d", "pyladiescon"] 15 | interval: 1s 16 | volumes: 17 | - ./docker-compose/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d 18 | - pgdata:/var/lib/postgresql/data 19 | 20 | redis: 21 | image: redis:7-bullseye 22 | ports: 23 | - "6379:6379" 24 | healthcheck: 25 | test: ["CMD", "redis-cli","ping"] 26 | interval: 1s 27 | 28 | web: 29 | build: 30 | target: dev 31 | image: pyladiescon-portal-web:docker-compose 32 | command: python manage.py runserver 0.0.0.0:8000 33 | volumes: 34 | - .:/code 35 | - ./media:/code/media 36 | ports: 37 | - "8000:8000" 38 | environment: 39 | DEBUG: True 40 | DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1,[::1] 41 | SECRET_KEY: verysecure 42 | DATABASE_URL: postgresql://pyladiescon:pyladiescon@postgres:5432/pyladiescon 43 | DJANGO_DEFAULT_FROM_EMAIL: PyLadiesCon 44 | DJANGO_EMAIL_HOST: maildev 45 | DJANGO_EMAIL_PORT: 1025 46 | depends_on: 47 | redis: 48 | condition: service_healthy 49 | postgres: 50 | condition: service_healthy 51 | 52 | maildev: 53 | image: maildev/maildev:2.2.1 54 | ports: 55 | - "1080:1080" 56 | - "1025:1025" 57 | 58 | volumes: 59 | pgdata: 60 | -------------------------------------------------------------------------------- /templates/account/phone_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage_phone.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Change Phone" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Change Phone" %} 10 | {% endelement %} 11 | {% url 'account_change_phone' as action_url %} 12 | {% element form method="post" action=action_url %} 13 | {% slot body %} 14 | {% csrf_token %} 15 | {% if phone %} 16 | {% element field id="current_phone" disabled=True type="tel" value=phone %} 17 | {% slot label %} 18 | {% translate "Current phone" %}: 19 | {% endslot %} 20 | {% if not phone_verified %} 21 | {% slot help_text %} 22 | {% blocktranslate %}Your phone number is still pending verification.{% endblocktranslate %} 23 | {% element button form="verify-phone" type="submit" tags="minor,secondary" %} 24 | {% trans "Re-send Verification" %} 25 | {% endelement %} 26 | {% endslot %} 27 | {% endif %} 28 | {% endelement %} 29 | {% endif %} 30 | {% element field id=form.phone.auto_id name="phone" value=form.phone.value errors=form.phone.errors type="tel" %} 31 | {% slot label %} 32 | {% translate "Change to" %}: 33 | {% endslot %} 34 | {% endelement %} 35 | {% endslot %} 36 | {% slot actions %} 37 | {% element button name="action_add" type="submit" %} 38 | {% trans "Change Phone" %} 39 | {% endelement %} 40 | {% endslot %} 41 | {% endelement %} 42 | {% if not phone_verified %} 43 | 50 | {% endif %} 51 | {% endblock content %} 52 | -------------------------------------------------------------------------------- /attendee/management/commands/fetch_pretix_orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management command to fetch orders from Pretix and collect Attendee data. 3 | """ 4 | 5 | from django.core.management.base import BaseCommand 6 | 7 | from attendee.models import AttendeeProfile, PretixOrder 8 | from common.pretix_wrapper import PRETIX_EVENT_SLUG, PRETIX_ORG, PretixWrapper 9 | 10 | 11 | class Command(BaseCommand): 12 | help = "Fetch Pretix orders and sync attendee demographics" 13 | 14 | def handle(self, *args, **options): 15 | pretix_wrapper = PretixWrapper(PRETIX_ORG, PRETIX_EVENT_SLUG) 16 | orders_synced = 0 17 | profiles_synced = 0 18 | 19 | for order in pretix_wrapper.get_orders(): 20 | order_code = order["code"] 21 | pretix_order, created = PretixOrder.objects.get_or_create( 22 | order_code=order_code 23 | ) 24 | pretix_order.from_pretix_data(order) 25 | pretix_order.save() 26 | orders_synced += 1 27 | 28 | # Sync attendee profile for paid orders 29 | profile, profile_created = AttendeeProfile.objects.get_or_create( 30 | order=pretix_order 31 | ) 32 | profile.from_pretix_data(order) 33 | profile.save() 34 | profiles_synced += 1 35 | 36 | if profile_created: 37 | self.stdout.write( 38 | self.style.SUCCESS( 39 | f"Created attendee profile for order {order_code}" 40 | ) 41 | ) 42 | 43 | self.stdout.write( 44 | self.style.SUCCESS( 45 | f"Successfully synced {orders_synced} orders and {profiles_synced} attendee profiles" 46 | ) 47 | ) 48 | -------------------------------------------------------------------------------- /templates/pyladies_chapter/index.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% block body %} 5 | {% endblock body %} 6 | {% block content %} 7 | 39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /sponsorship/emails.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.mail import send_mail 3 | from django.template.loader import render_to_string 4 | 5 | from sponsorship.constants import PSF_ACCOUNTING_EMAIL, SPONSORSHIP_COMMITTEE_EMAIL 6 | 7 | 8 | def send_sponsorship_status_emails(profile): 9 | user = profile.user 10 | 11 | # Email to sponsor 12 | sponsor_subject = "Your Sponsorship Profile Has Been Approved" 13 | sponsor_message = render_to_string( 14 | "sponsorship/email/sponsor_status_update.txt", 15 | {"user": user, "profile": profile}, 16 | ) 17 | send_mail( 18 | sponsor_subject, sponsor_message, settings.DEFAULT_FROM_EMAIL, [user.email] 19 | ) 20 | 21 | # Email to internal team (hardcoded for now) 22 | team_subject = f"New Sponsorship Approved: {profile.organization_name}" 23 | team_message = render_to_string( 24 | "sponsorship/email/team_status_notification.txt", 25 | {"user": user, "profile": profile}, 26 | ) 27 | send_mail( 28 | team_subject, 29 | team_message, 30 | settings.DEFAULT_FROM_EMAIL, 31 | ["team@example.com"], # Replace with actual team emails later 32 | ) 33 | 34 | 35 | def send_psf_invoice_request_email(profile): 36 | """Send email to PSF accounting team requesting sponsorship contract preparation.""" 37 | from common.markdown_emails import send_markdown_email 38 | 39 | subject = f"PyLadiesCon Sponsorship Contract Request: {profile.organization_name}" 40 | 41 | psf_accounting_emails = [PSF_ACCOUNTING_EMAIL, SPONSORSHIP_COMMITTEE_EMAIL] 42 | 43 | send_markdown_email( 44 | subject=subject, 45 | recipient_list=psf_accounting_emails, 46 | markdown_template="sponsorship/email/psf_invoice_request.md", 47 | context={"profile": profile}, 48 | ) 49 | -------------------------------------------------------------------------------- /templates/socialaccount/connections.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base_manage.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Account Connections" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Account Connections" %} 10 | {% endelement %} 11 | {% if form.accounts %} 12 | {% element p %} 13 | {% blocktrans %}You can sign in to your account using any of the following third-party accounts:{% endblocktrans %} 14 | {% endelement %} 15 | {% url "socialaccount_connections" as action_url %} 16 | {% element form form=form method="post" action=action_url %} 17 | {% slot body %} 18 | {% csrf_token %} 19 | {% for acc in form.fields.account.choices %} 20 | {% with account=acc.0.instance.get_provider_account %} 21 | {% setvar radio_id %} 22 | id_account_{{ account.account.pk }} 23 | {% endsetvar %} 24 | {% setvar tags %} 25 | socialaccount,{{ account.account.provider }} 26 | {% endsetvar %} 27 | {% element field id=radio_id type="radio" name="account" value=account.account.pk %} 28 | {% slot label %} 29 | {{ account }} 30 | {% element badge tags=tags %} 31 | {{ account.get_brand.name }} 32 | {% endelement %} 33 | {% endslot %} 34 | {% endelement %} 35 | {% endwith %} 36 | {% endfor %} 37 | {% endslot %} 38 | {% slot actions %} 39 | {% element button tags="delete,danger" type="submit" %} 40 | {% trans "Remove" %} 41 | {% endelement %} 42 | {% endslot %} 43 | {% endelement %} 44 | {% else %} 45 | {% element p %} 46 | {% trans "You currently have no third-party accounts connected to this account." %} 47 | {% endelement %} 48 | {% endif %} 49 | {% element h2 %} 50 | {% trans "Add a Third-Party Account" %} 51 | {% endelement %} 52 | {% include "socialaccount/snippets/provider_list.html" with process="connect" %} 53 | {% include "socialaccount/snippets/login_extra.html" %} 54 | {% endblock content %} 55 | -------------------------------------------------------------------------------- /attendee/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from import_export import resources 3 | from import_export.admin import ImportExportModelAdmin 4 | 5 | from .models import AttendeeProfile, PretixOrder 6 | 7 | 8 | @admin.register(PretixOrder) 9 | class PretixOrderAdmin(admin.ModelAdmin): 10 | list_display = ( 11 | "order_code", 12 | "status", 13 | "email", 14 | "name", 15 | "total", 16 | "datetime", 17 | "cancellation_date", 18 | "last_modified", 19 | "url", 20 | "is_anonymous", 21 | ) 22 | list_filter = ("status", "is_anonymous") 23 | search_fields = ("order_code", "email", "name") 24 | 25 | 26 | class AttendeeProfileResource(resources.ModelResource): 27 | class Meta: 28 | model = AttendeeProfile 29 | fields = ( 30 | "order", 31 | "order__name", 32 | "order__email", 33 | "order__status", 34 | "may_share_email_with_sponsor", 35 | "chapter_description", 36 | "chapter_email", 37 | "chapter_website", 38 | ) 39 | 40 | 41 | @admin.register(AttendeeProfile) 42 | class AttendeeProfileAdmin(ImportExportModelAdmin): 43 | list_display = ( 44 | "order", 45 | "city", 46 | "country", 47 | "current_position", 48 | "experience_level", 49 | "may_share_email_with_sponsor", 50 | "pyladies_chapter", 51 | "age_range", 52 | "organization_name", 53 | ) 54 | list_filter = ( 55 | "country", 56 | "experience_level", 57 | "may_share_email_with_sponsor", 58 | "age_range", 59 | ) 60 | search_fields = ( 61 | "order__order_code", 62 | "current_position", 63 | "country", 64 | "city", 65 | "participated_in_previous_event", 66 | ) 67 | readonly_fields = ("raw_answers",) 68 | resource_classes = [AttendeeProfileResource] 69 | -------------------------------------------------------------------------------- /portal/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user 2 | from django.http import JsonResponse 3 | from django.shortcuts import redirect, render 4 | 5 | from portal.common import get_stats_cached_values 6 | from portal_account.models import PortalProfile 7 | from volunteer.models import VolunteerProfile 8 | 9 | 10 | def index(request): 11 | """ 12 | Show personalized dashboard if user is authenticated and has a profile. 13 | Redirect to profile creation page if user is authenticated but has not completed their profile. 14 | Show landing page if user is not authenticated. 15 | """ 16 | context = {} 17 | 18 | user = get_user(request) 19 | if user.is_authenticated: 20 | if not PortalProfile.objects.filter(user=user).exists(): 21 | return redirect("portal_account:portal_profile_new") 22 | volunteer_profile = VolunteerProfile.objects.filter(user=user).first() 23 | context["volunteer_profile"] = volunteer_profile 24 | context["roles"] = volunteer_profile.roles.all() if volunteer_profile else [] 25 | else: 26 | context["volunteer_profile"] = None 27 | context["roles"] = [] 28 | 29 | context["stats"] = get_stats_cached_values() 30 | 31 | return render(request, "portal/index.html", context) 32 | 33 | 34 | def stats(request): 35 | """ 36 | Show Interesting Public Stats 37 | """ 38 | context = {} 39 | 40 | context["stats"] = get_stats_cached_values() 41 | 42 | return render(request, "portal/stats.html", context) 43 | 44 | 45 | def stats_json(request): 46 | """ 47 | Return the stats as a JSON response 48 | """ 49 | context = {} 50 | 51 | context["stats"] = get_stats_cached_values() 52 | 53 | return JsonResponse(context) 54 | 55 | 56 | def dashboard_gallery(request): 57 | """ 58 | Show a gallery of dashboards. 59 | """ 60 | context = {} 61 | 62 | return render(request, "portal/dashboard_gallery.html", context) 63 | -------------------------------------------------------------------------------- /volunteer/migrations/0007_team_open_to_new_members_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.3 on 2025-10-03 03:57 2 | 3 | from django.db import migrations, models 4 | 5 | import volunteer.constants 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("volunteer", "0006_alter_volunteerprofile_teams"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="team", 17 | name="open_to_new_members", 18 | field=models.BooleanField(default=True), 19 | ), 20 | migrations.AlterField( 21 | model_name="volunteerprofile", 22 | name="application_status", 23 | field=models.CharField( 24 | choices=[ 25 | ( 26 | volunteer.constants.ApplicationStatus["PENDING"], 27 | volunteer.constants.ApplicationStatus["PENDING"], 28 | ), 29 | ( 30 | volunteer.constants.ApplicationStatus["APPROVED"], 31 | volunteer.constants.ApplicationStatus["APPROVED"], 32 | ), 33 | ( 34 | volunteer.constants.ApplicationStatus["REJECTED"], 35 | volunteer.constants.ApplicationStatus["REJECTED"], 36 | ), 37 | ( 38 | volunteer.constants.ApplicationStatus["CANCELLED"], 39 | volunteer.constants.ApplicationStatus["CANCELLED"], 40 | ), 41 | ( 42 | volunteer.constants.ApplicationStatus["WAITLISTED"], 43 | volunteer.constants.ApplicationStatus["WAITLISTED"], 44 | ), 45 | ], 46 | default=volunteer.constants.ApplicationStatus["PENDING"], 47 | max_length=50, 48 | ), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /templates/allauth/elements/field.html: -------------------------------------------------------------------------------- 1 | {% load allauth %} 2 | {{ attrs.errors }} 3 |

    4 | {% if attrs.type == "textarea" %} 5 | 9 | 17 | {% else %} 18 | {% if attrs.type != "checkbox" and attrs.type != "radio" %} 19 | 23 | {% endif %} 24 | 34 | {% if attrs.type == "checkbox" or attrs.type == "radio" %} 35 | 39 | {% endif %} 40 | {% endif %} 41 | {% if slots.help_text %} 42 | 43 | {% slot help_text %} 44 | {% endslot %} 45 | 46 | {% endif %} 47 |

    48 | -------------------------------------------------------------------------------- /templates/account/base_confirm_code.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block content %} 5 | {% setvar title_ %} 6 | {% block title %} 7 | {% endblock title %} 8 | {% endsetvar %} 9 | {% setvar action_url %} 10 | {% block action_url %} 11 | {% endblock action_url %} 12 | {% endsetvar %} 13 | {% setvar extra_tags %} 14 | {% block extra_tags %} 15 | {% endblock extra_tags %} 16 | {% endsetvar %} 17 | {% setvar form_tags %} 18 | entrance,{{ extra_tags }} 19 | {% endsetvar %} 20 | {% setvar submit_button_tags %} 21 | prominent,confirm,{{ extra_tags }} 22 | {% endsetvar %} 23 | {% setvar recipient %} 24 | {% block recipient %} 25 | {% endblock recipient %} 26 | {% endsetvar %} 27 | {% element h1 %} 28 | {{ title_ }} 29 | {% endelement %} 30 | {% element p %} 31 | {% blocktranslate %}We've sent a code to {{ recipient }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} 32 | {% endelement %} 33 | {% element form form=form method="post" action=action_url tags=form_tags %} 34 | {% slot body %} 35 | {% csrf_token %} 36 | {% element fields form=form unlabeled=True %} 37 | {% endelement %} 38 | {{ redirect_field }} 39 | {% endslot %} 40 | {% slot actions %} 41 | {% element button type="submit" tags=submit_button_tags %} 42 | {% translate "Confirm" %} 43 | {% endelement %} 44 | {% if cancel_url %} 45 | {% element button href=cancel_url tags="link,cancel" %} 46 | {% translate "Cancel" %} 47 | {% endelement %} 48 | {% else %} 49 | {% element button type="submit" form="logout-from-stage" tags="link,cancel" %} 50 | {% translate "Cancel" %} 51 | {% endelement %} 52 | {% endif %} 53 | {% endslot %} 54 | {% endelement %} 55 | {% if not cancel_url %} 56 |
    59 | 60 | {% csrf_token %} 61 |
    62 | {% endif %} 63 | {% endblock content %} 64 | -------------------------------------------------------------------------------- /portal_account/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms import ModelForm 3 | 4 | from .models import PortalProfile 5 | 6 | 7 | class PortalProfileForm(ModelForm): 8 | 9 | username = forms.CharField(disabled=True) 10 | first_name = forms.CharField() 11 | last_name = forms.CharField() 12 | email = forms.CharField(disabled=True) 13 | coc_agreement = forms.BooleanField(disabled=True, required=False) 14 | tos_agreement = forms.BooleanField(disabled=True, required=False) 15 | 16 | class Meta: 17 | model = PortalProfile 18 | fields = ["pronouns", "profile_picture", "coc_agreement", "tos_agreement"] 19 | 20 | def clean(self): 21 | cleaned_data = super().clean() 22 | self.instance.username = cleaned_data.get("username") 23 | self.email = cleaned_data.get("email") 24 | return cleaned_data 25 | 26 | def __init__(self, *args, **kwargs): 27 | self.user = kwargs.pop("user", None) 28 | super().__init__(*args, **kwargs) 29 | if self.user: 30 | self.fields["username"].initial = self.user.username 31 | self.fields["email"].initial = self.user.email 32 | self.fields["first_name"].initial = self.user.first_name 33 | self.fields["last_name"].initial = self.user.last_name 34 | 35 | # fix field order 36 | self.order_fields( 37 | [ 38 | "username", 39 | "first_name", 40 | "last_name", 41 | "email", 42 | "pronouns", 43 | "profile_picture", 44 | "coc_agreement", 45 | "tos_agreement", 46 | ] 47 | ) 48 | 49 | def save(self, commit=True): 50 | """ """ 51 | user = self.user 52 | user.first_name = self.cleaned_data["first_name"] 53 | user.last_name = self.cleaned_data["last_name"] 54 | user.save() 55 | self.instance.user = user 56 | portal_profile = super().save(commit) 57 | return portal_profile 58 | -------------------------------------------------------------------------------- /templates/portal/dashboard_gallery.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% block body %} 5 | {% endblock body %} 6 | {% block content %} 7 | 41 | {% endblock content %} 42 | -------------------------------------------------------------------------------- /templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_entrance.html" %} 2 | {% load i18n %} 3 | {% load allauth account %} 4 | {% block head_title %} 5 | {% trans "Sign In" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Sign In" %} 10 | {% endelement %} 11 | {% if not SOCIALACCOUNT_ONLY %} 12 | {% setvar link %} 13 | 14 | {% endsetvar %} 15 | {% setvar end_link %} 16 | 17 | {% endsetvar %} 18 | {% element p %} 19 | {% blocktranslate %}If you have not created an account yet, then please {{ link }}sign up{{ end_link }} first.{% endblocktranslate %} 20 | {% endelement %} 21 | {% url 'account_login' as login_url %} 22 | {% element form form=form method="post" action=login_url tags="entrance,login" %} 23 | {% slot body %} 24 | {% csrf_token %} 25 | {% element fields form=form unlabeled=True %} 26 | {% endelement %} 27 | {{ redirect_field }} 28 | {% endslot %} 29 | {% slot actions %} 30 | {% element button type="submit" tags="prominent,login" %} 31 | {% trans "Sign In" %} 32 | {% endelement %} 33 | {% endslot %} 34 | {% endelement %} 35 | {% endif %} 36 | {% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %} 37 | {% element hr %} 38 | {% endelement %} 39 | {% element button_group vertical=True %} 40 | {% if PASSKEY_LOGIN_ENABLED %} 41 | {% element button type="submit" form="mfa_login" id="passkey_login" tags="prominent,login,outline,primary" %} 42 | {% trans "Sign in with a passkey" %} 43 | {% endelement %} 44 | {% endif %} 45 | {% if LOGIN_BY_CODE_ENABLED %} 46 | {% element button href=request_login_code_url tags="prominent,login,outline,primary" %} 47 | {% trans "Send me a sign-in code" %} 48 | {% endelement %} 49 | {% endif %} 50 | {% endelement %} 51 | {% endif %} 52 | {% if SOCIALACCOUNT_ENABLED %} 53 | {% include "socialaccount/snippets/login.html" with page_layout="entrance" %} 54 | {% endif %} 55 | {% endblock content %} 56 | {% block extra_body %} 57 | {{ block.super }} 58 | {% if PASSKEY_LOGIN_ENABLED %} 59 | {% include "mfa/webauthn/snippets/login_script.html" with button_id="passkey_login" %} 60 | {% endif %} 61 | {% endblock extra_body %} 62 | -------------------------------------------------------------------------------- /templates/team/index.html: -------------------------------------------------------------------------------- 1 | {% extends "portal/base.html" %} 2 | {% load allauth i18n %} 3 | {% load django_bootstrap5 %} 4 | {% block body %} 5 | {% endblock body %} 6 | {% block content %} 7 | 39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from attendee.models import ( 4 | PRETIX_ANONYMOUS_DONATION_QUESTION_IDENTIFIER, 5 | PRETIX_NOT_ANONYMOUS_ANSWER_IDENTIFIER, 6 | ) 7 | from volunteer.models import Language 8 | 9 | 10 | @pytest.fixture 11 | def portal_user(db, django_user_model): 12 | username = "testuser" 13 | password = "testpassword" 14 | email = "test@example.com" 15 | first_name = "fname" 16 | last_name = "lname" 17 | return django_user_model.objects.create_user( 18 | username=username, 19 | password=password, 20 | email=email, 21 | first_name=first_name, 22 | last_name=last_name, 23 | ) 24 | 25 | 26 | @pytest.fixture 27 | def admin_user(db, django_user_model): 28 | username = "adminuser" 29 | password = "adminpassword" 30 | email = "admin@example.com" 31 | first_name = "admin_fname" 32 | last_name = "admin_lname" 33 | return django_user_model.objects.create_superuser( 34 | username=username, 35 | password=password, 36 | email=email, 37 | first_name=first_name, 38 | last_name=last_name, 39 | ) 40 | 41 | 42 | @pytest.fixture 43 | def language(db): 44 | return Language.objects.create(code="en", name="English") 45 | 46 | 47 | @pytest.fixture 48 | def pretix_order_data(): 49 | return { 50 | "code": "ORDER123", 51 | "event": "2025", 52 | "status": "p", 53 | "testmode": False, 54 | "email": "attendee@example.com", 55 | "datetime": "2025-11-13T17:12:03.989259+01:00", 56 | "total": "30.00", 57 | "positions": [ 58 | { 59 | "attendee_name": "Example Attendee", 60 | "answers": [ 61 | {"question_identifier": "QUESTION123", "option_identifiers": []}, 62 | { 63 | "option_identifiers": [PRETIX_NOT_ANONYMOUS_ANSWER_IDENTIFIER], 64 | "question_identifier": PRETIX_ANONYMOUS_DONATION_QUESTION_IDENTIFIER, 65 | }, 66 | ], 67 | } 68 | ], 69 | "last_modified": "2025-11-13T17:12:07.002602+01:00", 70 | "url": "https://someurl/", 71 | "cancellation_date": None, 72 | } 73 | -------------------------------------------------------------------------------- /volunteer/migrations/0008_pyladieschapter_volunteerprofile_chapter.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-10-28 03:18 2 | 3 | import django.db.models.deletion 4 | import django.db.models.functions.text 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("portal", "0002_alter_basemodel_creation_date_and_more"), 12 | ("volunteer", "0007_team_open_to_new_members_and_more"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="PyladiesChapter", 18 | fields=[ 19 | ( 20 | "basemodel_ptr", 21 | models.OneToOneField( 22 | auto_created=True, 23 | on_delete=django.db.models.deletion.CASCADE, 24 | parent_link=True, 25 | primary_key=True, 26 | serialize=False, 27 | to="portal.basemodel", 28 | ), 29 | ), 30 | ("chapter_name", models.CharField(max_length=100)), 31 | ("chapter_description", models.CharField(max_length=100)), 32 | ( 33 | "chapter_email", 34 | models.EmailField(blank=True, max_length=254, null=True), 35 | ), 36 | ("chapter_website", models.URLField(blank=True, null=True)), 37 | ], 38 | options={ 39 | "constraints": [ 40 | models.UniqueConstraint( 41 | django.db.models.functions.text.Lower("chapter_name"), 42 | name="chapter_name", 43 | ) 44 | ], 45 | }, 46 | bases=("portal.basemodel",), 47 | ), 48 | migrations.AddField( 49 | model_name="volunteerprofile", 50 | name="chapter", 51 | field=models.ForeignKey( 52 | blank=True, 53 | null=True, 54 | on_delete=django.db.models.deletion.SET_NULL, 55 | related_name="chapter_member", 56 | to="volunteer.pyladieschapter", 57 | ), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /sponsorship/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | 4 | from .models import SponsorshipProfile 5 | 6 | 7 | class SponsorshipProfileForm(forms.ModelForm): 8 | 9 | main_contact_user = forms.ModelChoiceField( 10 | queryset=User.objects.filter(is_staff=True), 11 | help_text="Required. Main contact person from PyLadiesCon. Defaults to the person who creates the profile.", 12 | label="Internal Contact *", 13 | ) 14 | 15 | class Meta: 16 | model = SponsorshipProfile 17 | fields = [ 18 | "main_contact_user", 19 | "organization_name", 20 | "sponsor_contact_name", 21 | "sponsors_contact_email", 22 | "sponsorship_tier", 23 | "sponsorship_override_amount", 24 | "po_number", 25 | "organization_address", 26 | "logo", 27 | "company_description", 28 | "progress_status", 29 | "github_issue_url", 30 | ] 31 | widgets = { 32 | "company_description": forms.Textarea( 33 | attrs={ 34 | "rows": 4, 35 | } 36 | ), 37 | } 38 | help_texts = { 39 | "organization_name": "Required", 40 | "progress_status": "Required", 41 | "sponsorship_override_amount": "Optional. If set, this amount will override the default sponsorship tier amount." 42 | " Keep blank to use the default tier amount.", 43 | } 44 | labels = { 45 | "organization_name": "Organization Name *", 46 | "progress_status": "Progress Status *", 47 | } 48 | 49 | def __init__(self, *args, **kwargs): 50 | self.user = kwargs.pop("user", None) 51 | # set the main contact user to the currently logged in user 52 | # for Now only admin users can be the creator of sponsorship profiles 53 | super().__init__(*args, **kwargs) 54 | if self.user: 55 | self.fields["main_contact_user"].initial = self.user.id 56 | 57 | def save(self, commit=True): 58 | if self.user: 59 | self.instance.user = self.user 60 | 61 | sponsorship_profile = super().save(commit) 62 | return sponsorship_profile 63 | -------------------------------------------------------------------------------- /portal/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for portal project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | from django.contrib import admin 21 | from django.urls import include, path 22 | 23 | from portal import views 24 | from volunteer import views as volunteer_view 25 | 26 | urlpatterns = [ 27 | path("", views.index, name="index"), 28 | path("volunteer/", include("volunteer.urls", namespace="volunteer")), 29 | path("admin/", admin.site.urls), 30 | path("accounts/", include("allauth.urls")), 31 | path("sponsorship/", include("sponsorship.urls", namespace="sponsorship")), 32 | path("webhooks/", include("webhooks.urls", namespace="webhooks")), 33 | path( 34 | "portal_account/", 35 | include("portal_account.urls", namespace="portal_account"), 36 | ), 37 | path( 38 | "teams/", 39 | volunteer_view.TeamList.as_view(), 40 | name="teams", 41 | ), 42 | path( 43 | "chapters/", 44 | volunteer_view.PyladiesChaptersList.as_view(), 45 | name="chapters", 46 | ), 47 | path( 48 | "teams/", 49 | volunteer_view.TeamView.as_view(), 50 | name="team_detail", 51 | ), 52 | path("i18n/", include("django.conf.urls.i18n")), 53 | path( 54 | "stats/", 55 | views.stats, 56 | name="portal_stats", 57 | ), 58 | path( 59 | "stats.json", 60 | views.stats_json, 61 | name="portal_stats_json", 62 | ), 63 | path( 64 | "dashboard_gallery", 65 | views.dashboard_gallery, 66 | name="dashboard_gallery", 67 | ), 68 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 69 | -------------------------------------------------------------------------------- /templates/volunteer/volunteer_stats.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 | Volunteers Signed Up 8 |
    9 |

    10 | {{ stats.volunteer_signups_count }} 11 |

    12 | for {{ stats.volunteer_teams_count }} teams 13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 | Volunteers Onboarded 21 |
    22 |

    23 | {{ stats.volunteer_onboarded_count }} 24 |

    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | About our Volunteers 33 |
    34 | They speak 35 |

    36 | {{ stats.volunteer_languages_count }} Languages 37 |

    38 | They're from 39 |

    40 | {{ stats.volunteer_pyladies_chapters_count }} PyLadies Chapters 41 |

    42 |
    43 |
    44 |
    45 |
    46 |
    47 | {% for breakdown in stats.volunteer_breakdown %} 48 |
    49 | {# djlint:off H021 #} 50 |
    51 |
    52 | {# djlint:on H021 #} 53 |
    54 | {% endfor %} 55 |
    56 |
    57 | -------------------------------------------------------------------------------- /templates/account/email_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base_manage_email.html" %} 2 | {% load i18n %} 3 | {% load allauth %} 4 | {% block head_title %} 5 | {% trans "Email Address" %} 6 | {% endblock head_title %} 7 | {% block content %} 8 | {% element h1 %} 9 | {% trans "Email Address" %} 10 | {% endelement %} 11 | {% if not emailaddresses %} 12 | {% include "account/snippets/warn_no_email.html" %} 13 | {% endif %} 14 | {% url 'account_email' as action_url %} 15 | {% element form method="post" action=action_url %} 16 | {% slot body %} 17 | {% csrf_token %} 18 | {% if current_emailaddress %} 19 | {% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %} 20 | {% slot label %} 21 | {% translate "Current email" %}: 22 | {% endslot %} 23 | {% endelement %} 24 | {% endif %} 25 | {% if new_emailaddress %} 26 | {% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %} 27 | {% slot label %} 28 | {% if not current_emailaddress %} 29 | {% translate "Current email" %}: 30 | {% else %} 31 | {% translate "Changing to" %}: 32 | {% endif %} 33 | {% endslot %} 34 | {% slot help_text %} 35 | {% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %} 36 | {% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %} 37 | {% trans "Re-send Verification" %} 38 | {% endelement %} 39 | {% if current_emailaddress %} 40 | {% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %} 41 | {% trans "Cancel Change" %} 42 | {% endelement %} 43 | {% endif %} 44 | {% endslot %} 45 | {% endelement %} 46 | {% endif %} 47 | {% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %} 48 | {% slot label %} 49 | {% translate "Change to" %}: 50 | {% endslot %} 51 | {% endelement %} 52 | {% endslot %} 53 | {% slot actions %} 54 | {% element button name="action_add" type="submit" %} 55 | {% trans "Change Email" %} 56 | {% endelement %} 57 | {% endslot %} 58 | {% endelement %} 59 | {% if new_emailaddress %} 60 | 67 | {% endif %} 68 | {% endblock content %} 69 | -------------------------------------------------------------------------------- /sponsorship/migrations/0002_sponsorshiptier_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.3 on 2025-10-03 01:09 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("sponsorship", "0001_initial"), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="SponsorshipTier", 18 | fields=[ 19 | ( 20 | "id", 21 | models.BigAutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("name", models.CharField(max_length=100)), 29 | ("amount", models.DecimalField(decimal_places=2, max_digits=10)), 30 | ("description", models.TextField()), 31 | ], 32 | ), 33 | migrations.RemoveField( 34 | model_name="sponsorshipprofile", 35 | name="sponsorship_type", 36 | ), 37 | migrations.AlterField( 38 | model_name="sponsorshipprofile", 39 | name="main_contact_user", 40 | field=models.ForeignKey( 41 | blank=True, 42 | null=True, 43 | on_delete=django.db.models.deletion.SET_NULL, 44 | related_name="main_contact_user", 45 | to=settings.AUTH_USER_MODEL, 46 | ), 47 | ), 48 | migrations.AlterField( 49 | model_name="sponsorshipprofile", 50 | name="user", 51 | field=models.ForeignKey( 52 | blank=True, 53 | null=True, 54 | on_delete=django.db.models.deletion.SET_NULL, 55 | related_name="sponsorship_user", 56 | to=settings.AUTH_USER_MODEL, 57 | ), 58 | ), 59 | migrations.AddField( 60 | model_name="sponsorshipprofile", 61 | name="sponsorship_tier", 62 | field=models.ForeignKey( 63 | blank=True, 64 | null=True, 65 | on_delete=django.db.models.deletion.SET_NULL, 66 | to="sponsorship.sponsorshiptier", 67 | ), 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /portal_account/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import redirect, render 3 | from django.urls import reverse_lazy 4 | from django.views.generic import DetailView 5 | from django.views.generic.edit import CreateView, UpdateView 6 | 7 | from .forms import PortalProfileForm 8 | from .models import PortalProfile 9 | 10 | 11 | @login_required 12 | def index(request): 13 | context = {} 14 | try: 15 | profile = PortalProfile.objects.get(user=request.user) 16 | context["profile_id"] = profile.id 17 | except PortalProfile.DoesNotExist: 18 | context["profile_id"] = None 19 | return render(request, "portal_account/index.html", context) 20 | 21 | 22 | class PortalProfileView(DetailView): 23 | model = PortalProfile 24 | 25 | def get(self, request, *args, **kwargs): 26 | self.object = self.get_object() 27 | if not self.object or self.object.user != request.user: 28 | return redirect("portal_account:index") 29 | return super(PortalProfileView, self).get(request, *args, **kwargs) 30 | 31 | 32 | class PortalProfileCreate(CreateView): 33 | model = PortalProfile 34 | template_name = "portal_account/portalprofile_form.html" 35 | success_url = reverse_lazy("index") 36 | form_class = PortalProfileForm 37 | 38 | def get(self, request, *args, **kwargs): 39 | if PortalProfile.objects.filter(user__id=request.user.id).exists(): 40 | return redirect("portal_account:index") 41 | return super(PortalProfileCreate, self).get(request, *args, **kwargs) 42 | 43 | def get_form_kwargs(self): 44 | kwargs = super(PortalProfileCreate, self).get_form_kwargs() 45 | kwargs.update({"user": self.request.user}) 46 | return kwargs 47 | 48 | 49 | class PortalProfileUpdate(UpdateView): 50 | model = PortalProfile 51 | template_name = "portal_account/portalprofile_form.html" 52 | success_url = reverse_lazy("index") 53 | form_class = PortalProfileForm 54 | 55 | def get(self, request, *args, **kwargs): 56 | self.object = self.get_object() 57 | if not self.object or self.object.user != request.user: 58 | return redirect("portal_account:index") 59 | return super(PortalProfileUpdate, self).get(request, *args, **kwargs) 60 | 61 | def get_form_kwargs(self): 62 | kwargs = super(PortalProfileUpdate, self).get_form_kwargs() 63 | kwargs.update({"user": self.request.user}) 64 | return kwargs 65 | -------------------------------------------------------------------------------- /templates/mfa/webauthn/authenticator_list.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa/webauthn/base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load allauth %} 5 | {% load humanize %} 6 | {% block content %} 7 | {% element h1 %} 8 | {% trans "Security Keys" %} 9 | {% endelement %} 10 | {% if authenticators|length == 0 %} 11 | {% element p %} 12 | {% blocktranslate %}No security keys have been added.{% endblocktranslate %} 13 | {% endelement %} 14 | {% else %} 15 | {% element table %} 16 | {% element thead %} 17 | {% element th %} 18 | {% translate "Key" %} 19 | {% endelement %} 20 | {% element th %} 21 | {% translate "Usage" %} 22 | {% endelement %} 23 | {% element th %} 24 | {% endelement %} 25 | {% endelement %} 26 | {% element tbody %} 27 | {% for authenticator in authenticators %} 28 | {% element tr %} 29 | {% element td %} 30 | {{ authenticator }} 31 | {% if authenticator.wrap.is_passwordless is True %} 32 | {% element badge tags="mfa,key,primary" %} 33 | {% translate "Passkey" %} 34 | {% endelement %} 35 | {% elif authenticator.wrap.is_passwordless is False %} 36 | {% element badge tags="mfa,key,secondary" %} 37 | {% translate "Security key" %} 38 | {% endelement %} 39 | {% else %} 40 | {% element badge title=_("This key does not indicate whether it is a passkey.") tags="mfa,key,warning" %} 41 | {% translate "Unspecified" %} 42 | {% endelement %} 43 | {% endif %} 44 | {% endelement %} 45 | {% element td %} 46 | {% blocktranslate with created_at=authenticator.created_at|date:"SHORT_DATE_FORMAT" %}Added on {{ created_at }}{% endblocktranslate %}. 47 | {% if authenticator.last_used_at %} 48 | {% blocktranslate with last_used=authenticator.last_used_at|naturaltime %}Last used {{ last_used }}{% endblocktranslate %} 49 | {% else %} 50 | Not used. 51 | {% endif %} 52 | {% endelement %} 53 | {% element td align="right" %} 54 | {% url 'mfa_edit_webauthn' pk=authenticator.pk as edit_url %} 55 | {% element button tags="mfa,authenticator,edit,tool" href=edit_url %} 56 | {% translate "Edit" %} 57 | {% endelement %} 58 | {% url "mfa_remove_webauthn" pk=authenticator.pk as remove_url %} 59 | {% element button tags="mfa,authenticator,danger,delete,tool" href=remove_url %} 60 | {% translate "Remove" %} 61 | {% endelement %} 62 | {% endelement %} 63 | {% endelement %} 64 | {% endfor %} 65 | {% endelement %} 66 | {% endelement %} 67 | {% endif %} 68 | {% url "mfa_add_webauthn" as add_url %} 69 | {% element button href=add_url %} 70 | {% translate "Add" %} 71 | {% endelement %} 72 | {% endblock content %} 73 | -------------------------------------------------------------------------------- /common/pretix_wrapper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from django.conf import settings 3 | 4 | from portal.constants import BASE_PRETIX_URL 5 | 6 | statuses = [] 7 | 8 | PRETIX_CANCELLED_STATUS = "c" 9 | PRETIX_PAID_STATUS = "p" 10 | 11 | PRETIX_WEBHOOK_ORDER_PAID = "pretix.event.order.paid" 12 | PRETIX_WEBHOOK_ORDER_CANCELLED = "pretix.event.order.canceled" 13 | 14 | PRETIX_ALLOWED_WEBHOOK_ACTIONS = [ 15 | PRETIX_WEBHOOK_ORDER_PAID, 16 | PRETIX_WEBHOOK_ORDER_CANCELLED, 17 | ] 18 | PRETIX_ORG = "pyladiescon" 19 | PRETIX_EVENT_SLUG = "2025" 20 | 21 | 22 | class PretixWrapper: 23 | def __init__(self, org, event_slug): 24 | token = settings.PRETIX_API_TOKEN 25 | if not token: 26 | raise ValueError("PRETIX_API_TOKEN not set") 27 | self.headers = {"Authorization": f"Token {settings.PRETIX_API_TOKEN}"} 28 | self.org = org 29 | self.event_slug = event_slug 30 | 31 | def get_orders(self): 32 | """Get all orders from pretix for the given event. 33 | 34 | We only want to care about non-testmode orders, fully paid orders, and canceled orders. 35 | If we found fully-paid orders: record the values to add to our stats. 36 | If we found cancelled orders: lookup the order and mark it as cancelled so that we can exclude from stats. 37 | """ 38 | params = {} 39 | has_next = True 40 | url = ( 41 | BASE_PRETIX_URL + f"organizers/{self.org}/events/{self.event_slug}/orders/" 42 | ) 43 | index = 0 44 | 45 | while has_next: 46 | response = requests.get(url, headers=self.headers, params=params) 47 | url = response.json()["next"] 48 | has_next = url is not None 49 | for r in response.json()["results"]: 50 | index += 1 51 | if r["testmode"] is False: 52 | if r["status"] in [PRETIX_PAID_STATUS, PRETIX_CANCELLED_STATUS]: 53 | yield r 54 | 55 | def get_order_by_code(self, order_code): 56 | """Get a single order by its code.""" 57 | url = ( 58 | BASE_PRETIX_URL 59 | + f"organizers/{self.org}/events/{self.event_slug}/orders/{order_code}/" 60 | ) 61 | response = requests.get(url, headers=self.headers) 62 | if response.status_code == 200: 63 | return response.json() 64 | else: 65 | raise Exception( 66 | f"Failed to get order {order_code}: {response.status_code} {response.text}" 67 | ) 68 | --------------------------------------------------------------------------------