From 21fbaa5634346862c5146b4f0be9e304636e751b Mon Sep 17 00:00:00 2001 From: CaptainJack2491 Date: Mon, 22 Sep 2025 12:18:58 +0100 Subject: [PATCH 1/6] Updats minutes links The new link points to the new minutes repo. Also updated the tests related to it. --- djangoproject/templates/base_foundation.html | 7 +++-- foundation/tests.py | 30 ++------------------ 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/djangoproject/templates/base_foundation.html b/djangoproject/templates/base_foundation.html index 877368019f..1940e3b6ca 100644 --- a/djangoproject/templates/base_foundation.html +++ b/djangoproject/templates/base_foundation.html @@ -26,7 +26,10 @@

About the foundation

  • Organizing a Django conference
  • {% translate "Latest DSF meeting minutes" %}

    - {% render_latest_meeting_minute_entries 2 %} - {% translate "More meeting minutes" %} +

    + {% blocktrans with minutes_url="https://github.com/django/dsf-minutes" %} + The latest meeting minutes can be found on the minutes repository. + {% endblocktrans %} +

    {% endblock %} diff --git a/foundation/tests.py b/foundation/tests.py index 38569f3756..d80487ca0c 100644 --- a/foundation/tests.py +++ b/foundation/tests.py @@ -5,7 +5,7 @@ from django.urls import reverse from djmoney.money import Money -from .models import ApprovedGrant, BoardMember, Business, Meeting, Office, Term +from .models import ApprovedGrant, BoardMember, Meeting, Office, Term class MeetingTestCase(TestCase): @@ -82,34 +82,8 @@ def test_latest_meeting_minutes(self): "treasurer_report": "Hello World", "title": "DSF Board monthly meeting", } - latest_meeting = Meeting.objects.create( - date=date(2023, 5, 12), **common_meeting_data - ) - previous_meeting = Meeting.objects.create( - date=date(2023, 4, 12), **common_meeting_data - ) Meeting.objects.create(date=date(2023, 3, 12), **common_meeting_data) - common_business_data = { - "body": "Example", - "body_html": "Example", - "business_type": "New", - "meeting": latest_meeting, - } - Business.objects.create(title="Business item 1", **common_business_data) - Business.objects.create(title="Business item 2", **common_business_data) - Business.objects.create(title="Business item 3", **common_business_data) - response = self.client.get(reverse("foundation_meeting_archive_index")) self.assertContains(response, "Latest DSF meeting minutes") - - self.assertContains(response, "DSF Board monthly meeting, May 12, 2023") - self.assertContains(response, latest_meeting.get_absolute_url()) - self.assertContains(response, "DSF Board monthly meeting, April 12, 2023") - self.assertContains(response, previous_meeting.get_absolute_url()) - self.assertNotContains(response, "DSF Board monthly meeting, March 12, 2023") - - self.assertContains(response, "New and Ongoing business", count=1) - self.assertContains(response, "Business item 1") - self.assertContains(response, "Business item 2") - self.assertContains(response, "Business item 3") + self.assertContains(response, "https://github.com/django/dsf-minutes") From 3b548d872ec3336f75ea6e24cc8e8b50128356c5 Mon Sep 17 00:00:00 2001 From: CaptainJack2491 Date: Mon, 22 Sep 2025 16:06:28 +0100 Subject: [PATCH 2/6] Removed refrences of Meetings The tests need to be rewritten to not use Meetings. --- djangoproject/templates/base_foundation.html | 2 +- .../templates/foundation/meeting_archive.html | 21 -- .../foundation/meeting_archive_day.html | 17 -- .../foundation/meeting_archive_month.html | 15 -- .../foundation/meeting_archive_year.html | 15 -- .../templates/foundation/meeting_detail.html | 106 -------- .../templates/foundation/meeting_snippet.html | 25 -- djangoproject/urls/www.py | 7 - foundation/admin.py | 91 ------- foundation/feeds.py | 25 -- foundation/models.py | 228 ------------------ foundation/templatetags/__init__.py | 0 foundation/templatetags/meetings.py | 11 - foundation/tests.py | 70 +----- foundation/urls/meetings.py | 22 +- foundation/views.py | 53 ---- 16 files changed, 6 insertions(+), 702 deletions(-) delete mode 100644 djangoproject/templates/foundation/meeting_archive.html delete mode 100644 djangoproject/templates/foundation/meeting_archive_day.html delete mode 100644 djangoproject/templates/foundation/meeting_archive_month.html delete mode 100644 djangoproject/templates/foundation/meeting_archive_year.html delete mode 100644 djangoproject/templates/foundation/meeting_detail.html delete mode 100644 djangoproject/templates/foundation/meeting_snippet.html delete mode 100644 foundation/feeds.py delete mode 100644 foundation/templatetags/__init__.py delete mode 100644 foundation/templatetags/meetings.py diff --git a/djangoproject/templates/base_foundation.html b/djangoproject/templates/base_foundation.html index 1940e3b6ca..9ca599d9e6 100644 --- a/djangoproject/templates/base_foundation.html +++ b/djangoproject/templates/base_foundation.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load fundraising_extras meetings i18n %} +{% load fundraising_extras i18n %} {% block og_title %}Django Software Foundation{% endblock %} diff --git a/djangoproject/templates/foundation/meeting_archive.html b/djangoproject/templates/foundation/meeting_archive.html deleted file mode 100644 index ef724d229f..0000000000 --- a/djangoproject/templates/foundation/meeting_archive.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "base_foundation.html" %} -{% load i18n %} - -{% block og_title %}{% translate "Meeting minutes archive" %}{% endblock %} -{% block og_description %}{% translate "View meeting minutes" %}{% endblock %} - -{% block head_extra %} - -{% endblock %} - -{% block content %} -

    {% translate "Meeting minutes archive" %}

    - -

    {% translate "Select a year to view meeting minutes:" %}

    - - -{% endblock %} diff --git a/djangoproject/templates/foundation/meeting_archive_day.html b/djangoproject/templates/foundation/meeting_archive_day.html deleted file mode 100644 index 0cdc2ce431..0000000000 --- a/djangoproject/templates/foundation/meeting_archive_day.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base_foundation.html" %} -{% load i18n %} - -{% block og_title %}{% translate "Meeting minutes archive" %}{% endblock %} -{% block og_description %}{% blocktranslate trimmed with day=day|date:"DATE_FORMAT" %} - View meeting minutes for {{ day }}{% endblocktranslate %}{% endblock %} - -{% block content %} -

    {% blocktranslate trimmed with day=day|date:"DATE_FORMAT" %} - Meeting minutes archive: {{ day }}{% endblocktranslate %}

    - - -{% endblock %} diff --git a/djangoproject/templates/foundation/meeting_archive_month.html b/djangoproject/templates/foundation/meeting_archive_month.html deleted file mode 100644 index 9c8ac61c3e..0000000000 --- a/djangoproject/templates/foundation/meeting_archive_month.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base_foundation.html" %} -{% load i18n %} - -{% block og_title %}{% translate "Meeting minutes archive" %}{% endblock %} -{% block og_description %}{% blocktranslate with month=month|date:"F, Y" %}View meeting minutes for {{ month }}{% endblocktranslate %}{% endblock %} - -{% block content %} -

    {% blocktranslate with month=month|date:"F, Y" %}Meeting minutes archive: {{ month }}{% endblocktranslate %}

    - - -{% endblock %} diff --git a/djangoproject/templates/foundation/meeting_archive_year.html b/djangoproject/templates/foundation/meeting_archive_year.html deleted file mode 100644 index b869455fa9..0000000000 --- a/djangoproject/templates/foundation/meeting_archive_year.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base_foundation.html" %} -{% load i18n %} - -{% block og_title %}{% translate "Meeting minutes archive" %}{% endblock %} -{% block og_description %}{% blocktranslate with year=year|date:"Y" %}View meeting minutes for {{ year }}{% endblocktranslate %}{% endblock %} - -{% block content %} -

    {% blocktranslate with year=year|date:"Y" %}Meeting minutes archive: {{ year }}{% endblocktranslate %}

    - - -{% endblock %} diff --git a/djangoproject/templates/foundation/meeting_detail.html b/djangoproject/templates/foundation/meeting_detail.html deleted file mode 100644 index 8f767b8e85..0000000000 --- a/djangoproject/templates/foundation/meeting_detail.html +++ /dev/null @@ -1,106 +0,0 @@ -{% extends "base_foundation.html" %} -{% load i18n %} - -{% block og_title %}{% blocktranslate %}Meeting minutes: {{ meeting }}{% endblocktranslate %}{% endblock %} -{% block og_description %}{% blocktranslate %}Meeting minutes for {{ meeting }}{% endblocktranslate %}{% endblock %} - -{% block content %} - {% load djmoney %} - -

    {{ meeting }}

    - -

    - {% blocktranslate trimmed with name=meeting.leader.account.get_full_name %} - The meeting was led by {{ name }}. - {% endblocktranslate %} -

    - -

    {% translate "Board members in attendance were:" %}

    - - - - {% if meeting.non_board_attendees.all %} -

    {% translate "Also in attendance were:" %}

    - - - {% endif %} - -

    {% translate "Finances" %}

    - -

    {% translate "Balance" %}

    - -

    {{ meeting.treasurer_balance.currency.code }} {% money_localize meeting.treasurer_balance %}

    - - {% if meeting.treasurer_report %} -

    {% translate "Treasurer’s report" %}

    - {{ meeting.treasurer_report_html|safe }} - {% endif %} - - {% if meeting.grants_approved.all %} -

    {% translate "Grants approved" %}

    - - - {% endif %} - - {% if meeting.individual_members_approved.all %} -

    {% translate "Individual members approved" %}

    - - - {% endif %} - - {% if meeting.corporate_members_approved.all %} -

    {% translate "Corporate members approved" %}

    - - - {% endif %} - - {% if ongoing_business %} -

    {% translate "Ongoing business" %}

    - - {% for business in ongoing_business %} -

    {{ business.title }}

    - - {{ business.body_html|safe }} - {% endfor %} - {% endif %} - - {% if new_business %} -

    {% translate "New business" %}

    - - {% for business in new_business %} -

    {{ business.title }}

    - - {{ business.body_html|safe }} - {% endfor %} - {% endif %} - - {% if meeting.action_items.all %} -

    {% translate "Action items" %}

    - - - {% endif %} -{% endblock %} diff --git a/djangoproject/templates/foundation/meeting_snippet.html b/djangoproject/templates/foundation/meeting_snippet.html deleted file mode 100644 index 0e72198eb0..0000000000 --- a/djangoproject/templates/foundation/meeting_snippet.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load i18n %} - diff --git a/djangoproject/urls/www.py b/djangoproject/urls/www.py index 22edfa7361..0eefc6d9d3 100644 --- a/djangoproject/urls/www.py +++ b/djangoproject/urls/www.py @@ -13,7 +13,6 @@ from aggregator.feeds import CommunityAggregatorFeed, CommunityAggregatorFirehoseFeed from blog.feeds import WeblogEntryFeed from blog.sitemaps import WeblogSitemap -from foundation.feeds import FoundationMinutesFeed from foundation.views import CoreDevelopers admin.autodiscover() @@ -96,7 +95,6 @@ ), path("contact/", include("contact.urls")), path("foundation/django_core/", CoreDevelopers.as_view()), - path("foundation/minutes/", include("foundation.urls.meetings")), path("foundation/", include("members.urls")), path("fundraising/", include("fundraising.urls")), # Used by docs search suggestions @@ -116,11 +114,6 @@ name="aggregator-firehose-feed", ), path("rss/community//", CommunityAggregatorFeed(), name="aggregator-feed"), - path( - "rss/foundation/minutes/", - FoundationMinutesFeed(), - name="foundation-minutes-feed", - ), # django-push path("subscriber/", include("django_push.subscriber.urls")), # Trac schtuff diff --git a/foundation/admin.py b/foundation/admin.py index 1c905040e6..a9731ffd56 100644 --- a/foundation/admin.py +++ b/foundation/admin.py @@ -4,97 +4,6 @@ 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",) - - @admin.display(ordering="account__last_name") - def full_name(self, obj): - return obj.account.get_full_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",)} - - def get_changeform_initial_data(self, request): - title = _("DSF Board monthly meeting") - return { - "title": title, - "slug": slugify(title), - } - - class CoreAwardAdmin(admin.ModelAdmin): list_display = ["recipient", "cohort"] diff --git a/foundation/feeds.py b/foundation/feeds.py deleted file mode 100644 index 072a9c8e0a..0000000000 --- a/foundation/feeds.py +++ /dev/null @@ -1,25 +0,0 @@ -from datetime import datetime, time - -from django.contrib.syndication.views import Feed -from django.utils.timezone import make_aware -from django.utils.translation import gettext_lazy as _ - -from .models import Meeting - - -class FoundationMinutesFeed(Feed): - title = _("The DSF meeting minutes") - link = "https://www.djangoproject.com/foundation/minutes/" - description = _("The meeting minutes of the Django Software Foundation's board.") - - def items(self): - return Meeting.objects.order_by("-date")[:10] - - def item_pubdate(self, item): - return make_aware(datetime.combine(item.date, time.min)) - - def item_author_name(self, item): - return _("DSF Board") - - def item_title(self, item): - return str(item) diff --git a/foundation/models.py b/foundation/models.py index 7ae1fa4acf..8c5aebf40c 100644 --- a/foundation/models.py +++ b/foundation/models.py @@ -12,234 +12,6 @@ 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.PROTECT) - 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 f"{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 f"{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", - default=Decimal("0.0"), - ) - 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", - default=Decimal("0.0"), - currency_choices=[ - (c.code, c.name) - for i, c in CURRENCIES.items() - if c.code - in { - "USD", - "EUR", - "AUD", - "NGN", - } # This set of currencies was extracted from current usage - ], - ) - approved_at = models.ForeignKey( - Meeting, related_name="grants_approved", on_delete=models.CASCADE - ) - - class Meta: - ordering = ("entity",) - - def __str__(self): - return f"{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 - - class CoreAwardCohort(models.Model): """ A cohort of individuals -- such as "Q1 2021" -- receiving the Django Core diff --git a/foundation/templatetags/__init__.py b/foundation/templatetags/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/foundation/templatetags/meetings.py b/foundation/templatetags/meetings.py deleted file mode 100644 index 80aeec8e83..0000000000 --- a/foundation/templatetags/meetings.py +++ /dev/null @@ -1,11 +0,0 @@ -from django import template - -from ..models import Meeting - -register = template.Library() - - -@register.inclusion_tag("foundation/meeting_snippet.html") -def render_latest_meeting_minute_entries(num): - meetings = Meeting.objects.order_by("-date").prefetch_related("business")[:num] - return {"meetings": meetings} diff --git a/foundation/tests.py b/foundation/tests.py index d80487ca0c..b6fe6d83fb 100644 --- a/foundation/tests.py +++ b/foundation/tests.py @@ -1,12 +1,5 @@ -from datetime import date - from django.contrib.auth.models import User from django.test import TestCase -from django.urls import reverse -from djmoney.money import Money - -from .models import ApprovedGrant, BoardMember, Meeting, Office, Term - class MeetingTestCase(TestCase): @classmethod @@ -14,68 +7,13 @@ def setUpTestData(cls): cls.user = User.objects.create_superuser( "admin", "admin@example.com", "password" ) - cls.member = BoardMember.objects.create( - account=cls.user, - office=Office.objects.create(name="treasurer"), - term=Term.objects.create(year=2023), - ) - - def test_meeting_initial(self): - self.client.force_login(self.user) - response = self.client.get(reverse("admin:foundation_meeting_add")) - self.assertContains(response, "DSF Board monthly meeting") - self.assertContains(response, "dsf-board-monthly-meeting") - - def test_meeting_minutes_feed(self): - """ - Make sure that the meeting minutes RSS feed works - """ - Meeting.objects.create( - date=date.today(), - title="DSF Board monthly meeting", - slug="dsf-board-monthly-meeting", - leader=self.member, - treasurer_report="Hello World", - ) - response = self.client.get(reverse("foundation-minutes-feed")) - self.assertEqual(response.status_code, 200) - self.assertIn(b"DSF Board monthly meeting", response.content) + def test_latest_meeting_minutes(self): + pass - def test_meeting_details(self): - meeting = Meeting.objects.create( - date=date(2023, 1, 12), - title="DSF Board monthly meeting", - slug="dsf-board-monthly-meeting", - leader=self.member, - treasurer_report="Hello World", - ) - ApprovedGrant.objects.create( - entity="Django girls", - amount=Money("10000", "USD"), - approved_at=meeting, - ) - ApprovedGrant.objects.create( - entity="DjangoCon EU", - amount=Money(5000, "EUR"), - approved_at=meeting, - ) - response = self.client.get( - reverse( - "foundation_meeting_detail", - kwargs={ - "year": 2023, - "month": "jan", - "day": 12, - "slug": "dsf-board-monthly-meeting", - }, - ) - ) - self.assertContains(response, "DSF Board monthly meeting") - self.assertContains(response, "USD $10,000.00") - self.assertContains(response, "EUR €5,000.00") + # TODO: Find a way to initalize the foundation page without using the Meeting + # object - def test_latest_meeting_minutes(self): common_meeting_data = { "slug": "dsf-board-monthly-meeting", "leader": self.member, diff --git a/foundation/urls/meetings.py b/foundation/urls/meetings.py index 8198567a97..7b5e3de5c5 100644 --- a/foundation/urls/meetings.py +++ b/foundation/urls/meetings.py @@ -5,25 +5,5 @@ urlpatterns = [ path( "", views.MeetingArchiveIndex.as_view(), name="foundation_meeting_archive_index" - ), - path( - "/", - views.MeetingArchiveYear.as_view(), - name="foundation_meeting_archive_year", - ), - path( - "//", - views.MeetingArchiveMonth.as_view(), - name="foundation_meeting_archive_month", - ), - path( - "///", - views.MeetingArchiveDay.as_view(), - name="foundation_meeting_archive_day", - ), - path( - "////", - views.MeetingDetail.as_view(), - name="foundation_meeting_detail", - ), + ) ] diff --git a/foundation/views.py b/foundation/views.py index 0cef304de2..7cdba3db45 100644 --- a/foundation/views.py +++ b/foundation/views.py @@ -2,59 +2,6 @@ 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 - - class CoreDevelopers(generic.ListView): queryset = models.CoreAwardCohort.objects.prefetch_related("recipients").order_by( "-cohort_date" From cb79e0f069bae5bd2cc42bf9c874716dc2ae2f8d Mon Sep 17 00:00:00 2001 From: CaptainJack2491 Date: Tue, 23 Sep 2025 13:18:56 +0100 Subject: [PATCH 3/6] Updated tests and removed more refrences to the old tests have also added migrations --- accounts/forms.py | 7 -- accounts/tests.py | 11 --- foundation/admin.py | 1 - ...vedcorporatemember_approved_at_and_more.py | 83 +++++++++++++++++++ foundation/models.py | 11 --- foundation/tests.py | 22 +++-- foundation/urls/__init__.py | 0 foundation/urls/meetings.py | 9 -- 8 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 foundation/migrations/0008_remove_approvedcorporatemember_approved_at_and_more.py delete mode 100644 foundation/urls/__init__.py delete mode 100644 foundation/urls/meetings.py diff --git a/accounts/forms.py b/accounts/forms.py index f863c06787..6a05eeeb98 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -64,13 +64,6 @@ def clean(self): raise forms.ValidationError(_("Staff users cannot be deleted")) return cleaned_data - def add_errors_from_protectederror(self, exception): - """ - Convert the given ProtectedError exception object into validation - errors on the instance. - """ - self.add_error(None, _("User has protected data and cannot be deleted")) - @transaction.atomic() def delete(self): """ diff --git a/accounts/tests.py b/accounts/tests.py index c41d893b1a..89ef8aebde 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -218,17 +218,6 @@ def test_deletion_staff_forbidden(self): form = self.create_user_and_form(is_staff=True) self.assertFormError(form, None, ["Staff users cannot be deleted"]) - def test_user_with_protected_data(self): - form = self.create_user_and_form() - form.user.boardmember_set.create( - office=foundationmodels.Office.objects.create(name="test"), - term=foundationmodels.Term.objects.create(year=2000), - ) - form.delete() - self.assertFormError( - form, None, ["User has protected data and cannot be deleted"] - ) - def test_form_delete_method_requires_valid_form(self): form = self.create_user_and_form(is_staff=True) self.assertRaises(form.InvalidFormError, form.delete) diff --git a/foundation/admin.py b/foundation/admin.py index a9731ffd56..c0d7686f54 100644 --- a/foundation/admin.py +++ b/foundation/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from django.utils.text import slugify from django.utils.translation import gettext as _ from . import models diff --git a/foundation/migrations/0008_remove_approvedcorporatemember_approved_at_and_more.py b/foundation/migrations/0008_remove_approvedcorporatemember_approved_at_and_more.py new file mode 100644 index 0000000000..3670dde7c4 --- /dev/null +++ b/foundation/migrations/0008_remove_approvedcorporatemember_approved_at_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 5.2 on 2025-09-23 05:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('foundation', '0007_boardmember_account_protect'), + ] + + operations = [ + migrations.RemoveField( + model_name='approvedcorporatemember', + name='approved_at', + ), + migrations.RemoveField( + model_name='approvedgrant', + name='approved_at', + ), + migrations.RemoveField( + model_name='approvedindividualmember', + name='approved_at', + ), + migrations.RemoveField( + model_name='boardmember', + name='account', + ), + migrations.RemoveField( + model_name='boardmember', + name='office', + ), + migrations.RemoveField( + model_name='boardmember', + name='term', + ), + migrations.RemoveField( + model_name='meeting', + name='board_attendees', + ), + migrations.RemoveField( + model_name='meeting', + name='leader', + ), + migrations.RemoveField( + model_name='business', + name='meeting', + ), + migrations.RemoveField( + model_name='meeting', + name='non_board_attendees', + ), + migrations.DeleteModel( + name='ActionItem', + ), + migrations.DeleteModel( + name='ApprovedCorporateMember', + ), + migrations.DeleteModel( + name='ApprovedGrant', + ), + migrations.DeleteModel( + name='ApprovedIndividualMember', + ), + migrations.DeleteModel( + name='Office', + ), + migrations.DeleteModel( + name='Term', + ), + migrations.DeleteModel( + name='BoardMember', + ), + migrations.DeleteModel( + name='Business', + ), + migrations.DeleteModel( + name='Meeting', + ), + migrations.DeleteModel( + name='NonBoardAttendee', + ), + ] diff --git a/foundation/models.py b/foundation/models.py index 8c5aebf40c..54ed41bef1 100644 --- a/foundation/models.py +++ b/foundation/models.py @@ -1,16 +1,5 @@ -from decimal import Decimal - -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 django.utils.translation import gettext_lazy as _ -from djmoney.models.fields import MoneyField -from djmoney.settings import CURRENCIES -from docutils.core import publish_parts - -from blog.models import BLOG_DOCUTILS_SETTINGS - class CoreAwardCohort(models.Model): """ diff --git a/foundation/tests.py b/foundation/tests.py index b6fe6d83fb..40282b1249 100644 --- a/foundation/tests.py +++ b/foundation/tests.py @@ -1,4 +1,6 @@ from django.contrib.auth.models import User +from django.contrib.flatpages.models import FlatPage +from django.contrib.sites.models import Site from django.test import TestCase class MeetingTestCase(TestCase): @@ -7,21 +9,17 @@ def setUpTestData(cls): cls.user = User.objects.create_superuser( "admin", "admin@example.com", "password" ) + cls.site = Site.objects.get_current() def test_latest_meeting_minutes(self): - pass - - # TODO: Find a way to initalize the foundation page without using the Meeting - # object + page = FlatPage.objects.create( + title="Foundation", + url="/foundation/", + template_name="flatpages/foundation.html" + ) + page.sites.add(self.site) - common_meeting_data = { - "slug": "dsf-board-monthly-meeting", - "leader": self.member, - "treasurer_report": "Hello World", - "title": "DSF Board monthly meeting", - } - Meeting.objects.create(date=date(2023, 3, 12), **common_meeting_data) - response = self.client.get(reverse("foundation_meeting_archive_index")) + response = self.client.get("/foundation/") self.assertContains(response, "Latest DSF meeting minutes") self.assertContains(response, "https://github.com/django/dsf-minutes") diff --git a/foundation/urls/__init__.py b/foundation/urls/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/foundation/urls/meetings.py b/foundation/urls/meetings.py deleted file mode 100644 index 7b5e3de5c5..0000000000 --- a/foundation/urls/meetings.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from .. import views - -urlpatterns = [ - path( - "", views.MeetingArchiveIndex.as_view(), name="foundation_meeting_archive_index" - ) -] From 63e9bf5308576d8b62a7faa108c845da1dcf5396 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:54:55 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- accounts/tests.py | 1 - foundation/admin.py | 2 +- foundation/models.py | 1 + foundation/tests.py | 3 ++- foundation/views.py | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/accounts/tests.py b/accounts/tests.py index 89ef8aebde..4b0787ad0f 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -6,7 +6,6 @@ from django_hosts.resolvers import reverse from accounts.forms import DeleteProfileForm -from foundation import models as foundationmodels from tracdb.models import Revision, Ticket, TicketChange from tracdb.testutils import TracDBCreateDatabaseMixin diff --git a/foundation/admin.py b/foundation/admin.py index c0d7686f54..ae36372c4b 100644 --- a/foundation/admin.py +++ b/foundation/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin -from django.utils.translation import gettext as _ from . import models + class CoreAwardAdmin(admin.ModelAdmin): list_display = ["recipient", "cohort"] diff --git a/foundation/models.py b/foundation/models.py index 54ed41bef1..8540d3f0ff 100644 --- a/foundation/models.py +++ b/foundation/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ + class CoreAwardCohort(models.Model): """ A cohort of individuals -- such as "Q1 2021" -- receiving the Django Core diff --git a/foundation/tests.py b/foundation/tests.py index 40282b1249..edeafed873 100644 --- a/foundation/tests.py +++ b/foundation/tests.py @@ -3,6 +3,7 @@ from django.contrib.sites.models import Site from django.test import TestCase + class MeetingTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -15,7 +16,7 @@ def test_latest_meeting_minutes(self): page = FlatPage.objects.create( title="Foundation", url="/foundation/", - template_name="flatpages/foundation.html" + template_name="flatpages/foundation.html", ) page.sites.add(self.site) diff --git a/foundation/views.py b/foundation/views.py index 7cdba3db45..a66cf0d2ba 100644 --- a/foundation/views.py +++ b/foundation/views.py @@ -2,6 +2,7 @@ from . import models + class CoreDevelopers(generic.ListView): queryset = models.CoreAwardCohort.objects.prefetch_related("recipients").order_by( "-cohort_date" From a3f7f08d2c419934c1a802e9cd0b6d64a05bb21c Mon Sep 17 00:00:00 2001 From: Tom Carrick Date: Sat, 27 Sep 2025 14:06:45 +0200 Subject: [PATCH 5/6] Add redirects for old minutes --- djangoproject/urls/www.py | 7 +++- foundation/redirects.py | 79 +++++++++++++++++++++++++++++++++++++++ foundation/tests.py | 22 +++++++++++ foundation/views.py | 16 ++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 foundation/redirects.py diff --git a/djangoproject/urls/www.py b/djangoproject/urls/www.py index 0eefc6d9d3..6ac010f646 100644 --- a/djangoproject/urls/www.py +++ b/djangoproject/urls/www.py @@ -13,7 +13,7 @@ from aggregator.feeds import CommunityAggregatorFeed, CommunityAggregatorFirehoseFeed from blog.feeds import WeblogEntryFeed from blog.sitemaps import WeblogSitemap -from foundation.views import CoreDevelopers +from foundation.views import CoreDevelopers, minutes_redirect admin.autodiscover() @@ -95,6 +95,11 @@ ), path("contact/", include("contact.urls")), path("foundation/django_core/", CoreDevelopers.as_view()), + path( + "foundation/minutes/////", + minutes_redirect, + name="minutes_redirect", + ), path("foundation/", include("members.urls")), path("fundraising/", include("fundraising.urls")), # Used by docs search suggestions diff --git a/foundation/redirects.py b/foundation/redirects.py new file mode 100644 index 0000000000..5ffc6b4082 --- /dev/null +++ b/foundation/redirects.py @@ -0,0 +1,79 @@ +MINUTES_BASE_URL = "https://github.com/django/dsf-minutes/blob/main/" + +MINUTES_DATES = { + (2019, 1, 10), + (2019, 2, 14), + (2019, 3, 14), + (2019, 4, 25), + (2019, 6, 13), + (2019, 7, 11), + (2019, 8, 8), + (2019, 9, 26), + (2019, 10, 10), + (2019, 11, 21), + (2019, 12, 17), + (2020, 1, 9), + (2020, 2, 13), + (2020, 3, 12), + (2020, 4, 9), + (2020, 5, 14), + (2020, 6, 11), + (2020, 7, 9), + (2020, 8, 13), + (2020, 9, 10), + (2020, 10, 8), + (2020, 11, 12), + (2020, 12, 16), + (2021, 1, 14), + (2021, 2, 11), + (2021, 3, 11), + (2021, 4, 8), + (2021, 5, 13), + (2021, 6, 17), + (2021, 7, 8), + (2021, 8, 11), + (2021, 9, 9), + (2021, 10, 14), + (2021, 11, 11), + (2021, 12, 16), + (2022, 1, 13), + (2022, 2, 10), + (2022, 3, 10), + (2022, 4, 14), + (2022, 5, 12), + (2022, 6, 9), + (2022, 7, 14), + (2022, 8, 11), + (2022, 9, 8), + (2022, 10, 13), + (2022, 11, 11), + (2022, 12, 8), + (2023, 1, 12), + (2023, 2, 8), + (2023, 3, 9), + (2023, 4, 13), + (2023, 5, 13), + (2023, 6, 8), + (2023, 7, 14), + (2023, 9, 14), + (2023, 10, 12), + (2023, 11, 9), + (2023, 12, 14), + (2024, 1, 11), + (2024, 2, 8), + (2024, 3, 14), + (2024, 4, 11), + (2024, 5, 16), + (2024, 6, 6), + (2024, 6, 13), + (2024, 7, 18), + (2024, 8, 8), + (2024, 9, 12), + (2024, 10, 10), + (2024, 11, 19), + (2024, 12, 10), + (2025, 1, 9), + (2025, 2, 13), + (2025, 3, 13), + (2025, 4, 10), +} diff --git a/foundation/tests.py b/foundation/tests.py index edeafed873..efbb8c7211 100644 --- a/foundation/tests.py +++ b/foundation/tests.py @@ -2,6 +2,7 @@ from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import Site from django.test import TestCase +from django.urls import reverse class MeetingTestCase(TestCase): @@ -24,3 +25,24 @@ def test_latest_meeting_minutes(self): self.assertContains(response, "Latest DSF meeting minutes") self.assertContains(response, "https://github.com/django/dsf-minutes") + + def test_minutes_redirect(self): + url = reverse( + "minutes_redirect", + kwargs={"year": 2025, "month": "Feb", "day": 13, "slug": "foo"}, + ) + response = self.client.get(url) + self.assertRedirects( + response, + "https://github.com/django/dsf-minutes/blob/main/2025/2025-02-13.md", + status_code=301, + fetch_redirect_response=False, + ) + + def test_minutes_redirect_not_found(self): + url = reverse( + "minutes_redirect", + kwargs={"year": 2025, "month": "Jan", "day": 13, "slug": "foo"}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/foundation/views.py b/foundation/views.py index a66cf0d2ba..f3f155b1a0 100644 --- a/foundation/views.py +++ b/foundation/views.py @@ -1,9 +1,25 @@ +from datetime import datetime + +from django.http import Http404 +from django.shortcuts import redirect from django.views import generic from . import models +from . import redirects class CoreDevelopers(generic.ListView): queryset = models.CoreAwardCohort.objects.prefetch_related("recipients").order_by( "-cohort_date" ) + + +def minutes_redirect(request, year, month, day, slug): + minutes_date = datetime.strptime(f"{year}-{month}-{day}", "%Y-%b-%d").date() + year, month, day = minutes_date.timetuple()[:3] + if (year, month, day) not in redirects.MINUTES_DATES: + raise Http404 + return redirect( + f"{redirects.MINUTES_BASE_URL}{year}/{year}-{month:02}-{day:02}.md", + permanent=True, + ) From 5268c34c2563626ccbc1019036b62a2b1076d89b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:08:55 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- foundation/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/foundation/views.py b/foundation/views.py index f3f155b1a0..f959816e64 100644 --- a/foundation/views.py +++ b/foundation/views.py @@ -4,8 +4,7 @@ from django.shortcuts import redirect from django.views import generic -from . import models -from . import redirects +from . import models, redirects class CoreDevelopers(generic.ListView):