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 @@

{{ _("Other Giving Options") }}

{% if participant.bitcoin_address or own_account %} {% set addr = participant.bitcoin_address or '' %} {% set url = 'https://blockchain.info/address/'+addr if addr else 'javascript:;' %} - + -
+
{{ addr }}
-
+ {% if own_account %} +
- +
- - + +
+ {% endif %}
Bitcoin
{% if own_account %} - {% endif %} diff --git a/templates/profile-edit.html b/templates/profile-edit.html index 1517e601de..3194944d1d 100644 --- a/templates/profile-edit.html +++ b/templates/profile-edit.html @@ -1,35 +1,38 @@
-
+

{{ _("Statement") }} - - - +

-
- + {% for code, name in select_langs.items() %} + + {% endfor %} - - {{ _("Markdown supported.") }} + +

{{ _("Markdown supported.") }} {{ _("What is markdown?") }} ({{ _("View source.") }}) - -

+

+ + +
- {{ markdown.render(MAKING.format(participant.statement)) }} + {{ markdown.render(statement) if statement }}
- +
@@ -48,11 +51,9 @@

{{ _("Communities") }}

-
+

{{ _("Funding Goal") }} - -

{% if user.participant.goal > 0 %} @@ -65,9 +66,8 @@

{{ _("Funding Goal") }} {{ PATRON_NO_GOAL }} {% endif %}

-
- - + 0 %} checked="true"{% endif %}/> {{ _("Funding Goal") }}
-
-
- -
- +

