Skip to content

Database driven organizers page #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ dmypy.json
############################################
/node_modules/
/staticfiles/
/pythonsd/media/
/media/
/pythonsd/static/css/
/GIT_COMMIT
/BUILD_DATE
6 changes: 6 additions & 0 deletions assets/src/sass/_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
vertical-align: -.125rem;
}

.icon-1-5x {
width: 1.5rem;
height: 1.5rem;
vertical-align: -.125rem;
}

.icon-2x {
width: 2rem;
height: 2rem;
Expand Down
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
# --------------------------------------------------------------------------
DATABASES = {"default": dj_database_url.config(default="sqlite:///db.sqlite3")}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"


# Internationalization
Expand Down
4 changes: 4 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.contrib import admin
from django.urls import include
from django.urls import path
from django.conf.urls.static import static


urlpatterns = [
Expand All @@ -14,3 +15,6 @@
import debug_toolbar

urlpatterns = [path("__debug__", include(debug_toolbar.urls))] + urlpatterns

# We can't use `settings.MEDIA_URL` as the pattern since MEDIA_URL may be fully qualified
urlpatterns += static("/media/", document_root=settings.MEDIA_ROOT)
5 changes: 5 additions & 0 deletions pythonsd/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin

from .models import Organizer

admin.site.register(Organizer)
25 changes: 25 additions & 0 deletions pythonsd/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.2.25 on 2024-05-31 06:05

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Organizer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('meetup_url', models.URLField(blank=True, max_length=255)),
('linkedin_url', models.URLField(blank=True, max_length=255)),
('active', models.BooleanField(default=True, help_text='Set to False to hide this organizer from the organizers page')),
('photo', models.ImageField(help_text='Recommended size of 400*400px or larger square', upload_to='organizers/')),
],
),
]
Empty file added pythonsd/migrations/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions pythonsd/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import models


class Organizer(models.Model):
"""Meetup organizers - displayed on the organizers page."""

name = models.CharField(max_length=255)
meetup_url = models.URLField(max_length=255, blank=True)
linkedin_url = models.URLField(max_length=255, blank=True)
active = models.BooleanField(
default=True,
help_text="Set to False to hide this organizer from the organizers page",
)

# For production, store the image in Cloud Storage (S3, Appwrite, etc.)
photo = models.ImageField(
upload_to="organizers/",
help_text="Recommended size of 400*400px or larger square",
)

def __str__(self):
return self.name
6 changes: 3 additions & 3 deletions pythonsd/templates/pythonsd/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
<!-- <li class="nav-item">
<a class="nav-link" href="#">Support Us</a>
</li> -->
<!-- <li class="nav-item">
<a class="nav-link" href="#">Organizers</a>
</li> -->
<li class="nav-item">
<a class="nav-link" href="{% url 'organizers' %}">Organizers</a>
</li>
</ul>
</div>
</nav>
Expand Down
47 changes: 47 additions & 0 deletions pythonsd/templates/pythonsd/organizers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% extends 'pythonsd/base.html' %}


{% block title %}Organizers{% endblock title %}


{% block main %}
<div class="container mt-3">
<h1>Organizers</h1>

<p>If you would like to reach out, please contact the <a href="mailto:sandiegopython-organizers@googlegroups.com">Python SD Organizers</a>.</p>


