i18n + custom settings

This commit is contained in:
ljf 2021-02-19 00:14:04 +01:00
parent 69786aa3de
commit 0e15d68123
12 changed files with 200 additions and 80 deletions

View file

@ -22,25 +22,37 @@ source venv/bin/activate
pip3 install requirements.txt pip3 install requirements.txt
``` ```
Create a .env file with : Create a settings.py file with :
``` ```
PORT=8000 ENV = 'development'
DEBUG=True PORT = 8000
PROJECT_NAME=YunoHost DOMAIN = 'http://localhost:8000'
DOMAIN=http://localhost:8000 SECRET_KEY = '712AZPOC87HXD5SQSb12rd'
STATIC_DIR=assets SECRET_CSRF_KEY = '712AZPOC87HXD5SQSb12'
SECRET_CSRF_KEY=TO_CHANGE LANGUAGES = ['en', 'fr']
BABEL_TRANSLATION_DIRECTORIES = 'locales'
# Customization
CUSTOM = {}
CUSTOM['name'] = 'YunoHost'
CUSTOM['contact_url'] = 'mailto:donate-6521@yunohost.org'
CUSTOM['logo'] = 'https://yunohost.org/user/images/logo.png'
CUSTOM['favicon'] = 'https://yunohost.org/user/themes/yunohost-docs/images/favicon.png'
CUSTOM['currencies'] = [
('EUR', '€'),
('USD', '$')
]
# Stripe keys # Stripe keys
STRIPE_PUBLISHABLE_KEY=pk_test_gOgGjacs9YfvDJY03BRZ576O CUSTOM['stripe_publishable_key'] = 'pk_test_gOgGjacs9YfvDJY03BRZ576O'
STRIPE_SECRET_KEY=TO_REPLACE_BY_THE_GOOD_VALUE STRIPE_SECRET_KEY = 'sk_test_'
# Stripe subscription data # Stripe subscription data
ONE_TIME_EUR_DONATION=price_1IKuPVE7vOmTpJBiYMq7ztLH DONATION={'one_time':{}, 'recuring': {}}
RECURING_EUR_DONATION=price_1IKumjE7vOmTpJBikyqS2NqD DONATION['one_time']['EUR'] = 'price_1IKuPVE7vOmTpJBiYMq7ztLH'
DONATION['one_time']['USD'] = 'price_1IKuQfE7vOmTpJBi0A3nRGCJ'
ONE_TIME_USD_DONATION=price_1IKuQfE7vOmTpJBi0A3nRGCJ DONATION['recuring']['EUR'] = 'price_1IKumjE7vOmTpJBikyqS2NqD'
RECURING_USD_DONATION=price_1IKumAE7vOmTpJBiO4CEfa3Q DONATION['recuring']['USD'] = 'price_1IKumAE7vOmTpJBiO4CEfa3Q'
``` ```
``` ```

View file

