supporters: Add sustainer form with pre-canned amount options

This commit is contained in:
Ben Sturmfels 2024-10-22 16:10:07 +11:00
parent 7eb0f274f7
commit 82f8fbb758
Signed by: bsturmfels
GPG key ID: 023C05E2C9C068F0
13 changed files with 284 additions and 234 deletions

View file

@ -5,7 +5,8 @@ set -e # Abort on failure
git push git push
ssh debian@hickory.sfconservancy.org 'bash -s' << EOF ssh debian@hickory.sfconservancy.org 'bash -s' << EOF
set -x # Show output set -x # Show output
set -e set -e # Abort on errors
cd /var/www/website cd /var/www/website
sudo -u www-data git pull sudo -u www-data git pull
sudo chown www-data:www-data . sudo chown www-data:www-data .

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,16 +1,45 @@
from django import forms from django import forms
from django.utils.safestring import mark_safe
from django.urls import reverse
from .models import SustainerOrder from .models import SustainerOrder
MONTH_MINIMUM = 10
YEAR_MINIMUM = 120
class SustainerFormRenderer(forms.renderers.DjangoTemplates):
# Customised layout with labels on own row
field_template_name = 'supporters/field.html'
class ButtonRadioSelect(forms.widgets.RadioSelect):
"""Radio button styled like a button. BYO CSS."""
# Extra <span> wrappers to support CSS
option_template_name = 'supporters/buttonradio_option.html'
use_fieldset = False
class Media:
css = {
'all': ['css/buttonradio.css'],
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'button-select'
class SustainerForm(forms.ModelForm): class SustainerForm(forms.ModelForm):
amount_monthly = forms.IntegerField(initial=12, required=False) # Used to pre-fill the price selector
amount_option = forms.CharField(required=False)
template_name = 'supporters/sustainer_form.html'
class Meta: class Meta:
model = SustainerOrder model = SustainerOrder
fields = [ fields = [
'recurring',
'amount',
'name', 'name',
'email', 'email',
'amount',
'acknowledge_publicly', 'acknowledge_publicly',
'add_to_mailing_list', 'add_to_mailing_list',
'tshirt_size', 'tshirt_size',
@ -20,10 +49,46 @@ class SustainerForm(forms.ModelForm):
'zip_code', 'zip_code',
'country', 'country',
] ]
widgets = {
'recurring': ButtonRadioSelect(attrs={
'x-model': 'recurring',
# Reset the amount field and option when changing monthly/annually.
'x-on:change': '$refs.amount.value = null; amount_option = null',
}),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.renderer = SustainerFormRenderer()
self.fields['recurring'].label = ''
# So we can write to this field easily from Alpine JS.
self.fields['amount'].widget.attrs['x-ref'] = 'amount'
self.fields['amount'].widget.attrs['style'] = 'width: 5rem' self.fields['amount'].widget.attrs['style'] = 'width: 5rem'
self.fields['amount'].initial = 128 self.fields['email'].help_text = 'For your payment receipt'
self.fields['amount_monthly'].widget.attrs['style'] = 'width: 5rem' self.fields['tshirt_size'].help_text = mark_safe("""Sizing chart: <a href="https://sfconservancy.org/videos/women-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Women's</a>, <a href="https://sfconservancy.org/videos/men-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Men's</a>""")
self.fields['tshirt_size'].widget.attrs['x-model'] = 'tshirt_size' self.fields['tshirt_size'].widget.attrs['x-model'] = 'tshirt_size'
def clean(self):
super().clean()
recurring = self.cleaned_data.get('recurring', '')
amount = self.cleaned_data.get('amount', 0)
minimum = MONTH_MINIMUM if recurring == 'month' else YEAR_MINIMUM
donate_url = reverse('donate')
if amount < minimum:
self.add_error(
'',
mark_safe(f'${minimum:d} is a minimum for Conservancy Sustainers. <a href="{donate_url}">Donate smaller amounts here</a>.')
)
tshirt_size = self.cleaned_data.get('tshirt_size')
if tshirt_size and not all([
self.cleaned_data.get('street'),
self.cleaned_data.get('city'),
self.cleaned_data.get('country')
]):
self.add_error(
'street',
'No address provided'
)

View file

@ -0,0 +1,71 @@
# Generated by Django 5.1.2 on 2024-10-22 04:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('supporters', '0004_sustainerorder_payment_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='sustainerorder',
name='amount',
field=models.PositiveIntegerField(),
),
migrations.AlterField(
model_name='sustainerorder',
name='recurring',
field=models.CharField(
blank=True,
choices=[('', 'Once'), ('month', 'Monthly'), ('year', 'Annually')],
default='',
max_length=10,
),
),
migrations.AlterField(
model_name='sustainerorder',
name='tshirt_size',
field=models.CharField(
blank=True,
choices=[
('', [('', 'None')]),
(
"Men's",
[
("Men's S", "Men's S"),
("Men's M", "Men's M"),
("Men's L", "Men's L"),
("Men's XL", "Men's XL"),
("Men's 2XL", "Men's 2XL"),
],
),
(
"Standard women's",
[
("Standard women's S", "Standard women's S"),
("Standard women's M", "Standard women's M"),
("Standard women's L", "Standard women's L"),
("Standard women's XL", "Standard women's XL"),
("Standard women's 2XL", "Standard women's 2XL"),
],
),
(
"Fitted women's",
[
("Fitted women's S", "Fitted women's S"),
("Fitted women's M", "Fitted women's M"),
("Fitted women's L", "Fitted women's L"),
("Fitted women's XL", "Fitted women's XL"),
("Fitted women's 2XL", "Fitted women's 2XL"),
],
),
],
default='',
max_length=50,
verbose_name='T-shirt size',
),
),
]

