Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
20feabf
feat(notifications): implement transactional email notification system
thejoeejoee Mar 3, 2026
0981c6f
Merge branch 'develop' into feat-notifications
thejoeejoee Mar 3, 2026
82ee3ef
feat(notifications): impl
thejoeejoee Mar 3, 2026
3a2019e
fix: dockerfile
thejoeejoee Mar 6, 2026
1779077
feat(notifications): integrate email notifications into matching and …
thejoeejoee Mar 6, 2026
197103d
fix(notifications): remove dead code, fix P0 bugs, deduplicate INSTAL…
thejoeejoee Mar 6, 2026
e94ae55
chore(notifications): delete orphan Path A template files
thejoeejoee Mar 6, 2026
6379ff8
feat(notifications): add global email opt-out with preferences UI toggle
thejoeejoee Mar 6, 2026
ca0f9e5
feat(notifications): wire recipient_profile to all send_notification_…
thejoeejoee Mar 7, 2026
0b63d5c
fix(CR): user/user_profile and other fixes
thejoeejoee Mar 7, 2026
7c11169
fix(notifications): use CheckEnabledPluginsViewMixin for plugin detec…
thejoeejoee Mar 7, 2026
f0f44ed
chore(docker): drop deprecated compose version key
thejoeejoee Mar 7, 2026
b5be008
fix(sections): fall back to ROOT_DOMAIN when site domain mismatches
thejoeejoee Mar 7, 2026
d70a498
chore(dev): parametrize OrbStack domains with ROOT_DOMAIN
thejoeejoee Mar 7, 2026
3a8c8e8
fix(notifications): use in_space_of_section instead of in_space_of
thejoeejoee Mar 7, 2026
c3d968a
fix(notifications): address code review findings from branch review
thejoeejoee Mar 7, 2026
4b3f13a
fix(notifications): narrow bare except to ObjectDoesNotExist in mailer
thejoeejoee Mar 7, 2026
b1102a3
ci: add test workflow for pull requests
thejoeejoee Mar 7, 2026
b6d3982
fix(ci): add missing RECAPTCHA env vars for test workflow
thejoeejoee Mar 7, 2026
7e78249
fix(ci): add BUILD_DIR, STATIC_ROOT, MEDIA_ROOT env vars for test wor…
thejoeejoee Mar 7, 2026
c217e36
fix(ci): install setuptools for pkg_resources compatibility
thejoeejoee Mar 7, 2026
7e13401
fix(ci): use uv run --with setuptools for pkg_resources
thejoeejoee Mar 7, 2026
dd9e409
fix(ci): use venv python directly to preserve setuptools
thejoeejoee Mar 7, 2026
38e276a
fix(ci): install setuptools directly into venv python
thejoeejoee Mar 7, 2026
a2a201a
fix(ci): use uv pip instead of python -m pip for setuptools
thejoeejoee Mar 7, 2026
77edf6e
fix(ci): target venv explicitly for setuptools install
thejoeejoee Mar 7, 2026
ed3910f
fix(ci): pin setuptools<82 to retain pkg_resources
thejoeejoee Mar 7, 2026
24dd583
fix(ci): make database host configurable via env var for CI
thejoeejoee Mar 7, 2026
bfdba9c
fix(ci): fix SMTP backend, import path, and soft-cancel sent_at rollback
thejoeejoee Mar 7, 2026
f8d4959
fix(notifications): regenerate migration, fix template .user access a…
thejoeejoee Mar 7, 2026
3138c89
fix(tests): fix mock sections, stale factory field, and import path
thejoeejoee Mar 7, 2026
4734755
fix(tests): check global email opt-out in match notifications and fix…
thejoeejoee Mar 7, 2026
9bf7a3f
fix(tests): use empty whatsapp in UserProfileFactory to avoid validat…
thejoeejoee Mar 7, 2026
3fbd6a1
fix(CR): address bot review findings (send return, PII, URLs, templat…
thejoeejoee Mar 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Tests

on:
pull_request:
branches: [develop, main]

concurrency:
group: tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
env:
POSTGRES_DB: fiesta
POSTGRES_USER: fiesta
POSTGRES_PASSWORD: fiesta
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U fiesta"
--health-interval=10s
--health-timeout=5s
--health-retries=5

env:
DATABASE_URL: postgres://fiesta:fiesta@localhost:5432/fiesta
DJANGO_SETTINGS_MODULE: fiesta.settings
DJANGO_CONFIGURATION: Development
DJANGO_SECRET_KEY: ci-test-secret-key-not-for-production
DJANGO_RECAPTCHA_SITE_KEY: dummy-recaptcha-site-key
DJANGO_RECAPTCHA_SECRET_KEY: dummy-recaptcha-secret-key
DJANGO_BUILD_DIR: /tmp/build
DJANGO_STATIC_ROOT: /tmp/static
DJANGO_MEDIA_ROOT: /tmp/media
DJANGO_DATABASE_HOST: localhost
DJANGO_EMAIL_BACKEND: django.core.mail.backends.locmem.EmailBackend
ROOT_DOMAIN: fiesta.test
Comment thread
thejoeejoee marked this conversation as resolved.

steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
uv sync
uv pip install --python .venv 'setuptools<82'

- name: Run tests
working-directory: fiesta
run: ../.venv/bin/python manage.py test --verbosity 1
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ ENV UV_PROJECT_ENVIRONMENT=/venv
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync ${UV_SYNC_FLAGS} --no-install-project
# seed setuptools into venv for pkg_resources compat
RUN /venv/bin/pip install setuptools
# seed setuptools into venv for pkg_resources compat (setuptools<72 ships pkg_resources)
RUN uv pip install --python /venv/bin/python "setuptools<72"
Comment thread
thejoeejoee marked this conversation as resolved.
Comment thread
thejoeejoee marked this conversation as resolved.

# base runtime image
FROM ${PYTHON_IMAGE} as web-base
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ARG =

MODELS_PNG = models.png
GRAPH_MODELS_CMD = graph_models accounts plugins auth sections events \
universities esncards buddy_system \
universities esncards buddy_system pickup_system notifications \
--verbose-names --disable-sort-fields \
--pydot -X 'ContentType|Base*Model' \
-g -o $(MODELS_PNG)
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3.3'
services:
web:
# environment:
Expand Down Expand Up @@ -30,6 +29,9 @@ services:
- webpack_build:/usr/src/build

dockerproxy:
labels:
dev.orbstack.domains: "*.${ROOT_DOMAIN},${ROOT_DOMAIN}"
dev.orbstack.http-port: 80
environment:
- DISABLE_ACCESS_LOGS=1
volumes:
Expand Down
1 change: 0 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3.3'
services:
web:
build:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '3.3'
name: fiesta-plus
services:
web:
command: python manage.py runserver 0.0.0.0:8000
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.28 on 2026-03-06 23:52

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('accounts', '0027_userprofile_picture_height_userprofile_picture_width_and_more'),
]

