Repository: liberapay/liberapay.com

Issue:
Design of the donation form
This issue is for the upcoming overhaul of the donation form.


Discussion:
Here's a current screenshot of a donation form (<https://liberapay.com/Liberapay/donate>):

![spectacle h23369](https://cloud.githubusercontent.com/assets/1581590/25043110/cc98a3de-211d-11e7-8540-b534d6f8cd9b.png)

Here's a first prototype for the new version, based on the same design as #566:

![spectacle s23369](https://cloud.githubusercontent.com/assets/1581590/25043209/7ce44ca2-211e-11e7-8ab4-1357206823ba.png)

If you have ideas or suggestions please comment.
Adding something between "5€ per week" and "100€ per week" would make sense I guess. 20 or 25 maybe?
I'm not planning to modify the suggested amounts, but here's some data on popular ones:

```
> select amount, count(*) from current_tips where amount > 0 group by amount having count(*) > 2 order by amount; 
 amount | count 
--------+-------
   0.01 |   283
   0.02 |    18
   0.03 |     8
   0.05 |    29
   0.10 |    44
   0.12 |     6
   0.15 |     5
   0.20 |     9
   0.24 |    46
   0.25 |   122
   0.29 |     5
   0.30 |     7
   0.35 |     3
   0.40 |     3
   0.47 |    13
   0.50 |    47
   0.70 |     6
   0.93 |     5
   0.97 |     4
   1.00 |   133
   1.16 |    20
   1.93 |     4
   2.00 |    31
   2.31 |     9
   2.50 |     4
   3.00 |     9
   4.62 |     4
   5.00 |    36
   5.77 |     4
  10.00 |    11
  15.00 |     4
  20.00 |     4
  25.00 |     4
 100.00 |     9
(34 rows)
```
Indeed interesting data. I think you've mentioned before but can people change the currency to display currency values in their own currency while still being billed in pounds? Or at least see a basic conversion of their future charge in the present date.

Resolving patch:
diff --git a/liberapay/constants.py b/liberapay/constants.py
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -32,6 +32,8 @@ def check_bits(bits):
 
 Fees = namedtuple('Fees', ('var', 'fix'))
 
+StandardTip = namedtuple('StandardTip', 'label weekly monthly yearly')
+
 
 _ = lambda a: a
 
@@ -192,12 +194,22 @@ def check_bits(bits):
 SESSION_REFRESH = timedelta(hours=1)
 SESSION_TIMEOUT = timedelta(hours=6)
 
+
+def make_standard_tip(label, weekly):
+    return StandardTip(
+        label,
+        weekly,
+        weekly / PERIOD_CONVERSION_RATES['monthly'],
+        weekly / PERIOD_CONVERSION_RATES['yearly'],
+    )
+
+
 STANDARD_TIPS = (
-    (_("Symbolic ({0} per week)"), Decimal('0.01')),
-    (_("Small ({0} per week)"), Decimal('0.25')),
-    (_("Medium ({0} per week)"), Decimal('1.00')),
-    (_("Large ({0} per week)"), Decimal('5.00')),
-    (_("Maximum ({0} per week)"), DONATION_WEEKLY_MAX),
+    make_standard_tip(_("Symbolic"), Decimal('0.01')),
+    make_standard_tip(_("Small"), Decimal('0.25')),
+    make_standard_tip(_("Medium"), Decimal('1.00')),
+    make_standard_tip(_("Large"), Decimal('5.00')),
+    make_standard_tip(_("Maximum"), DONATION_WEEKLY_MAX),
 )
 
 USERNAME_MAX_SIZE = 32
diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py
--- a/liberapay/utils/__init__.py
+++ b/liberapay/utils/__init__.py
@@ -5,6 +5,7 @@
 from base64 import b64decode, b64encode
 from binascii import hexlify, unhexlify
 from datetime import date, datetime, timedelta
+from decimal import Decimal, InvalidOperation
 import errno
 import fnmatch
 from hashlib import sha256
@@ -22,7 +23,7 @@
 from markupsafe import Markup
 from postgres.cursors import SimpleCursorBase
 
-from liberapay.exceptions import AccountSuspended, AuthRequired, LoginRequired
+from liberapay.exceptions import AccountSuspended, AuthRequired, LoginRequired, InvalidNumber
 from liberapay.models.community import Community
 from liberapay.utils.i18n import Money
 from liberapay.website import website
@@ -362,3 +363,10 @@ def build_s3_object_url(key):
     sig_key = hmac.new(sig_key, b"aws4_request", sha256).digest()
     signature = hmac.new(sig_key, string_to_sign, sha256).hexdigest()
     return endpoint + "/" + key + "?" + querystring + "&X-Amz-Signature=" + signature
+
+
+def read_decimal_or_400(s):
+    try:
+        return Decimal(s)
+    except (InvalidOperation, ValueError):
+        raise InvalidNumber(s)


Test patch:
diff --git a/tests/py/test_tip_json.py b/tests/py/test_tip_json.py
--- a/tests/py/test_tip_json.py
+++ b/tests/py/test_tip_json.py
@@ -7,10 +7,8 @@
 
 class TestTipJson(Harness):
 
-    def tip(self, tipper, tippee, amount, period=None, raise_immediately=True):
-        data = {'amount': amount}
-        if period:
-            data['period'] = period
+    def tip(self, tipper, tippee, amount, period='weekly', raise_immediately=True):
+        data = {'amount': amount, 'period': period}
         return self.client.POST(
             "/%s/tip.json" % tippee,
             data,
diff --git a/tests/py/test_tips_json.py b/tests/py/test_tips_json.py
--- a/tests/py/test_tips_json.py
+++ b/tests/py/test_tips_json.py
@@ -14,8 +14,8 @@ def also_prune_variant(self, also_prune, tippees=1):
         test_tipper = self.make_participant("test_tipper")
 
         data = [
-            {'username': 'test_tippee1', 'amount': '1.00'},
-            {'username': 'test_tippee2', 'amount': '2.00'}
+            {'username': 'test_tippee1', 'amount': '1.00', 'period': 'weekly'},
+            {'username': 'test_tippee2', 'amount': '2.00', 'period': 'weekly'}
         ]
 
         response = self.client.POST('/test_tipper/tips.json',
@@ -27,7 +27,7 @@ def also_prune_variant(self, also_prune, tippees=1):
         assert response.code == 200
         assert len(json.loads(response.text)) == 2
 
-        data = [{'username': 'test_tippee2', 'amount': '1.00'}]
+        data = [{'username': 'test_tippee2', 'amount': '1.00', 'period': 'weekly'}]
         response = self.client.POST('/test_tipper/tips.json?also_prune=' + also_prune,
                                     body=json.dumps(data).encode('ascii'),
                                     content_type='application/json',
@@ -53,7 +53,7 @@ def test_get_response_with_tips(self):
         test_tipper = self.make_participant("test_tipper")
 
         response = self.client.POST('/test_tippee1/tip.json',
-                                    {'amount': '1.00'},
+                                    {'amount': '1.00', 'period': 'weekly'},
                                     auth_as=test_tipper,
                                     xhr=True,
                                     )