View file

@ -1,4 +1,3 @@
from django.core import validators
from django.db import models from django.db import models
@ -11,6 +10,7 @@ class Supporter(models.Model):
def test(self): def test(self):
return "TESTING" return "TESTING"
def __str__(self): def __str__(self):
return self.display_name return self.display_name
@ -20,9 +20,9 @@ class Supporter(models.Model):
class SustainerOrder(models.Model): class SustainerOrder(models.Model):
RENEW_CHOICES = [ RENEW_CHOICES = [
('', 'None'), ('', 'Once'),
('month', 'Monthly'), ('month', 'Monthly'),
('year', 'Annual'), ('year', 'Annually'),
] ]
TSHIRT_CHOICES = [ TSHIRT_CHOICES = [
( (
@ -64,17 +64,14 @@ class SustainerOrder(models.Model):
created_time = models.DateTimeField(auto_now_add=True) created_time = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
email = models.EmailField() email = models.EmailField()
amount = models.IntegerField( amount = models.PositiveIntegerField()
validators=[ recurring = models.CharField(max_length=10, choices=RENEW_CHOICES, blank=True, default='')
validators.MinValueValidator(100),
])
recurring = models.CharField(max_length=10)
payment_method = models.CharField(max_length=10, default='Stripe') payment_method = models.CharField(max_length=10, default='Stripe')
payment_id = models.CharField(max_length=255, blank=True) payment_id = models.CharField(max_length=255, blank=True)
paid_time = models.DateTimeField(null=True, blank=True) paid_time = models.DateTimeField(null=True, blank=True)
acknowledge_publicly = models.BooleanField(default=True) acknowledge_publicly = models.BooleanField(default=True)
add_to_mailing_list = models.BooleanField(default=True) add_to_mailing_list = models.BooleanField(default=True)
tshirt_size = models.CharField(max_length=50, choices=TSHIRT_CHOICES, blank=True) tshirt_size = models.CharField('T-shirt size', max_length=50, choices=TSHIRT_CHOICES, blank=True, default='')
street = models.CharField(max_length=255, blank=True) street = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True) city = models.CharField(max_length=255, blank=True)
state = models.CharField(max_length=255, blank=True) state = models.CharField(max_length=255, blank=True)

View file

@ -0,0 +1,31 @@
.button-select {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
}
.button-select label > span {
text-align: center;
display: inline-block;
padding: 0.5rem 0;
width: 100%;
background: #ddd;
border-radius: 4px;
border: 1px solid transparent;
}
.button-select label input {
opacity: 0;
position: absolute;
}
/* Wish we could use :has reliably. */
.button-select label input:focus + span {
border-color: #eee;
outline: 2px solid #666;
}
.button-select label input:checked + span {
color: white;
background: #666;
}

View file

@ -0,0 +1,2 @@
<!-- Custom <span> wrapper around the label to enable radio fields to be styled like buttons. -->
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} <span>{{ widget.label }}</span></label>{% endif %}

View file

@ -0,0 +1,11 @@
<!-- Labels on a separate line, custom help text layout -->
{% if field.use_fieldset %}
<fieldset{% if field.help_text and field.auto_id and "aria-describedby" not in field.field.widget.attrs %} aria-describedby="{{ field.auto_id }}_helptext"{% endif %}>
{% if field.label %}{{ field.legend_tag }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag }}{% endif %}
{% endif %}
{{ field.errors }}
<span class="db mt1">{{ field }}</span>
<div class="f7 black-60 mt1">{{ field.help_text }}</div>
{% if field.use_fieldset %}</fieldset>{% endif %}

View file

@ -7,31 +7,17 @@
{{ block.super }} {{ block.super }}
<style> <style>
@media screen and (min-width: 40em) { @media screen and (min-width: 40em) {
#sustainer-grid { #sustainer-grid-wrapper {
display: grid; display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: min-content 1fr;
gap: 1.5rem;
} }
} }
progress {
background: #ddd;
border: none;
}
progress::-moz-progress-bar {
background: #224c57;
}
progress::-webkit-progress-bar {
background: #ddd;
}
progress::-webkit-progress-value {
background: #224c57;
}
.btn:active { .btn:active {
transform: scale(1.05); transform: scale(1.05);
} }
.errorlist li {
color: #ff4136;
}
</style> </style>
<script defer src="{% static "js/vendor/alpine-3.14.1.js" %}"></script> <script defer src="{% static "js/vendor/alpine-3.14.1.js" %}"></script>
{% endblock %} {% endblock %}
@ -49,98 +35,98 @@
<p class="tr"><strong>Made Up Person</strong></p> <p class="tr"><strong>Made Up Person</strong></p>
</div> </div>
<div id="sustainer-grid" class="mv4"> <div id="sustainer-grid-wrapper" class="mv4" style="grid-template-columns: 2fr 1fr; gap: 1.5rem">
<div> <section>
<noscript> <noscript>
<p><strong>Hey there!</strong> We greatly respect you visiting our site <strong>without JavaScript</strong>. We make significant efforts to ensure our site works without JavaScript and where necessary to use only free software JavaScript.</p> <div style="padding: 1rem; border: 2px solid #0f0; margin-bottom: 1.5rem">
<p><strong>Unfortunately, our payment providers Stripe and PayPal do not work without JavaScript enabled</strong>. We also accept payment by <a href="#">paper check</a> and <a href="#">wire transfer</a> or otherwise please <a href="#">get in touch</a> and we'll try to work something out. Thanks for your support!</p> <p><marquee><strong>Hey there!</strong></marquee> Thanks for visiting our site <strong>without JavaScript</strong>! We respect your choice and make a significant effort to ensure our site works without JavaScript or, where necessary, to use only free software JavaScript.</p>
<p>The bad news is that <strong>our credit card/ACH payment services</strong> Stripe and PayPal <strong>don't work without JavaScript</strong>. We also don't currently have the resources to handle PCI compliant credit-card processing directly.</p>
<p>We'd still love to have you as a Sustainer though, and will gladly accept your payment by <a href="#">paper check</a> and <a href="#">wire transfer</a>. If those aren't feasible, please <a href="#">get in touch</a> and we'll try to work something out. Thanks for your support!</p>
<img src="{% static 'img/dancing-banana.gif' %}" alt="Dancing Banana">
</div>
</noscript> </noscript>
<!-- Alpine JS is used to show different payments amounts for monthly/annual, write the selected payment amount into the "amount" field, reset the seleted amount when you change monthly/annual and pop out the address when you select a T-shirt. -->
<form id="sustainer" method="post" action="." <form method="post" action="."
x-data="{ x-data="{
tshirt_size: '', recurring: '{{ form.recurring.value|escapejs }}',
tshirt_required: function () { return this.tshirt_size !== '' }, amount: parseInt('{{ form.amount.value|escapejs }}'),
recurring: '', amount_option: '{{ form.amount_option.value|escapejs }}',
amount_options: function() {
const MONTH_OPTIONS = [12, 25, 50, 100];
const YEAR_OPTIONS = [128, 250, 500, 1000];
return this.recurring === 'month' ? MONTH_OPTIONS : YEAR_OPTIONS;
},
tshirt_size: '{{ form.tshirt_size.value|escapejs }}',
}"> }">
{% csrf_token %} {% csrf_token %}
<fieldset class="bg-black-05 pa3 br3 center" style="max-width: 24rem; border: 1px solid #ccc"> {{ form.media }}
<fieldset class="bg-black-05 pa3 br3 center" style="border: 1px solid #ccc">
<legend class="b f5">Become a Sustainer</legend> <legend class="b f5">Become a Sustainer</legend>
{{ form.errors }} {{ form.non_field_errors }}
<div class="mb2"><label> <div>{{ form.recurring.as_field_group }}</div>
<label class="mr1"><input type="radio" name="recurring" value="" checked x-model="recurring"> Annual</label>
<label class="mr1"><input type="radio" name="recurring" value="month" x-model="recurring"> Monthly</label>
<label><input type="radio" name="recurring" value="year" x-model="recurring"> Annual Renew</label>
</label></div>
<div class="mb2">
<label>Amount<br>
<span class="mt1" x-show="recurring === ''">$ {{ form.amount }}</span>
<span class="mt1" x-show="recurring === 'month'">$ {{ form.amount_monthly }}</span>
<span class="mt1" x-show="recurring === 'year'">$ {{ form.amount }}</span>
</label>
</div>
<div class="mb2"><label>Name
<span class="db mt1">{{ form.name }}</span>
</label></div>
<div class="mb2"><label>Email
<span class="db mt1">{{ form.email }}</span>
</label>
<p class="f7 black-60 mt1">To send your receipt</p>
</div>
<div class="mv3"><label class="lh-title">{{ form.acknowledge_publicly }} Acknowledge me on the public <a href="">list of sustainers</a></label></div>
<div class="mv3"><label class="lh-title">{{ form.add_to_mailing_list }} Add me to the low-traffic <a href="https://lists.sfconservancy.org/pipermail/announce/">announcements</a> email list</label></div>
<div class="mv3">
<label>T-shirt:
<!-- Form field has an x-model attribute in forms.py. -->
<span class="db mt1">{{ form.tshirt_size }}</span>
</label>
<p class="f7 black-60 mt1">Sizing:
<a href="https://sfconservancy.org/videos/women-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Women's</a>,
<a href="https://sfconservancy.org/videos/men-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Men's</a></p>
<figure> <div class="mt3">
<div id="amount_options" class="button-select">
<template x-for="m in amount_options">
<label>
<input type="radio" name="amount_option" x-bind:value="m" x-on:change="$refs.amount.value = $event.currentTarget.value" x-model="amount_option" required>
<span>$<span x-text="m">12</span></span>
</label>
</template>
<label>
<input type="radio" name="amount_option" value="other" x-on:change="$refs.amount.value = ''" x-model="amount_option" required>
<span>Other</span>
</label>
</div>
<div class="mt2" x-show="amount_option === 'other'">
{{ form.amount.as_field_group }}
</div>
</div>
<div class="mt3">{{ form.name.as_field_group }}</div>
<div class="mt2">{{ form.email.as_field_group }}</div>
<div class="mt3"><label class="lh-title">{{ form.acknowledge_publicly }} Acknowledge me on the public <a href="">list of sustainers</a></label></div>
<div class="mt3"><label class="lh-title">{{ form.add_to_mailing_list }} Add me to the low-traffic <a href="https://lists.sfconservancy.org/pipermail/announce/">announcements</a> email list</label></div>
<div class="mt3">
{{ form.tshirt_size.as_field_group }}
<figure class="mt2">
<img src="/static/img/tshirt-2023.png" alt="Software Freedom Conservancy T-shirt" width="200px"> <img src="/static/img/tshirt-2023.png" alt="Software Freedom Conservancy T-shirt" width="200px">
</figure> </figure>
</div> </div>
<!-- Using Alpine.js to show/hide the address based on T-shirt choice. -->
<template x-if="tshirt_required">
<div id="address">
<div class="mb2">
<label>Postal address</label>
</div>
<div class="mb2"><label>Street
<span class="db mt1">{{ form.street }}</span>
</label></div>
<div class="mb2"><label>City
<span class="db mt1">{{ form.city }}</span>
</label></div>
<div class="mb2"><label>State/Region
<span class="db mt1">{{ form.state }}</span>
</label></div>
<div class="mb2"><label>Zip/Postal
<span class="db mt1">{{ form.zip_code }}</span>
</label></div>
<div class="mb2"><label>Country
<span class="db mt1">{{ form.country }}</span>
</label></div>
</div>
</template>
<div class="mt3"><button type="submit" class="pointer btn f5 pv2" style="width: 100%; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Become a Sustainer by<br>Credit Card or <abbr title="US Bank Direct Debit">ACH</button></div> <div id="address" x-show="tshirt_size !== ''">
<fieldset class="mt3">
<legend>Postal address</legend>
<div>{{ form.street.as_field_group }}</div>
<div class="mt2">{{ form.city.as_field_group }}</div>
<div class="mt2">{{ form.state.as_field_group }}</div>
<div class="mt2">{{ form.zip_code.as_field_group }}</div>
<div class="mt2">{{ form.country.as_field_group }}</div>
</fieldset>
</div>
<div class="mt3"><button type="submit" class="pointer btn f5 pv2" style="width: 100%; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Become a Sustainer by<br>Credit Card or <abbr title="US Bank Direct Debit">ACH</abbr></button></div>
</fieldset> </fieldset>
</form> </form>
<p class="f7 mv3">Credit card and ACH payments are processed with Stripe. We also accept payment by <a href="#">paper check</a> and <a href="#">wire transfer</a> and PayPal. Our sustainer program has a minimum of $120 USD per year, but we also accept <a href="#">donations of smaller amounts</a>.</p> <p class="f7 mv3">Credit card and ACH payments are processed with Stripe. We also accept payment by <a href="#">paper check</a> and <a href="#">wire transfer</a> and PayPal. Our sustainer program has a minimum of $120 USD per year, but we also accept <a href="#">donations of smaller amounts</a>.</p>
<div> <div class="mb4">
<a href="{% url "sustainers" %}"> <a href="{% url "sustainers" %}">
<button type="submit" class="pointer btn f5" style="height: 40px; width: 100%; font-weight: bold; color: white; background-color: var(--orange); opacity: 75%; border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Become a Sustainer by PayPal</button> <button type="submit" class="pointer btn f5" style="height: 40px; width: 100%; font-weight: bold; color: white; background-color: var(--orange); opacity: 75%; border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Become a Sustainer by PayPal</button>
</a> </a>
</div> </div>
</div> </section>
<div style="grid-row: 1 / span 2">
<section style="grid-row: 1 / span 2">
<video controls poster="https://sfconservancy.org/videos/sfc-introduction-video_poster.jpg" class="mb3"> <video controls poster="https://sfconservancy.org/videos/sfc-introduction-video_poster.jpg" class="mb3">
<source src="https://sfconservancy.org/videos/sfc-introduction_1080p.mp4"> <source src="https://sfconservancy.org/videos/sfc-introduction_1080p.mp4">
<track src="/docs/sfc-introduction-vtt-captions.txt" kind="subtitles" srclang="en" label="English"> <track src="/docs/sfc-introduction-vtt-captions.txt" kind="subtitles" srclang="en" label="English">
@ -175,6 +161,7 @@
software freedom that the world needs. <a href="/donate/">Please consider software freedom that the world needs. <a href="/donate/">Please consider
donating now!</a></p> donating now!</a></p>
<h2 class="">2024 in Review</h2>
<details id="YearInReview"> <details id="YearInReview">
<summary>Our Year in Review</summary> <summary>Our Year in Review</summary>
@ -247,7 +234,7 @@ account will be approved.</p>
</details> </details>
<details id="NewStaff"> <details id="NewStaff">
<summary>New staff!</summary> <summary>New Staff!</summary>
<p>SFC hired two additional employees this year! General Counsel Rick Sanders <p>SFC hired two additional employees this year! General Counsel Rick Sanders
joins the team to help with our continued legal needs. Rick has over 20 years joins the team to help with our continued legal needs. Rick has over 20 years
experience as a intellectual-property litigator. His expertise has been experience as a intellectual-property litigator. His expertise has been
@ -299,9 +286,8 @@ of FOSS and in critical infrastructure discussions and also presented in
classroom to educate students about software freedom.</p> classroom to educate students about software freedom.</p>
</details> </details>
<details id="Highlights"> <details id="Highlights">
<summary>Highlights from some of our projects</summary> <summary>Highlights From Our Member Projects</summary>
<p>We've raised, administered and/or facilitated $1.8 million to improve <p>We've raised, administered and/or facilitated $1.8 million to improve
software freedom directly! This includes contractors, interns and students, software freedom directly! This includes contractors, interns and students,
administrators, and grants for creation, distribution and maintenance of free administrators, and grants for creation, distribution and maintenance of free
@ -361,6 +347,5 @@ annual summit was hosted in Hamburg featuring incredible
technical talks, project planning and continues to build the momentum and technical talks, project planning and continues to build the momentum and
reach for reproducibility. </p> reach for reproducibility. </p>
</details> </details>
</div> </section>
</div>
{% endblock %} {% endblock %}

