Skip to content

Commit e1a3dd6

Browse files
authored
Merge pull request #156 from sandiegopython/davidfischer/Jer-Pha-organizers
Database driven organizers page
2 parents 1de8add + cb827a7 commit e1a3dd6

23 files changed

+355
-24
lines changed

.env/local.sample

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
# This is a sample of environment variables which are used only to run Docker locally.
22
# These are never used in production.
33

4+
# Django
5+
# ------------------------------------------------------------------------------
6+
# Run Django in production mode (DEBUG=False)
7+
DJANGO_SETTINGS_MODULE=config.settings.prod
8+
49
# Use a strong secret in production
510
SECRET_KEY="this-is-a-bad-secret"
611

7-
# In production, we use postgres but for testing a deployment, using SQLite is fine
8-
DATABASE_URL="sqlite:///db.sqlite3"
12+
13+
# PostgreSQL
14+
# ------------------------------------------------------------------------------
15+
# This must match .env/postgres
16+
DATABASE_URL=pgsql://localuser:localpass@postgres:5432/sandiegopython
17+
18+
19+
# Redis
20+
# ------------------------------------------------------------------------------
21+
REDIS_URL=redis://redis:6379/0
22+
23+
24+
# S3/R2 Media Storage
25+
# ------------------------------------------------------------------------------
26+
# If not empty, S3/R2 will be used for media storage
27+
AWS_S3_ACCESS_KEY_ID=
28+
AWS_S3_SECRET_ACCESS_KEY=
29+
AWS_STORAGE_BUCKET_NAME=
30+
# If using a custom domain for media storage, set the MEDIA_URL
31+
# and AWS_S3_CUSTOM_DOMAIN
32+
AWS_S3_CUSTOM_DOMAIN=
33+
MEDIA_URL=/media/
34+
# The endpoint URL is necessary for Cloudflare R2
35+
AWS_S3_ENDPOINT_URL=

.env/postgres

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# PostgreSQL
2+
# ------------------------------------------------------------------------------
3+
POSTGRES_HOST=postgres
4+
POSTGRES_PORT=5432
5+
POSTGRES_DB=sandiegopython
6+
POSTGRES_USER=localuser
7+
POSTGRES_PASSWORD=localpass

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ dmypy.json
131131
############################################
132132
/node_modules/
133133
/staticfiles/
134-
/pythonsd/media/
134+
/media/
135135
/pythonsd/static/css/
136136
/GIT_COMMIT
137137
/BUILD_DATE

Dockerfile

+10-8
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,24 @@ RUN apt-get install -y --no-install-recommends \
2020
make \
2121
build-essential \
2222
g++ \
23+
postgresql-client \
2324
git
2425

2526
RUN mkdir -p /code
2627

2728
WORKDIR /code
2829

29-
COPY . /code/
30+
# Requirements are installed here to ensure they will be cached.
31+
# https://docs.docker.com/build/cache/#use-the-dedicated-run-cache
32+
COPY ./requirements /requirements
33+
RUN pip install --upgrade pip
34+
RUN --mount=type=cache,target=/root/.cache/pip pip install -r /requirements/deployment.txt
35+
RUN --mount=type=cache,target=/root/.cache/pip pip install -r /requirements/local.txt
3036

31-
# Cache dependencies when building which should result in faster docker builds
32-
RUN --mount=type=cache,target=/root/.cache/pip set -ex && \
33-
pip install --upgrade --no-cache-dir pip && \
34-
pip install -r /code/requirements.txt && \
35-
pip install -r /code/requirements/local.txt
37+
COPY . /code/
3638

3739
# Build JS/static assets
38-
RUN npm install
40+
RUN --mount=type=cache,target=/root/.npm npm install
3941
RUN npm run build
4042

4143
RUN python manage.py collectstatic --noinput
@@ -52,4 +54,4 @@ RUN date -u +'%Y-%m-%dT%H:%M:%SZ' > BUILD_DATE
5254

5355
EXPOSE 8000
5456

55-
CMD ["gunicorn", "--timeout", "15", "--bind", ":8000", "--workers", "2", "--max-requests", "10000", "--max-requests-jitter", "100", "config.wsgi"]
57+
CMD ["gunicorn", "--timeout", "15", "--bind", ":8000", "--workers", "2", "--max-requests", "10000", "--max-requests-jitter", "100", "--log-file", "-", "config.wsgi"]

Makefile

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
.PHONY: help test clean deploy
1+
.PHONY: help test clean dockerbuild dockerserve dockershell deploy
2+
3+
4+
DOCKER_CONFIG=compose.yaml
25

36

