diff --git a/aggregator/admin.py b/aggregator/admin.py index ac00fc47f..29c2dbb7a 100644 --- a/aggregator/admin.py +++ b/aggregator/admin.py @@ -1,6 +1,13 @@ from django.contrib import admin -from .models import APPROVED_FEED, DENIED_FEED, Feed, FeedItem, FeedType +from .models import ( + APPROVED_FEED, + DENIED_FEED, + Feed, + FeedItem, + FeedType, + LocalDjangoCommunity, +) @admin.action(description="Mark selected feeds as approved.") @@ -41,3 +48,10 @@ def mark_denied(modeladmin, request, queryset): FeedType, prepopulated_fields={"slug": ("name",)}, ) + +admin.site.register( + LocalDjangoCommunity, + prepopulated_fields={"slug": ("name",)}, + list_filter=["name", "city", "country", "is_active"], + search_fields=["name", "city", "country"], +) diff --git a/aggregator/migrations/0004_add_local_django_community.py b/aggregator/migrations/0004_add_local_django_community.py new file mode 100644 index 000000000..a12c82e90 --- /dev/null +++ b/aggregator/migrations/0004_add_local_django_community.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.19 on 2023-06-05 03:45 + +from django.db import migrations, models +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('aggregator', '0003_increase_url_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='LocalDjangoCommunity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120)), + ('description', models.TextField()), + ('slug', models.SlugField(max_length=125)), + ('city', models.CharField(max_length=85)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('continent', models.CharField(choices=[('Africa', 'Africa'), ('North America', 'North America'), ('South America', 'South America'), ('Europe', 'Europe'), ('Asia', 'Asia'), ('Oceania', 'Oceania'), ('Antarctica', 'Antarctica')], max_length=15)), + ('website_url', models.URLField(blank=True, default=None, max_length=250, null=True)), + ('event_site_url', models.URLField(blank=True, default=None, max_length=250, null=True)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.AddConstraint( + model_name='localdjangocommunity', + constraint=models.CheckConstraint(check=models.Q(models.Q(('event_site_url__isnull', False), ('website_url__isnull', False)), models.Q(('event_site_url__isnull', False), ('website_url__isnull', True)), models.Q(('event_site_url__isnull', True), ('website_url__isnull', False)), _connector='OR'), name='website_url_and_or_event_site_url'), + ), + ] diff --git a/aggregator/models.py b/aggregator/models.py index b4e9b46d5..6a5fc4fec 100644 --- a/aggregator/models.py +++ b/aggregator/models.py @@ -4,7 +4,9 @@ import feedparser from django.conf import settings from django.contrib.auth.models import User +from django.core.exceptions import ValidationError from django.db import models +from django_countries.fields import CountryField from django_push.subscriber import signals as push_signals from django_push.subscriber.models import Subscription @@ -190,3 +192,49 @@ def feed_updated(sender, notification, **kwargs): push_signals.updated.connect(feed_updated) + +CONTINENTS = [ + ("Africa", "Africa"), + ("North America", "North America"), + ("South America", "South America"), + ("Europe", "Europe"), + ("Asia", "Asia"), + ("Oceania", "Oceania"), + ("Antarctica", "Antarctica"), +] + + +class LocalDjangoCommunity(models.Model): + name = models.CharField(max_length=120) + description = models.TextField() + slug = models.SlugField(max_length=125) + city = models.CharField(max_length=85) + country = CountryField() + continent = models.CharField(choices=CONTINENTS, max_length=15) + website_url = models.URLField(max_length=250, default=None, blank=True, null=True) + event_site_url = models.URLField( + max_length=250, default=None, blank=True, null=True + ) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.CheckConstraint( + check=( + models.Q(website_url__isnull=False, event_site_url__isnull=False) + | models.Q(website_url__isnull=True, event_site_url__isnull=False) + | models.Q(website_url__isnull=False, event_site_url__isnull=True) + ), + name="website_url_and_or_event_site_url", + ), + ] + + def clean(self): + if not self.website_url and not self.event_site_url: + raise ValidationError( + "You must provide at least a website or event site URL" + ) + + def __str__(self): + return self.name diff --git a/aggregator/urls.py b/aggregator/urls.py index b54d994d5..32f5af957 100644 --- a/aggregator/urls.py +++ b/aggregator/urls.py @@ -4,6 +4,11 @@ urlpatterns = [ path("", views.index, name="community-index"), + path( + "local/", + views.LocalDjangoCommunitiesListView.as_view(), + name="local-django-communities", + ), path("mine/", views.my_feeds, name="community-my-feeds"), path("/", views.FeedListView.as_view(), name="community-feed-list"), path("add//", views.add_feed, name="community-add-feed"), diff --git a/aggregator/views.py b/aggregator/views.py index 0d4ed14ac..18acb2e90 100644 --- a/aggregator/views.py +++ b/aggregator/views.py @@ -4,7 +4,7 @@ from django.views.generic.list import ListView from .forms import FeedModelForm -from .models import APPROVED_FEED, Feed, FeedItem, FeedType +from .models import APPROVED_FEED, Feed, FeedItem, FeedType, LocalDjangoCommunity def index(request): @@ -108,3 +108,16 @@ def delete_feed(request, feed_id): feed.delete() return redirect("community-my-feeds") return render(request, "aggregator/delete-confirm.html", {"feed": feed}) + + +class LocalDjangoCommunitiesListView(ListView): + """ + Shows a list of community meetups + """ + + model = LocalDjangoCommunity + context_object_name = "django_communities" + template_name = "aggregator/local-django-community.html" + + def get_queryset(self): + return self.model.objects.all().order_by("continent").values() diff --git a/djangoproject/scss/_dark-mode.scss b/djangoproject/scss/_dark-mode.scss index 1e1e6c2c8..8ce0b24e4 100644 --- a/djangoproject/scss/_dark-mode.scss +++ b/djangoproject/scss/_dark-mode.scss @@ -57,6 +57,9 @@ html[data-theme="light"], --search-mark-text: #{$green-dark}; --selection: #{$green-very-light}; + + --community-img-bg: #{$green-very-light}; + --community-img-fg: #{$green-dark}; } @media (prefers-color-scheme: dark) { @@ -105,6 +108,9 @@ html[data-theme="light"], --search-mark-text: #{$black-light-5}; --selection: #{$green-medium}; + + --community-img-bg: #{$green-dark}; + --community-img-fg: #{$white}; } body .homepage { @@ -201,6 +207,9 @@ html[data-theme="dark"] { --selection: #{$green-dark}; + --community-img-bg: #{$green-dark}; + --community-img-fg: #{$white}; + .img-release { filter: invert(1); } diff --git a/djangoproject/scss/_style.scss b/djangoproject/scss/_style.scss index 98fb0f73e..08621d897 100644 --- a/djangoproject/scss/_style.scss +++ b/djangoproject/scss/_style.scss @@ -3685,11 +3685,11 @@ ul.corporate-members li { flex-direction: column; justify-content: center; align-items: center; - background: var(--secondary-accent); + background: var(--community-img-bg); border-radius: 20px; } - .community-cta svg { - color: var(--body-fg); + .community-cta svg, h3 { + color: var(--community-img-fg); } } diff --git a/djangoproject/templates/aggregator/index.html b/djangoproject/templates/aggregator/index.html index cd4d464bb..a8b3022cc 100644 --- a/djangoproject/templates/aggregator/index.html +++ b/djangoproject/templates/aggregator/index.html @@ -45,6 +45,15 @@

Report an issue

Contribute to Django

+ +
+
+ {# Material Symbols - Copyright 2022 Google LLC - Used under terms of Apache 2.0 license #} + +
+

Local Django Community

+
+

Django RSS feeds

diff --git a/djangoproject/templates/aggregator/local-django-community.html b/djangoproject/templates/aggregator/local-django-community.html new file mode 100644 index 000000000..9b16c9f3e --- /dev/null +++ b/djangoproject/templates/aggregator/local-django-community.html @@ -0,0 +1,51 @@ +{% extends "base_community.html" %} + +{% block content %} + +{# Group meetups by country and then by location #} + {% regroup django_communities|dictsort:"continent" by continent as grouped_django_communities %} + + +

Local Django Communities

+ + {% if grouped_django_communities %}

Table of contents

{% endif %} + + + + {% for local_django_community in grouped_django_communities %} +
+

{{ local_django_community.grouper.title }}

+
    + {% for django_community in local_django_community.list %} +
  • +

    {{ django_community.name }}

    +

    {{ django_community.city }}, {{ django_community.country }}   + {% if django_community.is_active %} + Active + {% else %} + Inactive + {% endif %} +

    +

    {{ django_community.description|safe }}

    +

    + {% if django_community.website_url %} + Community Website   + {% endif %} + {% if django_community.event_site_url %} + Event Website + {% endif %} +

    +
  • + {% endfor %} +
+
+ {% empty %} + Local Django communities are coming soon. Please check back later. + + {% endfor %} +{##} +{% endblock %} diff --git a/requirements/common.txt b/requirements/common.txt index f2e035189..f38514998 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,5 +1,6 @@ Babel==2.10.1 django-contact-form==1.9 +django-countries==7.5.1 django-hosts==5.1 django-money==2.1.1 django-push==1.1