View file

@ -1,91 +0,0 @@
{% extends "base_conservancy.html" %}
{% load static %}
{% block subtitle %}Support Conservancy - {% endblock %}
{% block category %}sustainer{% endblock %}
{% block head %}
{{ block.super }}
<script defer src="{% static "js/vendor/alpine-3.14.1.js" %}"></script>
<style>
.btn:active {
transform: scale(1.05);
}
</style>
{% endblock %}
{% block content %}
<h1 class="lh-title tc mt4 mb0">Become a Sustainer Now</h1>
<p class="measure-wide center tc">Sustainers help us do our work in a strategic, long-term way.</p>
<div class="bg-black-05 pa3 br3 mb4 center" style="max-width: 24rem; border: 1px solid #ccc">
<form id="sustainer" method="post" action="."
x-data="{
tshirt_size: 'None',
tshirt_required: function () { return this.tshirt_size !== 'None' },
recurring: '',
}">
{% csrf_token %}
{{ form.errors }}
<div class="mb2"><label>Name
<span class="db mt1">{{ form.name }}</span>
</label></div>
<div class="mb2"><label>Email
<span class="db mt1">{{ form.email }}</span>
</label>
<p class="f7 black-60 mt1">To send your receipt</p>
</div>
<div class="mb2"><label>
<label class="mr1"><input type="radio" name="recurring" value="" checked x-model="recurring"> Once</label>
<label class="mr1"><input type="radio" name="recurring" value="month" x-model="recurring"> Monthly</label>
<label><input type="radio" name="recurring" value="year" x-model="recurring"> Annual</label>
</label></div>
<div class="mb2">
<label>Amount<br>
<span class="mt1" x-show="recurring === ''">$ {{ form.amount }}</span>
<span class="mt1" x-show="recurring === 'month'">$ {{ form.amount_monthly }}</span>
<span class="mt1" x-show="recurring === 'year'">$ {{ form.amount }}</span>
</label>
<button type="button" class="f7 pa1 lh-solid o-40" title="Multiply my impact">10x me</button>
</div>
<p class="f7 black-60 mt1">Giving is known to increase happiness, but are
you sure?
<a href="#">Undo</a></p>
<div class="mv3"><label class="lh-title">{{ form.acknowledge_publicly }} Acknowledge me on the public <a href="">list of sustainers</a></label></div>
<div class="mv3"><label class="lh-title">{{ form.add_to_mailing_list }} Add me to the low-traffic <a href="https://lists.sfconservancy.org/pipermail/announce/">announcements</a> email list</label></div>
<div class="mv3">
<label>T-shirt:
<!-- Form field has an x-model attribute in forms.py. -->
<span class="db mt1">{{ form.tshirt_size }}</span>
</label>
<p class="f7 black-60 mt1">Sizing:
<a href="https://sfconservancy.org/videos/women-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Women's</a>,
<a href="https://sfconservancy.org/videos/men-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Men's</a></p>
</div>
<!-- Using Alpine.js to show/hide the address based on T-shirt choice. -->
<template x-if="tshirt_required">
<fieldset id="address">
<legend>Postal address</legend>
<div class="mb2"><label>Street
<span class="db mt1">{{ form.street }}</span>
</label></div>
<div class="mb2"><label>City
<span class="db mt1">{{ form.city }}</span>
</label></div>
<div class="mb2"><label>State/Region
<span class="db mt1">{{ form.state }}</span>
</label></div>
<div class="mb2"><label>Zip/Postal
<span class="db mt1">{{ form.zip_code }}</span>
</label></div>
<div class="mb2"><label>Country
<span class="db mt1">{{ form.country }}</span>
</label></div>
</fieldset>
</template>
<div class="mt3"><button type="submit" class="btn" style="height: 40px; width: 100%; font-size: 18px; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Pay via Stripe</button></div>
<p class="f7 mt3">If you have concerns or issues paying with Stripe, we also accept payment by <a href="#">paper check</a> and <a href="#">wire transfer</a>.</p>
</form>
</div>
{% endblock %}