operations = [
migrations.AddField(
model_name='userprofile',
name='email_notifications_enabled',
field=models.BooleanField(default=True, verbose_name='email notifications'),
),
]
1 change: 1 addition & 0 deletions fiesta/apps/accounts/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class Preferences(enum.Flag):

# TODO: define formfield/widget to handle flagging
preferences = models.PositiveSmallIntegerField(default=0, verbose_name=_("user preferences as flags"))
email_notifications_enabled = models.BooleanField(default=True, verbose_name=_("email notifications"))

State = UserProfileState

Expand Down
17 changes: 17 additions & 0 deletions fiesta/apps/accounts/templates/accounts/user_profile/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@
Social Accounts
</a>
</li>
{% if request.in_space_of_section %}
<li>
<a href="{% url "notifications:preferences" %}">
<svg class="h-5 w-5"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0">
</path>
</svg>
{% trans "Notifications" %}
</a>
</li>
{% endif %}
</ul>
</div>
<div class="w-full md:w-3/4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.28 on 2026-03-03 14:17

import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('buddy_system', '0031_alter_buddysystemconfiguration_rolling_limit_and_more'),
]

operations = [
migrations.AddField(
model_name='buddysystemconfiguration',
name='email_notify_issuer_delay',
field=models.DurationField(default=datetime.timedelta(seconds=3600), help_text='Gives editors time to correct the match before the student is notified.', verbose_name='Delay before notifying the issuer'),
),
migrations.AddField(
model_name='buddysystemconfiguration',
name='email_notify_on_match',
field=models.BooleanField(default=True, verbose_name='Send email notifications on match'),
),
]
10 changes: 10 additions & 0 deletions fiesta/apps/buddy_system/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ class BuddySystemConfiguration(BaseRequestSystemConfiguration):
help_text=MatchingPoliciesRegister.DESCRIPTION,
)