{% if organizers %}
<div class="row row-cols-2 row-cols-md-4">
{% for organizer in organizers %}
<div class="col mb-4">
<div class="card">
<img src="{{ organizer.photo.url }}" class="card-img-top" alt="{{ organizer.name }}">
<div class="card-body">
<h5 class="card-title">{{ organizer.name }}</h5>
<ul class="list-inline">
{% if organizer.meetup_url %}
<li class="list-inline-item">
<a href="{{ organizer.meetup_url }}" rel="nofollow noopener noreferrer" target="_blank">
<svg class="text-muted icon-1-5x" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M99 414.3c1.1 5.7-2.3 11.1-8 12.3-5.4 1.1-10.9-2.3-12-8-1.1-5.4 2.3-11.1 7.7-12.3 5.4-1.2 11.1 2.3 12.3 8zm143.1 71.4c-6.3 4.6-8 13.4-3.7 20 4.6 6.6 13.4 8.3 20 3.7 6.3-4.6 8-13.4 3.4-20-4.2-6.5-13.1-8.3-19.7-3.7zm-86-462.3c6.3-1.4 10.3-7.7 8.9-14-1.1-6.6-7.4-10.6-13.7-9.1-6.3 1.4-10.3 7.7-9.1 14 1.4 6.6 7.6 10.6 13.9 9.1zM34.4 226.3c-10-6.9-23.7-4.3-30.6 6-6.9 10-4.3 24 5.7 30.9 10 7.1 23.7 4.6 30.6-5.7 6.9-10.4 4.3-24.1-5.7-31.2zm272-170.9c10.6-6.3 13.7-20 7.7-30.3-6.3-10.6-19.7-14-30-7.7s-13.7 20-7.4 30.6c6 10.3 19.4 13.7 29.7 7.4zm-191.1 58c7.7-5.4 9.4-16 4.3-23.7s-15.7-9.4-23.1-4.3c-7.7 5.4-9.4 16-4.3 23.7 5.1 7.8 15.6 9.5 23.1 4.3zm372.3 156c-7.4 1.7-12.3 9.1-10.6 16.9 1.4 7.4 8.9 12.3 16.3 10.6 7.4-1.4 12.3-8.9 10.6-16.6-1.5-7.4-8.9-12.3-16.3-10.9zm39.7-56.8c-1.1-5.7-6.6-9.1-12-8-5.7 1.1-9.1 6.9-8 12.6 1.1 5.4 6.6 9.1 12.3 8 5.4-1.5 9.1-6.9 7.7-12.6zM447 138.9c-8.6 6-10.6 17.7-4.9 26.3 5.7 8.6 17.4 10.6 26 4.9 8.3-6 10.3-17.7 4.6-26.3-5.7-8.7-17.4-10.9-25.7-4.9zm-6.3 139.4c26.3 43.1 15.1 100-26.3 129.1-17.4 12.3-37.1 17.7-56.9 17.1-12 47.1-69.4 64.6-105.1 32.6-1.1.9-2.6 1.7-3.7 2.9-39.1 27.1-92.3 17.4-119.4-22.3-9.7-14.3-14.6-30.6-15.1-46.9-65.4-10.9-90-94-41.1-139.7-28.3-46.9.6-107.4 53.4-114.9C151.6 70 234.1 38.6 290.1 82c67.4-22.3 136.3 29.4 130.9 101.1 41.1 12.6 52.8 66.9 19.7 95.2zm-70 74.3c-3.1-20.6-40.9-4.6-43.1-27.1-3.1-32 43.7-101.1 40-128-3.4-24-19.4-29.1-33.4-29.4-13.4-.3-16.9 2-21.4 4.6-2.9 1.7-6.6 4.9-11.7-.3-6.3-6-11.1-11.7-19.4-12.9-12.3-2-17.7 2-26.6 9.7-3.4 2.9-12 12.9-20 9.1-3.4-1.7-15.4-7.7-24-11.4-16.3-7.1-40 4.6-48.6 20-12.9 22.9-38 113.1-41.7 125.1-8.6 26.6 10.9 48.6 36.9 47.1 11.1-.6 18.3-4.6 25.4-17.4 4-7.4 41.7-107.7 44.6-112.6 2-3.4 8.9-8 14.6-5.1 5.7 3.1 6.9 9.4 6 15.1-1.1 9.7-28 70.9-28.9 77.7-3.4 22.9 26.9 26.6 38.6 4 3.7-7.1 45.7-92.6 49.4-98.3 4.3-6.3 7.4-8.3 11.7-8 3.1 0 8.3.9 7.1 10.9-1.4 9.4-35.1 72.3-38.9 87.7-4.6 20.6 6.6 41.4 24.9 50.6 11.4 5.7 62.5 15.7 58.5-11.1zm5.7 92.3c-10.3 7.4-12.9 22-5.7 32.6 7.1 10.6 21.4 13.1 32 6 10.6-7.4 13.1-22 6-32.6-7.4-10.6-21.7-13.5-32.3-6z"/></svg>
</a>
</li>
{% endif %}
{% if organizer.linkedin_url %}
<li class="list-inline-item">
<a href="{{ organizer.linkedin_url }}" rel="nofollow noopener noreferrer" target="_blank">
<svg class="text-muted icon-1-5x" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="currentColor" d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></svg>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}


</div>
{% endblock main %}
41 changes: 40 additions & 1 deletion pythonsd/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@

