Local Dev setup
You can either folllow the setup with Docker or without Docker, the instructions are listed below.
With Docker
To run locally Docker these are the steps.
Requirements
Have these installed first before continuing further.
- Docker
- Docker compose
- GNU Make
- GitHub CLI (optional, but recommended) https://cli.github.com/
Starting the local env
-
Fork the repo. See the GitHub docs for instructions on how to Fork a repo.
-
Clone your fork:
gh repo clone <personal-account>/flashlights
git clone <personal-account>/flashlights.git
- Start the local environment:
make serve
-
Open the browser and go to http://localhost:8000/ to see the app running.
-
Set the site domain (see Configure the site domain):
make manage set_site_domain localhost:8000
- Run the tests:
make test
Without Docker
To run locally without Docker these are the steps.
Requirements
Have these installed first before continuing further.
- Use Python 3.14+
- You can install different versions of Python using pyenv.
- You'll also need PostgreSQL (you can find the instructions here).
Starting the local env
-
Fork the repo. See the GitHub docs for instructions on how to Fork a repo.
-
Clone your fork:
gh repo clone <personal-account>/flashlights
git clone <personal-account>/flashlights.git
Install dependencies
Create a python environment and activate it:
python3 -m venv .env
source .env/bin/activate
python3 -m venv .env
.env\Scripts\activate
Install dependencies for development:
pip install -r requirements-dev.txt
If you want to run the docs you'll also need some docs dependencies:
pip install -r requirements-docs.txt
Postgres Setup
To run the application you'll need to have PostgreSQL running on your machine. Follow one of the guides below to install it:
Installation in MacOS
1. Install PostgreSQLbrew install postgresql
brew services start postgresql
Installation in Windows
Download the installer from here and follow the screen promptsNow export the environment variable below:
export SQL_USER=<your-user>
set SQL_USER=<your-user>
For example, Jess' username is "jesstemporal", so her command looks like this:
export SQL_USER=jesstemporal
set SQL_USER=jesstemporal
Applying Migrations
Now is time to create the database to store all the information:
python manage.py migrate
Configure the site domain
Links inside emails (account verification, speaker invitations, checklist
reminders) are built from the domain stored in Django's
sites framework,
not from the request. A fresh database has the placeholder example.com, so
every emailed link points at the wrong host until you change it. Set it once
after the first migrate:
make manage set_site_domain localhost:8000
python manage.py set_site_domain localhost:8000
The same value is editable in the admin under Sites. With DEBUG on,
speaker-portal links use http://; everywhere else they use https://.
Run the server
Add the other enviroment variables:
export SECRET_KEY=deadbeefcafe
export DJANGO_ALLOWED_HOSTS="localhost,127.0.0.1"
set SECRET_KEY=deadbeefcafe
set DJANGO_ALLOWED_HOSTS="localhost,127.0.0.1"
Then run the server:
python manage.py runserver
The session and CSRF cookies are named flashlights_sessionid and
flashlights_csrftoken, so another Django app on the same host does not sign
you out; set SESSION_COOKIE_NAME or CSRF_COOKIE_NAME to change them.
Speaker media uploads (optional)
Performance videos go straight from the browser to a private bucket in presigned multipart chunks (design ยง8.8). With no bucket configured the upload endpoints answer 503 and everything else works, so most local work needs none of this.
Once a video lands, a worker task measures it with ffprobe (the image
installs ffmpeg; rebuild it with make after pulling this change) on
the media Celery queue. The compose worker consumes both queues, so no
extra container is needed locally; production runs a separate
worker-media process (deployment guide). Without ffprobe the file row
says so and the video is still there. The tests never call the binary
unless they ask to: tests/speakers/conftest.py stubs it, and the one
test on a real fixture video (tests/speakers/fixtures/two-seconds.mp4)
is skipped when ffprobe is not installed.
Against a DigitalOcean Space
Production uses DigitalOcean Spaces, and a local server can upload to a Space of its own. Make one for development, separate from the public one the image fields use, and keep it private: the portal never sets an ACL on what it uploads, and reads it back only through presigned links.
- In the DigitalOcean console create a Space (for example
flashlights-media-dev) in a region such asnyc3, and turn File Listing off. Do not enable the CDN on it. - On the Space, add a CORS rule for the browser's direct uploads:
origin
http://localhost:8000, methodsPUTandGET, allowed headers*, exposed headerETag. - Create a Spaces access key (API, Spaces Keys) scoped to that Space.
- Give the
webandceleryservices the settings through acompose.override.ymlnext tocompose.yml(ignored by git, so the keys stay on your machine):
services:
web:
environment:
SPEAKER_MEDIA_BUCKET: flashlights-media-dev
AWS_S3_ENDPOINT_URL: https://nyc3.digitaloceanspaces.com
AWS_S3_REGION_NAME: nyc3
AWS_ACCESS_KEY_ID: <your Spaces key>
AWS_SECRET_ACCESS_KEY: <your Spaces secret>
celery:
environment:
SPEAKER_MEDIA_BUCKET: flashlights-media-dev
AWS_S3_ENDPOINT_URL: https://nyc3.digitaloceanspaces.com
AWS_S3_REGION_NAME: nyc3
AWS_ACCESS_KEY_ID: <your Spaces key>
AWS_SECRET_ACCESS_KEY: <your Spaces secret>
Then docker compose up -d --force-recreate web celery. Leave
USE_SPACES alone: it switches the image storage, which is a separate
matter.
Until the upload panel (task 5.2) exists there is no browser path, so check the wiring from a shell, which plays the browser's part with boto3:
docker compose exec web python manage.py shell -c "
from django.contrib.auth.models import User
from speakers.media import MediaBucket, start_upload, complete_upload
from speakers.models import Session
session = Session.objects.filter(conference__is_active=True).first()
user = User.objects.get(username='admin_user')
upload = start_upload(session=session, kind='RAW_VIDEO', language='', filename='check.bin', size_bytes=3, content_type='application/octet-stream', user=user)
bucket = MediaBucket.from_settings()
part = bucket.client.upload_part(Bucket=bucket.bucket, Key=upload.storage_key, UploadId=upload.upload_id, PartNumber=1, Body=b'ok!')
asset = complete_upload(upload, [{'number': 1, 'etag': part['ETag']}])
print(asset, asset.size_bytes, asset.download_url())
"
The printed link is presigned and opens the object for an hour; the same address without the signature is refused, which is the private Space doing its job.
Against a MinIO container (no account needed)
The presigned URLs sign the host they are for, so the portal and the
browser have to reach the bucket at the same address. With the portal in
Docker that means running MinIO inside the web container's network
namespace, so both see it as localhost:9000. This compose.override.yml
does it:
services:
web:
ports:
- "9000:9000"
environment:
SPEAKER_MEDIA_BUCKET: speaker-media
AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_REGION_NAME: us-east-1
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
celery:
# In the web container's network too, so the links the worker signs
# (the zip email) say localhost:9000 like the browser's.
network_mode: "service:web"
environment:
SPEAKER_MEDIA_BUCKET: speaker-media
AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_REGION_NAME: us-east-1
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
minio:
image: minio/minio
command: server /data
network_mode: "service:web"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- miniodata:/data
volumes:
miniodata:
Then docker compose up -d --force-recreate web celery minio and create
the bucket once:
docker compose exec web python -c "
import boto3, os
boto3.client('s3', endpoint_url=os.environ['AWS_S3_ENDPOINT_URL'],
aws_access_key_id='minioadmin', aws_secret_access_key='minioadmin',
region_name='us-east-1').create_bucket(Bucket='speaker-media')"
MinIO answers browser uploads from any origin and exposes the ETag
header, so no CORS rule is needed. Speakers see none of this until Media
for speakers is switched on in the admin under Speaker settings;
organizers see it all regardless.
Transcription needs the Whisper library and a model in the image: build
with WHISPER_MODEL=small docker compose build web celery
(about 500 MB more, once; --build-arg WHISPER_MODEL=small overrides the
empty default in compose.yml the same way), put
SPEAKER_TRANSCRIBE_ENGINE: local in the override's environment for web and celery, restart both, and switch
Auto transcribe on under Speaker settings. A short clip uploaded as a
raw video then gets a "machine draft" transcript on the session's Files
tab within a minute or two; "Transcribe this" on any video row starts one
by hand. Without the build argument the button says the portal has no
engine. Bulk download works against it too:
"Download files" on the sessions list, make an export, then run its
download.sh in an empty folder or ask for the zip (the worker builds it
and maildev at http://localhost:1080 gets the email). After pulling code
that adds a Celery task, restart the worker (docker compose restart
celery): a running worker only knows the tasks it started with and drops
a message for one it does not know. Log in as a performer (the sample data's
volunteer2 is on the performance) and upload from the session page;
leaving the page mid-upload and choosing the same file again should
continue from the parts that already landed.
SPEAKER_MEDIA_PART_SIZE, SPEAKER_MEDIA_MAX_BYTES, SPEAKER_MEDIA_URL_TTL
(how long a presigned link lives, 3600 seconds by default) and
SPEAKER_MEDIA_UPLOAD_TTL_HOURS have sensible defaults in portal/settings.py.
One command for everything: seed_dev_data
The quickest way to a usable local instance. It runs both generators
below, gives the accounts you will sign in as the password password123,
adds a few affiliations, adds a past in-person edition (2024: a venue, the
team recording the talks, the chat on Zulip) next to the online active one so
every form can be seen both ways, and prints the logins:
make seed
python manage.py seed_dev_data
| Account | Role |
|---|---|
admin_user |
Organizer (superuser) |
organizer_lena |
Organizer (staff) |
volunteer1 |
Volunteer, approved |
volunteer3 |
Volunteer, pending review |
ada |
Speaker, onboarded |
sponsor_contact1 |
Sponsor contact |
Like the generators, it refuses to run unless DEBUG=True. Running it
again is safe: existing rows are kept.
Generate Sample Data (Optional)
For local development and testing, you can generate sample data to populate your database with realistic test content. This command creates:
- 9 Users: 1 admin, 1 staff member, 5 volunteers, and 2 sponsor contacts
- 7 Volunteer Roles: Frontend Dev, Backend Dev, Content Writer, Social Media Manager, Designer, Reviewer, Event Coordinator
- 6 Teams: Website, Social Media, Content, Design, Program Committee, Sponsorship
- 5 Volunteer Profiles: With various application statuses (approved, pending, waitlisted, rejected)
- 5 Sponsorship Tiers: Ranging from $2,500 to $25,000
- 5 Sponsorship Profiles: With different progress statuses
- 5 Individual Donations: Random amounts between $5 and $300
Important: This command only works when DEBUG=True in your settings to prevent accidental use in production environments.
make manage generate_sample_data
python manage.py generate_sample_data
All generated users have the password: password123.
The command is idempotent, meaning you can run it multiple times without creating duplicate data.
Demo presets: switch what the instance looks like
To see the portal as different communities would, keep one database per preset and switch between them without reseeding:
make preset-list # presets, and which are built
make preset PRESET=bare # build it if needed, then serve it on 8002
make preset-reset PRESET=bare # rebuild it from scratch
make preset-stop
make preset runs scripts/preset.py on the host, not in the container,
against the compose Postgres. Each preset gets its own database
(flashlights_<name>), and switching restarts the dev server on
127.0.0.1:8002 with that preset's database and branding.
A preset is a directory holding:
seed.py, definingrun(stdout), which creates the editions, people and data (loaded bymanage.py load_preset <directory>, DEBUG only; its tasks run inline and its emails stay in memory);env, optionalKEY=valuelines for the branding (SITE_TITLE,BRAND_PRESET,DOCS_URL, the feature switches).
presets/bare ships here: one empty edition and an admin account.
presets/empty has only the admin superuser, not even an edition, and no
accepted agreements, for walking through setting up an instance from its
first screen. A
preset built from a real community's data (its organizers, speakers and
program) belongs outside this repository; add its folder with
PRESETS_PATH. Local settings go in two git-ignored files at the root:
# .preset-settings
PRESETS_PATH=~/my-presets
PRESET_DB=postgresql://flashlights:flashlights@localhost:5433
# Anything else is passed to the server.
DJANGO_EMAIL_HOST=localhost
DJANGO_EMAIL_PORT=1025
# local.mk
PYTHON = ~/path/to/venv/bin/python
Speaker portal sample data
A second command fills the active edition with speaker-portal data so every screen of the speaker module has something to show. It creates:
- Edition setup: switches the speaker module on, sets the conference dates when they are empty, seeds the default checklist templates, publishes the speaker, workshop and keynote guides and leaves a performer guide as a draft
- 4 Portal users: 1 staff organizer (
organizer_lena, the liaison for two presenters), 2 approved volunteers on teams (vol_mayaon Design Team,vol_kimon Media Team), 1 pending volunteer (vol_pending) - 7 Sessions: a workshop and a keynote (scheduled on the first conference day), a panel and a pre-recorded performance (confirmed), a talk (draft), plus the opening (scheduled) and a coffee break
- 6 Presenters, one per invitation state:
ada@example.com: accepted and onboarded workshop presenter, with checklist items that are done, skipped, overdue and due soongrace@example.com: onboarded keynote presenter, second presenter on the workshop and panel moderator, so she gets both the keynote and the workshop guidesdex@example.com: invited panelist who has not accepted yetmaria@example.com: onboarded performer whose uploaded video is over the length limit, which blocks the video-length itemsam@example.com: talk presenter who has not been invited yetnina@example.com: accepted host of the opening who has not been through the welcome page yet, so signing in as her shows that flow
- Action items with deadlines: one owned by Design Team, one assigned to a volunteer and overdue, one completed by a volunteer so the presenter can see who did it, and ad hoc items added by the organizer
- Proposals, with the edition taking them, one in each state:
vol_rosa: an approved volunteer who has also proposed a talk, still waiting for an answer, which is what "the same account does both" looks likeprop_tess: nothing but a portal account, her workshop approved, so she is on the program with a checklistprop_iris: one turned down (organizers can still approve it later) and one she took back, which she can edit and send again
Important: like generate_sample_data, this only works when
DEBUG=True. It is idempotent: rerunning it updates the data in place. Run
it after configuring the site domain so the
invitation and notification emails it sends link to localhost:8000.
make manage generate_speaker_sample_data
python manage.py generate_speaker_sample_data
Pass --conference <year or slug> to target an edition other than the active
one.
Signing in as each persona:
- Organizer and volunteer accounts use the password
password123. Sign in asvol_mayaand open My volunteering tasks under My volunteering to see the items assigned to her and to Design Team. - Presenter accounts were created through the invitation flow and have no password yet. Use Send me a sign-in code on the login page with the presenter's email address; the code arrives in maildev at http://localhost:1080 (Docker) or in the server terminal. Once signed in, the speaker dashboard offers to set a password.
- The organizer side of the speaker portal lives under Organize โ Speakers; the speaker side is the Speaking entry in the top menu.
- Proposer accounts (
prop_tess,prop_iris,vol_rosa) usepassword123as well. Their proposals are under My proposals; the organizers' queue is Organize โ Speakers โ Proposals, and each proposed session can also be answered from the session page itself.
Documentation Setup
The documentation is built using MKDocs and markdown.
Local docs setup
- Create and activate a virtual environment:
python3 -m venv venv
source .env/bin/activate
python3 -m venv venv
.env\Scripts\activate
- Install docs requirements:
pip install -r requirements-docs.txt
- Run the docs server:
mkdocs serve -a localhost:8888
- Open the browser and go to http://localhost:8888/ to see the docs running.
Docs Troubleshooting
Cairo library was not found
If you see the error Cairo library was not found, try the following instructions:
-
Check if the
Cairo librariesare listed in/opt/homebrew/libfolder. If not, follow the instructions on this page.