diff --git a/branch.sql b/branch.sql
new file mode 100644
index 0000000000..9842ef65c3
--- /dev/null
+++ b/branch.sql
@@ -0,0 +1,26 @@
+BEGIN;
+
+CREATE TABLE statements
+( participant bigint NOT NULL REFERENCES participants(id)
+, lang text NOT NULL
+, content text NOT NULL CHECK (content <> '')
+, UNIQUE (participant, lang)
+);
+
+INSERT INTO statements
+ SELECT id, 'en', concat('I am making the world better by ', statement)
+ FROM participants
+ WHERE statement <> ''
+ AND number = 'singular';
+
+INSERT INTO statements
+ SELECT id, 'en', concat('We are making the world better by ', statement)
+ FROM participants
+ WHERE statement <> ''
+ AND number = 'plural';
+
+CREATE FUNCTION enumerate(anyarray) RETURNS TABLE (rank bigint, value anyelement) AS $$
+ SELECT row_number() over() as rank, value FROM unnest($1) value;
+$$ LANGUAGE sql STABLE;
+
+END;
diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py
index 33d8b399fc..cbfec32dc6 100644
--- a/gratipay/models/participant.py
+++ b/gratipay/models/participant.py
@@ -208,9 +208,44 @@ def update_number(self, number):
# Statement
# =========
- def update_statement(self, statement):
- self.db.run("UPDATE participants SET statement=%s WHERE id=%s", (statement, self.id))
- self.set_attributes(statement=statement)
+ def get_statement(self, langs):
+ """Get the participant's statement in the language that best matches
+ the list provided.
+ """
+ return self.db.one("""
+ SELECT content, lang
+ FROM statements
+ JOIN enumerate(%(langs)s) langs ON langs.value = statements.lang
+ WHERE participant=%(id)s
+ ORDER BY langs.rank
+ LIMIT 1
+ """, dict(id=self.id, langs=langs), default=(None, None))
+
+ def get_statement_langs(self):
+ return self.db.all("SELECT lang FROM statements WHERE participant=%s",
+ (self.id,))
+
+ def upsert_statement(self, lang, statement):
+ if not statement:
+ self.db.run("DELETE FROM statements WHERE participant=%s AND lang=%s",
+ (self.id, lang))
+ return
+ r = self.db.one("""
+ UPDATE statements
+ SET content=%s
+ WHERE participant=%s
+ AND lang=%s
+ RETURNING true
+ """, (statement, self.id, lang))
+ if not r:
+ try:
+ self.db.run("""
+ INSERT INTO statements
+ (lang, content, participant)
+ VALUES (%s, %s, %s)
+ """, (lang, statement, self.id))
+ except IntegrityError:
+ return self.upsert_statement(lang, statement)
# Pricing
@@ -418,7 +453,7 @@ def clear_takes(self, cursor):
def clear_personal_information(self, cursor):
- """Clear personal information such as statement and goal.
+ """Clear personal information such as statements and goal.
"""
if self.IS_PLURAL:
self.remove_all_members(cursor)
@@ -433,10 +468,10 @@ def clear_personal_information(self, cursor):
);
DELETE FROM emails WHERE participant = %(username)s;
+ DELETE FROM statements WHERE participant=%(participant_id)s;
UPDATE participants
- SET statement=''
- , goal=NULL
+ SET goal=NULL
, anonymous_giving=False
, anonymous_receiving=False
, number='singular'
diff --git a/gratipay/utils/__init__.py b/gratipay/utils/__init__.py
index 0d6e7c7546..10a18e5a14 100644
--- a/gratipay/utils/__init__.py
+++ b/gratipay/utils/__init__.py
@@ -1,303 +1,19 @@
+# encoding: utf8
+
from __future__ import division
from datetime import datetime, timedelta
-import re
from aspen import Response
-from aspen.utils import typecheck, to_rfc822, utcnow
+from aspen.utils import to_rfc822, utcnow
import gratipay
from postgres.cursors import SimpleCursorBase
-from jinja2 import escape
-
-
-COUNTRIES = (
- ('AF', u'Afghanistan'),
- ('AX', u'\xc5land Islands'),
- ('AL', u'Albania'),
- ('DZ', u'Algeria'),
- ('AS', u'American Samoa'),
- ('AD', u'Andorra'),
- ('AO', u'Angola'),
- ('AI', u'Anguilla'),
- ('AQ', u'Antarctica'),
- ('AG', u'Antigua and Barbuda'),
- ('AR', u'Argentina'),
- ('AM', u'Armenia'),
- ('AW', u'Aruba'),
- ('AU', u'Australia'),
- ('AT', u'Austria'),
- ('AZ', u'Azerbaijan'),
- ('BS', u'Bahamas'),
- ('BH', u'Bahrain'),
- ('BD', u'Bangladesh'),
- ('BB', u'Barbados'),
- ('BY', u'Belarus'),
- ('BE', u'Belgium'),
- ('BZ', u'Belize'),
- ('BJ', u'Benin'),
- ('BM', u'Bermuda'),
- ('BT', u'Bhutan'),
- ('BO', u'Bolivia, Plurinational State of'),
- ('BQ', u'Bonaire, Sint Eustatius and Saba'),
- ('BA', u'Bosnia and Herzegovina'),
- ('BW', u'Botswana'),
- ('BV', u'Bouvet Island'),
- ('BR', u'Brazil'),
- ('IO', u'British Indian Ocean Territory'),
- ('BN', u'Brunei Darussalam'),
- ('BG', u'Bulgaria'),
- ('BF', u'Burkina Faso'),
- ('BI', u'Burundi'),
- ('KH', u'Cambodia'),
- ('CM', u'Cameroon'),
- ('CA', u'Canada'),
- ('CV', u'Cape Verde'),
- ('KY', u'Cayman Islands'),
- ('CF', u'Central African Republic'),
- ('TD', u'Chad'),
- ('CL', u'Chile'),
- ('CN', u'China'),
- ('CX', u'Christmas Island'),
- ('CC', u'Cocos (Keeling) Islands'),
- ('CO', u'Colombia'),
- ('KM', u'Comoros'),
- ('CG', u'Congo'),
- ('CD', u'Congo, The Democratic Republic of the'),
- ('CK', u'Cook Islands'),
- ('CR', u'Costa Rica'),
- ('CI', u"C\xf4te D'ivoire"),
- ('HR', u'Croatia'),
- ('CU', u'Cuba'),
- ('CW', u'Cura\xe7ao'),
- ('CY', u'Cyprus'),
- ('CZ', u'Czech Republic'),
- ('DK', u'Denmark'),
- ('DJ', u'Djibouti'),
- ('DM', u'Dominica'),
- ('DO', u'Dominican Republic'),
- ('EC', u'Ecuador'),
- ('EG', u'Egypt'),
- ('SV', u'El Salvador'),
- ('GQ', u'Equatorial Guinea'),
- ('ER', u'Eritrea'),
- ('EE', u'Estonia'),
- ('ET', u'Ethiopia'),
- ('FK', u'Falkland Islands (Malvinas)'),
- ('FO', u'Faroe Islands'),
- ('FJ', u'Fiji'),
- ('FI', u'Finland'),
- ('FR', u'France'),
- ('GF', u'French Guiana'),
- ('PF', u'French Polynesia'),
- ('TF', u'French Southern Territories'),
- ('GA', u'Gabon'),
- ('GM', u'Gambia'),
- ('GE', u'Georgia'),
- ('DE', u'Germany'),
- ('GH', u'Ghana'),
- ('GI', u'Gibraltar'),
- ('GR', u'Greece'),
- ('GL', u'Greenland'),
- ('GD', u'Grenada'),
- ('GP', u'Guadeloupe'),
- ('GU', u'Guam'),
- ('GT', u'Guatemala'),
- ('GG', u'Guernsey'),
- ('GN', u'Guinea'),
- ('GW', u'Guinea-bissau'),
- ('GY', u'Guyana'),
- ('HT', u'Haiti'),
- ('HM', u'Heard Island and McDonald Islands'),
- ('VA', u'Holy See (Vatican City State)'),
- ('HN', u'Honduras'),
- ('HK', u'Hong Kong'),
- ('HU', u'Hungary'),
- ('IS', u'Iceland'),
- ('IN', u'India'),
- ('ID', u'Indonesia'),
- ('IR', u'Iran, Islamic Republic of'),
- ('IQ', u'Iraq'),
- ('IE', u'Ireland'),
- ('IM', u'Isle of Man'),
- ('IL', u'Israel'),
- ('IT', u'Italy'),
- ('JM', u'Jamaica'),
- ('JP', u'Japan'),
- ('JE', u'Jersey'),
- ('JO', u'Jordan'),
- ('KZ', u'Kazakhstan'),
- ('KE', u'Kenya'),
- ('KI', u'Kiribati'),
- ('KP', u"Korea, Democratic People's Republic of"),
- ('KR', u'Korea, Republic of'),
- ('KW', u'Kuwait'),
- ('KG', u'Kyrgyzstan'),
- ('LA', u"Lao People's Democratic Republic"),
- ('LV', u'Latvia'),
- ('LB', u'Lebanon'),
- ('LS', u'Lesotho'),
- ('LR', u'Liberia'),
- ('LY', u'Libya'),
- ('LI', u'Liechtenstein'),
- ('LT', u'Lithuania'),
- ('LU', u'Luxembourg'),
- ('MO', u'Macao'),
- ('MK', u'Macedonia, The Former Yugoslav Republic of'),
- ('MG', u'Madagascar'),
- ('MW', u'Malawi'),
- ('MY', u'Malaysia'),
- ('MV', u'Maldives'),
- ('ML', u'Mali'),
- ('MT', u'Malta'),
- ('MH', u'Marshall Islands'),
- ('MQ', u'Martinique'),
- ('MR', u'Mauritania'),
- ('MU', u'Mauritius'),
- ('YT', u'Mayotte'),
- ('MX', u'Mexico'),
- ('FM', u'Micronesia, Federated States of'),
- ('MD', u'Moldova, Republic of'),
- ('MC', u'Monaco'),
- ('MN', u'Mongolia'),
- ('ME', u'Montenegro'),
- ('MS', u'Montserrat'),
- ('MA', u'Morocco'),
- ('MZ', u'Mozambique'),
- ('MM', u'Myanmar'),
- ('NA', u'Namibia'),
- ('NR', u'Nauru'),
- ('NP', u'Nepal'),
- ('NL', u'Netherlands'),
- ('NC', u'New Caledonia'),
- ('NZ', u'New Zealand'),
- ('NI', u'Nicaragua'),
- ('NE', u'Niger'),
- ('NG', u'Nigeria'),
- ('NU', u'Niue'),
- ('NF', u'Norfolk Island'),
- ('MP', u'Northern Mariana Islands'),
- ('NO', u'Norway'),
- ('OM', u'Oman'),
- ('PK', u'Pakistan'),
- ('PW', u'Palau'),
- ('PS', u'Palestinian Territory, Occupied'),
- ('PA', u'Panama'),
- ('PG', u'Papua New Guinea'),
- ('PY', u'Paraguay'),
- ('PE', u'Peru'),
- ('PH', u'Philippines'),
- ('PN', u'Pitcairn'),
- ('PL', u'Poland'),
- ('PT', u'Portugal'),
- ('PR', u'Puerto Rico'),
- ('QA', u'Qatar'),
- ('RE', u'R\xe9union'),
- ('RO', u'Romania'),
- ('RU', u'Russian Federation'),
- ('RW', u'Rwanda'),
- ('BL', u'Saint Barth\xe9lemy'),
- ('SH', u'Saint Helena, Ascension and Tristan Da Cunha'),
- ('KN', u'Saint Kitts and Nevis'),
- ('LC', u'Saint Lucia'),
- ('MF', u'Saint Martin (French Part)'),
- ('PM', u'Saint Pierre and Miquelon'),
- ('VC', u'Saint Vincent and the Grenadines'),
- ('WS', u'Samoa'),
- ('SM', u'San Marino'),
- ('ST', u'Sao Tome and Principe'),
- ('SA', u'Saudi Arabia'),
- ('SN', u'Senegal'),
- ('RS', u'Serbia'),
- ('SC', u'Seychelles'),
- ('SL', u'Sierra Leone'),
- ('SG', u'Singapore'),
- ('SX', u'Sint Maarten (Dutch Part)'),
- ('SK', u'Slovakia'),
- ('SI', u'Slovenia'),
- ('SB', u'Solomon Islands'),
- ('SO', u'Somalia'),
- ('ZA', u'South Africa'),
- ('GS', u'South Georgia and the South Sandwich Islands'),
- ('SS', u'South Sudan'),
- ('ES', u'Spain'),
- ('LK', u'Sri Lanka'),
- ('SD', u'Sudan'),
- ('SR', u'Suriname'),
- ('SJ', u'Svalbard and Jan Mayen'),
- ('SZ', u'Swaziland'),
- ('SE', u'Sweden'),
- ('CH', u'Switzerland'),
- ('SY', u'Syrian Arab Republic'),
- ('TW', u'Taiwan, Province of China'),
- ('TJ', u'Tajikistan'),
- ('TZ', u'Tanzania, United Republic of'),
- ('TH', u'Thailand'),
- ('TL', u'Timor-leste'),
- ('TG', u'Togo'),
- ('TK', u'Tokelau'),
- ('TO', u'Tonga'),
- ('TT', u'Trinidad and Tobago'),
- ('TN', u'Tunisia'),
- ('TR', u'Turkey'),
- ('TM', u'Turkmenistan'),
- ('TC', u'Turks and Caicos Islands'),
- ('TV', u'Tuvalu'),
- ('UG', u'Uganda'),
- ('UA', u'Ukraine'),
- ('AE', u'United Arab Emirates'),
- ('GB', u'United Kingdom'),
- ('US', u'United States'),
- ('UM', u'United States Minor Outlying Islands'),
- ('UY', u'Uruguay'),
- ('UZ', u'Uzbekistan'),
- ('VU', u'Vanuatu'),
- ('VE', u'Venezuela, Bolivarian Republic of'),
- ('VN', u'Viet Nam'),
- ('VG', u'Virgin Islands, British'),
- ('VI', u'Virgin Islands, U.S.'),
- ('WF', u'Wallis and Futuna'),
- ('EH', u'Western Sahara'),
- ('YE', u'Yemen'),
- ('ZM', u'Zambia'),
- ('ZW', u'Zimbabwe'),
-)
-COUNTRIES_MAP = dict(COUNTRIES)
+
# Difference between current time and credit card expiring date when
# card is considered as expiring
EXPIRING_DELTA = timedelta(days = 30)
-def wrap(u):
- """Given a unicode, return a unicode.
- """
- typecheck(u, unicode)
- linkified = linkify(u) # Do this first, because it calls xthml_escape.
- out = linkified.replace(u'\r\n', u'
\r\n').replace(u'\n', u'
\n')
- return out if out else '...'
-
-
-def linkify(u):
- escaped = unicode(escape(u))
-
- urls = re.compile(r"""
- ( # capture the entire URL
- (?:(https?://)|www\.) # capture the protocol or match www.
- [\w\d.-]*\w # the domain
- (?:/ # the path
- (?:\S*\(
- \S*[^\s.,;:'\"]|
- \S*[^\s.,;:'\"()]
- )*
- )?
- )
- """, re.VERBOSE|re.MULTILINE|re.UNICODE|re.IGNORECASE)
-
- return urls.sub(lambda m:
- '%s' % (
- m.group(1) if m.group(2) else 'http://'+m.group(1), m.group(1)
- )
- , escaped)
-
def dict_to_querystring(mapping):
if not mapping:
@@ -323,10 +39,6 @@ def canonicalize(path, base, canonical, given, arguments=None):
raise Response(302, headers={"Location": newpath})
-def plural(i, singular="", plural="s"):
- return singular if i == 1 else plural
-
-
def get_participant(request, restrict=True, resolve_unclaimed=True):
"""Given a Request, raise Response or return Participant.
@@ -417,17 +129,12 @@ def format_money(money):
return format % money
-def to_statement(prepend, string, length=140, append='...'):
- if prepend and string:
- statement = prepend.format(string)
- if len(string) > length:
- return statement[:length] + append
- elif len(string) > 0:
- return statement
- else:
- return string
- else:
+def excerpt_intro(text, length=175, append=u'…'):
+ if not text:
return ''
+ if len(text) > length:
+ return text[:length] + append
+ return text
def is_card_expiring(expiration_year, expiration_month):
diff --git a/gratipay/utils/fake_data.py b/gratipay/utils/fake_data.py
index 7366c8246e..13a07bcbf9 100644
--- a/gratipay/utils/fake_data.py
+++ b/gratipay/utils/fake_data.py
@@ -65,7 +65,6 @@ def fake_participant(db, number="singular", is_admin=False):
, id=fake_int_id()
, username=username
, username_lower=username.lower()
- , statement=fake_sentence()
, ctime=faker.date_time_this_year()
, is_admin=is_admin
, balance=fake_balance()
diff --git a/gratipay/utils/i18n.py b/gratipay/utils/i18n.py
index 3e746dc768..739d061981 100644
--- a/gratipay/utils/i18n.py
+++ b/gratipay/utils/i18n.py
@@ -7,20 +7,62 @@
from aspen.resources.pagination import parse_specline, split_and_escape
from aspen.utils import utcnow
-from babel.core import LOCALE_ALIASES
+from babel.core import LOCALE_ALIASES, Locale
from babel.dates import format_timedelta
from babel.messages.extract import extract_python
+from babel.messages.pofile import Catalog
from babel.numbers import (
format_currency, format_decimal, format_number, format_percent,
get_decimal_symbol, parse_decimal
)
+from collections import OrderedDict
import jinja2.ext
ALIASES = {k: v.lower() for k, v in LOCALE_ALIASES.items()}
ALIASES_R = {v: k for k, v in ALIASES.items()}
+
+
+def strip_accents(s):
+ return ''.join(c for c in normalize('NFKD', s) if not combining(c))
+
+
+def make_sorted_dict(keys, d):
+ items = ((k, d[k]) for k in keys)
+ return OrderedDict(sorted(items, key=lambda t: strip_accents(t[1])))
+
+
+COUNTRY_CODES = """
+ AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ
+ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR
+ CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR
+ GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU
+ ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ
+ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ
+ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF
+ PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI
+ SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR
+ TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW
+""".split()
+
+COUNTRIES = make_sorted_dict(COUNTRY_CODES, Locale('en').territories)
+
+LANGUAGE_CODES_2 = """
+ aa af ak am ar as az be bg bm bn bo br bs ca cs cy da de dz ee el en eo es
+ et eu fa ff fi fo fr ga gd gl gu gv ha he hi hr hu hy ia id ig ii is it ja
+ ka ki kk kl km kn ko ks kw ky lg ln lo lt lu lv mg mk ml mn mr ms mt my nb
+ nd ne nl nn nr om or os pa pl ps pt rm rn ro ru rw se sg si sk sl sn so sq
+ sr ss st sv sw ta te tg th ti tn to tr ts uk ur uz ve vi vo xh yo zh zu
+""".split()
+
+LANGUAGES_2 = make_sorted_dict(LANGUAGE_CODES_2, Locale('en').languages)
+
LOCALES = {}
-LOCALE_EN = None
+LOCALE_EN = LOCALES['en'] = Locale('en')
+LOCALE_EN.catalog = Catalog('en')
+LOCALE_EN.catalog.plural_func = lambda n: n != 1
+LOCALE_EN.countries = COUNTRIES
+LOCALE_EN.languages_2 = LANGUAGES_2
ternary_re = re.compile(r'^\(? *(.+?) *\? *(.+?) *: *(.+?) *\)?$')
@@ -100,10 +142,9 @@ def regularize_locales(locales):
if alias and alias not in locales_set:
# Insert "fr_fr" after "fr" if it's not somewhere in the list
yield alias
-
-
-def strip_accents(s):
- return ''.join(c for c in normalize('NFKD', s) if not combining(c))
+ if 'en' not in locales_set and 'en_us' not in locales_set:
+ yield 'en'
+ yield 'en_us'
def parse_accept_lang(accept_lang):
@@ -128,7 +169,7 @@ def format_currency_with_options(number, currency, locale='en', trailing_zeroes=
def set_up_i18n(website, request):
accept_lang = request.headers.get("Accept-Language", "")
- langs = request.accept_langs = parse_accept_lang(accept_lang)
+ langs = request.accept_langs = list(parse_accept_lang(accept_lang))
loc = match_lang(langs)
add_helpers_to_context(website.tell_sentry, request.context, loc, request)
diff --git a/gratipay/wireup.py b/gratipay/wireup.py
index 7898f04724..a3751e68d5 100644
--- a/gratipay/wireup.py
+++ b/gratipay/wireup.py
@@ -9,7 +9,7 @@
import aspen
from aspen.testing.client import Client
from babel.core import Locale
-from babel.messages.pofile import Catalog, read_po
+from babel.messages.pofile import read_po
from babel.numbers import parse_pattern
import balanced
import gratipay
@@ -30,10 +30,12 @@
from gratipay.models.community import Community
from gratipay.models.participant import Participant
from gratipay.models import GratipayDB
-from gratipay.utils import COUNTRIES, COUNTRIES_MAP, i18n
from gratipay.utils.cache_static import asset_etag
from gratipay.utils.emails import compile_email_spt
-from gratipay.utils.i18n import ALIASES, ALIASES_R, get_function_from_rule, strip_accents
+from gratipay.utils.i18n import (
+ ALIASES, ALIASES_R, COUNTRIES, LANGUAGES_2, LOCALES,
+ get_function_from_rule, make_sorted_dict
+)
def canonical(env):
gratipay.canonical_scheme = env.canonical_scheme
@@ -265,9 +267,8 @@ def clean_assets(website):
def load_i18n(project_root, tell_sentry):
# Load the locales
- key = lambda t: strip_accents(t[1])
localeDir = os.path.join(project_root, 'i18n', 'core')
- locales = i18n.LOCALES
+ locales = LOCALES
for file in os.listdir(localeDir):
try:
parts = file.split(".")
@@ -279,21 +280,16 @@ def load_i18n(project_root, tell_sentry):
c = l.catalog = read_po(f)
c.plural_func = get_function_from_rule(c.plural_expr)
try:
- l.countries_map = {k: l.territories[k] for k in COUNTRIES_MAP}
- l.countries = sorted(l.countries_map.items(), key=key)
+ l.countries = make_sorted_dict(COUNTRIES, l.territories)
except KeyError:
- l.countries_map = COUNTRIES_MAP
l.countries = COUNTRIES
+ try:
+ l.languages_2 = make_sorted_dict(LANGUAGES_2, l.languages)
+ except KeyError:
+ l.languages_2 = LANGUAGES_2
except Exception as e:
tell_sentry(e)
- # Add the default English locale
- locale_en = i18n.LOCALE_EN = locales['en'] = Locale('en')
- locale_en.catalog = Catalog('en')
- locale_en.catalog.plural_func = lambda n: n != 1
- locale_en.countries = COUNTRIES
- locale_en.countries_map = COUNTRIES_MAP
-
# Add aliases
for k, v in list(locales.items()):
locales.setdefault(ALIASES.get(k, k), v)
diff --git a/js/gratipay/account.js b/js/gratipay/account.js
index c0b5ea3192..f0ac441895 100644
--- a/js/gratipay/account.js
+++ b/js/gratipay/account.js
@@ -44,54 +44,39 @@ Gratipay.account.init = function() {
// Wire up username knob.
// ======================
- $('form.username button.edit').click(function(e) {
- e.preventDefault();
- e.stopPropagation();
- $('.username button.edit').hide();
- $('.username button.save').show();
- $('.username button.cancel').show();
- $('.username span.view').hide();
- $('.username input').show().focus();
- $('.username .warning').show();
- return false;
+ Gratipay.forms.jsEdit({
+ hideEditButton: true,
+ root: $('.username.js-edit'),
+ success: function(d) {
+ window.location.href = "/" + encodeURIComponent(d.username) + "/account/";
+ return false;
+ },
});
- $('form.username').submit(function(e) {
- e.preventDefault();
- $('#save-username').css('opacity', 0.5);
- function success(d) {
- window.location.href = "/" + encodeURIComponent(d.username) + "/";
- }
- function error(e) {
- $('#save-username').css('opacity', 1);
- Gratipay.notification(JSON.parse(e.responseText).error_message_long, 'error');
- }
+ // Wire up account type knob.
+ // ==========================
+
+ $('.number input').click(function() {
+ var $input = $(this);
jQuery.ajax(
- { url: "../username.json"
- , type: "POST"
- , dataType: 'json'
- , data: { username: $('input[name=username]').val() }
- , success: success
- , error: error
- }
- );
- return false;
- });
- $('.username button.cancel').click(function(e) {
- e.preventDefault();
- e.stopPropagation();
- finish_editing_username();
- return false;
+ { url: '../number.json'
+ , type: 'POST'
+ , data: {number: $input.val()}
+ , success: function(data) {
+ Gratipay.notification("Your account type has been changed.", 'success');
+ if (data.number === 'plural') {
+ $("#members-button").removeClass("hidden");
+ } else {
+ $("#members-button").addClass("hidden");
+ }
+ }
+ , error: function(r) {
+ $input.prop('checked', false);
+ Gratipay.notification(JSON.parse(r.responseText).error_message_long, 'error');
+ }
+ });
});
- function finish_editing_username() {
- $('.username button.edit').show();
- $('.username button.save').hide();
- $('.username button.cancel').hide();
- $('.username span.view').show();
- $('.username input').hide();
- $('.username .warning').hide();
- }
// Wire up aggregate giving knob.
diff --git a/js/gratipay/forms.js b/js/gratipay/forms.js
index eec5ed2482..b6e11133d9 100644
--- a/js/gratipay/forms.js
+++ b/js/gratipay/forms.js
@@ -77,3 +77,68 @@ Gratipay.forms.initCSRF = function() { // https://docs.djangoproject.com/en/de
}
});
};
+
+Gratipay.forms.jsEdit = function(params) {
+
+ var $root = $(params.root);
+ var $form = $root.find('form.edit');
+ var $view = $root.find('.view');
+ var $editButton = $root.find('button.edit');
+
+ $form.find('button').attr('type', 'button');
+ $form.find('button.save').attr('type', 'submit');
+
+ $editButton.prop('disabled', false);
+ $editButton.click(function(e) {
+ if (params.hideEditButton) $editButton.hide();
+ else $editButton.prop('disabled', true);
+ $form.css('display', $form.data('display') || 'block');
+ $view.hide();
+ });
+
+ function finish_editing() {
+ $editButton.show().prop('disabled', false);
+ $form.hide();
+ $view.show();
+ }
+ $root.find('button.cancel').click(finish_editing);
+
+ function post(e, confirmed) {
+ e.preventDefault();
+
+ var data = $form.serializeArray();
+ if (confirmed) data.push({name: 'confirmed', value: true});
+
+ var $inputs = $form.find(':not(:disabled)');
+ $inputs.prop('disabled', true);
+
+ $.ajax({
+ url: $form.attr('action'),
+ type: 'POST',
+ data: data,
+ dataType: 'json',
+ success: function (d) {
+ $inputs.prop('disabled', false);
+ if (d.confirm) {
+ if (confirm(d.confirm)) return post(e, true);
+ return;
+ }
+ var r = (params.success || function () {
+ if (d.html || d.html === '') {
+ $view.html(d.html);
+ if (d.html === '') window.location.reload();
+ }
+ }).call(this, d);
+ if (r !== false) finish_editing();
+ },
+ error: params.error || function (e) {
+ $inputs.prop('disabled', false);
+ error_message = JSON.parse(e.responseText).error_message_long;
+ Gratipay.notification(error_message || "Failure", 'error');
+ },
+ });
+ }
+
+ $form.on('submit', post);
+
+};
diff --git a/js/gratipay/profile.js b/js/gratipay/profile.js
index a73ab36152..84ec48a783 100644
--- a/js/gratipay/profile.js
+++ b/js/gratipay/profile.js
@@ -1,238 +1,88 @@
Gratipay.profile = {};
-Gratipay.profile.toNumber = function(number) {
- if (number == 'plural')
- Gratipay.profile.toPlural();
- else if (number == 'singular')
- Gratipay.profile.toSingular();
-};
-
-Gratipay.profile.toPlural = function() {
- $('.i-am').text('We are');
- $('.i-m').text("We're");
- $('.my').text("Our");
-};
-
-Gratipay.profile.toSingular = function() {
- $('.i-am').text('I am');
- $('.i-m').text("I'm");
- $('.my').text("My");
-};
-
Gratipay.profile.init = function() {
- ////////////////////////////////////////////////////////////
- // /
- // XXX This is ripe for refactoring. I ran out of steam. :-/
- // /
- ////////////////////////////////////////////////////////////
// Wire up textarea for statement.
// ===============================
$('textarea').focus();
- function start_editing_statement() {
- var h = $('.statement div.view').height();
- h = Math.max(h, 128);
- $('.statement textarea').height(h);
- $('.statement button.edit').hide();
- $('.statement button.save').show();
- $('.statement button.cancel').show();
- $('.statement div.view').hide();
- $('.statement div.edit').show(0, function() {
- $('.statement textarea').focus();
- });
- }
-
- function update_members_button(is_plural) {
- if (is_plural) {
- $("#members-button").removeClass("hidden")
- } else {
- $("#members-button").addClass("hidden")
- }
- }
+ Gratipay.forms.jsEdit({root: $('.statement.js-edit')});
- if ($('.statement textarea').val() === '') {
- start_editing_statement();
- }
- $('.statement button.edit').click(function(e) {
- e.preventDefault();
- e.stopPropagation();
- start_editing_statement();
- return false;
+ $('.statement textarea').on('change keyup paste', function(){
+ var changed = $(this).val() !== $(this).data('original');
+ $('.statement button.save').prop('disabled', !changed);
});
- $('form.statement').submit(function(e) {
- e.preventDefault();
-
- var is_plural = jQuery("#statement-select").val() === "plural";
- $('.statement button.save').css('opacity', 0.5);
- function success(d) {
- $('.statement .view').html(d.statement);
- var number = $('.statement select').val();
- Gratipay.profile.toNumber(number);
- finish_editing_statement();
- update_members_button(is_plural);
- }
- function error(e) {
- $('.statement button.save').css('opacity', 1);
- Gratipay.notification(JSON.parse(e.responseText).error_message_long, 'error');
+ $('.statement select.langs').on('change', function(e){
+ var $form = $('.statement form');
+ var $inputs = $form.find('button, input, select, textarea').filter(':not(:disabled)');
+ var $textarea = $form.find('textarea');
+ var $this = $(this);
+ var lang = $this.val();
+ if ($textarea.val() !== $textarea.data('original')) {
+ if(!confirm($textarea.data('confirm-discard'))) {
+ $this.val($this.data('value'));
+ return;
+ }
}
- jQuery.ajax(
- { url: "statement.json"
- , type: "POST"
- , success: success
- , error: error
- , data: { statement: $('.statement textarea').val()
- , number: $('.statement select').val()
- }
- }
- );
- return false;
+ $inputs.prop('disabled', true);
+ $textarea.val("Loading...");
+ $.ajax({
+ url: 'statement.json',
+ data: {lang: lang},
+ dataType: 'json',
+ success: function (d) {
+ $inputs.prop('disabled', false);
+ $textarea.attr('lang', lang);
+ $textarea.val(d.content || "");
+ $textarea.data('original', $textarea.val());
+ $form.find('button.save').prop('disabled', true);
+ $this.data('value', lang);
+ },
+ error: function (e) {
+ $inputs.prop('disabled', false);
+ error_message = JSON.parse(e.responseText).error_message_long;
+ Gratipay.notification(error_message || "Failure", 'error');
+ },
+ });
});
- $('.statement button.cancel').click(function(e) {
- e.preventDefault();
- e.stopPropagation();
- finish_editing_statement();
- return false;
+
+ $('.statement button.edit').on('click', function(){
+ $('.statement textarea').val('').data('original', '');
+ $('.statement select.langs').change();
});
- function finish_editing_statement() {
- $('.statement button.edit').show();
- $('.statement button.save').hide().css('opacity', 1);
- $('.statement button.cancel').hide();
- $('.statement div.view').show();
- $('.statement div.edit').hide();
- $('.statement .warning').hide();
- }
+ if ($('.statement div.view').html().trim() === '') {
+ $('.statement button.edit').click();
+ }
// Wire up goal knob.
// ==================
- $('.goal button.edit').click(function(e) {
- e.preventDefault();
- e.stopPropagation();
- $('.goal div.view').hide();
- $('.goal table.edit').show();
- $('.goal button.edit').hide();
- $('.goal button.save').show();
- $('.goal button.cancel').show();
- return false;
- });
- $('form.goal').submit(function(e) {
- e.preventDefault();
-
- var goal = $('input[name=goal]:checked');
-
- if(goal.val() === '-1') {
- var r = confirm(
- 'Warning: Doing this will remove all the tips you are currently receiving.\n\n'+
- 'That cannot be undone!'
- );
- if(!r) return;
- }
-
- $('.goal button.save').css('opacity', 0.5);
-
- function success(d) {
+ Gratipay.forms.jsEdit({
+ root: $('.goal.js-edit'),
+ success: function(d) {
+ var goal = $('input[name=goal]:checked');
var label = $('label[for=' + goal.attr('id') + ']');
- var newtext = '';
- if (label.length === 1)
- newtext = label.html();
- else
- { // custom goal is wonky
- newtext = label.html();
- newtext = newtext.replace('$', '$' + d.goal);
- newtext += $(label.get(1)).html();
- }
-
- if (parseFloat(d.goal) > 0)
- $('input[name=goal_custom]').val(d.goal);
+ var newtext = label.text().replace('$', d.goal_l);
$('.goal div.view').html(newtext);
- finish_editing_goal();
- }
- jQuery.ajax(
- { url: "goal.json"
- , type: "POST"
- , dataType: 'json'
- , data: { goal: goal.val()
- , goal_custom: $('[name=goal_custom]').val()
- }
- , success: success
- , error: function() {
- $('.goal button.save').css('opacity', 1);
- Gratipay.notification("Failed to change your funding goal. Please try again.", 'error');
- }
- }
- );
- return false;
- });
- $('.goal button.cancel').click(function(e) {
- e.preventDefault();
- e.stopPropagation();
- finish_editing_goal();
- return false;
+ },
});
- function finish_editing_goal() {
- $('.goal div.view').show();
- $('.goal table.edit').hide();
- $('.goal button.edit').show();
- $('.goal button.save').hide().css('opacity', 1);
- $('.goal button.cancel').hide();
- }
// Wire up bitcoin input.
// ======================
- $('.toggle-bitcoin').on("click", function() {
- $('.bitcoin').toggle();
- $('.toggle-bitcoin').hide();
- $('input.bitcoin').focus();
+ Gratipay.forms.jsEdit({
+ root: $('tr.bitcoin.js-edit'),
+ success: function(){ window.location.reload(); return false; },
});
- $('.bitcoin-submit')
- .on('click', '[type=submit]', function () {
- var $this = $(this);
- $this.css('opacity', 0.5);
- function success(d) {
- $('.bitcoin a.address').text(d.bitcoin_address);
- $('.toggle-bitcoin').show();
- $('.bitcoin').toggle();
- if (d.bitcoin_address === '') {
- $('.toggle-bitcoin').text('+ Add');
- $('.bitcoin .address').attr('href', '');
- } else {
- $('.toggle-bitcoin').text('Edit');
- $('.bitcoin .address').attr('href', 'https://blockchain.info/address/'+d.bitcoin_address);
- }
- $this.css('opacity', 1);
- }
-
- jQuery.ajax({
- url: "bitcoin.json",
- type: "POST",
- dataType: 'json',
- success: success,
- error: function () {
- $this.css('opacity', 1);
- Gratipay.notification("Invalid Bitcoin address. Please try again.", 'error');
- },
- data: {
- bitcoin_address: $('input.bitcoin').val()
- }
- }
- )
+ // Wire up account deletion.
+ // =========================
- return false;
- })
- .on('click', '[type=cancel]', function () {
- $('.bitcoin').toggle();
- $('.toggle-bitcoin').show();
-
- return false;
- });
$('.account-delete').on('click', function () {
var $this = $(this);
diff --git a/scss/modules.scss b/scss/modules.scss
index 54fa8e5016..a36e5d4701 100644
--- a/scss/modules.scss
+++ b/scss/modules.scss
@@ -285,26 +285,6 @@ ul.memberships {
font-size: 25px;
}
-#dnt {
- width: 100%
-}
-
-#dnt tr {
- border-top: 1px solid #DDD;
- border-bottom: 1px solid #DDD;
- width: 100%
-}
-
-td.dnt-label {
- height: 36pt;
- margin-bottom: 3px;
-}
-
-td.dnt-value {
- text-align: right;
- padding-right: 8px;
-}
-
.emails {
.label-primary, .set-primary { display: none; }
.verified {
diff --git a/scss/profile-edit.scss b/scss/profile-edit.scss
index 95ca76914e..64f6125539 100644
--- a/scss/profile-edit.scss
+++ b/scss/profile-edit.scss
@@ -1,48 +1,35 @@
#profile-edit {
- button.save, button.cancel {
+ .js-edit form.edit {
display: none;
}
-
.username {
input {
width: 6em;
- display: none;
}
.warning {
margin-top: 5px;
display: block;
color: red;
- display: none;
}
}
.statement {
- div.edit {
- display: none;
- }
textarea {
width: 98%;
- height: 126pt; /* 18pt * 7 rows; overriden in js */
padding: 1%;
- margin-top: 8px;
+ resize: vertical;
+ }
+ textarea, select.langs {
+ margin-top: 6px;
}
.help {
font: normal 12px/12px $Ideal;
}
}
.goal {
- table.edit {
- display: none;
- }
- td {
- text-align: left;
- }
#goal-custom {
text-align: right;
width: 6pc;
}
- li {
- margin-bottom: 1em;
- }
}
.api-key {
span {
@@ -57,7 +44,7 @@
}
}
}
- .email-submit, .bitcoin-submit {
+ .email-submit {
.address input {
width: 100%;
margin: 5px 0;
diff --git a/templates/connected-accounts.html b/templates/connected-accounts.html
index d72d57f352..31b4d6363d 100644
--- a/templates/connected-accounts.html
+++ b/templates/connected-accounts.html
@@ -27,29 +27,31 @@