from django import test
from django.core.cache import cache
from django.conf import settings
from django.urls import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
import responses
import webtest

from config import wsgi
from ..views import RecentVideosView
from ..views import UpcomingEventsView
from ..models import Organizer


# Bytes representing a valid 1-pixel PNG
ONE_PIXEL_PNG_BYTES = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00"
b"\x01\x08\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx"
b"\x9cc\xfa\xcf\x00\x00\x02\x07\x01\x02\x9a\x1c1q\x00\x00\x00"
b"\x00IEND\xaeB`\x82"
)


class TestBasicViews(test.TestCase):
Expand Down Expand Up @@ -46,6 +56,35 @@ def test_homepage(self):
)


class TestOrganizersView(test.TestCase):
def test_organizers(self):
org1 = Organizer(
name="First organizer",
meetup_url="http://example.com/meetup",
linkedin_url="http://example.com/linkedin",
active=True,
photo=SimpleUploadedFile(
name="test.png", content=ONE_PIXEL_PNG_BYTES, content_type="image/png"
),
)
org1.save()

org2 = Organizer(
name="Second organizer",
meetup_url="http://example.com/meetup",
active=False,
photo=SimpleUploadedFile(
name="test.png", content=ONE_PIXEL_PNG_BYTES, content_type="image/png"
),
)
org2.save()

response = self.client.get(reverse("organizers"))
self.assertContains(response, "<h1>Organizers</h1>")
self.assertContains(response, org1.name)
self.assertNotContains(response, org2.name)


class TestMeetupEventsView(test.TestCase):
def setUp(self):
self.url = reverse("upcoming_events")
Expand Down
38 changes: 38 additions & 0 deletions pythonsd/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django import test
from django.core.files.uploadedfile import SimpleUploadedFile

from ..models import Organizer


# Bytes representing a valid 1-pixel PNG
ONE_PIXEL_PNG_BYTES = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00"
b"\x01\x08\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx"
b"\x9cc\xfa\xcf\x00\x00\x02\x07\x01\x02\x9a\x1c1q\x00\x00\x00"
b"\x00IEND\xaeB`\x82"
)


class TestOrganizer(test.TestCase):
def setUp(self):
self.org1 = Organizer(
name="First organizer",
meetup_url="http://example.com/meetup",
linkedin_url="http://example.com/linkedin",
photo=SimpleUploadedFile(
name="test.png", content=ONE_PIXEL_PNG_BYTES, content_type="image/png"
),
)
self.org1.save()

self.org2 = Organizer(
name="Second organizer",
meetup_url="http://example.com/meetup",
photo=SimpleUploadedFile(
name="test.png", content=ONE_PIXEL_PNG_BYTES, content_type="image/png"
),
)
self.org2.save()

def test_str(self):
self.assertEqual(str(self.org1), self.org1.name)
5 changes: 5 additions & 0 deletions pythonsd/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
generic.TemplateView.as_view(template_name="pythonsd/code-of-conduct.html"),
name="code-of-conduct",
),
path(
r"organizers/",
views.OrganizersView.as_view(template_name="pythonsd/organizers.html"),
name="organizers",
),
# XHR/Async requests
path(r"xhr/events/", views.UpcomingEventsView.as_view(), name="upcoming_events"),
path(r"xhr/videos/", views.RecentVideosView.as_view(), name="recent_videos"),
Expand Down
13 changes: 13 additions & 0 deletions pythonsd/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import requests
from defusedxml import ElementTree

from .models import Organizer


CACHE_DURATION = 60 * 15 # 15 minutes
log = logging.getLogger(__file__)
Expand All @@ -21,6 +23,17 @@ class HomePageView(TemplateView):
template_name = "pythonsd/index.html"


class OrganizersView(TemplateView):
"""Displays SD Python organizers."""

template_name = "pythonsd/organizers.html"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["organizers"] = Organizer.objects.filter(active=True).order_by("name")
return context


@method_decorator(cache_page(CACHE_DURATION), name="dispatch")
class UpcomingEventsView(TemplateView):
"""Get upcoming events from Meetup."""
Expand Down
3 changes: 3 additions & 0 deletions requirements/common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ django-enforce-host==1.1.0

# For parsing YouTube's XML feed
defusedxml==0.7.1

# Used for image field handling
pillow==10.3.0
Loading