Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,7 @@ docker_environment_init.sh
.envrc

# Exclude Makefiles
Makefile
Makefile

# Exclude files create at runtime - by webpack and storybook
common/static/
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# https://hub.docker.com/r/nikolaik/python-nodejs
FROM nikolaik/python-nodejs:python3.10-nodejs16

# Refresh Yarn repo GPG key (old key 62D54FD4003F6525 expired; fetch the
# current one so apt-get update succeeds on bookworm-based images).
Comment thread
ddfridley marked this conversation as resolved.
# The nikolaik base image ships a Yarn apt source whose bundled GPG key
# (62D54FD4003F6525) expired, and Debian Bookworm dropped the legacy apt-key
# trust store in favour of per-source signed-by keyrings. This block:
# 1. Downloads the current Yarn public key as a binary keyring.
# 2. Updates /etc/apt/sources.list.d/yarn.list to reference it via signed-by.
# Without this, `apt-get update` fails with "NO_PUBKEY 62D54FD4003F6525".
# To verify: docker build --no-cache and watch the apt-get update step.
RUN curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \
| gpg --dearmor -o /usr/share/keyrings/yarn-archive-keyring.gpg \
&& sed -i 's|^deb \[.*\] https://dl.yarnpkg.com|deb [signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com|;s|^deb https://dl.yarnpkg.com|deb [signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com|' \
/etc/apt/sources.list.d/yarn.list 2>/dev/null || true

# This to get GDAL thanks to https://stackoverflow.com/questions/62546706/how-do-i-install-gdal-in-a-python-docker-environment
RUN apt-get update && apt-get install

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Generated by Django 4.2.3 on 2026-06-30 18:12
#
# Note: This auto-generated migration resolves Django 4+'s implicit requirement
# that all models must track `BigAutoField` updates explicitly. Generating this
# file was necessary to unblock CI/CD pipelines, because `manage.py migrate`
# will aggressively abort (skipping other pending migrations) if it detects
# unwritten schema changes on the target database server.

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('civictechprojects', '0063_alter_taggedcategory_tag_and_more'),
]

