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 all 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
31 changes: 29 additions & 2 deletions .env/local.sample
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
# This is a sample of environment variables which are used only to run Docker locally.
# These are never used in production.

# Django
# ------------------------------------------------------------------------------
# Run Django in production mode (DEBUG=False)
DJANGO_SETTINGS_MODULE=config.settings.prod

# Use a strong secret in production
SECRET_KEY="this-is-a-bad-secret"

# In production, we use postgres but for testing a deployment, using SQLite is fine
DATABASE_URL="sqlite:///db.sqlite3"

# PostgreSQL
# ------------------------------------------------------------------------------
# This must match .env/postgres
DATABASE_URL=pgsql://localuser:localpass@postgres:5432/sandiegopython


# Redis
# ------------------------------------------------------------------------------
REDIS_URL=redis://redis:6379/0


# S3/R2 Media Storage
# ------------------------------------------------------------------------------
# If not empty, S3/R2 will be used for media storage
AWS_S3_ACCESS_KEY_ID=
AWS_S3_SECRET_ACCESS_KEY=
AWS_STORAGE_BUCKET_NAME=
# If using a custom domain for media storage, set the MEDIA_URL
# and AWS_S3_CUSTOM_DOMAIN
AWS_S3_CUSTOM_DOMAIN=
MEDIA_URL=/media/
# The endpoint URL is necessary for Cloudflare R2
AWS_S3_ENDPOINT_URL=
7 changes: 7 additions & 0 deletions .env/postgres
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=sandiegopython
POSTGRES_USER=localuser
POSTGRES_PASSWORD=localpass
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
18 changes: 10 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,24 @@ RUN apt-get install -y --no-install-recommends \
make \
build-essential \
g++ \
postgresql-client \
git

RUN mkdir -p /code

WORKDIR /code

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

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

# Build JS/static assets
RUN npm install
RUN --mount=type=cache,target=/root/.npm npm install
RUN npm run build

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

EXPOSE 8000

CMD ["gunicorn", "--timeout", "15", "--bind", ":8000", "--workers", "2", "--max-requests", "10000", "--max-requests-jitter", "100", "config.wsgi"]
CMD ["gunicorn", "--timeout", "15", "--bind", ":8000", "--workers", "2", "--max-requests", "10000", "--max-requests-jitter", "100", "--log-file", "-", "config.wsgi"]
24 changes: 23 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
.PHONY: help test clean deploy
.PHONY: help test clean dockerbuild dockerserve dockershell deploy


DOCKER_CONFIG=compose.yaml


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


Expand All @@ -14,6 +21,21 @@ test:
clean:
rm -rf assets/dist/*

# Build the local multi-container application
# This command can take a while the first time
dockerbuild:
docker compose -f $(DOCKER_CONFIG) build

# You should run "dockerbuild" at least once before running this
# It isn't a dependency because running "dockerbuild" can take some time
dockerserve:
docker compose -f $(DOCKER_CONFIG) up

# Use this command to inspect the container, run management commands,
# or run anything else on the Django container
dockershell:
docker compose -f $(DOCKER_CONFIG) run --rm django /bin/bash

# Build and deploy the production container
deploy:
flyctl deploy
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,19 @@ you can build the container and run it locally:
cp .env/local.sample .env/local

# Build the docker image for sandiegopython.org
docker buildx build -t sandiegopython.org .

# Start a development server on http://localhost:8000
docker run --env-file=".env/local" --publish=8000:8000 sandiegopython.org

# You can start a shell to the container with the following:
docker run --env-file=".env/local" -it sandiegopython.org /bin/bash
# Use Docker compose to have Redis and PostgreSQL just like in production
# Note: Docker is used in production but Docker compose is just for development
make dockerbuild

# Start a development web server on http://localhost:8000
# Use ctrl+C to stop
make dockerserve

# While the server is running,
# you can start a bash shell to the container with the following:
# Once you have a bash shell, you can run migrations,
# manually connect to the local Postgres database or anything else
make dockershell
```


Expand Down
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
40 changes: 40 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Docker Compose Local Development Setup
#
# This starts a local multi-container development environment
# with Postgres, Redis, and Django.
# The configuration comes from .env/local and .env/postgres
#
# To run:
# $ make dockerbuild
# $ make dockerserve

volumes:
local_postgres_data: {}

services:
django:
build:
context: .
dockerfile: ./Dockerfile
image: sandiegopython_local_django
depends_on:
- postgres
env_file:
- ./.env/local
- ./.env/postgres
ports:
- "${SANDIEGOPYTHON_DJANGO_PORT:-8000}:8000"
# Allow us to run `docker attach` and get
# control on STDIN and be able to debug our code with interactive pdb
stdin_open: true
tty: true

postgres:
image: postgres:15.2
volumes:
- local_postgres_data:/var/lib/postgresql/data
env_file:
- ./.env/postgres

redis:
image: redis:5.0
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -115,7 +116,7 @@
os.path.join(BASE_DIR, "pythonsd", "static"),
]

MEDIA_URL = "/media/"
MEDIA_URL = os.environ.get("MEDIA_URL", default="/media/")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")


Expand Down
16 changes: 16 additions & 0 deletions config/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@
ADMIN_URL = os.environ.get("ADMIN_URL", "admin")


# Django-storages
# https://django-storages.readthedocs.io
# --------------------------------------------------------------------------
# Optionally store media files in S3/R2/etc.
AWS_S3_ACCESS_KEY_ID = os.environ.get("AWS_S3_ACCESS_KEY_ID")
AWS_S3_SECRET_ACCESS_KEY = os.environ.get("AWS_S3_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME")
# When using media storage with a custom domain
# set this and set MEDIA_URL
AWS_S3_CUSTOM_DOMAIN = os.environ.get("AWS_S3_CUSTOM_DOMAIN")
# The endpoint URL is necessary for Cloudflare R2
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", default=None)
if AWS_S3_ACCESS_KEY_ID and AWS_S3_SECRET_ACCESS_KEY and AWS_STORAGE_BUCKET_NAME:
DEFAULT_FILE_STORAGE = "storages.backends.s3.S3Storage"


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
# --------------------------------------------------------------------------
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, R2, 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 %}
Loading
Loading