View file

@ -10,5 +10,4 @@ urlpatterns = [
path('success/', views.success), path('success/', views.success),
path('webhook/', views.webhook), path('webhook/', views.webhook),
path('stripe/', views.sustainers_stripe), path('stripe/', views.sustainers_stripe),
path('stripe2/', views.sustainers_stripe2, name='stripe2'),
] ]

View file

@ -79,11 +79,7 @@ def sustainers_stripe(request):
if request.method == 'POST': if request.method == 'POST':
form = forms.SustainerForm(request.POST) form = forms.SustainerForm(request.POST)
if form.is_valid(): if form.is_valid():
order = form.save(commit=False) order = form.save()
order.recurring = form.data['recurring']
if order.recurring == 'month':
order.amount = form.cleaned_data['amount_monthly']
order.save()
base_url = f'{request.scheme}://{request.get_host()}' base_url = f'{request.scheme}://{request.get_host()}'
stripe_checkout_url = create_checkout_session(order.id, order.email, order.amount, order.recurring, base_url) stripe_checkout_url = create_checkout_session(order.id, order.email, order.amount, order.recurring, base_url)
return redirect(stripe_checkout_url) return redirect(stripe_checkout_url)
@ -92,23 +88,6 @@ def sustainers_stripe(request):
return render(request, 'supporters/sustainers_stripe.html', {'form': form}) return render(request, 'supporters/sustainers_stripe.html', {'form': form})
def sustainers_stripe2(request):
if request.method == 'POST':
form = forms.SustainerForm(request.POST)
if form.is_valid():
order = form.save(commit=False)
order.recurring = form.data['recurring']
if order.recurring == 'month':
order.amount = form.cleaned_data['amount_monthly']
order.save()
base_url = f'{request.scheme}://{request.get_host()}'
stripe_checkout_url = create_checkout_session(order.id, order.email, order.amount, order.recurring, base_url)
return redirect(stripe_checkout_url)
else:
form = forms.SustainerForm()
return render(request, 'supporters/sustainers_stripe2.html', {'form': form})
stripe.api_key = settings.STRIPE_API_KEY stripe.api_key = settings.STRIPE_API_KEY
if stripe.api_key == '': if stripe.api_key == '':
logger.warning('Missing STRIPE_API_KEY') logger.warning('Missing STRIPE_API_KEY')

View file

@ -51,7 +51,7 @@ urlpatterns = [
re_path(r'^about/', views.content), re_path(r'^about/', views.content),
re_path(r'^activities/', views.content), re_path(r'^activities/', views.content),
re_path(r'^copyleft-compliance/', views.content, {'fundraiser_sought': 'vmware-match-0'}), re_path(r'^copyleft-compliance/', views.content, {'fundraiser_sought': 'vmware-match-0'}),
re_path(r'^donate/', views.content), path('donate/', views.content, name='donate'),
path('fossy/', views.content), path('fossy/', views.content),
re_path(r'^GiveUpGitHub/', views.content), re_path(r'^GiveUpGitHub/', views.content),
re_path(r'^learn/', views.content), re_path(r'^learn/', views.content),