email_notify_on_match = models.BooleanField(
default=True,
verbose_name=_("Send email notifications on match"),
)
email_notify_issuer_delay = models.DurationField(
default=datetime.timedelta(hours=1),
verbose_name=_("Delay before notifying the issuer"),
help_text=_("Gives editors time to correct the match before the student is notified."),
)

@property
def matching_policy_instance(self) -> BaseMatchingPolicy:
return MatchingPoliciesRegister.get_policy(self)
Expand Down
16 changes: 16 additions & 0 deletions fiesta/apps/buddy_system/views/editor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import logging

from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
Expand All @@ -19,6 +21,8 @@
from apps.utils.breadcrumbs import with_breadcrumb, with_object_breadcrumb, with_plugin_home_breadcrumb
from apps.utils.views import AjaxViewMixin

logger = logging.getLogger(__name__)


class BuddyRequestsTable(BaseRequestsTable):
match_request = TemplateColumn(
Expand Down Expand Up @@ -91,6 +95,18 @@ class QuickBuddyMatchView(BaseQuickRequestMatchView):
form_url = "buddy_system:quick-match"
match_model = BuddyRequestMatch

def after_match_created(self, match, fiesta_request) -> None:
from apps.notifications.services.match import notify_buddy_match

try:
notify_buddy_match(
match=match,
request=fiesta_request,
section=self.request.in_space_of_section,
)
except Exception:
logger.exception("Failed to send buddy match notification for match pk=%s", match.pk)


class UpdateBuddyRequestStateView(BaseUpdateRequestStateView):
model = BuddyRequest
Expand Down
16 changes: 16 additions & 0 deletions fiesta/apps/buddy_system/views/matching.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import logging

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.template.loader import render_to_string
from django.urls import reverse_lazy
Expand All @@ -18,6 +20,8 @@
from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin
from apps.utils.breadcrumbs import with_breadcrumb, with_plugin_home_breadcrumb

logger = logging.getLogger(__name__)


@with_plugin_home_breadcrumb
@with_breadcrumb(_("Waiting Requests"))
Expand Down Expand Up @@ -94,6 +98,18 @@ def get_form(self, form_class=None):
)
return form

def after_match_created(self, match, fiesta_request) -> None:
from apps.notifications.services.match import notify_buddy_match

try:
notify_buddy_match(
match=match,
request=fiesta_request,
section=self.request.in_space_of_section,
)
except Exception:
logger.exception("Failed to send buddy match notification for match pk=%s", match.pk)


class ServeFilesFromBuddiesMixin:
@classmethod
Expand Down
1 change: 0 additions & 1 deletion fiesta/apps/dashboard/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@


class DashboardConfiguration(BasePluginConfiguration):

class Meta:
verbose_name = _("dashboard configuration")
verbose_name_plural = _("dashboard configurations")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.28 on 2026-03-07 20:56

from django.db import migrations
import djmoney.models.fields


class Migration(migrations.Migration):

dependencies = [
('events', '0004_alter_pricevariant_amount_currency'),
]