operations = [
migrations.AlterField(
model_name='event',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='eventconferenceroom',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='eventconferenceroomparticipant',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='eventlocationtimezone',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='eventproject',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='group',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='namerecord',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='project',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='projectcommit',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='projectfavorite',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='projectfile',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='projectlink',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='projectposition',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='projectrelationship',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='rsvpvolunteerrelation',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedcategory',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedeventorganization',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedissueareas',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedorganization',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedorganizationtype',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedpositionrole',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedrsvpvolunteerrole',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedstage',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedtechnologies',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='taggedvolunteerrole',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='testimonial',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='useralert',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='volunteerrelation',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]
46 changes: 7 additions & 39 deletions common/components/common/integrations/NewsletterSignup.jsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,19 @@
// @flow

import React from "react";

// In mailchimp go to forms > other forms choose the subscription landing page to get the url
class NewsletterSignup extends React.Component {
render() {
return (
<div id="mc_embed_signup">
<form
action="https://democracylab.us3.list-manage.com/subscribe/post?u=72af92d0a817dcbf3aa960ee0&amp;id=d3b4c4d81c"
method="post"
id="mc-embedded-subscribe-form"
name="mc-embedded-subscribe-form"
className="validate"
<a
href="https://mailchi.mp/democracylab/subscribe"
target="_blank"
noValidate
rel="noopener noreferrer"
className={this.props.btnClass}
>
<div id="mc_embed_signup_scroll">
<div className="mc-field-group SocialFooter-signupcontainer">
<label htmlFor="mce-EMAIL" />
<input
type="submit"
value="Subscribe"
name="subscribe"
id="mc-embedded-subscribe"
className={this.props.btnClass}
/>
</div>
<div id="mce-responses" className="clear">
<div
className="response mc_display_none"
id="mce-error-response"
/>
<div
className="response mc_display_none"
id="mce-success-response"
/>
</div>
<div className="mc_embed_hidden" aria-hidden="true">
<input
type="text"
name="b_72af92d0a817dcbf3aa960ee0_d3b4c4d81c"
tabIndex="-1"
defaultValue=""
/>
</div>
</div>
</form>
Subscribe
</a>
</div>
);
}
Expand Down
30 changes: 30 additions & 0 deletions common/components/controllers/SignUpController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import DjangoCSRFToken from "django-react-csrftoken";
import React from "react";
import ReCAPTCHA from "react-google-recaptcha";
import type { Validator } from "../forms/FormValidation.jsx";
import FormValidation from "../forms/FormValidation.jsx";
import metrics from "../utils/metrics.js";
Expand All @@ -27,6 +28,7 @@ type State = {|
validations: $ReadOnlyArray<Validator>,
termsOpen: boolean,
didCheckTerms: boolean,
reCaptchaValue: ?string,
isValid: boolean,
|};

Expand All @@ -50,6 +52,7 @@ class SignUpController extends React.Component<Props, State> {
password2: "",
termsOpen: false,
didCheckTerms: false,
reCaptchaValue: null,
isValid: false,
validations: [
{
Expand Down Expand Up @@ -88,10 +91,23 @@ class SignUpController extends React.Component<Props, State> {
checkFunc: (state: State) => state.didCheckTerms,
errorMessage: "Agree to terms of service",
},
{
checkFunc: (state: State) =>
!this.isCaptchaEnabled() || !_.isEmpty(state.reCaptchaValue),
errorMessage: "Please complete the captcha",
},
],
};
}

isCaptchaEnabled(): boolean {
return !_.isEmpty(window.GR_SITEKEY);
}

reCaptchaOnChange(value: ?string): void {
this.setState({ reCaptchaValue: value });
}

onValidationCheck(isValid: boolean): void {
if (isValid !== this.state.isValid) {
this.setState({ isValid });
Expand Down Expand Up @@ -153,6 +169,11 @@ class SignUpController extends React.Component<Props, State> {
/>
</div>
<input name="password" value={this.state.password1} type="hidden" />
<input
name="reCaptchaValue"
value={this.state.reCaptchaValue || ""}
type="hidden"
/>

<div>
<input name="newsletter_signup" type="checkbox" />
Expand Down Expand Up @@ -206,6 +227,15 @@ class SignUpController extends React.Component<Props, State> {
formState={this.state}
/>

{this.isCaptchaEnabled() ? (
<div className="LogInController-captcha">
<ReCAPTCHA
sitekey={window.GR_SITEKEY}
onChange={this.reCaptchaOnChange.bind(this)}
/>
</div>
) : null}

<Button
variant="success"
className="LogInController-signInButton"
Expand Down
18 changes: 17 additions & 1 deletion common/helpers/mailing_list.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
Mailchimp setup instructions
Comment thread
ddfridley marked this conversation as resolved.

see https://docs.google.com/document/d/1OLQPFFJ8oz_BxpuxRxKKdZ2brmlUkVN3ICTdbA_axxY/edit?tab=t.0#heading=h.91rbt9r6gf9e
and keep it up to date

"""

import threading
from mailchimp3 import MailChimp
from django.conf import settings
Expand All @@ -16,11 +24,19 @@ def print_error(self, err_msg):
err_msg = "Failed to subscribe {first} {last}({email}) to mailing list: {err_msg}".format(
first=self.first_name,
last=self.last_name,
email=self.email,
email=self.masked_email(),
err_msg=err_msg,
)
print(err_msg)

def masked_email(self):
Comment thread
ddfridley marked this conversation as resolved.
if not self.email or '@' not in self.email:
return '***'

local, domain = self.email.split('@', 1)
visible = local[:2] if len(local) >= 2 else local[:1]
return '{local}***@{domain}'.format(local=visible, domain=domain)

def run(self):
if settings.MAILCHIMP_API_KEY is None:
self.print_error("MAILCHIMP_API_KEY not set")
Expand Down
24 changes: 24 additions & 0 deletions common/migrations/0003_alter_tag_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.3 on 2026-06-30 18:12
#
# Note: This auto-generated migration resolves Django 4+'s implicit requirement
# that all models must track `BigAutoField` updates explicitly. Generating this
# file was necessary to unblock CI/CD pipelines, because `manage.py migrate`
# will aggressively abort (skipping other pending migrations) if it detects
# unwritten schema changes on the target database server.

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('common', '0002_auto_20190501_1846'),
]

operations = [
migrations.AlterField(
model_name='tag',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]
Loading