diff options
| author | James Bennett <james@b-list.org> | 2019-05-08 16:54:24 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-05-08 16:54:24 -0400 |
| commit | 8d3cfdda9bdd2f6c43f1a25a02273b3038648061 (patch) | |
| tree | 88f30a636d0f2d19c5f815c680ad09c8128e8451 /foundation | |
| parent | 792913a496fe50837b82cf81d637c81f9ae2aaab (diff) | |
Fixed #609: Added app for board meeting minutes. (#907)
Diffstat (limited to 'foundation')
| -rw-r--r-- | foundation/__init__.py | 0 | ||||
| -rw-r--r-- | foundation/admin.py | 73 | ||||
| -rw-r--r-- | foundation/migrations/0001_initial.py | 245 | ||||
| -rw-r--r-- | foundation/migrations/__init__.py | 0 | ||||
| -rw-r--r-- | foundation/models.py | 204 | ||||
| -rw-r--r-- | foundation/templatetags/__init__.py | 0 | ||||
| -rw-r--r-- | foundation/templatetags/foundation.py | 38 | ||||
| -rw-r--r-- | foundation/urls/__init__.py | 0 | ||||
| -rw-r--r-- | foundation/urls/meetings.py | 22 | ||||
| -rw-r--r-- | foundation/views.py | 45 |
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 |