operations = [
migrations.AlterField(
model_name='pricevariant',
name='amount_currency',
field=djmoney.models.fields.CurrencyField(choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghan Afghani'), ('AFA', 'Afghan Afghani (1927–2002)'), ('ALL', 'Albanian Lek'), ('ALK', 'Albanian Lek (1946–1965)'), ('DZD', 'Algerian Dinar'), ('ADP', 'Andorran Peseta'), ('AOA', 'Angolan Kwanza'), ('AOK', 'Angolan Kwanza (1977–1991)'), ('AON', 'Angolan New Kwanza (1990–2000)'), ('AOR', 'Angolan Readjusted Kwanza (1995–1999)'), ('ARA', 'Argentine Austral'), ('ARS', 'Argentine Peso'), ('ARM', 'Argentine Peso (1881–1970)'), ('ARP', 'Argentine Peso (1983–1985)'), ('ARL', 'Argentine Peso Ley (1970–1983)'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Florin'), ('AUD', 'Australian Dollar'), ('ATS', 'Austrian Schilling'), ('AZN', 'Azerbaijani Manat'), ('AZM', 'Azerbaijani Manat (1993–2006)'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('BDT', 'Bangladeshi Taka'), ('BBD', 'Barbadian Dollar'), ('BYN', 'Belarusian Ruble'), ('BYB', 'Belarusian Ruble (1994–1999)'), ('BYR', 'Belarusian Ruble (2000–2016)'), ('BEF', 'Belgian Franc'), ('BEC', 'Belgian Franc (convertible)'), ('BEL', 'Belgian Franc (financial)'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudan Dollar'), ('BTN', 'Bhutanese Ngultrum'), ('BOB', 'Bolivian Boliviano'), ('BOL', 'Bolivian Boliviano (1863–1963)'), ('BOV', 'Bolivian Mvdol'), ('BOP', 'Bolivian Peso'), ('VED', 'Bolívar Soberano'), ('BAM', 'Bosnia-Herzegovina Convertible Mark'), ('BAD', 'Bosnia-Herzegovina Dinar (1992–1994)'), ('BAN', 'Bosnia-Herzegovina New Dinar (1994–1997)'), ('BWP', 'Botswanan Pula'), ('BRC', 'Brazilian Cruzado (1986–1989)'), ('BRZ', 'Brazilian Cruzeiro (1942–1967)'), ('BRE', 'Brazilian Cruzeiro (1990–1993)'), ('BRR', 'Brazilian Cruzeiro (1993–1994)'), ('BRN', 'Brazilian New Cruzado (1989–1990)'), ('BRB', 'Brazilian New Cruzeiro (1967–1986)'), ('BRL', 'Brazilian Real'), ('GBP', 'British Pound'), ('BND', 'Brunei Dollar'), ('BGL', 'Bulgarian Hard Lev'), ('BGN', 'Bulgarian Lev'), ('BGO', 'Bulgarian Lev (1879–1952)'), ('BGM', 'Bulgarian Socialist Lev'), ('BUK', 'Burmese Kyat'), ('BIF', 'Burundian Franc'), ('XPF', 'CFP Franc'), ('KHR', 'Cambodian Riel'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verdean Escudo'), ('KYD', 'Cayman Islands Dollar'), ('XAF', 'Central African CFA Franc'), ('CLE', 'Chilean Escudo'), ('CLP', 'Chilean Peso'), ('CLF', 'Chilean Unit of Account (UF)'), ('CNX', 'Chinese People’s Bank Dollar'), ('CNY', 'Chinese Yuan'), ('CNH', 'Chinese Yuan (offshore)'), ('COP', 'Colombian Peso'), ('COU', 'Colombian Real Value Unit'), ('KMF', 'Comorian Franc'), ('CDF', 'Congolese Franc'), ('CRC', 'Costa Rican Colón'), ('HRD', 'Croatian Dinar'), ('HRK', 'Croatian Kuna'), ('CUC', 'Cuban Convertible Peso'), ('CUP', 'Cuban Peso'), ('CYP', 'Cypriot Pound'), ('CZK', 'Czech Koruna'), ('CSK', 'Czechoslovak Hard Koruna'), ('DKK', 'Danish Krone'), ('DJF', 'Djiboutian Franc'), ('DOP', 'Dominican Peso'), ('NLG', 'Dutch Guilder'), ('XCD', 'East Caribbean Dollar'), ('DDM', 'East German Mark'), ('ECS', 'Ecuadorian Sucre'), ('ECV', 'Ecuadorian Unit of Constant Value'), ('EGP', 'Egyptian Pound'), ('GQE', 'Equatorial Guinean Ekwele'), ('ERN', 'Eritrean Nakfa'), ('EEK', 'Estonian Kroon'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBA', 'European Composite Unit'), ('XEU', 'European Currency Unit'), ('XBB', 'European Monetary Unit'), ('XBC', 'European Unit of Account (XBC)'), ('XBD', 'European Unit of Account (XBD)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fijian Dollar'), ('FIM', 'Finnish Markka'), ('FRF', 'French Franc'), ('XFO', 'French Gold Franc'), ('XFU', 'French UIC-Franc'), ('GMD', 'Gambian Dalasi'), ('GEK', 'Georgian Kupon Larit'), ('GEL', 'Georgian Lari'), ('DEM', 'German Mark'), ('GHS', 'Ghanaian Cedi'), ('GHC', 'Ghanaian Cedi (1979–2007)'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('GRD', 'Greek Drachma'), ('GTQ', 'Guatemalan Quetzal'), ('GWP', 'Guinea-Bissau Peso'), ('GNF', 'Guinean Franc'), ('GNS', 'Guinean Syli'), ('GYD', 'Guyanaese Dollar'), ('HTG', 'Haitian Gourde'), ('HNL', 'Honduran Lempira'), ('HKD', 'Hong Kong Dollar'), ('HUF', 'Hungarian Forint'), ('IMP', 'IMP'), ('ISK', 'Icelandic Króna'), ('ISJ', 'Icelandic Króna (1918–1981)'), ('INR', 'Indian Rupee'), ('IDR', 'Indonesian Rupiah'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IEP', 'Irish Pound'), ('ILS', 'Israeli New Shekel'), ('ILP', 'Israeli Pound'), ('ILR', 'Israeli Shekel (1980–1985)'), ('ITL', 'Italian Lira'), ('JMD', 'Jamaican Dollar'), ('JPY', 'Japanese Yen'), ('JOD', 'Jordanian Dinar'), ('KZT', 'Kazakhstani Tenge'), ('KES', 'Kenyan Shilling'), ('KWD', 'Kuwaiti Dinar'), ('KGS', 'Kyrgystani Som'), ('LAK', 'Laotian Kip'), ('LVL', 'Latvian Lats'), ('LVR', 'Latvian Ruble'), ('LBP', 'Lebanese Pound'), ('LSL', 'Lesotho Loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('LTL', 'Lithuanian Litas'), ('LTT', 'Lithuanian Talonas'), ('LUL', 'Luxembourg Financial Franc'), ('LUC', 'Luxembourgian Convertible Franc'), ('LUF', 'Luxembourgian Franc'), ('MOP', 'Macanese Pataca'), ('MKD', 'Macedonian Denar'), ('MKN', 'Macedonian Denar (1992–1993)'), ('MGA', 'Malagasy Ariary'), ('MGF', 'Malagasy Franc'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('MVR', 'Maldivian Rufiyaa'), ('MVP', 'Maldivian Rupee (1947–1981)'), ('MLF', 'Malian Franc'), ('MTL', 'Maltese Lira'), ('MTP', 'Maltese Pound'), ('MRU', 'Mauritanian Ouguiya'), ('MRO', 'Mauritanian Ouguiya (1973–2017)'), ('MUR', 'Mauritian Rupee'), ('MXV', 'Mexican Investment Unit'), ('MXN', 'Mexican Peso'), ('MXP', 'Mexican Silver Peso (1861–1992)'), ('MDC', 'Moldovan Cupon'), ('MDL', 'Moldovan Leu'), ('MCF', 'Monegasque Franc'), ('MNT', 'Mongolian Tugrik'), ('MAD', 'Moroccan Dirham'), ('MAF', 'Moroccan Franc'), ('MZE', 'Mozambican Escudo'), ('MZN', 'Mozambican Metical'), ('MZM', 'Mozambican Metical (1980–2006)'), ('MMK', 'Myanmar Kyat'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillean Guilder'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('NIO', 'Nicaraguan Córdoba'), ('NIC', 'Nicaraguan Córdoba (1988–1991)'), ('NGN', 'Nigerian Naira'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('OMR', 'Omani Rial'), ('PKR', 'Pakistani Rupee'), ('XPD', 'Palladium'), ('PAB', 'Panamanian Balboa'), ('PGK', 'Papua New Guinean Kina'), ('PYG', 'Paraguayan Guarani'), ('PEI', 'Peruvian Inti'), ('PEN', 'Peruvian Sol'), ('PES', 'Peruvian Sol (1863–1965)'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('PLN', 'Polish Zloty'), ('PLZ', 'Polish Zloty (1950–1995)'), ('PTE', 'Portuguese Escudo'), ('GWE', 'Portuguese Guinea Escudo'), ('QAR', 'Qatari Riyal'), ('XRE', 'RINET Funds'), ('RHD', 'Rhodesian Dollar'), ('RON', 'Romanian Leu'), ('ROL', 'Romanian Leu (1952–2006)'), ('RUB', 'Russian Ruble'), ('RUR', 'Russian Ruble (1991–1998)'), ('RWF', 'Rwandan Franc'), ('SVC', 'Salvadoran Colón'), ('WST', 'Samoan Tala'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('CSD', 'Serbian Dinar (2002–2006)'), ('SCR', 'Seychellois Rupee'), ('SLE', 'Sierra Leonean Leone'), ('SLL', 'Sierra Leonean Leone (1964—2022)'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SKK', 'Slovak Koruna'), ('SIT', 'Slovenian Tolar'), ('SBD', 'Solomon Islands Dollar'), ('SOS', 'Somali Shilling'), ('ZAR', 'South African Rand'), ('ZAL', 'South African Rand (financial)'), ('KRH', 'South Korean Hwan (1953–1962)'), ('KRW', 'South Korean Won'), ('KRO', 'South Korean Won (1945–1953)'), ('SSP', 'South Sudanese Pound'), ('SUR', 'Soviet Rouble'), ('ESP', 'Spanish Peseta'), ('ESA', 'Spanish Peseta (A account)'), ('ESB', 'Spanish Peseta (convertible account)'), ('XDR', 'Special Drawing Rights'), ('LKR', 'Sri Lankan Rupee'), ('SHP', 'St. Helena Pound'), ('XSU', 'Sucre'), ('SDD', 'Sudanese Dinar (1992–2007)'), ('SDG', 'Sudanese Pound'), ('SDP', 'Sudanese Pound (1957–1998)'), ('SRD', 'Surinamese Dollar'), ('SRG', 'Surinamese Guilder'), ('SZL', 'Swazi Lilangeni'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('STN', 'São Tomé & Príncipe Dobra'), ('STD', 'São Tomé & Príncipe Dobra (1977–2017)'), ('TVD', 'TVD'), ('TJR', 'Tajikistani Ruble'), ('TJS', 'Tajikistani Somoni'), ('TZS', 'Tanzanian Shilling'), ('XTS', 'Testing Currency Code'), ('THB', 'Thai Baht'), ('TPE', 'Timorese Escudo'), ('TOP', 'Tongan Paʻanga'), ('TTD', 'Trinidad & Tobago Dollar'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TRL', 'Turkish Lira (1922–2005)'), ('TMT', 'Turkmenistani Manat'), ('TMM', 'Turkmenistani Manat (1993–2009)'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('USS', 'US Dollar (Same day)'), ('UGX', 'Ugandan Shilling'), ('UGS', 'Ugandan Shilling (1966–1987)'), ('UAH', 'Ukrainian Hryvnia'), ('UAK', 'Ukrainian Karbovanets'), ('AED', 'United Arab Emirates Dirham'), ('UYW', 'Uruguayan Nominal Wage Index Unit'), ('UYU', 'Uruguayan Peso'), ('UYP', 'Uruguayan Peso (1975–1993)'), ('UYI', 'Uruguayan Peso (Indexed Units)'), ('UZS', 'Uzbekistani Som'), ('VUV', 'Vanuatu Vatu'), ('VES', 'Venezuelan Bolívar'), ('VEB', 'Venezuelan Bolívar (1871–2008)'), ('VEF', 'Venezuelan Bolívar (2008–2018)'), ('VND', 'Vietnamese Dong'), ('VNN', 'Vietnamese Dong (1978–1985)'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('XOF', 'West African CFA Franc'), ('YDD', 'Yemeni Dinar'), ('YER', 'Yemeni Rial'), ('YUN', 'Yugoslavian Convertible Dinar (1990–1992)'), ('YUD', 'Yugoslavian Hard Dinar (1966–1990)'), ('YUM', 'Yugoslavian New Dinar (1994–2002)'), ('YUR', 'Yugoslavian Reformed Dinar (1992–1993)'), ('ZWN', 'ZWN'), ('ZRN', 'Zairean New Zaire (1993–1998)'), ('ZRZ', 'Zairean Zaire (1971–1993)'), ('ZMW', 'Zambian Kwacha'), ('ZMK', 'Zambian Kwacha (1968–2012)'), ('ZWD', 'Zimbabwean Dollar (1980–2008)'), ('ZWR', 'Zimbabwean Dollar (2008)'), ('ZWL', 'Zimbabwean Dollar (2009–2024)')], default='CZK', editable=False, max_length=3),
),
Comment thread
thejoeejoee marked this conversation as resolved.
]
Loading
Loading