supporters: Add sustainer form with pre-canned amount options
This commit is contained in:
parent
7eb0f274f7
commit
82f8fbb758
13 changed files with 284 additions and 234 deletions
|
@ -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 .
|
||||||
|
|
BIN
conservancy/static/img/dancing-banana.gif
Normal file
BIN
conservancy/static/img/dancing-banana.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -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'
|
||||||
|
)
|
||||||
|
|
|
@ -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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
31
conservancy/supporters/static/css/buttonradio.css
Normal file
31
conservancy/supporters/static/css/buttonradio.css
Normal 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;
|
||||||
|
}
|
|
@ -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 %}
|
11
conservancy/supporters/templates/supporters/field.html
Normal file
11
conservancy/supporters/templates/supporters/field.html
Normal 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 %}
|
|
@ -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>
|
||||||
</noscript>
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<form id="sustainer" method="post" action="."
|
<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>
|
||||||
x-data="{
|
|
||||||
tshirt_size: '',
|
|
||||||
tshirt_required: function () { return this.tshirt_size !== '' },
|
|
||||||
recurring: '',
|
|
||||||
}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<fieldset class="bg-black-05 pa3 br3 center" style="max-width: 24rem; border: 1px solid #ccc">
|
|
||||||
<legend class="b f5">Become a Sustainer</legend>
|
|
||||||
{{ form.errors }}
|
|
||||||
<div class="mb2"><label>
|
|
||||||
<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>
|
<img src="{% static 'img/dancing-banana.gif' %}" alt="Dancing Banana">
|
||||||
<img src="/static/img/tshirt-2023.png" alt="Software Freedom Conservancy T-shirt" width="200px">
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
|
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</noscript>
|
||||||
|
|
||||||
<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>
|
<!-- 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. -->
|
||||||
</fieldset>
|
<form method="post" action="."
|
||||||
</form>
|
x-data="{
|
||||||
|
recurring: '{{ form.recurring.value|escapejs }}',
|
||||||
|
amount: parseInt('{{ form.amount.value|escapejs }}'),
|
||||||
|
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 %}
|
||||||
|
{{ form.media }}
|
||||||
|
<fieldset class="bg-black-05 pa3 br3 center" style="border: 1px solid #ccc">
|
||||||
|
<legend class="b f5">Become a Sustainer</legend>
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
<div>{{ form.recurring.as_field_group }}</div>
|
||||||
|
|
||||||
<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 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>
|
<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">
|
||||||
|
</figure>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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'),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue