From 86e2415e73dbfaa456a22b1c7a69320286128afc Mon Sep 17 00:00:00 2001 From: Ben Sturmfels Date: Tue, 22 Oct 2024 16:10:07 +1100 Subject: [PATCH] supporters: Add sustainer form with pre-canned amount options --- bin/deploy | 3 +- conservancy/static/img/dancing-banana.gif | Bin 0 -> 2233 bytes conservancy/supporters/forms.py | 75 ++++++- ...05_alter_sustainerorder_amount_and_more.py | 71 +++++++ conservancy/supporters/models.py | 15 +- .../supporters/static/css/buttonradio.css | 31 +++ .../supporters/buttonradio_option.html | 2 + .../templates/supporters/field.html | 11 + .../supporters/sustainers_stripe.html | 193 ++++++++---------- .../supporters/sustainers_stripe2.html | 91 --------- conservancy/supporters/urls.py | 1 - conservancy/supporters/views.py | 23 +-- conservancy/urls.py | 2 +- 13 files changed, 284 insertions(+), 234 deletions(-) create mode 100644 conservancy/static/img/dancing-banana.gif create mode 100644 conservancy/supporters/migrations/0005_alter_sustainerorder_amount_and_more.py create mode 100644 conservancy/supporters/static/css/buttonradio.css create mode 100644 conservancy/supporters/templates/supporters/buttonradio_option.html create mode 100644 conservancy/supporters/templates/supporters/field.html delete mode 100644 conservancy/supporters/templates/supporters/sustainers_stripe2.html diff --git a/bin/deploy b/bin/deploy index 823e27d7..024aaac0 100755 --- a/bin/deploy +++ b/bin/deploy @@ -5,7 +5,8 @@ set -e # Abort on failure git push ssh debian@hickory.sfconservancy.org 'bash -s' << EOF set -x # Show output -set -e +set -e # Abort on errors + cd /var/www/website sudo -u www-data git pull sudo chown www-data:www-data . diff --git a/conservancy/static/img/dancing-banana.gif b/conservancy/static/img/dancing-banana.gif new file mode 100644 index 0000000000000000000000000000000000000000..ef3c9119ed61f48e034dd4ea3f12c806c7c6d93f GIT binary patch literal 2233 zcmd7TX;f3`83*uJyq91IqJp5#)CvlRC@xTETyb2u2pY#_oK9D(rF7KkX`LCSc06sB zEgMT9H{_B9OhO<#*&rbSqCgU|q7Xz}Kw;1sml0)>)X}Jo$M&3h#-4udJqX4F3jgbta==u4~e-DuL0sWz)J4h91)H$hdgG;Y6;I=t98D^7Ui(bcG?Jbd#uwQ zZS_QYT#(g+w|b(zF4*mcuiGJyCp_?wBXP(D^@o34iQayA$O{kkL+-&y*C5{I!?}Gi z7ks!OA3W@XuMfi`BaxGX%L4vc{s1lzhz$gxv{CnD^e&+*O?;~pU=fOV>WPSW-WA*Mo zs|Py1aNo{QN}_(I`sH_zpIu#>Z`(nCP)Pf$F=I3vo&P~w@&>{I)5q-O%7Zk*{v|I- zGw|T_f0DdEAXNhJQdLU_v9Wa=Di1-=3XtYTaT!3iASqH=v7&jsT1&#vEnCzL99Dy3 z(NasTN!PNaUSpXcd72$djvS*P12s%c3BKSWlaf~GpCwa$zs=4Fy_34@o69( zK^P(`qp4XaD+SA%RcbPWA(6IK3A;K-((0b`OtZ#P)z^Y{YThxLTFh>%6J_aJJ;NIB z#hyyk9N2p1=u^b?n{DKO zBxG(LXB?96d}~c^X|AY#+u`x_L-&IN3miV`FWY4WD!^fjF7Tqejg5->iS9 zF3kH^>Jp>t>Ps<1*jUoE2@y47L`18q2OCyul~T2ir$@ot8JkpfmKv~9BkJM+qDw}y zSNDTXUMrE;?(4fcbEfCYjhmxm#9lCNzM#ExyBwP_*ss~7u<|S1Hs=GSjs0zT5TCH_ z*ucckf5GlKn|_MDh_>Qrb=ao;dHL?N*+D@^0zpZVC{mOQzJduN};ewmip!6lY58HXLqLk`ZLN2Va^Hmnmp1+ zl{c6EG>r>yK1nRF>`nd&w=nxevlw$OXozpev%TB9k|QcvbJH9#7HBC>{Nx>SZJm=tZX9%!0WrW#Wvc8=CT zI!~tBdmZFmZH`@CT&vUK>D71FQ`%b%-4`fvYPqe|(g|>eF1?%61Xh2IoBrm~q>dtb z^v$r6yt+|;IPghe!p1leBz47=ojAOXte&^LH0t7bTWrkACz+4G5?E<--uwGTf058p zBYb=;{UmjC*Pi?D8=j_oJ}()2r(oeJ&TQ_kvIfzI`SV%7Un;t8ezq(=>P^f6#CB 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): - 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: model = SustainerOrder fields = [ + 'recurring', + 'amount', 'name', 'email', - 'amount', 'acknowledge_publicly', 'add_to_mailing_list', 'tshirt_size', @@ -20,10 +49,46 @@ class SustainerForm(forms.ModelForm): 'zip_code', '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): 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'].initial = 128 - self.fields['amount_monthly'].widget.attrs['style'] = 'width: 5rem' + self.fields['email'].help_text = 'For your payment receipt' + self.fields['tshirt_size'].help_text = mark_safe("""Sizing chart: Women's, Men's""") 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. Donate smaller amounts here.') + ) + 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' + ) diff --git a/conservancy/supporters/migrations/0005_alter_sustainerorder_amount_and_more.py b/conservancy/supporters/migrations/0005_alter_sustainerorder_amount_and_more.py new file mode 100644 index 00000000..2db41fef --- /dev/null +++ b/conservancy/supporters/migrations/0005_alter_sustainerorder_amount_and_more.py @@ -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', + ), + ), + ] diff --git a/conservancy/supporters/models.py b/conservancy/supporters/models.py index edca7a1d..c8d3ec87 100644 --- a/conservancy/supporters/models.py +++ b/conservancy/supporters/models.py @@ -1,4 +1,3 @@ -from django.core import validators from django.db import models @@ -11,6 +10,7 @@ class Supporter(models.Model): def test(self): return "TESTING" + def __str__(self): return self.display_name @@ -20,9 +20,9 @@ class Supporter(models.Model): class SustainerOrder(models.Model): RENEW_CHOICES = [ - ('', 'None'), + ('', 'Once'), ('month', 'Monthly'), - ('year', 'Annual'), + ('year', 'Annually'), ] TSHIRT_CHOICES = [ ( @@ -64,17 +64,14 @@ class SustainerOrder(models.Model): created_time = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=255) email = models.EmailField() - amount = models.IntegerField( - validators=[ - validators.MinValueValidator(100), - ]) - recurring = models.CharField(max_length=10) + amount = models.PositiveIntegerField() + recurring = models.CharField(max_length=10, choices=RENEW_CHOICES, blank=True, default='') payment_method = models.CharField(max_length=10, default='Stripe') payment_id = models.CharField(max_length=255, blank=True) paid_time = models.DateTimeField(null=True, blank=True) acknowledge_publicly = 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) city = models.CharField(max_length=255, blank=True) state = models.CharField(max_length=255, blank=True) diff --git a/conservancy/supporters/static/css/buttonradio.css b/conservancy/supporters/static/css/buttonradio.css new file mode 100644 index 00000000..f5d2c723 --- /dev/null +++ b/conservancy/supporters/static/css/buttonradio.css @@ -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; +} diff --git a/conservancy/supporters/templates/supporters/buttonradio_option.html b/conservancy/supporters/templates/supporters/buttonradio_option.html new file mode 100644 index 00000000..b7c91a04 --- /dev/null +++ b/conservancy/supporters/templates/supporters/buttonradio_option.html @@ -0,0 +1,2 @@ + +{% if widget.wrap_label %}{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label }}{% endif %} diff --git a/conservancy/supporters/templates/supporters/field.html b/conservancy/supporters/templates/supporters/field.html new file mode 100644 index 00000000..9a8b305d --- /dev/null +++ b/conservancy/supporters/templates/supporters/field.html @@ -0,0 +1,11 @@ + +{% if field.use_fieldset %} + + {% if field.label %}{{ field.legend_tag }}{% endif %} +{% else %} + {% if field.label %}{{ field.label_tag }}{% endif %} +{% endif %} +{{ field.errors }} +{{ field }} +
{{ field.help_text }}
+{% if field.use_fieldset %}{% endif %} diff --git a/conservancy/supporters/templates/supporters/sustainers_stripe.html b/conservancy/supporters/templates/supporters/sustainers_stripe.html index 8c007112..7d6b8a3b 100644 --- a/conservancy/supporters/templates/supporters/sustainers_stripe.html +++ b/conservancy/supporters/templates/supporters/sustainers_stripe.html @@ -7,31 +7,17 @@ {{ block.super }} - {% endblock %} @@ -49,98 +35,98 @@

