summaryrefslogtreecommitdiff
path: root/foundation
diff options
context:
space:
mode:
authorJames Bennett <james@b-list.org>2019-05-08 16:54:24 -0400
committerGitHub <noreply@github.com>2019-05-08 16:54:24 -0400
commit8d3cfdda9bdd2f6c43f1a25a02273b3038648061 (patch)
tree88f30a636d0f2d19c5f815c680ad09c8128e8451 /foundation
parent792913a496fe50837b82cf81d637c81f9ae2aaab (diff)
Fixed #609: Added app for board meeting minutes. (#907)
Diffstat (limited to 'foundation')
-rw-r--r--foundation/__init__.py0
-rw-r--r--foundation/admin.py73
-rw-r--r--foundation/migrations/0001_initial.py245
-rw-r--r--foundation/migrations/__init__.py0
-rw-r--r--foundation/models.py204
-rw-r--r--foundation/templatetags/__init__.py0
-rw-r--r--foundation/templatetags/foundation.py38
-rw-r--r--foundation/urls/__init__.py0
-rw-r--r--foundation/urls/meetings.py22
-rw-r--r--foundation/views.py45
10 files changed, 627 insertions, 0 deletions
diff --git a/foundation/__init__.py b/foundation/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/foundation/__init__.py
diff --git a/foundation/admin.py b/foundation/admin.py
new file mode 100644
index 00000000..998e27f9
--- /dev/null
+++ b/foundation/admin.py
@@ -0,0 +1,73 @@
+from django.contrib import admin
+
+from . import models
+
+
+@admin.register(models.Office)
+class OfficeAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(models.Term)
+class TermAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(models.BoardMember)
+class BoardMemberAdmin(admin.ModelAdmin):
+ list_display = ('full_name', 'office', 'term')
+ list_filter = ('office', 'term')
+ list_select_related = True
+ raw_id_fields = ('account',)
+
+ def full_name(self, obj):
+ return obj.account.get_full_name()
+ full_name.admin_order_field = 'account__last_name'
+
+
+@admin.register(models.NonBoardAttendee)
+class NonBoardAttendeeAdmin(admin.ModelAdmin):
+ pass
+
+
+class GrantInline(admin.TabularInline):
+ model = models.ApprovedGrant
+
+
+class IndividualMemberInline(admin.TabularInline):
+ model = models.ApprovedIndividualMember
+
+
+class CorporateMemberInline(admin.TabularInline):
+ model = models.ApprovedCorporateMember
+
+
+class BusinessInline(admin.StackedInline):
+ model = models.Business
+
+
+class ActionItemInline(admin.StackedInline):
+ model = models.ActionItem
+
+
+@admin.register(models.Meeting)
+class MeetingAdmin(admin.ModelAdmin):
+ fieldsets = (
+ ('Metadata', {
+ 'fields': ('title', 'slug', 'date', 'leader', 'board_attendees', 'non_board_attendees'),
+ }),
+ ('Treasurer report', {
+ 'fields': ('treasurer_balance', 'treasurer_report'),
+ }),
+ )
+ filter_horizontal = ('board_attendees', 'non_board_attendees')
+ inlines = [
+ GrantInline,
+ IndividualMemberInline,
+ CorporateMemberInline,
+ BusinessInline,
+ ActionItemInline
+ ]
+ list_display = ('title', 'date')
+ list_filter = ('date',)
+ prepopulated_fields = {'slug': ('title',)}
diff --git a/foundation/migrations/0001_initial.py b/foundation/migrations/0001_initial.py
new file mode 100644
index 00000000..a2a1ca58
--- /dev/null
+++ b/foundation/migrations/0001_initial.py
@@ -0,0 +1,245 @@
+# Generated by Django 2.1.7 on 2019-04-16 21:54
+
+from decimal import Decimal
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import djmoney.models.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ActionItem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('responsible', models.CharField(max_length=255)),
+ ('task', models.TextField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ApprovedCorporateMember',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ApprovedGrant',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('entity', models.CharField(max_length=255)),
+ ('amount_currency', djmoney.models.fields.CurrencyField(
+ choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghani'), ('DZD', 'Algerian Dinar'), ('ARS', 'Argentine Peso'),
+ ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Guilder'), ('AUD', 'Australian Dollar'), ('AZN', 'Azerbaijanian Manat'),
+ ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('THB', 'Baht'), ('PAB', 'Balboa'), ('BBD', 'Barbados Dollar'),
+ ('BYN', 'Belarussian Ruble'), ('BYR', 'Belarussian Ruble'), ('BZD', 'Belize Dollar'),
+ ('BMD', 'Bermudian Dollar (customarily known as Bermuda Dollar)'), ('BTN', 'Bhutanese ngultrum'),
+ ('VEF', 'Bolivar Fuerte'), ('BOB', 'Boliviano'), ('XBA', 'Bond Markets Units European Composite Unit (EURCO)'),
+ ('BRL', 'Brazilian Real'), ('BND', 'Brunei Dollar'), ('BGN', 'Bulgarian Lev'), ('BIF', 'Burundi Franc'),
+ ('XOF', 'CFA Franc BCEAO'), ('XAF', 'CFA franc BEAC'), ('XPF', 'CFP Franc'), ('CAD', 'Canadian Dollar'),
+ ('CVE', 'Cape Verde Escudo'), ('KYD', 'Cayman Islands Dollar'), ('CLP', 'Chilean peso'),
+ ('XTS', 'Codes specifically reserved for testing purposes'), ('COP', 'Colombian peso'), ('KMF', 'Comoro Franc'),
+ ('CDF', 'Congolese franc'), ('BAM', 'Convertible Marks'), ('NIO', 'Cordoba Oro'), ('CRC', 'Costa Rican Colon'),
+ ('HRK', 'Croatian Kuna'), ('CUP', 'Cuban Peso'), ('CUC', 'Cuban convertible peso'), ('CZK', 'Czech Koruna'), ('GMD', 'Dalasi'),
+ ('DKK', 'Danish Krone'), ('MKD', 'Denar'), ('DJF', 'Djibouti Franc'), ('STD', 'Dobra'), ('DOP', 'Dominican Peso'),
+ ('VND', 'Dong'), ('XCD', 'East Caribbean Dollar'), ('EGP', 'Egyptian Pound'), ('SVC', 'El Salvador Colon'),
+ ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBB', 'European Monetary Unit (E.M.U.-6)'),
+ ('XBD', 'European Unit of Account 17(E.U.A.-17)'), ('XBC', 'European Unit of Account 9(E.U.A.-9)'),
+ ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fiji Dollar'), ('HUF', 'Forint'), ('GHS', 'Ghana Cedi'), ('GIP', 'Gibraltar Pound'),
+ ('XAU', 'Gold'), ('XFO', 'Gold-Franc'), ('PYG', 'Guarani'), ('GNF', 'Guinea Franc'), ('GYD', 'Guyana Dollar'),
+ ('HTG', 'Haitian gourde'), ('HKD', 'Hong Kong Dollar'), ('UAH', 'Hryvnia'), ('ISK', 'Iceland Krona'), ('INR', 'Indian Rupee'),
+ ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IMP', 'Isle of Man Pound'), ('JMD', 'Jamaican Dollar'),
+ ('JOD', 'Jordanian Dinar'), ('KES', 'Kenyan Shilling'), ('PGK', 'Kina'), ('LAK', 'Kip'), ('KWD', 'Kuwaiti Dinar'),
+ ('AOA', 'Kwanza'), ('MMK', 'Kyat'), ('GEL', 'Lari'), ('LVL', 'Latvian Lats'), ('LBP', 'Lebanese Pound'), ('ALL', 'Lek'),
+ ('HNL', 'Lempira'), ('SLL', 'Leone'), ('LSL', 'Lesotho loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'),
+ ('SZL', 'Lilangeni'), ('LTL', 'Lithuanian Litas'), ('MGA', 'Malagasy Ariary'), ('MWK', 'Malawian Kwacha'),
+ ('MYR', 'Malaysian Ringgit'), ('TMM', 'Manat'), ('MUR', 'Mauritius Rupee'), ('MZN', 'Metical'),
+ ('MXV', 'Mexican Unidad de Inversion (UDI)'), ('MXN', 'Mexican peso'), ('MDL', 'Moldovan Leu'), ('MAD', 'Moroccan Dirham'),
+ ('BOV', 'Mvdol'), ('NGN', 'Naira'), ('ERN', 'Nakfa'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'),
+ ('ANG', 'Netherlands Antillian Guilder'), ('ILS', 'New Israeli Sheqel'), ('RON', 'New Leu'), ('TWD', 'New Taiwan Dollar'),
+ ('NZD', 'New Zealand Dollar'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('PEN', 'Nuevo Sol'), ('MRO', 'Ouguiya'),
+ ('TOP', 'Paanga'), ('PKR', 'Pakistan Rupee'), ('XPD', 'Palladium'), ('MOP', 'Pataca'), ('PHP', 'Philippine Peso'),
+ ('XPT', 'Platinum'), ('GBP', 'Pound Sterling'), ('BWP', 'Pula'), ('QAR', 'Qatari Rial'), ('GTQ', 'Quetzal'), ('ZAR', 'Rand'),
+ ('OMR', 'Rial Omani'), ('KHR', 'Riel'), ('MVR', 'Rufiyaa'), ('IDR', 'Rupiah'), ('RUB', 'Russian Ruble'), ('RWF', 'Rwanda Franc'),
+ ('XDR', 'SDR'), ('SHP', 'Saint Helena Pound'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('SCR', 'Seychelles Rupee'),
+ ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SBD', 'Solomon Islands Dollar'), ('KGS', 'Som'), ('SOS', 'Somali Shilling'),
+ ('TJS', 'Somoni'), ('SSP', 'South Sudanese Pound'), ('LKR', 'Sri Lanka Rupee'), ('XSU', 'Sucre'), ('SDG', 'Sudanese Pound'),
+ ('SRD', 'Surinam Dollar'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('BDT', 'Taka'),
+ ('WST', 'Tala'), ('TZS', 'Tanzanian Shilling'), ('KZT', 'Tenge'),
+ ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TTD', 'Trinidad and Tobago Dollar'),
+ ('MNT', 'Tugrik'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TMT', 'Turkmenistan New Manat'), ('TVD', 'Tuvalu dollar'),
+ ('AED', 'UAE Dirham'), ('XFU', 'UIC-Franc'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('UGX', 'Uganda Shilling'),
+ ('CLF', 'Unidad de Fomento'), ('COU', 'Unidad de Valor Real'), ('UYI', 'Uruguay Peso en Unidades Indexadas (URUIURUI)'),
+ ('UYU', 'Uruguayan peso'), ('UZS', 'Uzbekistan Sum'), ('VUV', 'Vatu'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('KRW', 'Won'),
+ ('YER', 'Yemeni Rial'), ('JPY', 'Yen'), ('CNY', 'Yuan Renminbi'), ('ZMK', 'Zambian Kwacha'), ('ZMW', 'Zambian Kwacha'),
+ ('ZWD', 'Zimbabwe Dollar A/06'), ('ZWN', 'Zimbabwe dollar A/08'), ('ZWL', 'Zimbabwe dollar A/09'), ('PLN', 'Zloty')],
+ default='USD', editable=False, max_length=3)),
+ ('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), default_currency='USD', max_digits=10)),
+ ],
+ options={
+ 'ordering': ('entity',),
+ },
+ ),
+ migrations.CreateModel(
+ name='ApprovedIndividualMember',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='BoardMember',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Business',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('body', models.TextField()),
+ ('body_html', models.TextField(editable=False)),
+ ('business_type', models.CharField(choices=[('new', 'New'), ('ongoing', 'Ongoing')], max_length=25)),
+ ],
+ options={
+ 'verbose_name_plural': 'Business',
+ 'ordering': ('title',),
+ },
+ ),
+ migrations.CreateModel(
+ name='Meeting',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date', models.DateField()),
+ ('title', models.CharField(max_length=255)),
+ ('slug', models.SlugField()),
+ ('treasurer_balance_currency', djmoney.models.fields.CurrencyField(
+ choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghani'), ('DZD', 'Algerian Dinar'), ('ARS', 'Argentine Peso'),
+ ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Guilder'), ('AUD', 'Australian Dollar'), ('AZN', 'Azerbaijanian Manat'),
+ ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('THB', 'Baht'), ('PAB', 'Balboa'), ('BBD', 'Barbados Dollar'),
+ ('BYN', 'Belarussian Ruble'), ('BYR', 'Belarussian Ruble'), ('BZD', 'Belize Dollar'),
+ ('BMD', 'Bermudian Dollar (customarily known as Bermuda Dollar)'), ('BTN', 'Bhutanese ngultrum'),
+ ('VEF', 'Bolivar Fuerte'), ('BOB', 'Boliviano'), ('XBA', 'Bond Markets Units European Composite Unit (EURCO)'),
+ ('BRL', 'Brazilian Real'), ('BND', 'Brunei Dollar'), ('BGN', 'Bulgarian Lev'), ('BIF', 'Burundi Franc'),
+ ('XOF', 'CFA Franc BCEAO'), ('XAF', 'CFA franc BEAC'), ('XPF', 'CFP Franc'), ('CAD', 'Canadian Dollar'),
+ ('CVE', 'Cape Verde Escudo'), ('KYD', 'Cayman Islands Dollar'), ('CLP', 'Chilean peso'),
+ ('XTS', 'Codes specifically reserved for testing purposes'), ('COP', 'Colombian peso'), ('KMF', 'Comoro Franc'),
+ ('CDF', 'Congolese franc'), ('BAM', 'Convertible Marks'), ('NIO', 'Cordoba Oro'), ('CRC', 'Costa Rican Colon'),
+ ('HRK', 'Croatian Kuna'), ('CUP', 'Cuban Peso'), ('CUC', 'Cuban convertible peso'), ('CZK', 'Czech Koruna'), ('GMD', 'Dalasi'),
+ ('DKK', 'Danish Krone'), ('MKD', 'Denar'), ('DJF', 'Djibouti Franc'), ('STD', 'Dobra'), ('DOP', 'Dominican Peso'),
+ ('VND', 'Dong'), ('XCD', 'East Caribbean Dollar'), ('EGP', 'Egyptian Pound'), ('SVC', 'El Salvador Colon'),
+ ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBB', 'European Monetary Unit (E.M.U.-6)'),
+ ('XBD', 'European Unit of Account 17(E.U.A.-17)'), ('XBC', 'European Unit of Account 9(E.U.A.-9)'),
+ ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fiji Dollar'), ('HUF', 'Forint'), ('GHS', 'Ghana Cedi'), ('GIP', 'Gibraltar Pound'),
+ ('XAU', 'Gold'), ('XFO', 'Gold-Franc'), ('PYG', 'Guarani'), ('GNF', 'Guinea Franc'), ('GYD', 'Guyana Dollar'),
+ ('HTG', 'Haitian gourde'), ('HKD', 'Hong Kong Dollar'), ('UAH', 'Hryvnia'), ('ISK', 'Iceland Krona'), ('INR', 'Indian Rupee'),
+ ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IMP', 'Isle of Man Pound'), ('JMD', 'Jamaican Dollar'),
+ ('JOD', 'Jordanian Dinar'), ('KES', 'Kenyan Shilling'), ('PGK', 'Kina'), ('LAK', 'Kip'), ('KWD', 'Kuwaiti Dinar'),
+ ('AOA', 'Kwanza'), ('MMK', 'Kyat'), ('GEL', 'Lari'), ('LVL', 'Latvian Lats'), ('LBP', 'Lebanese Pound'), ('ALL', 'Lek'),
+ ('HNL', 'Lempira'), ('SLL', 'Leone'), ('LSL', 'Lesotho loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'),
+ ('SZL', 'Lilangeni'), ('LTL', 'Lithuanian Litas'), ('MGA', 'Malagasy Ariary'), ('MWK', 'Malawian Kwacha'),
+ ('MYR', 'Malaysian Ringgit'), ('TMM', 'Manat'), ('MUR', 'Mauritius Rupee'), ('MZN', 'Metical'),
+ ('MXV', 'Mexican Unidad de Inversion (UDI)'), ('MXN', 'Mexican peso'), ('MDL', 'Moldovan Leu'), ('MAD', 'Moroccan Dirham'),
+ ('BOV', 'Mvdol'), ('NGN', 'Naira'), ('ERN', 'Nakfa'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'),
+ ('ANG', 'Netherlands Antillian Guilder'), ('ILS', 'New Israeli Sheqel'), ('RON', 'New Leu'), ('TWD', 'New Taiwan Dollar'),
+ ('NZD', 'New Zealand Dollar'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('PEN', 'Nuevo Sol'), ('MRO', 'Ouguiya'),
+ ('TOP', 'Paanga'), ('PKR', 'Pakistan Rupee'), ('XPD', 'Palladium'), ('MOP', 'Pataca'), ('PHP', 'Philippine Peso'),
+ ('XPT', 'Platinum'), ('GBP', 'Pound Sterling'), ('BWP', 'Pula'), ('QAR', 'Qatari Rial'), ('GTQ', 'Quetzal'), ('ZAR', 'Rand'),
+ ('OMR', 'Rial Omani'), ('KHR', 'Riel'), ('MVR', 'Rufiyaa'), ('IDR', 'Rupiah'), ('RUB', 'Russian Ruble'), ('RWF', 'Rwanda Franc'),
+ ('XDR', 'SDR'), ('SHP', 'Saint Helena Pound'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('SCR', 'Seychelles Rupee'),
+ ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SBD', 'Solomon Islands Dollar'), ('KGS', 'Som'), ('SOS', 'Somali Shilling'),
+ ('TJS', 'Somoni'), ('SSP', 'South Sudanese Pound'), ('LKR', 'Sri Lanka Rupee'), ('XSU', 'Sucre'), ('SDG', 'Sudanese Pound'),
+ ('SRD', 'Surinam Dollar'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('BDT', 'Taka'),
+ ('WST', 'Tala'), ('TZS', 'Tanzanian Shilling'), ('KZT', 'Tenge'),
+ ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TTD', 'Trinidad and Tobago Dollar'),
+ ('MNT', 'Tugrik'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TMT', 'Turkmenistan New Manat'), ('TVD', 'Tuvalu dollar'),
+ ('AED', 'UAE Dirham'), ('XFU', 'UIC-Franc'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('UGX', 'Uganda Shilling'),
+ ('CLF', 'Unidad de Fomento'), ('COU', 'Unidad de Valor Real'), ('UYI', 'Uruguay Peso en Unidades Indexadas (URUIURUI)'),
+ ('UYU', 'Uruguayan peso'), ('UZS', 'Uzbekistan Sum'), ('VUV', 'Vatu'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('KRW', 'Won'),
+ ('YER', 'Yemeni Rial'), ('JPY', 'Yen'), ('CNY', 'Yuan Renminbi'), ('ZMK', 'Zambian Kwacha'), ('ZMW', 'Zambian Kwacha'),
+ ('ZWD', 'Zimbabwe Dollar A/06'), ('ZWN', 'Zimbabwe dollar A/08'), ('ZWL', 'Zimbabwe dollar A/09'), ('PLN', 'Zloty')],
+ default='USD', editable=False, max_length=3)),
+ ('treasurer_balance', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), default_currency='USD', max_digits=10)),
+ ('treasurer_report', models.TextField(blank=True)),
+ ('treasurer_report_html', models.TextField(editable=False)),
+ ('board_attendees', models.ManyToManyField(related_name='meetings_attended', to='foundation.BoardMember')),
+ ('leader', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meetings_led', to='foundation.BoardMember')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='NonBoardAttendee',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ('role', models.CharField(max_length=100)),
+ ],
+ options={
+ 'verbose_name': 'Non-board attendee',
+ 'verbose_name_plural': 'Non-board attendees',
+ },
+ ),
+ migrations.CreateModel(
+ name='Office',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Term',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('year', models.CharField(max_length=4, unique=True)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='meeting',
+ name='non_board_attendees',
+ field=models.ManyToManyField(blank=True, related_name='meetings_attended', to='foundation.NonBoardAttendee'),
+ ),
+ migrations.AddField(
+ model_name='business',
+ name='meeting',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='business', to='foundation.Meeting'),
+ ),
+ migrations.AddField(
+ model_name='boardmember',
+ name='office',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='holders', to='foundation.Office'),
+ ),
+ migrations.AddField(
+ model_name='boardmember',
+ name='term',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='board_members', to='foundation.Term'),
+ ),
+ migrations.AddField(
+ model_name='approvedindividualmember',
+ name='approved_at',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='individual_members_approved', to='foundation.Meeting'),
+ ),
+ migrations.AddField(
+ model_name='approvedgrant',
+ name='approved_at',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants_approved', to='foundation.Meeting'),
+ ),
+ migrations.AddField(
+ model_name='approvedcorporatemember',
+ name='approved_at',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='corporate_members_approved', to='foundation.Meeting'),
+ ),
+ migrations.AddField(
+ model_name='actionitem',
+ name='meeting',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='action_items', to='foundation.Meeting'),
+ ),
+ ]
diff --git a/foundation/migrations/__init__.py b/foundation/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/foundation/migrations/__init__.py
diff --git a/foundation/models.py b/foundation/models.py
new file mode 100644
index 00000000..d5b885ae
--- /dev/null
+++ b/foundation/models.py
@@ -0,0 +1,204 @@
+from django.conf import settings
+from django.db import models
+from django.urls import reverse
+from django.utils.dateformat import format as date_format
+from djmoney.models.fields import MoneyField
+from docutils.core import publish_parts
+
+from blog.models import BLOG_DOCUTILS_SETTINGS
+
+
+class Office(models.Model):
+ """
+ An office held by a DSF Board member.
+
+ """
+ name = models.CharField(max_length=100, unique=True)
+
+ def __str__(self):
+ return self.name
+
+
+class Term(models.Model):
+ """
+ A term in which DSF Board members served.
+
+ """
+ year = models.CharField(max_length=4, unique=True)
+
+ def __str__(self):
+ return self.year
+
+
+class BoardMember(models.Model):
+ """
+ A DSF Board member.
+
+ """
+ account = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ office = models.ForeignKey(
+ Office, related_name='holders', on_delete=models.CASCADE
+ )
+ term = models.ForeignKey(
+ Term, related_name='board_members', on_delete=models.CASCADE
+ )
+
+ def __str__(self):
+ return "{} ({} - {})".format(
+ self.account.get_full_name(), self.office, self.term.year)
+
+
+class NonBoardAttendee(models.Model):
+ """
+ A non-Board member attending a Board meeting.
+
+ """
+ name = models.CharField(max_length=255)
+ role = models.CharField(max_length=100)
+
+ class Meta:
+ verbose_name = 'Non-board attendee'
+ verbose_name_plural = 'Non-board attendees'
+
+ def __str__(self):
+ return "{} ({})".format(self.name, self.role)
+
+
+class Meeting(models.Model):
+ """
+ A meeting of the DSF Board.
+
+ """
+ date = models.DateField()
+ title = models.CharField(max_length=255)
+ slug = models.SlugField()
+ leader = models.ForeignKey(BoardMember, related_name='meetings_led', on_delete=models.CASCADE)
+ board_attendees = models.ManyToManyField(BoardMember, related_name='meetings_attended')
+ non_board_attendees = models.ManyToManyField(
+ NonBoardAttendee, related_name='meetings_attended', blank=True
+ )
+ treasurer_balance = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
+ treasurer_report = models.TextField(blank=True)
+ treasurer_report_html = models.TextField(editable=False)
+
+ def __str__(self):
+ return "{}, {}".format(
+ self.title, date_format(self.date, "F j, Y")
+ )
+
+ def save(self, *args, **kwargs):
+ if self.treasurer_report:
+ self.treasurer_report_html = publish_parts(
+ source=self.treasurer_report, writer_name='html',
+ settings_overrides=BLOG_DOCUTILS_SETTINGS
+ )['fragment']
+ super().save(*args, **kwargs)
+
+ def get_absolute_url(self):
+ return reverse(
+ 'foundation_meeting_detail',
+ args=(),
+ kwargs={
+ 'year': self.date.strftime('%Y'),
+ 'month': self.date.strftime('%b').lower(),
+ 'day': self.date.strftime('%d'),
+ 'slug': self.slug
+ }
+ )
+
+
+class ApprovedGrant(models.Model):
+ """
+ A grant approved by the DSF Board.
+
+ """
+ entity = models.CharField(max_length=255)
+ amount = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
+ approved_at = models.ForeignKey(
+ Meeting, related_name='grants_approved', on_delete=models.CASCADE
+ )
+
+ class Meta:
+ ordering = ('entity',)
+
+ def __str__(self):
+ return "{}: {}".format(self.entity, self.amount)
+
+
+class ApprovedIndividualMember(models.Model):
+ """
+ An individual DSF member approved by the Board.
+
+ """
+ name = models.CharField(max_length=255)
+ approved_at = models.ForeignKey(
+ Meeting, related_name='individual_members_approved', on_delete=models.CASCADE
+ )
+
+ def __str__(self):
+ return self.name
+
+
+class ApprovedCorporateMember(models.Model):
+ """
+ A corporate DSF member approved by the Board.
+
+ """
+ name = models.CharField(max_length=255)
+ approved_at = models.ForeignKey(
+ Meeting, related_name='corporate_members_approved', on_delete=models.CASCADE
+ )
+
+ def __str__(self):
+ return self.name
+
+
+class Business(models.Model):
+ """
+ Business of the DSF Board.
+
+ """
+ NEW = 'new'
+ ONGOING = 'ongoing'
+
+ TYPE_CHOICES = (
+ (NEW, 'New'),
+ (ONGOING, 'Ongoing'),
+ )
+
+ title = models.CharField(max_length=255)
+ body = models.TextField()
+ body_html = models.TextField(editable=False)
+ business_type = models.CharField(max_length=25, choices=TYPE_CHOICES)
+ meeting = models.ForeignKey(
+ Meeting, related_name='business', on_delete=models.CASCADE
+ )
+
+ class Meta:
+ ordering = ('title',)
+ verbose_name_plural = 'Business'
+
+ def __str__(self):
+ return self.title
+
+ def save(self, *args, **kwargs):
+ self.body_html = publish_parts(
+ source=self.body, writer_name='html',
+ settings_overrides=BLOG_DOCUTILS_SETTINGS
+ )['fragment']
+ super().save(*args, **kwargs)
+
+
+class ActionItem(models.Model):
+ """
+ A task to be completed by an attendee of a DSF Board meeting.
+
+ """
+ responsible = models.CharField(max_length=255)
+ task = models.TextField()
+ meeting = models.ForeignKey(
+ Meeting, related_name='action_items', on_delete=models.CASCADE
+ )
+
+ def __str__(self):
+ return self.task
diff --git a/foundation/templatetags/__init__.py b/foundation/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/foundation/templatetags/__init__.py
diff --git a/foundation/templatetags/foundation.py b/foundation/templatetags/foundation.py
new file mode 100644
index 00000000..97a12787
--- /dev/null
+++ b/foundation/templatetags/foundation.py
@@ -0,0 +1,38 @@
+from decimal import ROUND_HALF_EVEN
+
+from django import template
+import moneyed
+from moneyed.localization import _format, format_money, _sign
+
+
+register = template.Library()
+
+
+# The default currency formatting of py-moneyed/djmoney doesn't do
+# what we want, so we set up a custom one here, applying a consistent
+# format that always prefixes the three-letter currency code and
+# symbol.
+DJANGO = "django"
+
+_format(DJANGO, group_size=3, group_separator=",", decimal_point=".",
+ positive_sign="", trailing_positive_sign="",
+ negative_sign="-", trailing_negative_sign="",
+ rounding_method=ROUND_HALF_EVEN)
+
+# The DSF mostly only deals in USD with occasional grants iN EUR, but
+# we set up a few other currencies here just to be safe.
+#
+# Any currencies not defined here will fall back to the py-moneyed
+# default formatter.
+_sign(DJANGO, moneyed.AUD, prefix='AUD $')
+_sign(DJANGO, moneyed.CAD, prefix='CAD $')
+_sign(DJANGO, moneyed.EUR, prefix='EUR €')
+_sign(DJANGO, moneyed.GBP, prefix='GBP £')
+_sign(DJANGO, moneyed.JPY, prefix='JPY ¥')
+_sign(DJANGO, moneyed.NZD, prefix='NZD $')
+_sign(DJANGO, moneyed.USD, prefix='USD $')
+
+
+@register.filter
+def currency(value):
+ return format_money(value, locale=DJANGO)
diff --git a/foundation/urls/__init__.py b/foundation/urls/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/foundation/urls/__init__.py
diff --git a/foundation/urls/meetings.py b/foundation/urls/meetings.py
new file mode 100644
index 00000000..22f91295
--- /dev/null
+++ b/foundation/urls/meetings.py
@@ -0,0 +1,22 @@
+from django.urls import path
+
+from .. import views
+
+
+urlpatterns = [
+ path('',
+ views.MeetingArchiveIndex.as_view(),
+ name='foundation_meeting_archive_index'),
+ path('<int:year>/',
+ views.MeetingArchiveYear.as_view(),
+ name='foundation_meeting_archive_year'),
+ path('<int:year>/<str:month>/',
+ views.MeetingArchiveMonth.as_view(),
+ name='foundation_meeting_archive_month'),
+ path('<int:year>/<str:month>/<int:day>/',
+ views.MeetingArchiveDay.as_view(),
+ name='foundation_meeting_archive_day'),
+ path('<int:year>/<str:month>/<int:day>/<str:slug>/',
+ views.MeetingDetail.as_view(),
+ name='foundation_meeting_detail'),
+]
diff --git a/foundation/views.py b/foundation/views.py
new file mode 100644
index 00000000..80445582
--- /dev/null
+++ b/foundation/views.py
@@ -0,0 +1,45 @@
+from django.views import generic
+
+from . import models
+
+
+class MeetingMixin:
+ date_field = 'date'
+ model = models.Meeting
+
+
+class MeetingArchiveIndex(MeetingMixin, generic.ArchiveIndexView):
+ pass
+
+
+class MeetingArchiveYear(MeetingMixin, generic.YearArchiveView):
+ make_object_list = True
+
+
+class MeetingArchiveMonth(MeetingMixin, generic.MonthArchiveView):
+ pass
+
+
+class MeetingArchiveDay(MeetingMixin, generic.DayArchiveView):
+ pass
+
+
+class MeetingDetail(MeetingMixin, generic.DateDetailView):
+ context_object_name = 'meeting'
+
+ def get_queryset(self):
+ return super().get_queryset().select_related('leader').prefetch_related(
+ 'grants_approved', 'individual_members_approved', 'corporate_members_approved',
+ 'business', 'action_items', 'board_attendees', 'non_board_attendees'
+ )
+
+ def get_context_data(self, **kwargs):
+ context_data = super().get_context_data(**kwargs)
+ meeting = context_data['object']
+ context_data['ongoing_business'] = meeting.business.filter(
+ business_type=models.Business.ONGOING
+ )
+ context_data['new_business'] = meeting.business.filter(
+ business_type=models.Business.NEW
+ )
+ return context_data