47
help:
58
@echo "Please use \`make <target>' where <target> is one of"
69
@echo " test Run the full test suite"
710
@echo " clean Delete assets processed by webpack"
11+
@echo " dockerbuild Build the Docker compose dev environment"
12+
@echo " dockerserve Run the Docker containers for the site"
13+
@echo " (starts a webserver on http://localhost:8000)"
14+
@echo " dockershell Connect to a bash shell on the Django Docker container"
815
@echo " deploy Deploy the app to fly.io"
916

1017

@@ -14,6 +21,21 @@ test:
1421
clean:
1522
rm -rf assets/dist/*
1623

24+
# Build the local multi-container application
25+
# This command can take a while the first time
26+
dockerbuild:
27+
docker compose -f $(DOCKER_CONFIG) build
28+
29+
# You should run "dockerbuild" at least once before running this
30+
# It isn't a dependency because running "dockerbuild" can take some time
31+
dockerserve:
32+
docker compose -f $(DOCKER_CONFIG) up
33+
34+
# Use this command to inspect the container, run management commands,
35+
# or run anything else on the Django container
36+
dockershell:
37+
docker compose -f $(DOCKER_CONFIG) run --rm django /bin/bash
38+
1739
# Build and deploy the production container
1840
deploy:
1941
flyctl deploy

README.md

+13-7
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,19 @@ you can build the container and run it locally:
4242
cp .env/local.sample .env/local
4343

4444
# Build the docker image for sandiegopython.org
45-
docker buildx build -t sandiegopython.org .
46-
47-
# Start a development server on http://localhost:8000
48-
docker run --env-file=".env/local" --publish=8000:8000 sandiegopython.org
49-
50-
# You can start a shell to the container with the following:
51-
docker run --env-file=".env/local" -it sandiegopython.org /bin/bash
45+
# Use Docker compose to have Redis and PostgreSQL just like in production
46+
# Note: Docker is used in production but Docker compose is just for development
47+
make dockerbuild
48+
49+
# Start a development web server on http://localhost:8000
50+
# Use ctrl+C to stop
51+
make dockerserve
52+
53+
# While the server is running,
54+
# you can start a bash shell to the container with the following:
55+
# Once you have a bash shell, you can run migrations,
56+
# manually connect to the local Postgres database or anything else
57+
make dockershell
5258
```
5359

5460

assets/src/sass/_theme.scss

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
vertical-align: -.125rem;
1717
}
1818

19+
.icon-1-5x {
20+
width: 1.5rem;
21+
height: 1.5rem;
22+
vertical-align: -.125rem;
23+
}
24+
1925
.icon-2x {
2026
width: 2rem;
2127
height: 2rem;

compose.yaml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Docker Compose Local Development Setup
2+
#
3+
# This starts a local multi-container development environment
4+
# with Postgres, Redis, and Django.
5+
# The configuration comes from .env/local and .env/postgres
6+
#
7+
# To run:
8+
# $ make dockerbuild
9+
# $ make dockerserve
10+
11+
volumes:
12+
local_postgres_data: {}
13+
14+
services:
15+
django:
16+
build:
17+
context: .
18+
dockerfile: ./Dockerfile
19+
image: sandiegopython_local_django
20+
depends_on:
21+
- postgres
22+
env_file:
23+
- ./.env/local
24+
- ./.env/postgres
25+
ports:
26+
- "${SANDIEGOPYTHON_DJANGO_PORT:-8000}:8000"
27+
# Allow us to run `docker attach` and get
28+
# control on STDIN and be able to debug our code with interactive pdb
29+
stdin_open: true
30+
tty: true
31+
32+
postgres:
33+
image: postgres:15.2
34+
volumes:
35+
- local_postgres_data:/var/lib/postgresql/data
36+
env_file:
37+
- ./.env/postgres
38+
39+
redis:
40+
image: redis:5.0

config/settings/base.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
8686
# --------------------------------------------------------------------------
8787
DATABASES = {"default": dj_database_url.config(default="sqlite:///db.sqlite3")}
88+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
8889

8990

9091
# Internationalization
@@ -115,7 +116,7 @@
115116
os.path.join(BASE_DIR, "pythonsd", "static"),
116117
]
117118

118-
MEDIA_URL = "/media/"
119+
MEDIA_URL = os.environ.get("MEDIA_URL", default="/media/")
119120
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
120121

121122

config/settings/prod.py

+16
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@
3434
ADMIN_URL = os.environ.get("ADMIN_URL", "admin")
3535

3636

37+
# Django-storages
38+
# https://django-storages.readthedocs.io
39+
# --------------------------------------------------------------------------
40+
# Optionally store media files in S3/R2/etc.
41+
AWS_S3_ACCESS_KEY_ID = os.environ.get("AWS_S3_ACCESS_KEY_ID")
42+
AWS_S3_SECRET_ACCESS_KEY = os.environ.get("AWS_S3_SECRET_ACCESS_KEY")
43+
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME")
44+
# When using media storage with a custom domain
45+
# set this and set MEDIA_URL
46+
AWS_S3_CUSTOM_DOMAIN = os.environ.get("AWS_S3_CUSTOM_DOMAIN")
47+
# The endpoint URL is necessary for Cloudflare R2
48+
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", default=None)
49+
if AWS_S3_ACCESS_KEY_ID and AWS_S3_SECRET_ACCESS_KEY and AWS_STORAGE_BUCKET_NAME:
50+
DEFAULT_FILE_STORAGE = "storages.backends.s3.S3Storage"
51+
52+
3753
# Database
3854
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
3955
# --------------------------------------------------------------------------

config/urls.py

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.contrib import admin
33
from django.urls import include
44
from django.urls import path
5+
from django.conf.urls.static import static
56

67

78
urlpatterns = [
@@ -14,3 +15,6 @@
1415
import debug_toolbar
1516

1617
urlpatterns = [path("__debug__", include(debug_toolbar.urls))] + urlpatterns
18+
19+
# We can't use `settings.MEDIA_URL` as the pattern since MEDIA_URL may be fully qualified
20+
urlpatterns += static("/media/", document_root=settings.MEDIA_ROOT)

pythonsd/admin.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.contrib import admin
2+
3+
from .models import Organizer
4+
5+
admin.site.register(Organizer)

pythonsd/migrations/0001_initial.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 3.2.25 on 2024-05-31 06:05
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = [
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='Organizer',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('name', models.CharField(max_length=255)),
19+
('meetup_url', models.URLField(blank=True, max_length=255)),
20+
('linkedin_url', models.URLField(blank=True, max_length=255)),
21+
('active', models.BooleanField(default=True, help_text='Set to False to hide this organizer from the organizers page')),
22+
('photo', models.ImageField(help_text='Recommended size of 400*400px or larger square', upload_to='organizers/')),
23+
],
24+
),
25+
]

pythonsd/migrations/__init__.py

Whitespace-only changes.

pythonsd/models.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import models
2+
3+
4+
class Organizer(models.Model):
5+
"""Meetup organizers - displayed on the organizers page."""
6+
7+
name = models.CharField(max_length=255)
8+
meetup_url = models.URLField(max_length=255, blank=True)
9+
linkedin_url = models.URLField(max_length=255, blank=True)
10+
active = models.BooleanField(
11+
default=True,
12+
help_text="Set to False to hide this organizer from the organizers page",
13+
)
14+
15+
# For production, store the image in Cloud Storage (S3, R2, Appwrite, etc.)
16+
photo = models.ImageField(
17+
upload_to="organizers/",
18+
help_text="Recommended size of 400*400px or larger square",
19+
)
20+
21+
def __str__(self):
22+
return self.name

pythonsd/templates/pythonsd/base.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
<!-- <li class="nav-item">
4040
<a class="nav-link" href="#">Support Us</a>
4141
</li> -->
42-
<!-- <li class="nav-item">
43-
<a class="nav-link" href="#">Organizers</a>
44-
</li> -->
42+
<li class="nav-item">
43+
<a class="nav-link" href="{% url 'organizers' %}">Organizers</a>
44+
</li>
4545
</ul>
4646
</div>
4747
</nav>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{% extends 'pythonsd/base.html' %}
2+
3+
4+
{% block title %}Organizers{% endblock title %}
5+
6+
7+
{% block main %}
8+
<div class="container mt-3">
9+
<h1>Organizers</h1>
10+
11+
<p>If you would like to reach out, please contact the <a href="mailto:[email protected]">Python SD Organizers</a>.</p>
12+
13+
14+
{% if organizers %}
15+
<div class="row row-cols-2 row-cols-md-4">
16+
{% for organizer in organizers %}
17+
<div class="col mb-4">
18+
<div class="card">
19+
<img src="{{ organizer.photo.url }}" class="card-img-top" alt="{{ organizer.name }}">
20+
<div class="card-body">
21+
<h5 class="card-title">{{ organizer.name }}</h5>
22+
<ul class="list-inline">
23+
{% if organizer.meetup_url %}
24+
<li class="list-inline-item">
25+
<a href="{{ organizer.meetup_url }}" rel="nofollow noopener noreferrer" target="_blank">
26+
<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>
27+
</a>
28+
</li>
29+
{% endif %}
30+
{% if organizer.linkedin_url %}
31+
<li class="list-inline-item">
32+
<a href="{{ organizer.linkedin_url }}" rel="nofollow noopener noreferrer" target="_blank">
33+
<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>
34+
</a>
35+
</li>
36+
{% endif %}
37+
</ul>
38+
</div>
39+
</div>
40+
</div>
41+
{% endfor %}
42+
</div>
43+
{% endif %}
44+
45+
46+
</div>
47+
{% endblock main %}

0 commit comments

Comments
 (0)