Made Up Person

-
-
- +
+
+
+ +
-
+ {% endblock %} diff --git a/conservancy/supporters/templates/supporters/sustainers_stripe2.html b/conservancy/supporters/templates/supporters/sustainers_stripe2.html deleted file mode 100644 index db88b46a..00000000 --- a/conservancy/supporters/templates/supporters/sustainers_stripe2.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends "base_conservancy.html" %} -{% load static %} -{% block subtitle %}Support Conservancy - {% endblock %} -{% block category %}sustainer{% endblock %} - -{% block head %} - {{ block.super }} - - -{% endblock %} - -{% block content %} -

Become a Sustainer Now

-

Sustainers help us do our work in a strategic, long-term way.

- -
-
- {% csrf_token %} - {{ form.errors }} -
-
-

To send your receipt

-
-
-
- - -
-

Giving is known to increase happiness, but are - you sure? - Undo

-
-
-
- -

Sizing: - Women's, - Men's

-
- - - -
- -

If you have concerns or issues paying with Stripe, we also accept payment by paper check and wire transfer.

-
-
-{% endblock %} diff --git a/conservancy/supporters/urls.py b/conservancy/supporters/urls.py index 23ea1948..9d9a25b2 100644 --- a/conservancy/supporters/urls.py +++ b/conservancy/supporters/urls.py @@ -10,5 +10,4 @@ urlpatterns = [ path('success/', views.success), path('webhook/', views.webhook), path('stripe/', views.sustainers_stripe), - path('stripe2/', views.sustainers_stripe2, name='stripe2'), ] diff --git a/conservancy/supporters/views.py b/conservancy/supporters/views.py index 7ec1bdd6..c968613d 100644 --- a/conservancy/supporters/views.py +++ b/conservancy/supporters/views.py @@ -79,11 +79,7 @@ def sustainers_stripe(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() + order = form.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) @@ -92,23 +88,6 @@ def sustainers_stripe(request): 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 if stripe.api_key == '': logger.warning('Missing STRIPE_API_KEY') diff --git a/conservancy/urls.py b/conservancy/urls.py index e8450778..6a963915 100644 --- a/conservancy/urls.py +++ b/conservancy/urls.py @@ -51,7 +51,7 @@ urlpatterns = [ re_path(r'^about/', views.content), re_path(r'^activities/', views.content), 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), re_path(r'^GiveUpGitHub/', views.content), re_path(r'^learn/', views.content),