@ -4,9 +4,9 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Donate to YunoHost</title> <title>{{ _('Donate to %(name)s', name=name) }}</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" /> <link rel="icon" href="{{ favicon }}" type="image/x-icon" />
<link href="./css/bootstrap-5.0.0-beta2.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> <link href="./css/bootstrap-5.0.0-beta2.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link href="./css/global.css" rel="stylesheet" /> <link href="./css/global.css" rel="stylesheet" />
</head> </head>
@ -14,10 +14,10 @@
<body class="text-center"> <body class="text-center">
<main class="form-donate"> <main class="form-donate">
<div> <div>
<img src="logo.png" class="mb-4" alt="" width="74" height="74"/> <img src="{{ logo }}" class="mb-4" alt="" width="74" height="74"/>
<h1 class="h3 mb-3 fw-normal">Your payment was canceled</h1> <h1 class="h3 mb-3 fw-normal">{{ _('Your payment was canceled') }}</h1>
<button onclick="window.location.href = '/';" class="w-100 btn btn-lg btn-primary">Go back to the donate form</button> <button onclick="window.location.href = '/';" class="w-100 btn btn-lg btn-primary">{{ _('Go back to the donate form') }}</button>
</div> </div>
</main> </main>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -4,9 +4,9 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Donate to YunoHost</title> <title>{{ _('Donate to %(name)s', name=name) }}</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" /> <link rel="icon" href="{{ favicon }}" type="image/x-icon" />
<link href="./css/bootstrap-5.0.0-beta2.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> <link href="./css/bootstrap-5.0.0-beta2.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link href="./css/global.css" rel="stylesheet" /> <link href="./css/global.css" rel="stylesheet" />
<script src="https://js.stripe.com/v3/"></script> <script src="https://js.stripe.com/v3/"></script>
@ -16,22 +16,25 @@
<body class="text-center"> <body class="text-center">
<main class="form-donate"> <main class="form-donate">
<div> <div>
<img src="logo.png" class="mb-4" alt="" width="74" height="74"/> <img src="{{ logo }}" class="mb-4" alt="" width="74" height="74"/>
<h1 class="h3 mb-3 fw-normal">I want to give to YunoHost</h1> <h1 class="h3 mb-3 fw-normal">{{ _('I want to give to %(name)s', name=name) }}</h1>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="hidden" id="csrf" value="{{ csrf }}" />
<input type="hidden" id="public_key" value="{{ stripe_publishable_key }}" />
<input type="number" min="0" max="9999" class="form-control" id="quantity" value="10" aria-label="Amount"> <input type="number" min="0" max="9999" class="form-control" id="quantity" value="10" aria-label="Amount">
<select class="form-control" id="currency"> <select class="form-control" id="currency" style="width:40px">
<option value="EUR"> {% for iso, symbol in currencies %}
<option value="USD">$ (USD) <option value="{{ iso }}">{{ symbol }}
{% endfor %}
</select> </select>
<select class="form-control" id="frequency"> <select class="form-control" id="frequency">
<option value="RECURING"> / month <option value="recuring"> {{ _('/ month') }}
<option value="ONE_TIME"> one time <option value="one_time"> {{ _('one time') }}
</select> </select>
</div> </div>
<button id="submit" class="w-100 btn btn-lg btn-primary">Donate</button> <button id="submit" class="w-100 btn btn-lg btn-primary">{{ _('Donate') }}</button>
<p><a href="{{ contact_url }}">{{ _('If you want to stop a monthly donation contact us') }}</a></p>
<div id="error-message"></div> <div id="error-message"></div>
</div> </div>
</main> </main>

View file