+ + + + +
{% include "templates/charts-for-user.html" %} diff --git a/tests/py/test_close.py b/tests/py/test_close.py index c3e6eefab5..6d168d3464 100644 --- a/tests/py/test_close.py +++ b/tests/py/test_close.py @@ -279,7 +279,6 @@ def test_ctr_clears_multiple_tips_receiving(self): @mock.patch.object(Participant, '_mailer') def test_cpi_clears_personal_information(self, mailer): alice = self.make_participant( 'alice' - , statement='not forgetting to be awesome!' , goal=100 , anonymous_giving=True , anonymous_receiving=True @@ -293,13 +292,14 @@ def test_cpi_clears_personal_information(self, mailer): , receiving=40 , npatrons=21 ) + alice.upsert_statement('en', 'not forgetting to be awesome!') alice.add_email('alice@example.net') with self.db.get_cursor() as cursor: alice.clear_personal_information(cursor) new_alice = Participant.from_username('alice') - assert alice.statement == new_alice.statement == '' + assert alice.get_statement(['en']) == (None, None) assert alice.goal == new_alice.goal == None assert alice.anonymous_giving == new_alice.anonymous_giving == False assert alice.anonymous_receiving == new_alice.anonymous_receiving == False diff --git a/tests/py/test_goal_json.py b/tests/py/test_goal_json.py index 4fafd1c184..7621119687 100644 --- a/tests/py/test_goal_json.py +++ b/tests/py/test_goal_json.py @@ -27,23 +27,23 @@ def test_participant_can_set_their_goal_to_null(self): def test_participant_can_set_their_goal_to_zero(self): response = self.change_goal("0") - actual = json.loads(response.body)['goal'] - assert actual == "0" + actual = json.loads(response.body)['goal_l'] + assert actual == "$0" def test_participant_can_set_their_goal_to_a_custom_amount(self): response = self.change_goal("custom", "100.00") - actual = json.loads(response.body)['goal'] - assert actual == "100" + actual = json.loads(response.body)['goal_l'] + assert actual == "$100" def test_custom_amounts_can_include_comma(self): response = self.change_goal("custom", "1,100.00") - actual = json.loads(response.body)['goal'] - assert actual == "1,100" + actual = json.loads(response.body)['goal_l'] + assert actual == "$1,100" def test_wonky_custom_amounts_are_standardized(self): response = self.change_goal("custom", ",100,100.00000") - actual = json.loads(response.body)['goal'] - assert actual == "100,100" + actual = json.loads(response.body)['goal_l'] + assert actual == "$100,100" def test_anonymous_gets_404(self): response = self.change_goal("100.00", auth_as=None, expecting_error=True) diff --git a/tests/py/test_number_json.py b/tests/py/test_number_json.py new file mode 100644 index 0000000000..49a58671df --- /dev/null +++ b/tests/py/test_number_json.py @@ -0,0 +1,27 @@ +from __future__ import print_function, unicode_literals + +import json + +from gratipay.testing import Harness + + +class Tests(Harness): + + def change_number(self, number, auth_as='alice', expecting_error=False): + self.make_participant('alice', claimed_time='now') + + method = self.client.POST if not expecting_error else self.client.PxST + response = method( "/alice/number.json" + , {'number': number} + , auth_as=auth_as + ) + return response + + def test_participant_can_change_their_number(self): + response = self.change_number('plural') + actual = json.loads(response.body)['number'] + assert actual == 'plural' + + def test_invalid_is_400(self): + response = self.change_number('none', expecting_error=True) + assert response.code == 400, response.code diff --git a/tests/py/test_pages.py b/tests/py/test_pages.py index 87bfa275ac..6bcc64d955 100644 --- a/tests/py/test_pages.py +++ b/tests/py/test_pages.py @@ -144,3 +144,8 @@ def test_giving_page_shows_cancelled(self): expected2 = "cancelled 1 tip" assert expected1 in actual assert expected2 in actual + + def test_new_participant_can_edit_profile(self): + self.make_participant('alice', claimed_time='now') + body = self.client.GET("/alice/", auth_as="alice").body + assert b'Edit' in body diff --git a/tests/py/test_statement_json.py b/tests/py/test_statement_json.py index ef370d36c8..e92aed3912 100644 --- a/tests/py/test_statement_json.py +++ b/tests/py/test_statement_json.py @@ -7,35 +7,26 @@ class Tests(Harness): - def change_statement(self, statement, number='singular', auth_as='alice', + def change_statement(self, lang, statement, auth_as='alice', expecting_error=False): - self.make_participant('alice') + self.make_participant('alice', claimed_time='now') method = self.client.POST if not expecting_error else self.client.PxST response = method( "/alice/statement.json" - , {'statement': statement, 'number': number} + , {'lang': lang, 'content': statement} , auth_as=auth_as ) return response def test_participant_can_change_their_statement(self): - response = self.change_statement('being awesome.') - actual = json.loads(response.body)['statement'] - assert actual == '

I am making the world better by being awesome.

\n' - - def test_participant_can_change_their_number(self): - response = self.change_statement('', 'plural') - actual = json.loads(response.body)['number'] - assert actual == 'plural' - - def test_anonymous_gets_404(self): - response = self.change_statement( 'being awesome.' - , 'singular' + response = self.change_statement('en', 'Lorem ipsum') + actual = json.loads(response.body)['html'] + assert actual == '

Lorem ipsum

\n' + + def test_anonymous_gets_403(self): + response = self.change_statement( 'en' + , 'Some statement' , auth_as=None , expecting_error=True ) - assert response.code == 404, response.code - - def test_invalid_is_400(self): - response = self.change_statement('', 'none', expecting_error=True) - assert response.code == 400, response.code + assert response.code == 403, response.code diff --git a/tests/py/test_utils.py b/tests/py/test_utils.py index aab87e14fb..ac618da521 100644 --- a/tests/py/test_utils.py +++ b/tests/py/test_utils.py @@ -47,26 +47,6 @@ def test_dict_to_querystring_converts_empty_dict_to_querystring(self): actual = utils.dict_to_querystring({}) assert actual == expected - def test_linkify_linkifies_url_with_www(self): - expected = 'http://www.example.com' - actual = utils.linkify('http://www.example.com') - assert actual == expected - - def test_linkify_linkifies_url_without_www(self): - expected = 'http://example.com' - actual = utils.linkify('http://example.com') - assert actual == expected - - def test_linkify_linkifies_url_with_uppercase_letters(self): - expected = 'Http://Www.Example.Com' - actual = utils.linkify('Http://Www.Example.Com') - assert actual == expected - - def test_linkify_works_without_protocol(self): - expected = 'www.example.com' - actual = utils.linkify('www.example.com') - assert actual == expected - def test_short_difference_is_expiring(self): expiring = datetime.utcnow() + timedelta(days = 1) expiring = utils.is_card_expiring(expiring.year, expiring.month) diff --git a/www/%username/account/close.spt b/www/%username/account/close.spt index ff3aece7d0..206af451ba 100644 --- a/www/%username/account/close.spt +++ b/www/%username/account/close.spt @@ -106,12 +106,11 @@ if POST: href="https://github.com/gratipay/gratipay.com/issues/397">data retention policy).

-

Things we clear immediately include your “making the - world better” statement, any funding goal, the tips - you're receiving, and those you're giving. You'll also be - removed from any communities and teams you were a part of. If - you're closing a team account, all team members will be removed - from the team.

+

Things we clear immediately include your profile statement, + any funding goal, the tips you're receiving, and those you're + giving. You'll also be removed from any communities and teams + you were a part of. If you're closing a team account, all team + members will be removed from the team.

We specifically don't delete your past giving and receiving history on the site, because that information also diff --git a/www/%username/account/index.html.spt b/www/%username/account/index.html.spt index 425dc2abf2..f25ac0f943 100644 --- a/www/%username/account/index.html.spt +++ b/www/%username/account/index.html.spt @@ -25,35 +25,29 @@ emails = participant.get_emails()

- -
+

{{ _("You are {0}", "" + escape(participant.username) + "") }} - - - - + + + + + + + {{ _("Have you linked to your Gratipay profile from other websites? Be sure to update those links!") }} + +

+
-

- {{ _("Have you linked to your Gratipay profile from other websites? Be sure to update those links!") }} -

+

{{ _("Account type") }}

+
+
+
-

- -
- {% if participant.IS_SINGULAR %} - - {% endif %} -

+ {% if not user.ANON and (user.ADMIN or user.participant == participant) %}

{{ _("Available Money") }}

@@ -169,7 +163,6 @@ emails = participant.get_emails() -

{{ _("API Key") }}

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
@@ -182,22 +175,30 @@ emails = participant.get_emails()

{{ _("Privacy") }}

-
- - - - -
- {{ _("Do Not Track") }} - - {% if request.headers.get('DNT') == '1' %} - {{ _("On").upper() }} - {% else %} - {{ _("Off").upper() }} - {% endif %} -
+

+ +
+ {% if participant.IS_SINGULAR %} + + {% endif %} +

+

+ {{ _("Do Not Track") }}: + {% if request.headers.get('DNT') == '1' %} + {{ _("On").upper() }} + {% else %} + {{ _("Off").upper() }} + {% endif %} +

-

{{ _("Close") }}

diff --git a/www/%username/bitcoin.json.spt b/www/%username/bitcoin.json.spt index f5a03c9302..3b510c8863 100644 --- a/www/%username/bitcoin.json.spt +++ b/www/%username/bitcoin.json.spt @@ -13,7 +13,7 @@ request.allow("POST") bit_address = request.body['bitcoin_address'].strip() if bit_address != '' and not bitcoin.validate(bit_address): - raise Response(400) + raise Response(400, _("This is not a valid Bitcoin address.")) with website.db.get_cursor() as c: add_event(c, 'participant', dict(id=user.participant.id, action='set', values=dict(bitcoin_address=bit_address))) diff --git a/www/%username/goal.json.spt b/www/%username/goal.json.spt index 0f9d5ee207..5b10951f0f 100644 --- a/www/%username/goal.json.spt +++ b/www/%username/goal.json.spt @@ -23,11 +23,15 @@ if goal is not None: except NumberFormatError: raise Response(400, "Bad input.") -participant = get_participant(request) -participant.update_goal(goal) - -if goal is not None: - goal = format_decimal(goal) +if goal == -1 and not request.body.get("confirmed"): + msg = _("Warning: Doing this will remove all the tips you are currently receiving." + "\n\nThat cannot be undone!") + r = {'confirm': msg} +else: + participant = get_participant(request) + participant.update_goal(goal) + goal_l = None if goal is None else format_currency(goal, 'USD', trailing_zeroes=False) + r = {"goal": goal, "goal_l": goal_l} [---] application/json via json_dump -{"goal": goal} +r diff --git a/www/%username/index.html.spt b/www/%username/index.html.spt index 2fa84f5282..6a73dea3bd 100644 --- a/www/%username/index.html.spt +++ b/www/%username/index.html.spt @@ -1,16 +1,12 @@ """Show information about a single participant. It might be you! """ -from gratipay.utils import get_participant, wrap, plural, markdown +from collections import OrderedDict + from gratipay.models import community +from gratipay.utils import excerpt_intro, get_participant, markdown LONG_STATEMENT = 256 -def _clip(text, n): - text = text.replace('\n', ' ') - if len(text) > n: - text = text[:(n - 4)] + '...' - return text - [-----------------------------------------------------------------------------] participant = get_participant(request, restrict=False) @@ -27,20 +23,23 @@ if participant.anonymous_receiving: accounts = participant.get_accounts_elsewhere() -long_statement = len(participant.statement) > LONG_STATEMENT +statement, stmt_lang = participant.get_statement(request.accept_langs) +long_statement = len(statement or '') > LONG_STATEMENT +if user.participant == participant: + pref_langs = set(request.accept_langs + participant.get_statement_langs()) + select_langs = OrderedDict((k,v) for k, v in locale.languages_2.items() if k in pref_langs) + select_langs.update([('', '---')]) # Separator + select_langs.update(locale.languages_2) + stmt_placeholder = _("You don't have a profile statement in this language yet.") + confirm_discard = _("You haven't saved your changes, are you sure you want to discard them?") communities = community.get_list_for(website.db, participant.id) -I_AM_MAKING = _("I am making the world better by {0}") -WE_ARE_MAKING = _("We are making the world better by {0}") - if participant.number == 'singular': - MAKING = I_AM_MAKING GRATEFUL = _("I'm grateful for gifts, but don't have a specific funding goal.") PATRON = _("I'm here as a patron.") PATRON_NO_GIFTS = _("I'm here as a patron, and politely decline to receive gifts.") GOAL_RAW = _("My goal is to receive {0} per week on Gratipay.") else: - MAKING = WE_ARE_MAKING GRATEFUL = _("We're grateful for gifts, but don't have a specific funding goal.") PATRON = _("We're here as a patron.") PATRON_NO_GIFTS = _("We're here as a patron, and politely decline to receive gifts.") @@ -62,8 +61,8 @@ def with_others(obj): - {% if participant.statement %} - + {% if statement %} + {% else %} {% endif %} @@ -97,11 +96,11 @@ $(document).ready(function() {
{% else %}
- {% if participant.statement %} + {% if statement %}
-

{{ STATEMENT }}

+

{{ _("Statement") }}

- {{ markdown.render(MAKING.format(participant.statement)) }} + {{ markdown.render(statement) }}
{% include "templates/team-listing.html" %} {% include "templates/community-listing.html" %} diff --git a/www/%username/number.json.spt b/www/%username/number.json.spt new file mode 100644 index 0000000000..4aec71ffd3 --- /dev/null +++ b/www/%username/number.json.spt @@ -0,0 +1,24 @@ +from __future__ import print_function, unicode_literals + +from aspen import Response +from gratipay.utils import get_participant +from gratipay.exceptions import ProblemChangingNumber + +[-----------------------------------------------------------------------------] + +request.allow("POST") +participant = get_participant(request, restrict=True) + +number = request.body["number"] + +if number not in ("singular", "plural"): + raise Response(400) + +if number != participant.number: + try: + participant.update_number(number) + except ProblemChangingNumber, e: + raise Response(400, unicode(e)) + +[---] application/json via json_dump +{"number": number} diff --git a/www/%username/receipts/%exchange_id.int.html.spt b/www/%username/receipts/%exchange_id.int.html.spt index 5284c7ed57..042430c1a9 100644 --- a/www/%username/receipts/%exchange_id.int.html.spt +++ b/www/%username/receipts/%exchange_id.int.html.spt @@ -101,7 +101,7 @@ if exchange is None: {{ card['city_town'] }}{% if card['city_town'] %},{% endif %} {{ card['state'] }} {{ card['zip'] }}
- {{ locale.countries_map.get(card['country'], '') }} + {{ locale.countries.get(card['country'], '') }}
diff --git a/www/%username/statement.json.spt b/www/%username/statement.json.spt index 187e44ec73..ac15870d13 100644 --- a/www/%username/statement.json.spt +++ b/www/%username/statement.json.spt @@ -1,32 +1,26 @@ from __future__ import print_function, unicode_literals from aspen import Response -from gratipay.utils import wrap, markdown -from gratipay.exceptions import ProblemChangingNumber +from gratipay.utils import get_participant, markdown +from gratipay.utils.i18n import LANGUAGES_2 [-----------------------------------------------------------------------------] -if user.ANON: - raise Response(404) -request.allow("POST") +participant = get_participant(request, restrict=True) -statement = request.body["statement"] -number = request.body["number"] +if POST: + lang = request.body['lang'] + content = request.body['content'] -if number not in ("singular", "plural"): - raise Response(400) + if lang not in LANGUAGES_2: + raise Response(400, "unknown lang") -if number != user.participant.number: - try: - user.participant.update_number(number) - except ProblemChangingNumber, e: - raise Response(400, unicode(e)) -user.participant.update_statement(statement) + participant.upsert_statement(lang, content) + r = {"html": markdown.render(content)} -I_AM_MAKING = _("I am making the world better by {0}") -WE_ARE_MAKING = _("We are making the world better by {0}") - -MAKING = I_AM_MAKING if (number == 'singular') else WE_ARE_MAKING +else: + lang = qs['lang'] + r = {"content": participant.get_statement([lang])[0]} [---] application/json via json_dump -{"statement": markdown.render(MAKING.format(statement)), "number": wrap(number)} +r diff --git a/www/about/teams/index.spt b/www/about/teams/index.spt index 612ff69b9e..ef71562e55 100644 --- a/www/about/teams/index.spt +++ b/www/about/teams/index.spt @@ -19,15 +19,8 @@ make use of those funds to help support them in reaching their team's goals.

Creating a team

-

Team accounts are created just like regular accounts. Just login. Once you've -setup your team account there are three steps to change it from a regular -account into a team account:

- -
    -
  1. On your team's Profile page, edit the "Statement" section.
  2. -
  3. Change "I am" into "We are".
  4. -
  5. Save the change.
  6. -
+

Team accounts are created just like regular accounts. Just login. Then go to +your Account page and change from "Individual" to "Team".

After you saved the change, the navigation section on the profile page should include a new "Members" button. This section will list all of the team's members diff --git a/www/bank-account.html.spt b/www/bank-account.html.spt index a74aef098e..b602e12540 100644 --- a/www/bank-account.html.spt +++ b/www/bank-account.html.spt @@ -128,7 +128,7 @@ title = _("Bank Account")

-
diff --git a/www/credit-card.html.spt b/www/credit-card.html.spt index 4dd5bc9e4f..983b43bd41 100644 --- a/www/credit-card.html.spt +++ b/www/credit-card.html.spt @@ -158,7 +158,7 @@ title = _("Credit Card") diff --git a/www/for/%slug/index.html.spt b/www/for/%slug/index.html.spt index f2841aa5d2..85d1a12d85 100644 --- a/www/for/%slug/index.html.spt +++ b/www/for/%slug/index.html.spt @@ -1,6 +1,8 @@ +from itertools import chain + from aspen import Response from gratipay.models.community import name_pattern, slugize, Community -from gratipay.utils import format_money, to_statement +from gratipay.utils import excerpt_intro, format_money from gratipay.utils.query_cache import QueryCache LUXURY = 4 @@ -9,9 +11,6 @@ query_cache = QueryCache(website.db, threshold=20) [---] -I_AM_MAKING = _("I am making the world better by {0}") -WE_ARE_MAKING = _("We are making the world better by {0}") - _slug = path['slug'] if name_pattern.match(_slug) is None: raise Response(404) @@ -47,9 +46,9 @@ new_participants = query_cache.all(""" -- new participants on community page SELECT username + , id , claimed_time , avatar_url - , statement , number FROM participants p JOIN current_community_members cc ON cc.participant = p.id @@ -66,10 +65,10 @@ givers = query_cache.all(""" -- top givers on community page SELECT username + , id , anonymous_giving AS anonymous , giving AS amount , avatar_url - , statement , number FROM participants p JOIN current_community_members cc ON cc.participant = p.id AND cc.slug = %s @@ -89,10 +88,10 @@ receivers = query_cache.all(""" -- top receivers on community page SELECT username + , id , anonymous_receiving AS anonymous , receiving AS amount , avatar_url - , statement , number FROM participants p JOIN current_community_members cc ON cc.participant = p.id AND cc.slug = %s @@ -104,6 +103,26 @@ receivers = query_cache.all(""" OFFSET %s """, (community.slug, limit, offset)) + + +# Fetch statements + +ids = tuple(p.id for p in chain(new_participants, givers, receivers)) +if ids: + statements = website.db.all(""" + SELECT DISTINCT ON (participant) participant, content + FROM statements s + JOIN enumerate(%s) langs ON langs.value = s.lang + WHERE participant IN %s + ORDER BY participant, langs.rank ASC + """, (request.accept_langs, ids)) + statements = {s.participant: s.content for s in statements} +else: + statements = {} + +for p in chain(new_participants, givers, receivers): + p.__dict__['statement'] = statements.get(p.id) + [---] {% from 'templates/avatar-url.html' import avatar_url with context %} @@ -183,7 +202,7 @@ $(document).ready(function() { {% for i, participant in enumerate(new_participants, start=1) %} LUXURY %} class="luxury"{% endif %}> + data-tip="{{ excerpt_intro(participant.statement)|e }}"> @@ -213,7 +232,7 @@ $(document).ready(function() { {% else %} + data-tip="{{ excerpt_intro(giver.statement) }}"> @@ -243,7 +262,7 @@ $(document).ready(function() { {% else %} + data-tip="{{ excerpt_intro(receiver.statement) }}"> diff --git a/www/for/index.html.spt b/www/for/index.html.spt index 017207a620..c3d3f1c482 100644 --- a/www/for/index.html.spt +++ b/www/for/index.html.spt @@ -1,5 +1,4 @@ from gratipay.models import community -from gratipay.utils import plural [-----------------------------]