@ -1,24 +1,12 @@
// Fetch your Stripe publishable key to initialize Stripe.js window.stripe = Stripe(document.getElementById('public_key').value);
// In practice, you might just hard code the publishable API
// key here.
fetch('/config')
.then(function (result) {
return result.json();
})
.then(function (json) {
window.config = json;
window.stripe = Stripe(config.publicKey);
});
// When the form is submitted... // When the form is submitted...
var submitBtn = document.querySelector('#submit'); var submitBtn = document.querySelector('#submit');
submitBtn.addEventListener('click', function (evt) { submitBtn.addEventListener('click', function (evt) {
var inputEl = document.getElementById('quantity'); var quantity = parseInt(document.getElementById('quantity').value);
var quantity = parseInt(inputEl.value); var currency = document.getElementById('currency').value;
inputEl = document.getElementById('currency'); var frequency = document.getElementById('frequency').value;
var currency = inputEl.value; var csrf = document.getElementById('csrf').value;
inputEl = document.getElementById('frequency');
var frequency = inputEl.value;
// Create the checkout session. // Create the checkout session.
fetch('/create-checkout-session', { fetch('/create-checkout-session', {
@ -27,7 +15,7 @@ submitBtn.addEventListener('click', function (evt) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
user_csrf: window.config.csrf, user_csrf: csrf,
quantity: quantity, quantity: quantity,
currency: currency, currency: currency,
frequency: frequency frequency: frequency

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -4,9 +4,9 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Donate to YunoHost</title> <title>{{ _('Donate to %(name)s', name=name) }}</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" /> <link rel="icon" href="{{ favicon }}" type="image/x-icon" />
<link href="./css/bootstrap-5.0.0-beta2.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> <link href="./css/bootstrap-5.0.0-beta2.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link href="./css/global.css" rel="stylesheet" /> <link href="./css/global.css" rel="stylesheet" />
</head> </head>
@ -14,10 +14,10 @@
<body class="text-center"> <body class="text-center">
<main class="form-donate"> <main class="form-donate">
<div> <div>
<img src="logo.png" class="mb-4" alt="" width="74" height="74"/> <img src="{{ logo }}" class="mb-4" alt="" width="74" height="74"/>
<h1 class="h3 mb-3 fw-normal">Thanks for your donation 🙂</h1> <h1 class="h3 mb-3 fw-normal">{{ _('Thanks for your donation 🙂') }}</h1>
<button onclick="window.location.href = '/';" class="w-100 btn btn-lg btn-primary">Go back to the donate form</button> <button onclick="window.location.href = '/';" class="w-100 btn btn-lg btn-primary">{{ _('Go back to the donate form') }}</button>
</div> </div>
</main> </main>

3
babel.cfg Normal file
View file

@ -0,0 +1,3 @@
[python: server.py]
[jinja2: assets/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View file

@ -0,0 +1,54 @@
# English translations for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-02-18 23:32+0100\n"
"PO-Revision-Date: 2021-02-18 23:33+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n"
#: assets/canceled.html:7 assets/index.html:7 assets/success.html:7
#, python-format
msgid "Donate to %(name)s"
msgstr ""
#: assets/canceled.html:18
msgid "Your payment was canceled"
msgstr ""
#: assets/canceled.html:20 assets/success.html:20
msgid "Go back to the donate form"
msgstr ""
#: assets/index.html:20
#, python-format
msgid "I want to give to %(name)s"
msgstr ""
#: assets/index.html:31
msgid "/ month"
msgstr ""
#: assets/index.html:32
msgid "one time"
msgstr ""
#: assets/index.html:36
msgid "Donate"
msgstr ""
#: assets/success.html:18
msgid "Thanks for your donation 🙂"
msgstr ""

View file

@ -0,0 +1,54 @@
# French translations for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-02-18 23:32+0100\n"
"PO-Revision-Date: 2021-02-18 23:33+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
"Language-Team: fr <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n"
#: assets/canceled.html:7 assets/index.html:7 assets/success.html:7
#, python-format
msgid "Donate to %(name)s"
msgstr "Donner à %(name)s"
#: assets/canceled.html:18
msgid "Your payment was canceled"
msgstr "Votre paiement a été annulé"
#: assets/canceled.html:20 assets/success.html:20
msgid "Go back to the donate form"
msgstr "Retourner au formulaire de don"
#: assets/index.html:20
#, python-format
msgid "I want to give to %(name)s"
msgstr "Je veux donner à %(name)s"
#: assets/index.html:31
msgid "/ month"
msgstr "/ mois"
#: assets/index.html:32
msgid "one time"
msgstr "une fois"
#: assets/index.html:36
msgid "Donate"
msgstr "Donner"
#: assets/success.html:18
msgid "Thanks for your donation 🙂"
msgstr "Merci pour votre don 🙂"

View file

@ -12,3 +12,4 @@ stripe==2.47.0
toml==0.9.6 toml==0.9.6
urllib3==1.25.3 urllib3==1.25.3
flask-simple-csrf flask-simple-csrf
flask-babel

View file

@ -13,25 +13,21 @@ import random
import string import string
from flask import Flask, render_template, jsonify, request, send_from_directory, session from flask import Flask, render_template, jsonify, request, send_from_directory, session
from flask_babel import Babel, _
from flask_simple_csrf import CSRF from flask_simple_csrf import CSRF
from dotenv import load_dotenv, find_dotenv
# Setup Stripe python client library.
load_dotenv(find_dotenv())
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
stripe.api_version = os.getenv('STRIPE_API_VERSION')
static_dir = str(os.path.abspath(os.path.join( static_dir = str(os.path.abspath(os.path.join(
__file__, "..", os.getenv("STATIC_DIR")))) __file__, "..", 'assets')))
app = Flask(__name__, static_folder=static_dir, app = Flask(__name__, static_folder=static_dir,
static_url_path="", template_folder=static_dir) static_url_path="", template_folder=static_dir)
app.secret_key = os.getenv('SECRET_KEY') app.config.from_pyfile('settings.py')
stripe.api_key = app.config['STRIPE_SECRET_KEY']
CSRF = CSRF(config={ CSRF = CSRF(config={
'SECRET_CSRF_KEY':os.getenv('SECRET_CSRF_KEY') 'SECRET_CSRF_KEY': app.config['SECRET_CSRF_KEY']
}) })
app = CSRF.init_app(app) app = CSRF.init_app(app)
babel = Babel(app)
@app.before_request @app.before_request
def before_request(): def before_request():
@ -39,43 +35,52 @@ def before_request():
session['USER_CSRF'] = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(64)) session['USER_CSRF'] = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(64))
session['CSRF_TOKEN'] = CSRF.create(session['USER_CSRF']) session['CSRF_TOKEN'] = CSRF.create(session['USER_CSRF'])
@babel.localeselector
def get_locale():
return 'fr' #request.accept_languages.best_match(app.config['LANGUAGES'])
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def get_index(): def get_index():
return render_template('index.html') return render_template('index.html', **app.config['CUSTOM'],
csrf=session['USER_CSRF'])
@app.route('/config', methods=['GET']) @app.route('/success', methods=['GET'])
def get_publishable_key(): def get_success():
return jsonify({ return render_template('success.html', **app.config['CUSTOM'])
'publicKey': os.getenv('STRIPE_PUBLISHABLE_KEY'),
'name': os.getenv('PROJECT_NAME'),
'csrf': session['USER_CSRF'], @app.route('/canceled', methods=['GET'])
}) def get_canceled():
return render_template('canceled.html', **app.config['CUSTOM'])
@app.route('/create-checkout-session', methods=['POST']) @app.route('/create-checkout-session', methods=['POST'])
def create_checkout_session(): def create_checkout_session():
data = json.loads(request.data) data = json.loads(request.data)
domain_url = os.getenv('DOMAIN') domain_url = app.config['DOMAIN']
try: try:
donation = app.config['DONATION']
currencies = [iso for iso, symbol in app.config['CUSTOM']['currencies']]
if CSRF.verify(data['user_csrf'], session['CSRF_TOKEN']) is False or \ if CSRF.verify(data['user_csrf'], session['CSRF_TOKEN']) is False or \
data['frequency'] not in ['RECURING', 'ONE_TIME'] or \ data['frequency'] not in ['recuring', 'one_time'] or \
data['currency'] not in ['EUR', 'USD'] or \ data['currency'] not in currencies or \
int(data['quantity']) <= 0: int(data['quantity']) <= 0:
return jsonify(error="Bad value"), 400 return jsonify(error="Bad value"), 400
# Create new Checkout Session for the order # Create new Checkout Session for the order
price = f"{data['frequency']}_{data['currency']}_DONATION" price = donation[data['frequency']][data['currency']]
mode = "payment" if data['frequency'] == 'ONE_TIME' else "subscription" mode = "payment" if data['frequency'] == 'one_time' else "subscription"
checkout_session = stripe.checkout.Session.create( checkout_session = stripe.checkout.Session.create(
success_url=domain_url + success_url=domain_url +
"/success.html?session_id={CHECKOUT_SESSION_ID}", "/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url=domain_url + "/canceled.html", cancel_url=domain_url + "/canceled",
payment_method_types= ["card"], payment_method_types= ["card"],
mode=mode, mode=mode,
line_items=[ line_items=[
{ {
"price": os.getenv(price), "price": price,
"quantity": data['quantity'] "quantity": data['quantity']
} }
] ]
@ -87,4 +92,4 @@ def create_checkout_session():
if __name__ == '__main__': if __name__ == '__main__':
app.run(port=os.getenv('PORT'), debug=os.getenv('DEBUG')) app.run(port=app.config['PORT'], debug=app.debug)