rename, clean up, adapt, internationalize

This commit is contained in:
Changaco 2015-05-24 18:52:23 +02:00
parent 22a57a7b9a
commit e0f03b3b30
306 changed files with 1204 additions and 10746 deletions

View File

@ -1,5 +1,5 @@
[extractors]
spt = gratipay.utils.i18n:extract_spt
spt = liberapay.utils.i18n:extract_spt
[python: **.py]
[jinja2: **.html]

View File

@ -10,8 +10,3 @@ trim_trailing_whitespace = true
# Makefiles require us to use tabs, so we're gonna keep tabs with Makefiles
[Makefile]
indent_style = tab
# These files are generated programmatically, so we're going to keep with the
# program
[{package,bower}.json]
indent_size = 2

17
.gitignore vendored
View File

@ -1,24 +1,11 @@
env/
gratipay.egg-info/
*.egg-info/
*~
*.pyc
local.env
tests/env
*.idea/
tests/local.env
*.egg-info/
nosetests.xml
.cache
.coverage
.noseids
.deps
.sass-cache/
.vagrant
node_modules/
.DS_Store
docs/_build
docs/gratipay
docs/gratipay.rst
_vimrc_local.vim
.transifexrc
npm-debug.log
phantomjsdriver.log

View File

@ -1,6 +0,0 @@
{ "asi": true
, "laxbreak": true
, "laxcomma": true
, "boss": true
, "shadow": true
}

View File

@ -1,6 +1,3 @@
# Files removed before slug
# compilation on Heroku
# Files removed before slug compilation on Heroku
# See: https://devcenter.heroku.com/articles/slug-compiler#slug-size
img-src/
tests/
jstests/

View File

@ -4,27 +4,22 @@ addons:
branches:
only:
- master
before_install:
- node --version
cache:
directories:
- env/bin
- env/lib/python2.7/site-packages
install:
- if [ "${TRAVIS_BRANCH}" = "master" -a "${TRAVIS_PULL_REQUEST}" = "false" ]; then rm -rf env; fi
- touch requirements.txt package.json
- make env
- make node_modules
- touch requirements.txt && make env
before_script:
- echo "DATABASE_URL=dbname=gratipay" | tee -a tests/local.env local.env
- psql -U postgres -c 'CREATE DATABASE "gratipay";'
- psql -U postgres -c 'CREATE DATABASE liberapay_tests;'
- if [ "${TRAVIS_BRANCH}" = "master" -a "${TRAVIS_PULL_REQUEST}" = "false" ]; then rm -rfv tests/py/fixtures; fi
script: make test
notifications:
email: false
irc:
channels:
- "chat.freenode.net#gratipay"
- "chat.freenode.net#liberapay"
on_success: change
on_failure: always
template:

View File

@ -1,8 +0,0 @@
[main]
host = https://www.transifex.com
[gratipay.core]
source_file = i18n/core.pot
source_lang = en
file_filter = i18n/core/<lang>.po
type = PO

View File

@ -1,14 +0,0 @@
## Contributing to Gratipay
If you are opening a new issue or submitting a pull request, **go for it!**
Don't be afraid that it's a dumb idea or a duplicate of another issue or an
unwanted change or whatever. Maybe it is! We're still glad to have you! :^)
**License.** Gratipay is [licensed under
CC0](https://github.com/gratipay/gratipay.com/tree/master/COPYING), i.e.,
it's public domain.
**More info.** If you want to really get involved, then check out our full
documentation for contributors:
http://inside.gratipay.com/

151
COPYING
View File

@ -1,151 +0,0 @@
Gratipay is in the public domain, via the "CC0 1.0 Universal - Public Domain
Dedication" from the Creative Commons.
## Summary of "CC0 1.0 Universal - Public Domain Dedication"
http://creativecommons.org/publicdomain/zero/1.0/
No Copyright
The person who associated a work with this deed has dedicated the work to the
public domain by waiving all of his or her rights to the work worldwide under
copyright law, including all related and neighboring rights, to the extent
allowed by law.
You can copy, modify, distribute and perform the work, even for commercial
purposes, all without asking permission. See Other Information below.
Other Information
- In no way are the patent or trademark rights of any person affected by
CC0, nor are the rights that other persons may have in the work or in
how the work is used, such as publicity or privacy rights.
- Unless expressly stated otherwise, the person who associated a work
with this deed makes no warranties about the work, and disclaims
liability for all uses of the work, to the fullest extent permitted by
applicable law.
- When using or citing the work, you should not imply endorsement by the
author or the affirmer.
## CC0 1.0 Universal - Public Domain Dedication
http://creativecommons.org/publicdomain/zero/1.0/legalcode
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator and
subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for the
purpose of contributing to a commons of creative, cultural and scientific works
("Commons") that the public can reliably and without fear of later claims of
infringement build upon, modify, incorporate in other works, reuse and
redistribute as freely as possible in any form whatsoever and for any purposes,
including without limitation commercial purposes. These owners may contribute
to the Commons to promote the ideal of a free culture and the further
production of creative, cultural and scientific works, or to gain reputation or
greater distribution for their Work in part through the use and efforts of
others.
For these and/or other purposes and motivations, and without any expectation of
additional consideration or compensation, the person associating CC0 with a
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
publicly distribute the Work under its terms, with knowledge of his or her
Copyright and Related Rights in the Work and the meaning and intended legal
effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not limited to,
the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or likeness
depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
vii. other similar, equivalent or corresponding rights throughout the world
based on applicable law or treaty, and any national implementations
thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
and Related Rights and associated claims and causes of action, whether now
known or unknown (including existing as well as future claims and causes of
action), in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number of
copies, and (iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
the Waiver for the benefit of each member of the public at large and to the
detriment of Affirmer's heirs and successors, fully intending that such Waiver
shall not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of the Work
by the public as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be
judged legally invalid or ineffective under applicable law, then the Waiver
shall be preserved to the maximum extent permitted taking into account
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
is so judged Affirmer hereby grants to each affected person a royalty-free, non
transferable, non sublicensable, non exclusive, irrevocable and unconditional
license to exercise Affirmer's Copyright and Related Rights in the Work (i) in
all territories worldwide, (ii) for the maximum duration provided by applicable
law or treaty (including future time extensions), (iii) in any current or
future medium and for any number of copies, and (iv) for any purpose
whatsoever, including without limitation commercial, advertising or promotional
purposes (the "License"). The License shall be deemed effective as of the date
CC0 was applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder of the
License, and in such case Affirmer hereby affirms that he or she will not (i)
exercise any of his or her remaining Copyright and Related Rights in the Work
or (ii) assert any associated claims and causes of action with respect to the
Work, in either case contrary to Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied, statutory
or otherwise, including without limitation warranties of title,
merchantability, fitness for a particular purpose, non infringement, or
the absence of latent or other defects, accuracy, or the present or
absence of errors, whether or not discoverable, all to the greatest
extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@ -1,54 +0,0 @@
# Dockerfile to build and run Gratipay
# Version 0.2 (March 10, 2015)
################################################## General Information ##################################################
FROM ubuntu:14.04
MAINTAINER Mihir Singh (@citruspi)
ENV DEBIAN_FRONTEND noninteractive
################################################## Install Dependencies #################################################
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" > /etc/apt/sources.list.d/pgdg.list
RUN apt-get -y install wget
RUN wget --quiet --no-check-certificate https://www.postgresql.org/media/keys/ACCC4CF8.asc
RUN apt-key add ACCC4CF8.asc
RUN apt-get -y update
RUN apt-get -y install \
git \
gcc \
make \
g++ \
libpq-dev \
python-dev \
python-pip \
postgresql-9.3 \
postgresql-contrib-9.3 \
language-pack-en
################################################## Configure Postgres #################################################
RUN /etc/init.d/postgresql start && su postgres -c "createuser --superuser root" && su postgres -c "createdb gratipay"
################################################# Copy files + Setup Gratipay ##########################################
COPY ./ /srv/gratipay.com/
WORKDIR /srv/gratipay.com
RUN make env && /etc/init.d/postgresql start && make schema && make schema data
################################################ Create a Launch Script ###############################################
RUN echo "#!/bin/bash" >> /usr/bin/gratipay && \
echo "/etc/init.d/postgresql start" >> /usr/bin/gratipay && \
echo "cd /srv/gratipay.com && make run" >> /usr/bin/gratipay && \
chmod +x /usr/bin/gratipay
################################################### Launch command #####################################################
CMD ["/usr/bin/gratipay"]

View File

@ -1,132 +0,0 @@
var http = require('http');
var spawn = require('child_process').spawn;
var fs = require('fs');
var ini = require('ini');
var yaml = require('js-yaml');
var path = require('path');
// Add node_modules/.bin to PATH. We'll need it later for phantomjs
process.env.PATH = process.env.PATH + path.delimiter +
path.join(process.cwd(), 'node_modules', '.bin');
module.exports = function(grunt) {
'use strict';
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
env: ini.parse(
fs.readFileSync('defaults.env', 'utf8') +
fs.readFileSync('tests/test.env', 'utf8') +
(fs.existsSync('tests/local.env') ?
fs.readFileSync('tests/local.env', 'utf8') : '')
),
watch: {
gruntfile: {
files: '<%= jshint.gruntfile %>',
tasks: 'jshint:gruntfile'
},
js: {
files: '<%= jshint.js %>',
tasks: ['jshint:js', 'webdriver']
},
tests: {
files: '<%= jshint.tests %>',
tasks: ['jshint:tests', 'webdriver']
}
},
jshint: {
gruntfile: 'Gruntfile.js',
js: 'js/**/*.{js,json}',
tests: 'tests/js/**/*.js',
options: {
jshintrc: '.jshintrc',
globals: {
Gratipay: true,
_gttp: true,
gttpURI: true,
alert: true
}
}
},
webdriver: {
tests: {
tests: 'tests/js/test_*.js'
},
options: {
desiredCapabilities: {
browserName: 'phantomjs'
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-webdriver');
grunt.registerTask('default', ['test']);
grunt.registerTask('test', ['jshint', 'aspen:start', 'webdriver']);
grunt.registerTask('aspen:start', 'Start Aspen (if necessary)', function() {
var done = this.async();
grunt.config.requires('env.CANONICAL_HOST');
var canonicalHost = grunt.config.get('env.CANONICAL_HOST') || 'localhost:8537';
var port = parseInt(canonicalHost.split(':').pop());
http.get('http://' + canonicalHost + '/', function(res) {
grunt.log.writeln('Aspen seems to be running already. Doing nothing.');
done();
})
.on('error', function(e) {
grunt.log.write('Starting Aspen...');
var started = false;
var aspen_out = [];
var bin = 'env/' + (process.platform == 'win32' ? 'Scripts' : 'bin');
var child = spawn(
bin + '/gunicorn',
['--bind', ':' + port, '--workers', '1', 'gratipay.main:website'],
{ env: grunt.config.get('env') }
);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', function(data) { aspen_out.push(data); });
child.stderr.on('data', function(data) {
aspen_out.push(data);
if (!started && /Starting gunicorn /.test(data)) {
grunt.log.writeln(' started.');
setTimeout(function() {
started = true;
done();
}, 1000);
}
});
child.on('exit', function() {
if (!started) {
grunt.log.writeln(' failed!');
grunt.log.write(aspen_out);
grunt.fail.fatal('Something went wrong when starting Aspen :<');
}
});
process.on('exit', child.kill.bind(child));
});
});
};

View File

@ -2,14 +2,13 @@ python := "$(shell { command -v python2.7 || command -v python; } 2>/dev/null)"
# Set the relative path to installed binaries under the project virtualenv.
# NOTE: Creating a virtualenv on Windows places binaries in the 'Scripts' directory.
bin_dir := $(shell $(python) -c 'import sys; bin = "Scripts" if sys.platform == "win32" else "bin"; print(bin)')
bin_dir := $(shell $(python) -c 'import sys; print("Scripts" if sys.platform == "win32" else "bin")')
env_bin := env/$(bin_dir)
venv := "./vendor/virtualenv-12.0.7.py"
test_env_files := defaults.env,tests/test.env,tests/local.env
pip := $(env_bin)/pip
honcho := $(env_bin)/honcho
honcho_run := $(honcho) run -e defaults.env,local.env
py_test := $(honcho) run -e $(test_env_files) $(env_bin)/py.test
with_local_env := $(env_bin)/honcho run -e defaults.env,local.env
with_tests_env := $(env_bin)/honcho run -e $(test_env_files)
py_test := $(with_tests_env) $(env_bin)/py.test
ifdef PYTEST
pytest = ./tests/py/$(PYTEST)
@ -17,87 +16,65 @@ else
pytest = ./tests/py/
endif
env: requirements.txt requirements_tests.txt setup.py
$(python) $(venv) \
--prompt="[gratipay] " \
--extra-search-dir=./vendor/ \
--always-copy \
./env/
$(pip) install --no-index -f ./vendor/ -r requirements.txt
$(pip) install --no-index -f ./vendor/ -r requirements_tests.txt
$(pip) install -e ./
env: requirements.txt requirements_tests.txt
$(python) -m virtualenv ./env/
$(pip) install -r requirements.txt
$(pip) install -r requirements_tests.txt
@touch env
clean:
rm -rf env *.egg *.egg-info
find . -name \*.pyc -delete
schema: env
$(honcho_run) ./recreate-schema.sh
$(with_local_env) ./recreate-schema.sh
schema-diff: test-schema
pg_dump -sO `heroku config:get DATABASE_URL -a gratipay` >prod.sql
$(honcho) run -e $(test_env_files) sh -c 'pg_dump -sO "$$DATABASE_URL"' >local.sql
pg_dump -sO `heroku config:get DATABASE_URL -a liberapay` >prod.sql
$(with_tests_env) sh -c 'pg_dump -sO "$$DATABASE_URL"' >local.sql
diff -uw prod.sql local.sql
rm prod.sql local.sql
data:
$(honcho_run) $(env_bin)/fake_data fake_data
data: env
$(with_local_env) $(env_bin)/python -m liberapay.utils.fake_data
run: env
PATH=$(env_bin):$(PATH) $(honcho_run) web
$(with_local_env) make --no-print-directory run_
stop:
pkill gunicorn
run_:
$(env_bin)/$(shell grep -E '^web: ' Procfile | cut -d' ' -f2-)
py: env
$(honcho_run) $(env_bin)/python -i gratipay/main.py
$(with_local_env) $(env_bin)/python -i liberapay/main.py
test-schema: env
$(honcho) run -e $(test_env_files) ./recreate-schema.sh
$(with_tests_env) ./recreate-schema.sh test
pyflakes: env
$(env_bin)/pyflakes *.py bin gratipay tasks tests
test: test-schema pytest jstest
pytest: env
$(py_test) --cov gratipay $(pytest)
@$(MAKE) --no-print-directory pyflakes
retest: env
$(py_test) ./tests/py/ --lf
@$(MAKE) --no-print-directory pyflakes
test-cov: env
$(py_test) --cov-report html --cov gratipay ./tests/py/
$(env_bin)/pyflakes liberapay tests
test: test-schema pytest
tests: test
node_modules: package.json
npm install --no-bin-links
@if [ -d node_modules ]; then touch node_modules; fi
pytest: env
PYTHONPATH=. $(py_test) --cov liberapay $(pytest)
@$(MAKE) --no-print-directory pyflakes
jstest: node_modules
node_modules/grunt-cli/bin/grunt test
pytest-cov: env
PYTHONPATH=. $(py_test) --cov-report html --cov liberapay ./tests/py/
transifexrc:
@echo '[https://www.transifex.com]' >.transifexrc
@echo 'hostname = https://www.transifex.com' >>.transifexrc
@echo "password = $$TRANSIFEX_PASS" >>.transifexrc
@echo 'token = ' >>.transifexrc
@echo "username = $$TRANSIFEX_USER" >>.transifexrc
pytest-re: env
PYTHONPATH=. $(py_test) --lf ./tests/py/
@$(MAKE) --no-print-directory pyflakes
tx:
@if [ ! -x $(env_bin)/tx ]; then $(env_bin)/pip install transifex-client; fi
i18n: env tx
$(env_bin)/pybabel extract -F .babel_extract --no-wrap -o i18n/core.pot emails gratipay templates www
i18n: env
$(env_bin)/pybabel extract -F .babel_extract --no-wrap -o i18n/core.pot emails liberapay templates www
i18n_upload: i18n
$(env_bin)/tx push -s
rm i18n/*.pot
i18n_download: env tx
i18n_download: env
$(env_bin)/tx pull -a -f --mode=reviewed --minimum-perc=50
@for f in i18n/*/*.po; do \
sed -E -e '/^"POT?-[^-]+-Date: /d' \

View File

@ -1 +1 @@
web: gunicorn gratipay.main:website --bind :$PORT $GUNICORN_OPTS
web: gunicorn liberapay.main:website --bind :$PORT $GUNICORN_OPTS

565
README.md
View File

@ -1,555 +1,76 @@
# Welcome to Gratipay [<img height="26px" src="https://raw.githubusercontent.com/gratipay/gratipay.com/master/www/assets/gratipay.opengraph.png"/>](https://gratipay.com/)
[Liberapay](http://liberapay.com) is a recurrent donations platform, forked from
[Gratipay](http://gratipay.com).
[![Build Status](http://img.shields.io/travis/gratipay/gratipay.com/master.svg)](https://travis-ci.org/gratipay/gratipay.com)
[![Coverage Status](https://img.shields.io/coveralls/gratipay/gratipay.com.svg)](https://coveralls.io/r/gratipay/gratipay.com?branch=master)
[![HuBoard badge](http://img.shields.io/badge/Hu-Board-7965cc.svg)](https://huboard.com/gratipay/gratipay.com)
[![Open Bounties](https://api.bountysource.com/badge/team?team_id=423&style=bounties_received)](https://www.bountysource.com/teams/gratipay/issues)
[Gratipay](http://gratipay.com) is a weekly gift exchange, helping to create a culture of generosity.
If you'd like to learn more, check out <https://gratipay.com/about>.
If you'd like to contribute to Gratipay, check out <http://inside.gratipay.com>.
## Contributing
Quick Start
===========
### Installation
Local
-----
Firstly, make sure you have the following dependencies installed:
Given Python 2.7, Postgres 9.3, and a C/make toolchain:
- python 2.7
- virtualenv
- postgresql
- make
```
$ git clone git@github.com:gratipay/gratipay.com.git
$ cd gratipay.com
$ sudo -u postgres createuser --superuser $USER
$ createdb gratipay
$ make schema data
$ make run
```
Then run:
And/or:
make env
```
$ make test
```
Now you'll need to create two postgres databases, here's the simplest way of doing it:
[Read more](#table-of-contents).
sudo -u postgres createuser --superuser $USER
createdb liberapay
createdb liberapay_tests
Then you can set up the DB:
Vagrant
-------
make schema
Given VirtualBox 4.3 and Vagrant 1.6.x:
### Configuration
```
$ vagrant up
```
Environment variables are used for configuration, the default values are in
`defaults.env` and `tests/test.env`. You can override them in
`local.env` and `tests/local.env` respectively.
[Read more](#vagrant-1).
### Running
Once you've installed everything and set up the database, you can run the app:
Docker
-------
make run
Given some version(?) of Docker:
It should now be accessible at [http://localhost:8339/](http://localhost:8339/).
```
$ docker build -t gratipay .
$ docker run -p 8537:8537 gratipay
```
You can create some fake users to make it look more like the real site:
[Read more](#docker-1).
make data
### Modifying the database schema
Table of Contents
=================
The DB schema is in `sql/schema.sql`, but don't modify that file directly,
instead put the changes in `sql/branch.sql`. During deployment that script will
be run on the production DB and the changes will be merged into `sql/schema.sql`.
- [Installation](#installation)
- [Dependencies](#dependencies)
- [Building](#building)
- [Launching](#launching)
- [Configuring](#configuring)
- [Vagrant](#vagrant)
- [Docker](#docker)
- [Help!](#help)
- [Configuration](https://github.com/gratipay/gratipay.com/wiki/Configuration)
- [Modifying CSS](#modifying-css)
- [Modifying the Database](#modifying-the-database)
- [Testing](#testing-)
- [Setting up a Database](#local-database-setup)
- [API](#api)
- [Implementations](#api-implementations)
- [Glossary](#glossary)
That process is semi-automated by `release.sh`.
Installation
============
Thanks for hacking on Gratipay! Be sure to review
[CONTRIBUTING](https://github.com/gratipay/gratipay.com/blob/master/CONTRIBUTING.md#readme)
as well if that's what you're planning to do.
Dependencies
------------
Building `gratipay.com` requires [Python
2.7](http://python.org/download/releases/2.7.4/), and a gcc/make toolchain.
All Python library dependencies are bundled in the repo (under `vendor/`). If
you are receiving issues from `psycopg2`, please [ensure that its needs are
met](http://initd.org/psycopg/docs/faq.html#problems-compiling-and-deploying-psycopg2).
On Debian or Ubuntu you will need the following packages:
$ sudo apt-get install postgresql-9.3 postgresql-contrib libpq-dev python-dev
On OS X you can [download Postgres directly](http://www.postgresql.org/download/macosx/) or install through [Homebrew](http://brew.sh/):
$ brew install postgresql
To configure local Postgres create default role (if it hasnt been created already) and database.
$ sudo -u postgres createuser --superuser $USER
$ createdb gratipay
If you are getting an error about `unknown argument: '-mno-fused-madd'` when
running `make`, then add
`Wno-error=unused-command-line-argument-hard-error-in-future` to your
`ARCHFLAGS` environment variable and run `make clean env` again (see [this Stack Overflow answer
for more information](http://stackoverflow.com/a/22355874/347246)):
$ ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future make clean env
Building
--------
All Python dependencies (including virtualenv) are bundled with Gratipay in the
vendor/ directory. Gratipay is designed so that you don't manage its
virtualenv directly and you don't download its dependencies at build
time.
The included `Makefile` contains several targets. Configuration options
are stored in default_local.env file while overrides are in local.env.
To create virtualenv enviroment with all python dependencies installed
in a sandbox:
$ make env
If you haven't run Gratipay for a while, you can reinstall the dependencies:
$ make clean env
Add the necessary schemas and insert dummy data into postgres:
$ make schema
$ make data
Launching
---------
Once you've installed Python and Postgres and set up a database, you can use
make to build and launch Gratipay:
$ make run
If you don't have make, look at the Makefile to see what steps you need
to perform to build and launch Gratipay. The Makefile is pretty simple and
straightforward.
If Gratipay launches successfully it will look like this:
```
$ make run
PATH=env/bin:{lots-more-of-your-own-PATH} env/bin/honcho run -e defaults.env,local.env web
2014-07-22 14:53:09 [1258] [INFO] Starting gunicorn 18.0
2014-07-22 14:53:09 [1258] [INFO] Listening at: http://0.0.0.0:8537 (1258)
2014-07-22 14:53:09 [1258] [INFO] Using worker: sync
2014-07-22 14:53:09 [1261] [INFO] Booting worker with pid: 1261
pid-1261 thread-140735191843600 (MainThread) Reading configuration from defaults, environment, and command line.
pid-1261 thread-140735191843600 (MainThread) changes_reload False default
pid-1261 thread-140735191843600 (MainThread) changes_reload True environment variable ASPEN_CHANGES_RELOAD=yes
pid-1261 thread-140735191843600 (MainThread) charset_dynamic UTF-8 default
pid-1261 thread-140735191843600 (MainThread) charset_static None default
pid-1261 thread-140735191843600 (MainThread) configuration_scripts [] default
pid-1261 thread-140735191843600 (MainThread) indices [u'index.html', u'index.json', u'index', u'index.html.spt', u'index.json.spt', u'index.spt'] default
pid-1261 thread-140735191843600 (MainThread) list_directories False default
pid-1261 thread-140735191843600 (MainThread) logging_threshold 0 default
pid-1261 thread-140735191843600 (MainThread) media_type_default text/plain default
pid-1261 thread-140735191843600 (MainThread) media_type_json application/json default
pid-1261 thread-140735191843600 (MainThread) project_root None default
pid-1261 thread-140735191843600 (MainThread) project_root . environment variable ASPEN_PROJECT_ROOT=.
pid-1261 thread-140735191843600 (MainThread) renderer_default stdlib_percent default
pid-1261 thread-140735191843600 (MainThread) show_tracebacks False default
pid-1261 thread-140735191843600 (MainThread) show_tracebacks True environment variable ASPEN_SHOW_TRACEBACKS=yes
pid-1261 thread-140735191843600 (MainThread) www_root None default
pid-1261 thread-140735191843600 (MainThread) www_root www/ environment variable ASPEN_WWW_ROOT=www/
pid-1261 thread-140735191843600 (MainThread) project_root is relative to CWD: '.'.
pid-1261 thread-140735191843600 (MainThread) project_root set to /Users/whit537/personal/gratipay/gratipay.com.
pid-1261 thread-140735191843600 (MainThread) Found plugin for renderer 'jinja2'
pid-1261 thread-140735191843600 (MainThread) Won't log to Sentry (SENTRY_DSN is empty).
pid-1261 thread-140735191843600 (MainThread) Renderers (*ed are unavailable, CAPS is default):
pid-1261 thread-140735191843600 (MainThread) stdlib_percent
pid-1261 thread-140735191843600 (MainThread) json_dump
pid-1261 thread-140735191843600 (MainThread) stdlib_format
pid-1261 thread-140735191843600 (MainThread) JINJA2
pid-1261 thread-140735191843600 (MainThread) stdlib_template
```
You should then find this in your browser at
[http://localhost:8537/](http://localhost:8537/):
![Success](https://raw.github.com/gratipay/gratipay.com/master/img-src/success.png)
Congratulations! Sign in using Twitter or GitHub and you're off and
running. At some point, try [running the test suite](#testing-).
Configuring
-----------
Gratipay's default configuration lives in [`defaults.env`](https://github.com/gratipay/gratipay.com/blob/master/defaults.env).
If you'd like to override some settings, create a file named `local.env` to store them.
The following explains some of the content of that file:
The `BALANCED_API_SECRET` is a test marketplace. To generate a new secret for
your own testing run this command:
curl -X POST https://api.balancedpayments.com/v1/api_keys | grep secret
Grab that secret and also create a new marketplace to test against:
curl -X POST https://api.balancedpayments.com/v1/marketplaces -u <your_secret>:
The site works without this, except for the credit card page. Visit the
[Balanced Documentation](https://www.balancedpayments.com/docs) if you want to
know more about creating marketplaces.
The `GITHUB_*` keys are for a gratipay-dev application in the Gratipay
organization on Github. It points back to localhost:8537, which is where
Gratipay will be running if you start it locally with `make run`. Similarly
with the `TWITTER_*` keys, but there they required us to spell it `127.0.0.1`.
If you wish to use a different username or database name for the database, you
should override the `DATABASE_URL` in `local.env` using the following format:
DATABASE_URL=postgres://<username>@localhost/<database name>
The `MANDRILL_KEY` value in `defaults.env` is for a test mail server, which
won't actually send email to you. If you need to receive email during
development then sign up for an account of your own at
[Mandrill](http://mandrill.com/) and override `MANDRILL_KEY` in your
`local.env`.
Vagrant
-------
If you have Vagrant installed, you can run Gratipay by running `vagrant up` from the project directory. Please note that if you ever switch between running Gratipay on your own machine to Vagrant or vice versa, you will need to run `make clean`.
If you're using Vagrant for the first time you'll need [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed. If you're on Linux you'll need to install `nfs-kernel-server`.
The `Vagrantfile` will download a custom made image from the Internet. If you have a slow internet connection, you can download a local copy of this file, by running:
`curl https://downloads.gratipay.com/gratipay.box`
Once downloaded into the top of the project tree, our Vagrantfile will use this local file automatically when you run `vagrant up`. Vagrant is setup to use key based SSH authentication, if you're prompted for a password when logging in please use `vagrant`.
**Mac users:** If you're prompted for a password during initial installation, it's sudo and you should enter your Mac OS password.
**Ubuntu users:** If you experience problems, please see [this
issue](https://github.com/gratipay/gratipay.com/pull/2321#issuecomment-41455169).
As mentioned, you will also need to be wary of projects that are nested
in encrypted directories.
Docker
------------
You can also install/run Gratipay with Docker.
Build it with the included Dockerfile:
```
$ git clone git@github.com:gratipay/gratipay.com.git
$ cd gratipay.com
$ docker build -t gratipay .
```
Once you've built the image, you can launch a container:
```
$ docker run -d -p 8537:8537 gratipay
```
Check it out at [localhost:8537](http://localhost:8537/)!
To edit files and have those changes reflect in the running container, mount your local folder when you execute the run command:
```
$ docker run -d -v $PWD:/srv/gratipay.com -p 8537:8537 gratipay
```
You can get the running container's ID with `docker ps`. With that, you can
- view the logs:
```
$ docker logs [container_id]
```
- run commands within the project root:
```
$ docker exec [container_id] make schema
$ docker exec [container_id] make data
```
Once you're done, kill the running container:
```
$ docker kill [container_id]
```
Help!
-----
If you get stuck somewhere along the way, you can find help in the #gratipay
channel on [Freenode](http://webchat.freenode.net/) or in the [issue
tracker](/gratipay/gratipay.com/issues/new) here on GitHub.
Thanks for installing Gratipay! :smiley:
Modifying CSS
=============
We use SCSS, with files stored in `scss/`. All of the individual files are
combined in `scss/gratipay.scss` which itself is compiled by `libsass` in
`www/assets/%version/gratipay.css.spt` on each request.
Modifying the Database
======================
We write SQL, specifically the [PostgreSQL
variant](http://www.postgresql.org/docs/9.3/static/). We keep our database
schema in
[`schema.sql`](https://github.com/gratipay/gratipay.com/blob/master/sql/schema.sql),
and we write schema changes for each PR branch in a `sql/branch.sql` file, which
then gets run against production and merged into `sql/schema.sql` during
deployment.
Testing [![Build Status](http://img.shields.io/travis/gratipay/gratipay.com/master.svg)](https://travis-ci.org/gratipay/gratipay.com)
=======
Please write unit tests for all new code and all code you change. Gratipay's
test suite uses the py.test test runner, which will be installed into the
virtualenv you get by running `make env`. As a rule of thumb, each test case
should perform one assertion.
### Testing [![Build Status](https://travis-ci.org/liberapay/liberapay.com.svg)](https://travis-ci.org/liberapay/liberapay.com)
The easiest way to run the test suite is:
$ make test
make test
However, the test suite deletes data in all tables in the public schema of the
database configured in your testing environment.
That recreates the test DB's schema and runs all the tests. To speed things up
you can also use the following commands:
To invoke py.test directly you should use the `honcho` utility that comes
with the install. First `make tests/env`, activate the virtualenv and then:
- `make pytest` only runs the python tests without recreating the test DB
- `make pytest-re` does the same but only runs the tests that failed in the previous run
[gratipay] $ cd tests/
[gratipay] $ honcho run -e defaults.env,local.env py.test
### Help
Local Database Setup
--------------------
For the best development experience, you need a local
installation of [Postgres](http://www.postgresql.org/download/). The best
version of Postgres to use is 9.3.2, because that's what we're using in
production at Heroku. You need at least 9.2, because we depend on being able to
specify a URI to `psql`, and that was added in 9.2.
+ Mac: use Homerew: `brew install postgres`
+ Ubuntu: use Apt: `apt-get install postgresql postgresql-contrib libpq-dev`
To setup the instance for gratipay's needs run:
$ sudo -u postgres createuser --superuser $USER
$ createdb gratipay
$ createdb gratipay-test
You can speed up the test suite when using a regular HDD by running:
$ psql -q gratipay-test -c 'alter database "gratipay-test" set synchronous_commit to off'
### Schema
Once Postgres is set up, run:
$ make schema
Which populates the database named by `DATABASE_URL` with the schema from `sql/schema.sql`.
### Example data
The gratipay database created in the last step is empty. To populate it with
some fake data, so that more of the site is functional, run this command:
$ make data
If there is something you can't seem to figure out by yourself, come ask us in
the IRC channel #liberapay on [Freenode](http://webchat.freenode.net/).
API
===
## License
The Gratipay API is comprised of these six endpoints:
**[/about/charts.json](https://gratipay.com/about/charts.json)**
([source](https://github.com/gratipay/gratipay.com/tree/master/www/about/charts.json.spt))&mdash;<i>public</i>&mdash;Returns
an array of objects, one per week, showing aggregate numbers over time. The
[stats](https://gratipay.com/about/stats) page uses this.
**[/about/paydays.json](https://gratipay.com/about/paydays.json)**
([source](https://github.com/gratipay/gratipay.com/tree/master/www/about/paydays.json.spt))&mdash;<i>public</i>&mdash;Returns
an array of objects, one per week, showing aggregate numbers over time. The old
charts page used to use this.
**[/about/stats.json](https://gratipay.com/about/stats.json)**
([source](https://github.com/gratipay/gratipay.com/tree/master/www/about/stats.spt))&mdash;<i>public</i>&mdash;Returns
an object giving a point-in-time snapshot of Gratipay. The
[stats](https://gratipay.com/about/stats.html) page displays the same info.
**/`%username`/charts.json**
([example](https://gratipay.com/Gratipay/charts.json),
[source](https://github.com/gratipay/gratipay.com/tree/master/www/%25username/charts.json.spt))&mdash;<i>public</i>&mdash;Returns
an array of objects, one per week, showing aggregate numbers over time for the
given user.
**/`%username`/public.json**
([example](https://gratipay.com/Gratipay/public.json),
[source](https://github.com/gratipay/gratipay.com/tree/master/www/%25username/public.json.spt))&mdash;<i>public</i>&mdash;Returns an object with these keys:
- "receiving"&mdash;an estimate of the amount the given participant will
receive this week
- "my_tip"&mdash;logged-in user's tip to the Gratipay participant in
question; possible values are:
- `undefined` (key not present)&mdash;there is no logged-in user
- "self"&mdash;logged-in user is the participant in question
- `null`&mdash;user has never tipped this participant
- "0.00"&mdash;user used to tip this participant
- "3.00"&mdash;user tips this participant the given amount
<br><br>
- "goal"&mdash;funding goal of the given participant; possible values are:
- `undefined` (key not present)&mdash;participant is a patron (or has 0 as the goal)
- `null`&mdash;participant is grateful for gifts, but doesn't have a specific funding goal
- "100.00"&mdash;participant's goal is to receive the given amount per week
<br><br>
- "elsewhere"&mdash;participant's connected accounts elsewhere; returns an object with these keys:
- "bitbucket"&mdash;participant's Bitbucket account; possible values are:
- `undefined` (key not present)&mdash;no Bitbucket account connected
- `https://bitbucket.org/api/1.0/users/%bitbucket_username`
- "github"&mdash;participant's GitHub account; possible values are:
- `undefined` (key not present)&mdash;no GitHub account connected
- `https://api.github.com/users/%github_username`
- "twitter"&mdash;participant's Twitter account; possible values are:
- `undefined` (key not present)&mdash;no Twitter account connected
- `https://api.twitter.com/1.1/users/show.json?id=%twitter_immutable_id&include_entities=1`
- "openstreetmap"&mdash;participant's OpenStreetMap account; possible values are:
- `undefined` (key not present)&mdash;no OpenStreetMap account connected
- `http://www.openstreetmap.org/user/%openstreetmap_username`
**/`%username`/tips.json**
([source](https://github.com/gratipay/gratipay.com/tree/master/www/%25username/tips.json.spt))&mdash;<i>private</i>&mdash;Responds
to `GET` with an array of objects representing your current tips. `POST` the
same structure back in order to update tips in bulk (be sure to set
`Content-Type` to `application/json` instead of
`application/x-www-form-urlencoded`). You can `POST` a partial array to update
a subset of your tips. The response to a `POST` will be only the subset you
updated. If the `amount` is `"error"` then there will also be an `error`
attribute with a one-word error code. If you include an `also_prune` key in the
querystring (not the body!) with a value of `yes`, `true`, or `1`, then any
tips not in the array you `POST` will be zeroed out.
NOTE: The amounts must be encoded as a string (rather than a number).
Additionally, currently, the only supported platform is 'gratipay' ('gittip'
still works for backwards-compatibility).
This endpoint requires authentication. Look for your user ID and API key on your
[account page](https://gratipay.com/about/me/settings/), and pass them using basic
auth. E.g.:
```
curl https://gratipay.com/foobar/tips.json \
-u $userid:$api_key \
-X POST \
-d'[{"username":"bazbuz", "platform":"gratipay", "amount": "1.00"}]' \
-H"Content-Type: application/json"
```
API Implementations
-------------------
Below are some projects that use the Gratipay APIs, that can serve as inspiration
for your project!
### Renamed to Gratipay
- [Ruby: gratitude](https://github.com/JohnKellyFerguson/gratitude): a simple
ruby wrapper for the Gratipay API
- [php-curl-class](https://github.com/php-curl-class/php-curl-class/blob/master/examples/gratipay_send_tip.php): a php class to tip using the Gratipay API
- [gratipay-twisted](https://github.com/TigerND/gratipay-twisted): Gratipay client
for the Twisted framework
- [WordPress: WP-Gratipay](https://github.com/KakersUK/WP-Gratipay): a simple way to show a Gratipay widget on your WordPress site
### Still Using Gittip
These probably still work, but are using our [old name](https://medium.com/gratipay-blog/gratitude-gratipay-ef24ad5e41f9):
- [Drupal: Gittip](https://drupal.org/project/gittip): Includes a Gittip
giving field type to let you implement the Khan academy model for users on
your Drupal site. ([ticket](https://www.drupal.org/node/2332131))
- [Node.js: Node-Gittip](https://npmjs.org/package/gittip) (also see [Khan
Academy's setup](http://ejohn.org/blog/gittip-at-khan-academy/)) ([ticket](https://github.com/KevinTCoughlin/node-gittip/issues/1))
- [hubot-gittip](https://github.com/myplanetdigital/hubot-gittip): A Hubot
script for interacting with a shared Gratipay account. ([ticket](https://github.com/myplanetdigital/hubot-gittip/issues/6))
- [gittip-collab](https://github.com/engineyard/gittip-collab): A Khan-style
tool for managing a Gittip account as a team. ([ticket](https://github.com/engineyard/gittip-collab/issues/1))
- [WWW::Gittip](https://metacpan.org/pod/WWW::Gittip): A Perl module
implementing the Gittip API more or less ([ticket](https://rt.cpan.org/Public/Bug/Display.html?id=101103))
Glossary
========
**Account Elsewhere** - An entity's registration on a platform other than
Gratipay (e.g., Twitter).
**Entity** - An entity.
**Participant** - An entity registered with Gratipay.
**User** - A person using the Gratipay website. Can be authenticated or
anonymous. If authenticated, the user is guaranteed to also be a participant.
License
========
Gratipay is dedicated to public domain. See the text of [CC0 1.0 Universal](http://creativecommons.org/publicdomain/zero/1.0/) dedication in [COPYING](COPYING) here.
[CC0 Public Domain Dedication](http://creativecommons.org/publicdomain/zero/1.0/)

69
Vagrantfile vendored
View File

@ -1,69 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# We use multi-machine config here. First one to rebuild the box from scratch
# (it is normally commented) and second (faster) that download built version.
# Previous configs are:
# - https://github.com/gratipay/gratipay.com/blob/83312e60c6b31c298ffca61036baa9849044c75e/Vagrantfile
# - https://github.com/gratipay/gratipay.com/blob/fc0b4395e85259cdd17d9fe8560bf654abd756ce/Vagrantfile
# --- [ common access parameters ] ---
# Gratipay app is accessible at http://localhost:8537/ from host
#
config.vm.network :forwarded_port, guest: 8537, host: 8537
# Current folder is available as /vagrant from guest.
# We speed up access by installing and using NFS as described here:
# https://docs.vagrantup.com/v2/synced-folders/nfs.html
config.vm.synced_folder ".", "/vagrant", type: "nfs"
config.vm.network "private_network", ip: "172.27.36.119"
# http://serverfault.com/questions/453185/vagrant-virtualbox-dns-10-0-2-3-not-working
config.vm.provider "virtualbox" do |vb|
# VirtualBox DNS proxy is enabled by default, but it fails to refresh
# DHCP leases after resume from sleep or WiFi network change
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
# --- [ boxes ] ---
#
# after you bootstrap this box, saving it into a gratipay.box is as easy as
# $ vagrant package --output gratipay.box
#
config.vm.define "basebox" do |base|
# using Ubuntu 14.04, because it is what our hosting (Heroku) uses
# (search cedar-14 for details)
base.vm.box = "ubuntu/trusty64"
# --- install prerequisites ---
# [ ] use the same package versions as Heroku
# [ ] figure out how to fetch Heroku versions
base.vm.provision :shell, :path => "scripts/vagrant-debian.sh" # install system prerequisites
base.vm.provision :shell, :path => "scripts/vagrant-setup.sh" # apply user specific settings
end
#config.vm.define "gratipay" do |box|
# box.vm.box = "gratipay"
# box.vm.box_url = File.exist?("gratipay.box") ? "file://gratipay.box" : "https://downloads.gratipay.com/gratipay.box"
#end
config.vm.post_up_message = '
----[ Gratipay Vagrant VM ]--------------------
vagrant ssh
$ make run - start local Gratipay server
$ make test - run tests
-----------------------------------------------
'
end

View File

@ -1,83 +0,0 @@
#!/usr/bin/env bash
# Fail on error.
# ==============
set -e
# Be somewhere predictable.
# =========================
cd "`dirname $0`"
# Helpers
# =======
confirm () {
proceed=""
while [ "$proceed" != "y" ]; do
read -p"$1 (y/N) " proceed
if [ "$proceed" == "n" -o "$proceed" == "N" -o "$proceed" == "" ]
then
return 1
fi
done
return 0
}
require () {
if [ ! `which $1` ]; then
echo "The '$1' command was not found."
return 1
fi
return 0
}
get_filepath () {
TODAY="`date +%F`"
CANDIDATE="$DIRPATH/$TODAY.psql"
if [ -f "$CANDIDATE" ]
then
echo " $CANDIDATE" >&2
for x in {a..z}
do
CANDIDATE="$DIRPATH/$TODAY$x.psql"
if [ ! -f "$CANDIDATE" ]
then
break
fi
echo " $CANDIDATE" >&2
done
if [ -f "$CANDIDATE" ]
then
CANDIDATE=""
else
echo "----> $CANDIDATE" >&2
echo >&2
fi
fi
echo "$CANDIDATE"
}
# Work
# ====
require heroku
require pg_dump
DIRPATH="../backups"
FILEPATH=$(get_filepath)
if [ "$FILEPATH" = "" ]
then
exit "Too many backups!"
fi
confirm "Backup the Gratipay database to $FILEPATH?"
if [ $? -eq 0 ]; then
export PGSSLMODE=require
pg_dump `heroku config:get DATABASE_URL -a gratipay` > $FILEPATH
fi

View File

@ -1,56 +0,0 @@
#!/bin/sh
set -e # exit when any command fails
# Install base dependencies
sudo apt-get install -y g++ git language-pack-en libpq-dev make python-dev wget
# Get Postgres installed and running
pkgmissing() {
if dpkg -l | grep 'ii '$1' ' > /dev/null;
then
return 1;
fi;
return 0;
}
if pkgmissing postgresql-9.3; then
if ! dpkg -l postgresql-9.3
then
echo " configuring apt for postgres-9.3"
codename=`lsb_release -cs`
aptlist="deb http://apt.postgresql.org/pub/repos/apt/ $codename-pgdg main"
echo $aptlist | sudo tee /etc/apt/sources.list.d/pgdg.list > /dev/null
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
fi
sudo apt-get install -y postgresql-9.3 postgresql-contrib
sudo service postgresql start
fi
if [ -z ""`psql template1 -tAc "select usename from pg_user where usename='$USER'"` ];
then
sudo -u postgres createuser --superuser $USER
fi;
db_exists() {
if [ -n ""`psql template1 -tAc "select datname from pg_database where datname='$1'"` ];
then
return 0;
fi
return 1;
}
if ! db_exists gratipay-test;
then
createdb gratipay-test
psql -q gratipay-test -c 'alter database "gratipay-test" set synchronous_commit to off'
fi
if ! db_exists gratipay;
then
createdb gratipay
fi
echo "done"

View File

@ -1,294 +0,0 @@
#!/usr/bin/env python
"""This is a script for managing MassPay each week.
Most of our payouts are handled by Balanced, but they're limited to people in
the U.S. We need to payout to people outside the U.S. (#126), and while we work
on a long-term solution, we are using PayPal. However, we've grown past the
point that PayPal's Instant Transfer feature is workable. This script is for
interfacing with PayPal's MassPay feature.
This script provides for:
1. Computing an input CSV by hitting the Gratipay database directly.
2. Computing two output CSVs (one to upload to PayPal, the second to use for POSTing
the exchanges back to Gratipay)
3. POSTing the exchanges back to Gratipay via the HTTP API.
The idea is that you run steps 1 and 2, then run through the MassPay UI on the
PayPal website using the appropriate CSV from step 2, then run step 3.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import csv
import datetime
import getpass
import os
import sys
from decimal import Decimal as D, ROUND_HALF_UP
import requests
from gratipay import wireup
from httplib import IncompleteRead
os.chdir('../masspay')
ts = datetime.datetime.now().strftime('%Y-%m-%d')
INPUT_CSV = '{}.input.csv'.format(ts)
PAYPAL_CSV = '{}.output.paypal.csv'.format(ts)
GRATIPAY_CSV = '{}.output.gratipay.csv'.format(ts)
REPORT_CSV = '{}.report.paypal.csv'.format(ts)
def round_(d):
return d.quantize(D('0.01'), rounding=ROUND_HALF_UP)
def print_rule(w=80):
print("-" * w)
class Payee(object):
username = None
email = None
gross = None
gross_perc = None
fee = None
net = None
additional_note = ""
def __init__(self, rec):
self.username, self.email, fee_cap, amount = rec
self.gross = D(amount)
self.fee = D(0)
self.fee_cap = D(fee_cap)
self.net = self.gross
def assess_fee(self):
# In order to avoid slowly leaking escrow, we need to be careful about
# how we compute the fee. It's complicated, but it goes something like
# this:
#
# 1. We want to pass PayPal's fees through to each payee.
#
# 2. With MassPay there is no option to have the receiver pay the fee,
# as there is with Instant Transfer.
#
# 3. We have to subtract the fee before uploading the spreadsheet
# to PayPal.
#
# 4. If we upload 15.24, PayPal upcharges to 15.54.
#
# 6. If we upload 15.25, PayPal upcharges to 15.56.
#
# 7. They only accept whole cents. We can't upload 15.245.
#
# 8. What if we want to hit 15.55?
#
# 9. We can't.
#
# 10. Our solution is to leave a penny behind in Gratipay for
# affected payees.
#
# 11. BUT ... if we upload 1.25, PayPal upcharges to 1.28. Think about
# it.
#
# See also: https://github.com/gratipay/gratipay.com/issues/1673
# https://github.com/gratipay/gratipay.com/issues/2029
# https://github.com/gratipay/gratipay.com/issues/2198
# https://github.com/gratipay/gratipay.com/pull/2209
# https://github.com/gratipay/gratipay.com/issues/2296
target = net = self.gross
while 1:
net -= D('0.01')
fee = round_(net * D('0.02'))
fee = min(fee, self.fee_cap)
gross = net + fee
if gross <= target:
break
self.gross = gross
self.net = net
self.fee = fee
remainder = target - gross
if remainder > 0:
n = "{:.2} remaining due to PayPal rounding limitation.".format(remainder)
self.additional_note = n
return fee
def compute_input_csv():
db = wireup.db(wireup.env())
participants = db.all("""
SELECT p.*, r.address AS paypal_email, r.fee_cap AS paypal_fee_cap
FROM exchange_routes r
JOIN participants p ON p.id = r.participant
WHERE r.network = 'paypal'
AND p.balance > 0
ORDER BY p.balance DESC
""")
writer = csv.writer(open(INPUT_CSV, 'w+'))
print_rule(88)
headers = "username", "email", "fee cap", "balance", "tips", "amount"
print("{:<24}{:<32} {:^7} {:^7} {:^7} {:^7}".format(*headers))
print_rule(88)
total_gross = 0
for participant in participants:
total = participant.giving + participant.pledging
amount = participant.balance - total
if amount < 0.50:
# Minimum payout of 50 cents. I think that otherwise PayPal upcharges to a penny.
# See https://github.com/gratipay/gratipay.com/issues/1958.
continue
total_gross += amount
print("{:<24}{:<32} {:>7} {:>7} {:>7} {:>7}".format( participant.username
, participant.paypal_email
, participant.paypal_fee_cap
, participant.balance
, total
, amount
))
row = (participant.username, participant.paypal_email, participant.paypal_fee_cap, amount)
writer.writerow(row)
print(" "*80, "-"*7)
print("{:>88}".format(total_gross))
def compute_output_csvs():
payees = [Payee(rec) for rec in csv.reader(open(INPUT_CSV))]
payees.sort(key=lambda o: o.gross, reverse=True)
total_fees = sum([payee.assess_fee() for payee in payees]) # side-effective!
total_net = sum([p.net for p in payees])
total_gross = sum([p.gross for p in payees])
assert total_fees + total_net == total_gross
paypal_csv = csv.writer(open(PAYPAL_CSV, 'w+'))
gratipay_csv = csv.writer(open(GRATIPAY_CSV, 'w+'))
print_rule()
print("{:<24}{:<32} {:^7} {:^7} {:^7}".format("username", "email", "gross", "fee", "net"))
print_rule()
for payee in payees:
paypal_csv.writerow((payee.email, payee.net, "usd"))
gratipay_csv.writerow(( payee.username
, payee.email
, payee.gross
, payee.fee
, payee.net
, payee.additional_note
))
print("{username:<24}{email:<32} {gross:>7} {fee:>7} {net:>7}".format(**payee.__dict__))
print(" "*56, "-"*23)
print("{:>64} {:>7} {:>7}".format(total_gross, total_fees, total_net))
def load_statuses():
_status_map = {'Completed': 'succeeded', 'Denied': 'failed'} # PayPal -> Gratipay
statuses = {}
fp = open(REPORT_CSV)
for line in fp:
if line.startswith('Transaction ID,Recipient'):
break
for rec in csv.reader(fp):
statuses[rec[1]] = _status_map[rec[5]]
return statuses
def post_back_to_gratipay():
try:
gratipay_api_key = os.environ['GRATIPAY_API_KEY']
except KeyError:
gratipay_api_key = getpass.getpass("Gratipay API key: ")
try:
gratipay_base_url = os.environ['GRATIPAY_BASE_URL']
except KeyError:
gratipay_base_url = 'https://gratipay.com'
statuses = load_statuses()
nposts = 0
for username, email, gross, fee, net, additional_note in csv.reader(open(GRATIPAY_CSV)):
url = '{}/{}/history/record-an-exchange'.format(gratipay_base_url, username)
note = 'PayPal MassPay to {}.'.format(email)
if additional_note:
note += " " + additional_note
print(note)
status = statuses[email]
data = {'amount': '-' + net, 'fee': fee, 'note': note, 'status': status}
try:
response = requests.post(url, auth=(gratipay_api_key, ''), data=data)
except IncompleteRead:
print('IncompleteRead, proceeding (but double-check!)')
else:
if response.status_code == 200:
nposts += 1
else:
if response.status_code == 404:
print('Got 404, is your API key good? {}'.format(gratipay_api_key))
else:
print('... resulted in a {} response:'.format(response.status_code))
print(response.text)
raise SystemExit
print("POSTed MassPay back to Gratipay for {} users.".format(nposts))
def run_report():
"""Print a report to help Determine how much escrow we should store in PayPal.
"""
totals = []
max_masspay = max_weekly_growth = D(0)
for filename in os.listdir('.'):
if not filename.endswith('.input.csv'):
continue
datestamp = filename.split('.')[0]
totals.append(D(0))
for rec in csv.reader(open(filename)):
amount = rec[-1]
totals[-1] += D(amount)
max_masspay = max(max_masspay, totals[-1])
if len(totals) == 1:
print("{} {:8}".format(datestamp, totals[-1]))
else:
weekly_growth = totals[-1] / totals[-2]
max_weekly_growth = max(max_weekly_growth, weekly_growth)
print("{} {:8} {:4.1f}".format(datestamp, totals[-1], weekly_growth))
print()
print("Max Withdrawal: ${:9,.2f}".format(max_masspay))
print("Max Weekly Growth: {:8.1f}".format(max_weekly_growth))
print("5x Current: ${:9,.2f}".format(5 * totals[-1]))
def main():
if not sys.argv[1:]:
print("Looking for files for {} ...".format(ts))
for filename in (INPUT_CSV, PAYPAL_CSV, GRATIPAY_CSV):
print(" [{}] {}".format('x' if os.path.exists(filename) else ' ', filename))
print("Rerun with one of these options:")
print(" -i - hits db to generate input CSV (needs envvars via heroku + honcho)")
print(" -o - computes output CSVs (doesn't need anything but input CSV)")
print(" -p - posts back to Gratipay (prompts for API key)")
elif '-i' in sys.argv:
compute_input_csv()
elif '-o' in sys.argv:
compute_output_csvs()
elif '-p' in sys.argv:
post_back_to_gratipay()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print()

View File

@ -1,11 +0,0 @@
#!/usr/bin/env bash
# see https://github.com/gratipay/gratipay.com/issues/1771
dump=${1-`ls -r ~/gratipay-dumps/????-??-??.dump | head -1`}
cores=`grep -c ^processor /proc/cpuinfo`
echo "Clearing '$USER' database..."
psql -q -c "drop schema public cascade; create schema public;" 2> /dev/null
echo "Restoring $dump..."
pg_restore --jobs $cores --no-acl --no-owner -d $USER $dump

View File

@ -1,69 +0,0 @@
#!/usr/bin/env python
"""This is a command line utility for managing Gratipay backups.
Running this script gets you a `snapper> ` prompt with commands to take backups
and load them locally. Backups are managed as *.psql files in ../backups/, and
they're loaded into a local gratipay-bak database. Type 'help' or '?' at the
prompt for help.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import cmd
import os
import subprocess
class Snapper(cmd.Cmd):
prompt = 'snapper> '
root = '../backups'
dbname = 'gratipay-bak'
def do_EOF(self, line):
raise KeyboardInterrupt
def do_quit(self, line):
raise SystemExit
do_exit = do_quit
def do_new(self, line):
"""Take a new backup.
"""
subprocess.call('./backup.sh')
do_n = do_new
def do_list(self, line):
"""List available backups.
"""
filenames = self.get_filenames()
for i, filename in enumerate(filenames):
print('{:>2} {}'.format(i, filename[:-len('.psql')]))
do_l = do_list
def get_filenames(self):
return sorted([f for f in os.listdir(self.root) if f.endswith('.psql')])
def do_load(self, line):
"""Load a backup based on its number per `list`..
"""
try:
i = int(line)
filename = self.get_filenames()[i]
except (ValueError, KeyError):
print('\x1b[31;1mBad backup number!\x1b[0m')
print('\x1b[32;1mPick one of these:\x1b[0m')
self.do_list('')
else:
if subprocess.call(['dropdb', self.dbname]) == 0:
if subprocess.call(['createdb', self.dbname]) == 0:
subprocess.call( 'psql {} < {}/{}'.format(self.dbname, self.root, filename)
, shell=True
)
if __name__ == '__main__':
try:
Snapper().cmdloop()
except KeyboardInterrupt:
print()

View File

@ -1,180 +0,0 @@
#!/usr/bin/env python
"""This is a one-off script to update user_info for #1936.
This could be generalized for #900.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import os
import time
import sys
import requests
from gratipay import wireup
from requests_oauthlib import OAuth1
db = wireup.db(wireup.env())
def update_twitter():
oauth = OAuth1( os.environ['TWITTER_CONSUMER_KEY']
, os.environ['TWITTER_CONSUMER_SECRET']
, os.environ['TWITTER_ACCESS_TOKEN']
, os.environ['TWITTER_ACCESS_TOKEN_SECRET']
)
elsewhere = db.all("SELECT user_id FROM ELSEWHERE WHERE platform='twitter' ORDER BY id;")
url = "https://api.twitter.com/1.1/users/lookup.json"
while elsewhere:
batch = elsewhere[:100]
elsewhere = elsewhere[100:]
user_ids = ','.join([str(user_id) for user_id in batch])
response = requests.post(url, data={'user_id': user_ids}, auth=oauth)
# Log the rate-limit.
# ===================
nremaining = int(response.headers['X-RATE-LIMIT-REMAINING'])
reset = int(response.headers['X-RATE-LIMIT-RESET'])
print(nremaining, reset, time.time())
if response.status_code != 200:
# Who knows what happened?
# ========================
# Supposedly we shouldn't hit 429, at least.
print(response.status_code, response.text)
else:
# Update!
# =======
users = response.json()
with db.get_cursor() as c:
for user_info in users:
# flatten per upsert method in gratipay/elsewhere/__init__.py
for k, v in user_info.items():
user_info[k] = unicode(v)
user_id = user_info['id']
c.one("""
UPDATE elsewhere
SET user_info=%s
WHERE user_id=%s
AND platform='twitter'
RETURNING id
""", (user_info, user_id))
print("updated {} ({})".format(user_info['screen_name'], user_id))
# find deleted users
existing = set(u['id'] for u in users)
deleted = existing - set(batch)
for user_id in deleted:
c.one("""
UPDATE elsewhere
SET user_info=NULL
WHERE user_id=%s
AND platform='twitter'
RETURNING id
""", (user_id,))
print("orphan found: {}".format(user_id))
# Stay under our rate limit.
# =========================
# We get 180 per 15 minutes for the users/lookup endpoint, per:
#
# https://dev.twitter.com/docs/rate-limiting/1.1/limits
sleep_for = 5
if nremaining == 0:
sleep_for = reset - time.time()
sleep_for += 10 # Account for potential clock skew between us and Twitter.
time.sleep(sleep_for)
def update_github():
elsewhere = db.all("SELECT user_id FROM ELSEWHERE WHERE platform='github' ORDER BY id;")
url = "https://api.github.com/user/%s"
client_id = os.environ.get('GITHUB_CLIENT_ID')
client_secret = os.environ.get('GITHUB_CLIENT_SECRET')
for user_id in elsewhere:
response = requests.get(url % user_id, params={
'client_id': client_id,
'client_secret': client_secret
})
# Log the rate-limit.
# ===================
nremaining = int(response.headers['X-RATELIMIT-REMAINING'])
reset = int(response.headers['X-RATELIMIT-RESET'])
# https://developer.github.com/v3/#rate-limiting
now = time.time()
print(nremaining, reset, now, reset-now, end=' ')
status = response.status_code
if status == 200:
user_info = response.json()
# flatten per upsert method in gratipay/elsewhere/__init__.py
for k, v in user_info.items():
user_info[k] = unicode(v)
assert user_id == user_info['id']
db.one("""
UPDATE elsewhere
SET user_info=%s
WHERE user_id=%s
AND platform='github'
RETURNING id
""", (user_info, user_id))
print("updated {} ({})".format(user_info['login'], user_id))
elif status == 404:
db.one("""
UPDATE elsewhere
SET user_info=NULL
WHERE user_id=%s
AND platform='github'
RETURNING id
""", (user_id,))
print("orphan found: {}".format(user_id))
else:
# some other problem
print(response.status_code, response.text)
sleep_for = 0.5
if nremaining == 0:
sleep_for = reset - time.time()
sleep_for += 10 # Account for potential clock skew between us and them
time.sleep(sleep_for)
if __name__ == "__main__":
if len(sys.argv) == 1:
platform = raw_input("twitter or github?: ")
else:
platform = sys.argv[1]
if platform == 'twitter':
update_twitter()
elif platform == 'github':
update_github()

View File

@ -1,68 +1,63 @@
DATABASE_URL="dbname=gratipay"
DATABASE_URL="dbname=liberapay"
PYTHONDONTWRITEBYTECODE=true
PORT=8537
CANONICAL_HOST=localhost:8537
PORT=8339
CANONICAL_HOST=localhost:8339
CANONICAL_SCHEME=http
DATABASE_MAXCONN=10
GRATIPAY_ASSET_URL=/assets/
GRATIPAY_CACHE_STATIC=no
GRATIPAY_COMPRESS_ASSETS=no
ASSET_URL=/assets/
CACHE_STATIC=no
COMPRESS_ASSETS=no
BALANCED_API_SECRET=ak-test-2zFgxfVijBzn4xC9dLRtLkxoB38iNKNKR
COINBASE_API_KEY=uETKVUrnPuXzVaVj
COINBASE_API_SECRET=32zAkQCcHHYkGGn29VkvEZvn21PM1lgO
BALANCED_API_SECRET=
OAUTHLIB_INSECURE_TRANSPORT=1
OAUTHLIB_RELAX_TOKEN_SCOPE=1
GITHUB_CLIENT_ID=3785a9ac30df99feeef5
GITHUB_CLIENT_SECRET=5f3ba73babf40b59ce5cc34f909136208f3b81b6
GITHUB_CALLBACK=http://127.0.0.1:8537/on/github/associate
GITHUB_CLIENT_ID=18891d01e40e5aef93b8
GITHUB_CLIENT_SECRET=46f75669895e96029d57b64832d6f2c8e6291a0e
GITHUB_CALLBACK=http://127.0.0.1:8339/on/github/associate
BITBUCKET_CONSUMER_KEY=pp8wEHmS43qVE6yHj5
BITBUCKET_CONSUMER_SECRET=nmqwaZtwQWkpLE89Tc6XYKkWMxD84TbE
BITBUCKET_CALLBACK=http://127.0.0.1:8537/on/bitbucket/associate
BITBUCKET_CONSUMER_KEY=Fu9aQHB64WdVR7UxY4
BITBUCKET_CONSUMER_SECRET=Y5G6A9BaWDxn2gLKZwwkrGtVE3Zjd7y7
BITBUCKET_CALLBACK=http://127.0.0.1:8339/on/bitbucket/associate
TWITTER_CONSUMER_KEY=E9Jdz8gEa2jvJxeY8I9CVKjSi
TWITTER_CONSUMER_SECRET=1gbEU3sqBVzadrLl1zlUoJDZV0RcbOBbUb0D4Q04QWEwRJq7t0
TWITTER_CALLBACK=http://127.0.0.1:8537/on/twitter/associate
TWITTER_CONSUMER_KEY=h8bBZtoPNz63S5RkZdbo9R5zb
TWITTER_CONSUMER_SECRET=Jye64vkWxa2dQu64feTnk0BM3j4JO8ZlTa4EQvMDwrweLkwPaw
TWITTER_CALLBACK=http://127.0.0.1:8339/on/twitter/associate
FACEBOOK_APP_ID=522653061204254
FACEBOOK_APP_SECRET=d1cd7715dad7bc82c984d75cc2ece73f
FACEBOOK_CALLBACK=http://localhost:8537/on/facebook/associate
FACEBOOK_APP_ID=1418954898427187
FACEBOOK_APP_SECRET=3bcb5dc6ce821e5202870c1e6ef5bbc4
FACEBOOK_CALLBACK=http://localhost:8339/on/facebook/associate
GOOGLE_CLIENT_ID=536476350570-aigraucpr7s30auc031ef7hf1mcepumd.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=0V0vEns6cNyI8Uv-hxt7Ft2Q
GOOGLE_CALLBACK=http://localhost:8537/on/google/associate
GOOGLE_CLIENT_ID=547044792904-jikvtdgeigpot9ek7tsb5660t8rp9p77.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=7AkgPekyWr6stQWdM6y1TtV6
GOOGLE_CALLBACK=http://localhost:8339/on/google/associate
BOUNTYSOURCE_API_SECRET=e2BbqjNY60kC7V-Uq1dv2oHgGavbWm9pUJmiRHCApFZHDiY9aZyAspInhZaZ94x9
BOUNTYSOURCE_CALLBACK=http://127.0.0.1:8537/on/bountysource/associate
BOUNTYSOURCE_API_SECRET=
BOUNTYSOURCE_CALLBACK=http://127.0.0.1:8339/on/bountysource/associate
BOUNTYSOURCE_API_HOST=https://staging-api.bountysource.com
BOUNTYSOURCE_WWW_HOST=https://staging.bountysource.com
VENMO_CLIENT_ID=2031
VENMO_CLIENT_SECRET=YaBbmZQVmQjfgL7NFekpyJv4PayUhwhb
VENMO_CALLBACK=http://localhost:8537/on/venmo/associate
VENMO_CLIENT_ID=
VENMO_CLIENT_SECRET=
VENMO_CALLBACK=http://localhost:8339/on/venmo/associate
OPENSTREETMAP_CONSUMER_KEY=2pHsNzigNVLXcqa3Vq6QOolb26HL62DtOxw6TT5E
OPENSTREETMAP_CONSUMER_SECRET=vdNq2XeqNHOPOJ2rOABNbrKUv7GqHRgy7x46Brga
OPENSTREETMAP_CALLBACK=http://127.0.0.1:8537/on/openstreetmap/associate
OPENSTREETMAP_CONSUMER_KEY=
OPENSTREETMAP_CONSUMER_SECRET=
OPENSTREETMAP_CALLBACK=http://127.0.0.1:8339/on/openstreetmap/associate
OPENSTREETMAP_API_URL=http://www.openstreetmap.org/api/0.6
OPENSTREETMAP_AUTH_URL=http://www.openstreetmap.org
UPDATE_GLOBAL_STATS_EVERY=300
CHECK_DB_EVERY=600
DEQUEUE_EMAILS_EVERY=60
OPTIMIZELY_ID=
INCLUDE_PIWIK=no
SENTRY_DSN=
LOG_METRICS=0
ASPEN_CHANGES_RELOAD=yes
ASPEN_NETWORK_ADDRESS=:8537
ASPEN_NETWORK_ADDRESS=:8339
ASPEN_PROJECT_ROOT=.
ASPEN_SHOW_TRACEBACKS=yes
ASPEN_WWW_ROOT=www/
@ -70,6 +65,6 @@ ASPEN_WWW_ROOT=www/
# https://github.com/benoitc/gunicorn/issues/186
GUNICORN_OPTS="--workers=1 --timeout=99999999"
MANDRILL_KEY=Phh_Lm3RdPT5blqOPY4dVQ
MANDRILL_KEY=
RAISE_SIGNIN_NOTIFICATIONS=no

View File

@ -1,49 +0,0 @@
# Makefile for Sphinx documentation
# Trimmed up from the
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = ../env/bin/swaddle ../tests/env ../env/bin/sphinx-build
BUILDDIR = _build
# User-friendly check for sphinx-build
#ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
#$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
#endif
# Internal variables.
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(SPHINXOPTS) .
.PHONY: help clean html linkcheck doctest
default: html
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf gratipay* $(BUILDDIR)/*
rst:
AUTOLIB_LIBRARY_ROOT=../gratipay ./autolib.py
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -1,86 +0,0 @@
#!/usr/bin/env python
"""Generate *.rst files to mirror *.py files in a Python library.
This script is conceptually similar to the sphinx-apidoc script bundled with
Sphinx:
http://sphinx-doc.org/man/sphinx-apidoc.html
We produce different *.rst output, however.
"""
from __future__ import print_function, unicode_literals
import os
w = lambda f, s, *a, **kw: print(s.format(*a, **kw), file=f)
def rst_for_module(toc_path):
"""Given a toc_path, write rst and return a file object.
"""
f = open(toc_path + '.rst', 'w+')
heading = ":mod:`{}`".format(os.path.basename(toc_path))
dotted = toc_path.replace('/', '.')
w(f, heading)
w(f, "=" * len(heading))
w(f, ".. automodule:: {}", dotted)
return f
def rst_for_package(root, dirs, files):
"""Given ../mylib/path/to/package and lists of dir/file names, write rst.
"""
doc_path = root[3:]
if not os.path.isdir(doc_path):
os.mkdir(doc_path)
# Start a rst doc for this package.
# =================================
f = rst_for_module(doc_path)
# Add a table of contents.
# ========================
w(f, ".. toctree::")
def toc(doc_path, name):
parent = os.path.dirname(doc_path)
toc_path = os.path.join(doc_path[len(parent):].lstrip('/'), name)
if toc_path.endswith('.py'):
toc_path = toc_path[:-len('.py')]
w(f, " {}", toc_path)
return os.path.join(parent, toc_path)
for name in sorted(dirs + files):
if name in dirs:
toc(doc_path, name)
else:
if not name.endswith('.py'): continue
if name == '__init__.py': continue
toc_path = toc(doc_path, name)
# Write a rst file for each module.
# =================================
rst_for_module(toc_path)
def main():
library_root = os.environ['AUTOLIB_LIBRARY_ROOT']
for root, dirs, files in os.walk(library_root):
rst_for_package(root, dirs, files)
if __name__ == '__main__':
main()

View File

@ -1,267 +0,0 @@
# -*- coding: utf-8 -*-
#
# Gratipay documentation build configuration file, created by
# sphinx-quickstart on Thu Aug 8 23:20:15 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Gratipay'
copyright = u'2014, Gratipay, LLC'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '-'
# The full version, including alpha/beta/rc tags.
release = '-'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Generate RST files -------------------------------------------------------
# We do this in here instead of in the Makefile so that RTD picks this up.
os.environ['AUTOLIB_LIBRARY_ROOT'] = '../gratipay'
os.system("./autolib.py")
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Gratipaydoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Gratipay.tex', u'Gratipay Documentation',
u'Gratipay, LLC', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'gratipay', u'Gratipay Documentation',
[u'Gratipay, LLC'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Gratipay', u'Gratipay Documentation',
u'Gratipay, LLC', 'Gratipay', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
autodoc_default_flags = ['members', 'member-order: bysource']
import mock
MOCK_MODULES = ['scipy', 'scipy.sparse']
for mod_name in MOCK_MODULES:
sys.modules[mod_name] = mock.Mock()

View File

@ -1,36 +0,0 @@
gratipay.com
==============
Welcome! This is the documentation for programmers working on `gratipay.com`_
(not to be confused with programmers working with Gratipay's `web API`_).
.. _gratipay.com: https://github.com/gratipay/gratipay.com
.. _web API: https://github.com/gratipay/gratipay.com#api
DB Schema
---------
is_suspipicous on participant can be None, True or False. It represents unknown,
blacklisted or whitelisted user.
* whitelisted can transfer money out of gratipay
* unknown can move money within gratipay
* blacklisted cannot do anything
The exchanges table records movements of money into and out of Gratipay. The
``amount`` column shows a positive amount for payins and a negative amount for
payouts. The ``fee`` column is always positive. For both payins and payouts,
the ``amount`` does not include the ``fee`` (e.g., a $10 payin would result in
an ``amount`` of ``9.41`` and a ``fee`` of ``0.59``, and a $100 payout with a
2% fee would result in an ``amount`` of ``-98.04`` and a fee of ``1.96``).
Contents
--------
.. toctree::
:maxdepth: 2
gratipay Python library <gratipay>

View File

@ -1,242 +0,0 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Gratipay.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Gratipay.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

View File

@ -1,6 +1,6 @@
[---] text/html
<div style="text-align: center; padding: 20px 0; margin: 0;">
<img src="https://downloads.gratipay.com/email/gratipay.png" alt="Gratipay">
<img src="https://static.liberapay.com/logo/banner.png" alt="Liberapay">
</div>
<div style="text-align: center; font: normal 14px/21px Arial, sans-serif; color: #333; padding: 0 20%;">
@ -11,12 +11,9 @@ $body
<div style="font: normal 14px/21px Arial, sans-serif;">
{{ _("Something not right? Reply to this email for help.") }}
</div>
<div style="font: normal 10px/21px Arial, sans-serif;">
Sent by <a href="https://gratipay.com/" style="color: #999; text-decoration: underline;">Gratipay, LLC</a> | 716 Park Road, Ambridge, PA, 15003, USA
</div>
{% if include_unsubscribe %}
<div style="font: normal 10px/21px Arial, sans-serif; padding-top: 10px;">
To stop receiving emails like this, change your <a href="https://gratipay.com/about/me/settings/#notifications" style="color: #999; text-decoration: underline;">notification preferences</a>.
To stop receiving emails like this, change your <a href="https://liberapay.com/about/me/settings/#notifications" style="color: #999; text-decoration: underline;">notification preferences</a>.
</div>
{% endif %}
</div>
@ -30,9 +27,6 @@ $body
----
Sent by Gratipay, LLC, https://gratipay.com/
716 Park Road, Ambridge, PA, 15003, USA
{% if include_unsubscribe %}
To stop receiving emails like this, change your notification preferences by visiting https://gratipay.com/about/me/settings/#notifications
To stop receiving emails like this, change your notification preferences by visiting https://liberapay.com/about/me/settings/#notifications
{% endif %}

View File

@ -1,7 +1,7 @@
{{ _("Thanks for supporting {0}!", top_tippee) }}
[---] text/html
{{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Gratipay!",
{{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Liberapay!",
format_currency(exchange.amount + exchange.fee, 'USD'),
('<b><a href="{0}">{1}</a></b>'|safe).format(
participant.profile_url+'giving/',
@ -15,7 +15,7 @@
style="{{ button_style }}">{{ _("View Receipt") }}</a>
[---] text/plain
{{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Gratipay!",
{{ _("We charged your credit card {0} today, to fund your ongoing support for {1}. Thanks for using Liberapay!",
format_currency(exchange.amount + exchange.fee, 'USD'),
top_tippee if ntippees == 1 else ngettext('{0} and {n} other',
'{0} and {n} others',

View File

@ -1,21 +0,0 @@
{{ _("Connect to {0} on Gratipay?", username) }}
[---] text/html
A while ago we received a request to connect <b>{{ escape(email) }}</b> to the
{{ '<b><a href="https://gratipay.com/{0}/">{0}</a></b>'.format(username) }}
account on Gratipay (<a
href="https://medium.com/gratipay-blog/gratitude-gratipay-ef24ad5e41f9">formerly</a>
Gittip). Now we're finally sending a verification email! Ring a bell?
<br>
<br>
<a href="{{ link }}" style="color: #fff; text-decoration:none; display:inline-block; padding: 0 15px; background: #396; font: normal 14px/40px Arial, sans-serif; white-space: nowrap; border-radius: 3px">Yes, proceed!</a>
[---] text/plain
A while ago we received a request to connect `{{ email }}`
to the `{{ username }}` account on Gratipay (formerly Gittip).
Now we're finally sending a verification email!
Ring a bell? Follow this link to finish connecting your email:
{{ link }}

View File

@ -1,11 +1,11 @@
{{ _("{0} from {1} has joined Gratipay!", user_name, platform) }}
{{ _("{0} from {1} has joined Liberapay!", user_name, platform) }}
[---] text/html
{{ _("Your pledge to give {0} every week to {1} will be turned into action now that they have joined Gratipay. Huzzah!",
{{ _("Your pledge to give {0} every week to {1} will be turned into action now that they have joined Liberapay. Huzzah!",
format_currency(amount, 'USD'),
('<b><a href="{0}">{1}</a></b>'|safe).format(profile_url, user_name)) }}
[---] text/plain
{{ _("Your pledge to give {0} every week to {1} will be turned into action now that they have joined Gratipay. Huzzah!",
{{ _("Your pledge to give {0} every week to {1} will be turned into action now that they have joined Liberapay. Huzzah!",
format_currency(amount, 'USD'),
user_name) }}

View File

@ -1,15 +1,15 @@
{{ _("Connect to {0} on Gratipay?", username) }}
{{ _("Connect to {0} on Liberapay?", username) }}
[---] text/html
{{ _("We've received a request to connect {0} to the {1} account on Gratipay. Sound familiar?",
{{ _("We've received a request to connect {0} to the {1} account on Liberapay. Sound familiar?",
('<b>%s</b>'|safe) % email,
('<b><a href="https://gratipay.com/{0}">{0}</a></b>'|safe).format(username)) }}
('<b><a href="https://liberapay.com/{0}">{0}</a></b>'|safe).format(username)) }}
<br>
<br>
<a href="{{ link }}" style="{{ button_style }}">{{ _("Yes, proceed!") }}</a>
[---] text/plain
{{ _("We've received a request to connect {0} to the {1} account on Gratipay. Sound familiar?",
{{ _("We've received a request to connect {0} to the {1} account on Liberapay. Sound familiar?",
email, username) }}
{{ _("Follow this link to finish connecting your email:") }}

View File

@ -1,13 +1,13 @@
{{ _("Connecting {0} to {1} on Gratipay.", new_email, username) }}
{{ _("Connecting {0} to {1} on Liberapay.", new_email, username) }}
[---] text/html
{{ _("We are connecting {0} to the {1} account on Gratipay. This is a notification "
{{ _("We are connecting {0} to the {1} account on Liberapay. This is a notification "
"sent to {2} because that is the primary email address we have on file.",
('<b>%s</b>'|safe) % new_email,
('<b><a href="https://gratipay.com/{0}">{0}</a></b>'|safe).format(username),
('<b><a href="https://liberapay.com/{0}">{0}</a></b>'|safe).format(username),
('<b>%s</b>'|safe) % email) }}
[---] text/plain
{{ _("We are connecting {0} to the {1} account on Gratipay. This is a notification "
{{ _("We are connecting {0} to the {1} account on Liberapay. This is a notification "
"sent to {2} because that is the primary email address we have on file.",
new_email, username, email) }}

View File

@ -2,8 +2,8 @@ from __future__ import absolute_import, division, print_function, unicode_litera
from aspen.http import status_strings
from gratipay.utils import LazyResponse
from gratipay.utils.i18n import HTTP_ERRORS
from liberapay.utils import LazyResponse
from liberapay.utils.i18n import HTTP_ERRORS
[----------------------------------------]

View File

@ -1,122 +0,0 @@
#!/usr/bin/env python
"""\
Gratipay
~~~~~~
A personal funding platform.
Dependencies:
- Python 2.7
- Postgresql 9.2
To run:
$ gratipay.py
This will also initialize a local environment on the first run.
"""
import os
import sys
import shutil
from subprocess import check_call, check_output, STDOUT, CalledProcessError
is_win = sys.platform.startswith('win')
bin_dir = 'Scripts' if is_win else 'bin'
ext = '.exe' if is_win else ''
default_port = 8537
vendor_path = 'vendor'
env_path = 'env'
requirements_installed_path = os.path.join(env_path, '.requirements_installed')
bin_path = os.path.join(env_path, bin_dir)
default_config_path = 'default_local.env'
config_path = 'local.env'
virtualenv_path = os.path.join(vendor_path, 'virtualenv-12.0.7.py')
pip_path = os.path.join(bin_path, 'pip' + ext)
swaddle_path = os.path.join(bin_path, 'swaddle' + ext)
aspen_path = os.path.join(bin_path, 'aspen' + ext)
def main():
# TODO: Handle command-line arguments to override default config values
# e.g. the address and port to serve on, whether to run tests, etc
try:
bootstrap_environment()
except CalledProcessError as ex:
print ex.output
except EnvironmentError as ex:
print 'Error:', ex
return 1
run_server()
def bootstrap_environment():
ensure_dependencies()
init_config()
init_virtualenv()
install_requirements()
def ensure_dependencies():
if not shell('python', '--version', capture=True).startswith('Python 2.7'):
raise EnvironmentError('Python 2.7 is required.')
try:
shell('pg_config' + ext, capture=True)
except OSError as e:
if e.errno != os.errno.ENOENT:
raise
raise EnvironmentError('Postgresql is required. (Make sure pg_config is on your PATH.)')
def init_config():
if os.path.exists(config_path):
return
print 'Creating a %s file...' % config_path
shutil.copyfile(default_config_path, config_path)
def init_virtualenv():
if os.path.exists(env_path):
return
print 'Initializing virtualenv at %s...' % env_path
shell('python', virtualenv_path,
'--prompt="[gratipay] "',
'--extra-search-dir=' + vendor_path,
'--always-copy',
env_path)
def install_requirements():
# TODO: Detect when requirements.txt changes instead of checking for a file
if os.path.exists(requirements_installed_path):
return
print 'Installing requirements...'
shell(pip_path, 'install', '-r', 'requirements.txt')
shell(pip_path, 'install', os.path.join(vendor_path, 'nose-1.1.2.tar.gz'))
shell(pip_path, 'install', '-e', '.')
open(requirements_installed_path, 'w').close()
def run_server():
# TODO: Wait for Aspen to quit before exiting
shell(swaddle_path, config_path, aspen_path)
def shell(*args, **kwargs):
if kwargs.pop('capture', None):
return check_output(args, stderr=STDOUT, **kwargs)
return check_call(args, **kwargs)
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,3 +0,0 @@
The files in this directory are not meant to be modified by Pull Requests. If
you want to contribute to the internationalization of Gratipay please join our
translation teams on Transifex: https://www.transifex.com/projects/p/gratipay/

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="2048" height="2048" viewBox="-256 -384 2048 2048">
<rect x="-256" y="-384" width="2048" height="2048" fill="#D7E4DE" />
<circle cx="767" cy="640" r="767" fill="#BBB1A7" />
<path d="M 763,1046 413,573 q -16,-22 -24.5,-59 -8.5,-37 6,-85 14.5,-48 61.5,-79 40,-26 83,-25.5 43,0.5 73.5,17.5 30.5,17 54.5,45 36,40 96,40 59,0 95,-40 24,-28 54.5,-45 30.5,-17 73.5,-17.5 43,-0.5 84,25.5 46,31 60.5,79 14.5,48 6,85 -8.5,37 -24.5,59 z" fill="#FFFFFF" />
</svg>

Before

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

View File

@ -1,32 +1,26 @@
/* Main namespace.
* ===============
* Individual modules are in the gratipay/ directory.
*/
Liberapay = {};
Gratipay = {};
Gratipay.getCookie = function(key) {
Liberapay.getCookie = function(key) {
var o = new RegExp("(?:^|; ?)" + escape(key) + "=([^;]+)").exec(document.cookie);
return o && unescape(o[1]);
}
Gratipay.init = function() {
Gratipay.forms.initCSRF();
Gratipay.signIn();
Gratipay.signOut();
Gratipay.tips.initSupportGratipay();
Liberapay.init = function() {
Liberapay.forms.initCSRF();
Liberapay.signIn();
Liberapay.signOut();
};
Gratipay.error = function(jqXHR, textStatus, errorThrown) {
Liberapay.error = function(jqXHR, textStatus, errorThrown) {
var msg = null;
try {
msg = JSON.parse(jqXHR.responseText).error_message_long;
} catch(exc) {}
if(!msg) {
msg = "An error occurred (" + (errorThrown || textStatus) + ").\n" +
"Please contact support@gratipay.com if the problem persists.";
"Please contact support@liberapay.com if the problem persists.";
}
Gratipay.notification(msg, 'error', -1);
Liberapay.notification(msg, 'error', -1);
}
@ -34,12 +28,12 @@ Gratipay.error = function(jqXHR, textStatus, errorThrown) {
// ===================
// yanked from gttp.co/v1/api.js
Gratipay.each = function(a, fn) {
Liberapay.each = function(a, fn) {
for (var i=0; i<a.length; i++)
fn(a[i], i, length);
};
Gratipay.jsoncss = function(jsoncss) {
Liberapay.jsoncss = function(jsoncss) {
var out = '';
this.each(jsoncss, function(selector) {
@ -59,7 +53,7 @@ Gratipay.jsoncss = function(jsoncss) {
return this.jsonml(['style', out]);
};
Gratipay.jsonml = function(jsonml) {
Liberapay.jsonml = function(jsonml) {
var node = document.createElement(jsonml[0]),
_ = this;
@ -85,7 +79,7 @@ Gratipay.jsonml = function(jsonml) {
return node;
};
Gratipay.signIn = function() {
Liberapay.signIn = function() {
$('.sign-in > .dropdown').mouseenter(function(e) {
clearTimeout($(this).data('timeoutId'));
$(this).addClass('open');
@ -113,7 +107,7 @@ Gratipay.signIn = function() {
});
};
Gratipay.signOut = function() {
Liberapay.signOut = function() {
$('a#sign-out').click(function(e) {
e.preventDefault();
@ -124,7 +118,7 @@ Gratipay.signOut = function() {
success: function() {
window.location.href = window.location.href;
},
error: Gratipay.error
error: Liberapay.error
});
});
};

View File

@ -1,13 +1,13 @@
Gratipay.charts = {};
Liberapay.charts = {};
// After retrieving the JSON data, wait for document ready event.
Gratipay.charts.make = function(series) {
Liberapay.charts.make = function(series) {
$(document).ready(function() {
Gratipay.charts._make(series);
Liberapay.charts._make(series);
});
};
Gratipay.charts._make = function(series) {
Liberapay.charts._make = function(series) {
if (!series.length) {
$('.chart-wrapper').remove();
return;

27
js/communities.js Normal file
View File

@ -0,0 +1,27 @@
Liberapay.communities = {};
Liberapay.communities.update = function(name, is_member, success_callback, error_callback) {
jQuery.ajax(
{ type: 'POST'
, url: '/for/communities.json'
, data: {name: name, is_member: is_member}
, dataType: 'json'
, success: success_callback
, error: error_callback
}
);
};
Liberapay.communities.jumpTo = function(slug) {
window.location.href = "/for/" + slug + "/";
};
Liberapay.communities.join = function(name, success_callback, error_callback) {
Liberapay.communities.update(name, true, success_callback, error_callback);
};
Liberapay.communities.leave = function(name, callback) {
if (confirm("Are you sure you want to leave the " + name + " community?"))
Liberapay.communities.update(name, false, callback);
};

View File

@ -1,20 +0,0 @@
// Degrade the console obj where not present.
// ==========================================
// http://fbug.googlecode.com/svn/branches/firebug1.2/lite/firebugx.js
// Relaxed to allow for Chrome's console.
function mock_console()
{
var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
"dirxml", "group", "groupEnd", "time", "timeEnd", "count",
"trace", "profile", "profileEnd"];
window.console = {};
var mock = function() {};
for (var i=0, name; name = names[i]; i++)
window.console[name] = mock;
}
if (!window.console)
{
mock_console();
}

View File

@ -1,9 +1,9 @@
// Form Generics
// =============
Gratipay.forms = {};
Liberapay.forms = {};
Gratipay.forms.initCSRF = function() { // https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
Liberapay.forms.initCSRF = function() { // https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
jQuery(document).ajaxSend(function(event, xhr, settings) {
function sameOrigin(url) {
// url could be relative or scheme relative or absolute
@ -23,13 +23,12 @@ Gratipay.forms.initCSRF = function() { // https://docs.djangoproject.com/en/de
if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
// We have to avoid httponly on the csrf_token cookie because of this.
// https://github.com/gratipay/gratipay.com/issues/3030
xhr.setRequestHeader("X-CSRF-TOKEN", Gratipay.getCookie('csrf_token'));
xhr.setRequestHeader("X-CSRF-TOKEN", Liberapay.getCookie('csrf_token'));
}
});
};
Gratipay.forms.jsEdit = function(params) {
Liberapay.forms.jsEdit = function(params) {
var $root = $(params.root);
var $form = $root.find('form.edit');
@ -92,7 +91,7 @@ Gratipay.forms.jsEdit = function(params) {
},
error: params.error || [
function () { $inputs.prop('disabled', false); },
Gratipay.error,
Liberapay.error,
],
});
}
@ -101,14 +100,14 @@ Gratipay.forms.jsEdit = function(params) {
};
Gratipay.forms.clearInvalid = function($form) {
Liberapay.forms.clearInvalid = function($form) {
$form.find('.invalid').removeClass('invalid');
};
Gratipay.forms.focusInvalid = function($form) {
Liberapay.forms.focusInvalid = function($form) {
$form.find('.invalid').eq(0).focus();
};
Gratipay.forms.setInvalid = function($input, invalid) {
Liberapay.forms.setInvalid = function($input, invalid) {
$input.toggleClass('invalid', invalid);
};

View File

@ -1,27 +0,0 @@
Gratipay.communities = {};
Gratipay.communities.update = function(name, is_member, success_callback, error_callback) {
jQuery.ajax(
{ type: 'POST'
, url: '/for/communities.json'
, data: {name: name, is_member: is_member}
, dataType: 'json'
, success: success_callback
, error: error_callback
}
);
};
Gratipay.communities.jumpTo = function(slug) {
window.location.href = "/for/" + slug + "/";
};
Gratipay.communities.join = function(name, success_callback, error_callback) {
Gratipay.communities.update(name, true, success_callback, error_callback);
};
Gratipay.communities.leave = function(name, callback) {
if (confirm("Are you sure you want to leave the " + name + " community?"))
Gratipay.communities.update(name, false, callback);
};

View File

@ -1,2 +0,0 @@
// Localize gratipayURI for gttp.co widgets
gratipayURI = '/';

View File

@ -3,8 +3,8 @@ $(document).ready(function() {
e.preventDefault();
e.stopPropagation();
var platform = Gratipay.trim($('#jump select').val()),
val = Gratipay.trim($('#jump input').val());
var platform = Liberapay.trim($('#jump select').val()),
val = Liberapay.trim($('#jump input').val());
if (val) window.location = '/on/' + platform + '/' + val + '/';
});
});

View File

@ -2,14 +2,14 @@
* Display a notification
* Valid notification types are: "notice", "error", and "success".
*/
Gratipay.notification = function(text, type, timeout, closeCallback) {
Liberapay.notification = function(text, type, timeout, closeCallback) {
var type = type || 'notice';
var timeout = timeout || (type == 'error' ? 10000 : 5000);
var dialog = ['div', { 'class': 'notification notification-' + type }, [ 'div', text ]];
var $dialog = $([
Gratipay.jsonml(dialog),
Gratipay.jsonml(dialog)
Liberapay.jsonml(dialog),
Liberapay.jsonml(dialog)
]);
// Close if we're on the page the notification links to.
@ -33,15 +33,15 @@ Gratipay.notification = function(text, type, timeout, closeCallback) {
if (timeout > 0) setTimeout(close, timeout);
};
Gratipay.initNotifications = function(notifs) {
Liberapay.initNotifications = function(notifs) {
jQuery.each(notifs, function(k, notif) {
Gratipay.notification(notif.jsonml, notif.type, -1, function() {
Liberapay.notification(notif.jsonml, notif.type, -1, function() {
jQuery.ajax({
url: '/'+Gratipay.username+'/notifications.json',
url: '/'+Liberapay.username+'/notifications.json',
type: 'POST',
data: {remove: notif.name},
dataType: 'json',
error: Gratipay.error,
error: Liberapay.error,
});
});
});

View File

@ -1,29 +1,29 @@
/* Bank Account and Credit Card forms
*
* These two forms share some common wiring under the Gratipay.payments
* namespace, and each has unique code under the Gratipay.payments.{cc,ba}
* These two forms share some common wiring under the Liberapay.payments
* namespace, and each has unique code under the Liberapay.payments.{cc,ba}
* namespaces. Each form gets its own page so we only instantiate one of these
* at a time.
*
*/
Gratipay.payments = {};
Liberapay.payments = {};
// Common code
// ===========
Gratipay.payments.init = function() {
$('#delete').submit(Gratipay.payments.deleteRoute);
Liberapay.payments.init = function() {
$('#delete').submit(Liberapay.payments.deleteRoute);
// Lazily depend on Balanced.
var balanced_js = "https://js.balancedpayments.com/1.1/balanced.min.js";
jQuery.getScript(balanced_js, function() {
$('input[type!="hidden"]').eq(0).focus();
}).fail(Gratipay.error);
}).fail(Liberapay.error);
}
Gratipay.payments.deleteRoute = function(e) {
Liberapay.payments.deleteRoute = function(e) {
e.stopPropagation();
e.preventDefault();
@ -33,32 +33,32 @@ Gratipay.payments.deleteRoute = function(e) {
return false;
}
jQuery.ajax(
{ url: "/" + Gratipay.username + "/routes/delete.json"
{ url: "/" + Liberapay.username + "/routes/delete.json"
, data: {network: $this.data('network'), address: $this.data('address')}
, type: "POST"
, success: function() { window.location.reload(); }
, error: Gratipay.error
, error: Liberapay.error
}
);
return false;
};
Gratipay.payments.onError = function(response) {
Liberapay.payments.onError = function(response) {
$('button#save').prop('disabled', false);
var msg = response.status_code + ": " +
$.map(response.errors, function(obj) { return obj.description }).join(', ');
Gratipay.notification(msg, 'error', -1);
Liberapay.notification(msg, 'error', -1);
return msg;
};
Gratipay.payments.onSuccess = function(data) {
Liberapay.payments.onSuccess = function(data) {
$('button#save').prop('disabled', false);
window.location.reload();
};
Gratipay.payments.associate = function (network) { return function (response) {
Liberapay.payments.associate = function (network) { return function (response) {
if (response.status_code !== 201) {
return Gratipay.payments.onError(response);
return Liberapay.payments.onError(response);
}
/* The request to tokenize the thing succeeded. Now we need to associate it
@ -75,9 +75,9 @@ Gratipay.payments.associate = function (network) { return function (response) {
type: "POST",
data: data,
dataType: "json",
success: Gratipay.payments.onSuccess,
success: Liberapay.payments.onSuccess,
error: [
Gratipay.error,
Liberapay.error,
function() { $('button#save').prop('disabled', false); },
],
});
@ -87,18 +87,18 @@ Gratipay.payments.associate = function (network) { return function (response) {
// Bank Accounts
// =============
Gratipay.payments.ba = {};
Liberapay.payments.ba = {};
Gratipay.payments.ba.init = function() {
Gratipay.payments.init();
$('form#bank-account').submit(Gratipay.payments.ba.submit);
Liberapay.payments.ba.init = function() {
Liberapay.payments.init();
$('form#bank-account').submit(Liberapay.payments.ba.submit);
};
Gratipay.payments.ba.submit = function (e) {
Liberapay.payments.ba.submit = function (e) {
e.preventDefault();
$('button#save').prop('disabled', true);
Gratipay.forms.clearInvalid($(this));
Liberapay.forms.clearInvalid($(this));
var bankAccount = {
name: $('#account_name').val(),
@ -110,8 +110,8 @@ Gratipay.payments.ba.submit = function (e) {
// Validate routing number.
if (bankAccount.routing_number) {
if (!balanced.bankAccount.validateRoutingNumber(bankAccount.routing_number)) {
Gratipay.forms.setInvalid($('#routing_number'));
Gratipay.forms.focusInvalid($(this));
Liberapay.forms.setInvalid($('#routing_number'));
Liberapay.forms.focusInvalid($(this));
$('button#save').prop('disabled', false);
return false
}
@ -119,7 +119,7 @@ Gratipay.payments.ba.submit = function (e) {
// Okay, send the data to Balanced.
balanced.bankAccount.create( bankAccount
, Gratipay.payments.associate('balanced-ba')
, Liberapay.payments.associate('balanced-ba')
);
};
@ -127,12 +127,12 @@ Gratipay.payments.ba.submit = function (e) {
// Credit Cards
// ============
Gratipay.payments.cc = {};
Liberapay.payments.cc = {};
Gratipay.payments.cc.init = function() {
Gratipay.payments.init();
$('form#credit-card').submit(Gratipay.payments.cc.submit);
Gratipay.payments.cc.formatInputs(
Liberapay.payments.cc.init = function() {
Liberapay.payments.init();
$('form#credit-card').submit(Liberapay.payments.cc.submit);
Liberapay.payments.cc.formatInputs(
$('#card_number'),
$('#expiration_month'),
$('#expiration_year'),
@ -143,7 +143,7 @@ Gratipay.payments.cc.init = function() {
/* Most of the following code is taken from https://github.com/wangjohn/creditly */
Gratipay.payments.cc.formatInputs = function (cardNumberInput, expirationMonthInput, expirationYearInput, cvvInput) {
Liberapay.payments.cc.formatInputs = function (cardNumberInput, expirationMonthInput, expirationYearInput, cvvInput) {
function getInputValue(e, element) {
var inputValue = element.val().trim();
inputValue = inputValue + String.fromCharCode(e.which);
@ -264,12 +264,12 @@ Gratipay.payments.cc.formatInputs = function (cardNumberInput, expirationMonthIn
});
}
Gratipay.payments.cc.submit = function(e) {
Liberapay.payments.cc.submit = function(e) {
e.stopPropagation();
e.preventDefault();
$('button#save').prop('disabled', true);
Gratipay.forms.clearInvalid($(this));
Liberapay.forms.clearInvalid($(this));
// Adapt our form lingo to balanced nomenclature.
@ -312,16 +312,16 @@ Gratipay.payments.cc.submit = function(e) {
var is_cvv_invalid = !balanced.card.isSecurityCodeValid(credit_card.number,
credit_card.cvv);
Gratipay.forms.setInvalid($('#card_number'), is_card_number_invalid);
Gratipay.forms.setInvalid($('#expiration_month'), is_expiry_invalid);
Gratipay.forms.setInvalid($('#cvv'), is_cvv_invalid);
Liberapay.forms.setInvalid($('#card_number'), is_card_number_invalid);
Liberapay.forms.setInvalid($('#expiration_month'), is_expiry_invalid);
Liberapay.forms.setInvalid($('#cvv'), is_cvv_invalid);
if (is_card_number_invalid || is_expiry_invalid || is_cvv_invalid) {
$('button#save').prop('disabled', false);
Gratipay.forms.focusInvalid($(this));
Liberapay.forms.focusInvalid($(this));
return false;
}
balanced.card.create(credit_card, Gratipay.payments.associate('balanced-cc'));
balanced.card.create(credit_card, Liberapay.payments.associate('balanced-cc'));
return false;
};

View File

@ -1,13 +1,13 @@
Gratipay.profile = {};
Liberapay.profile = {};
Gratipay.profile.init = function() {
Liberapay.profile.init = function() {
// Wire up textarea for statement.
// ===============================
$('textarea').focus();
Gratipay.forms.jsEdit({
Liberapay.forms.jsEdit({
confirmBeforeUnload: true,
root: $('.statement.js-edit'),
});
@ -45,7 +45,7 @@ Gratipay.profile.init = function() {
},
error: [
function () { $inputs.prop('disabled', false); },
Gratipay.error,
Liberapay.error,
]
});
});
@ -62,7 +62,7 @@ Gratipay.profile.init = function() {
// Wire up goal knob.
// ==================
Gratipay.forms.jsEdit({
Liberapay.forms.jsEdit({
root: $('.goal.js-edit'),
success: function(d) {
var goal = $('input[name=goal]:checked');
@ -78,7 +78,7 @@ Gratipay.profile.init = function() {
$('tr.cryptocoin.js-edit').each(function() {
var $root = $(this);
Gratipay.forms.jsEdit({
Liberapay.forms.jsEdit({
root: $root,
success: function(){
var addr = $root.find('[name="address"]').val();
@ -87,7 +87,7 @@ Gratipay.profile.init = function() {
$root.addClass('not-empty');
},
});
$root.find('button.delete').click(Gratipay.payments.deleteRoute);
$root.find('button.delete').click(Liberapay.payments.deleteRoute);
});
@ -104,7 +104,7 @@ Gratipay.profile.init = function() {
success: function ( ) {
location.reload();
},
error: Gratipay.error,
error: Liberapay.error,
data: { platform: this.dataset.platform, user_id: this.dataset.user_id }
});

View File

@ -1,6 +1,6 @@
Gratipay.settings = {};
Liberapay.settings = {};
Gratipay.settings.post_email = function(e) {
Liberapay.settings.post_email = function(e) {
e.preventDefault();
var $this = $(this);
var action = this.className;
@ -17,7 +17,7 @@ Gratipay.settings.post_email = function(e) {
dataType: 'json',
success: function (msg) {
if (msg) {
Gratipay.notification(msg, 'success');
Liberapay.notification(msg, 'success');
}
if (action == 'add-email') {
$('input.add-email').val('');
@ -33,17 +33,17 @@ Gratipay.settings.post_email = function(e) {
},
error: [
function () { $inputs.prop('disabled', false); },
Gratipay.error
Liberapay.error
],
});
};
Gratipay.settings.init = function() {
Liberapay.settings.init = function() {
// Wire up username knob.
// ======================
Gratipay.forms.jsEdit({
Liberapay.forms.jsEdit({
hideEditButton: true,
root: $('.username.js-edit'),
success: function(d) {
@ -76,11 +76,11 @@ Gratipay.settings.init = function() {
}
if (data.number) {
$input.prop('checked', true);
Gratipay.notification(data.msg || "Success", 'success');
Liberapay.notification(data.msg || "Success", 'success');
$('li.members').toggleClass('hidden', data.number !== 'plural');
}
},
error: Gratipay.error,
error: Liberapay.error,
});
}
post();
@ -103,11 +103,11 @@ Gratipay.settings.init = function() {
, dataType: 'json'
, success: function(data) {
if (data.msg) {
Gratipay.notification(data.msg, 'success');
Liberapay.notification(data.msg, 'success');
}
$(e.target).attr('checked', data[field] ^ neg);
}
, error: Gratipay.error
, error: Liberapay.error
});
});
@ -123,11 +123,11 @@ Gratipay.settings.init = function() {
, data: {toggle: field, bits: bits}
, dataType: 'json'
, success: function(data) {
Gratipay.notification(data.msg, 'success');
Liberapay.notification(data.msg, 'success');
$(e.target).attr('checked', data.new_value & bits)
}
, error: [
Gratipay.error,
Liberapay.error,
function(){ $(e.target).attr('checked', !$(e.target).attr('checked')) },
]
});
@ -166,8 +166,8 @@ Gratipay.settings.init = function() {
// =============================
$('.emails button, .emails input').prop('disabled', false);
$('.emails button[class]').on('click', Gratipay.settings.post_email);
$('form.add-email').on('submit', Gratipay.settings.post_email);
$('.emails button[class]').on('click', Liberapay.settings.post_email);
$('form.add-email').on('submit', Liberapay.settings.post_email);
// Wire up close knob.

View File

@ -1,4 +1,4 @@
Gratipay.team = (function() {
Liberapay.team = (function() {
function init() {
$('#lookup-container form').submit(add);
$('#lookup-results').on('click', 'li', selectLookupResult);
@ -43,7 +43,7 @@ Gratipay.team = (function() {
var rows = [];
if (nmembers === 0) {
rows.push(Gratipay.jsonml(
rows.push(Liberapay.jsonml(
[ 'tr'
, ['td', {'colspan': '6', 'class': 'no-members'}, "No members"]
]
@ -62,7 +62,7 @@ Gratipay.team = (function() {
increase = 'max';
if (i < nmembers)
rows.push(Gratipay.jsonml(
rows.push(Liberapay.jsonml(
[ 'tr'
, ['td', {'class': 'n'}, (i === nmembers ? '' : nmembers - i)]
, ['td', ['a', {'href': '/'+member.username+'/'}, member.username]]
@ -73,7 +73,7 @@ Gratipay.team = (function() {
]
));
else if (nmembers > 0)
rows.push(Gratipay.jsonml(
rows.push(Liberapay.jsonml(
[ 'tr'
, ['td']
, ['td']
@ -106,7 +106,7 @@ Gratipay.team = (function() {
var items = [];
for (var i=0, len=results.length; i<len; i++) {
var result = results[i];
items.push(Gratipay.jsonml(
items.push(Liberapay.jsonml(
['li', {"data-id": result.id}, result.username]
));
}
@ -185,11 +185,11 @@ Gratipay.team = (function() {
}
}
if(d.success) {
Gratipay.notification(d.success, 'success');
Liberapay.notification(d.success, 'success');
}
drawRows(d.members);
}
, error: [resetTake, Gratipay.error]
, error: [resetTake, Liberapay.error]
});
}

View File

@ -1,14 +1,14 @@
Gratipay.tips = {};
Liberapay.tips = {};
Gratipay.tips.init = function() {
Liberapay.tips.init = function() {
Gratipay.forms.jsEdit({
Liberapay.forms.jsEdit({
confirmBeforeUnload: true,
hideEditButton: true,
root: $('.your-tip.js-edit'),
success: function(data) {
Gratipay.notification(data.msg, 'success');
Gratipay.tips.afterTipChange(data);
Liberapay.notification(data.msg, 'success');
Liberapay.tips.afterTipChange(data);
}
});
@ -33,32 +33,7 @@ Gratipay.tips.init = function() {
};
Gratipay.tips.initSupportGratipay = function() {
$('.support-gratipay button').click(function() {
var amount = parseFloat($(this).attr('data-amount'), 10);
Gratipay.tips.set('Gratipay', amount, function(data) {
Gratipay.notification(data.msg, 'success');
$('.support-gratipay').slideUp();
// If you're on your own giving page ...
var tip_on_giving = $('.your-tip[data-tippee="Gratipay"]');
if (tip_on_giving.length > 0) {
tip_on_giving[0].defaultValue = amount;
tip_on_giving.attr('value', amount.toFixed(2));
}
});
});
$('.support-gratipay .no-thanks').click(function(event) {
event.preventDefault();
jQuery.post('/ride-free.json')
.success(function() { $('.support-gratipay').slideUp(); })
.fail(Gratipay.error)
});
};
Gratipay.tips.afterTipChange = function(data) {
Liberapay.tips.afterTipChange = function(data) {
$('.my-total-giving').text(data.total_giving_l);
$('.total-receiving[data-tippee="'+data.tippee_id+'"]').text(data.total_receiving_tippee_l);
$('#payment-prompt').toggleClass('needed', data.amount > 0);
@ -75,12 +50,12 @@ Gratipay.tips.afterTipChange = function(data) {
};
Gratipay.tips.set = function(tippee, amount, callback) {
Liberapay.tips.set = function(tippee, amount, callback) {
// send request to change tip
$.post('/' + tippee + '/tip.json', { amount: amount }, function(data) {
if (callback) callback(data);
Gratipay.tips.afterTipChange(data);
Liberapay.tips.afterTipChange(data);
})
.fail(Gratipay.error);
.fail(Liberapay.error);
};

View File

@ -1,4 +1,4 @@
// strips Unicode & non-printable characters, then leading/trailing whitespace
Gratipay.trim = function(s) {
Liberapay.trim = function(s) {
return s.replace(/[^\x20-\x7F]/g, '').replace(/^\s+|\s+$/g, '');
}

View File

@ -1,6 +1,6 @@
Gratipay.upgrade = {};
Liberapay.upgrade = {};
Gratipay.upgrade.init = function () {
Liberapay.upgrade.init = function () {
var userAgent = navigator.userAgent.toLowerCase();
var browser = (userAgent.indexOf('msie') != -1) ? parseInt(userAgent.split('msie')[1], 10) : -1;
@ -17,4 +17,4 @@ Gratipay.upgrade.init = function () {
}
};
$(document).ready(Gratipay.upgrade.init);
$(document).ready(Liberapay.upgrade.init);

View File

@ -1,56 +1,11 @@
"""This is the Python library behind gratipay.com.
"""
import datetime
import locale
from decimal import Decimal
try: # XXX This can't be right.
locale.setlocale(locale.LC_ALL, "en_US.utf8")
except locale.Error:
import sys
if sys.platform == 'win32':
locale.setlocale(locale.LC_ALL, '')
else:
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
BIRTHDAY = datetime.date(2015, 5, 22)
BIRTHDAY = datetime.date(2012, 6, 1)
CARDINALS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
'eight', 'nine']
ORDINALS = ['zeroth', 'first', 'second', 'third', 'fourth', 'fifth', 'sixth',
'seventh', 'eighth', 'ninth', 'tenth']
MONTHS = [None, 'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December']
def age():
today = datetime.date.today()
nmonths = (12 - BIRTHDAY.month) \
+ (12 * (today.year - BIRTHDAY.year - 1)) \
+ (today.month)
plural = 's' if nmonths != 1 else ''
if nmonths < 10:
nmonths = CARDINALS[nmonths]
else:
nmonths = str(nmonths)
return "%s month%s" % (nmonths, plural)
class NotSane(Exception):
"""This is used when a sanity check fails.
A sanity check is when it really seems like the logic shouldn't allow the
condition to arise, but you never know.
"""
db = None # This global is wired in wireup. It's an instance of
# gratipay.postgres.PostgresManager.
MAX_TIP_SINGULAR = Decimal('100.00')
MAX_TIP_PLURAL = Decimal('1000.00')
MIN_TIP = Decimal('0.00')
MAX_TIP = Decimal('100.00')
MIN_TIP = Decimal('0.01')
RESTRICTED_IDS = None
@ -65,7 +20,7 @@ canonical_scheme = None
canonical_host = None
def canonize(request):
"""Enforce a certain scheme and hostname. Store these on request as well.
"""Enforce a certain scheme and hostname.
"""
scheme = request.headers.get('X-Forwarded-Proto', 'http') # per Heroku
host = request.headers['Host']
@ -83,7 +38,3 @@ def canonize(request):
# For non-idempotent methods, redirect to homepage.
url += '/'
request.redirect(url)
def set_version_header(response, website):
response.headers['X-Gratipay-Version'] = website.version

View File

@ -1,4 +1,4 @@
"""Functions for moving money between Gratipay and the outside world.
"""Functions for moving money between Liberapay and the outside world.
"""
from __future__ import unicode_literals
@ -8,10 +8,10 @@ import balanced
from aspen import log
from aspen.utils import typecheck
from gratipay.exceptions import NegativeBalance, NotWhitelisted
from gratipay.models import check_db
from gratipay.models.participant import Participant
from gratipay.models.exchange_route import ExchangeRoute
from liberapay.exceptions import NegativeBalance, NotWhitelisted
from liberapay.models import check_db
from liberapay.models.participant import Participant
from liberapay.models.exchange_route import ExchangeRoute
BALANCED_CLASSES = {
@ -88,7 +88,7 @@ def ach_credit(db, participant, withhold, minimum_credit=MINIMUM_CREDIT):
# Compute the amount to credit them.
# ==================================
# Leave money in Gratipay to cover their obligations next week (as these
# Leave money in the wallet to cover their obligations next week (as these
# currently stand).
balance = participant.balance
@ -155,8 +155,8 @@ def ach_credit(db, participant, withhold, minimum_credit=MINIMUM_CREDIT):
def create_card_hold(db, participant, amount):
"""Create a hold on the participant's credit card.
Amount should be the nominal amount. We'll compute Gratipay's fee below
this function and add it to amount to end up with charge_amount.
Amount should be the nominal amount. We'll compute fees below this function
and add it to amount to end up with charge_amount.
"""
typecheck(amount, Decimal)
@ -272,7 +272,7 @@ def record_exchange(db, route, amount, fee, participant, status, error=None):
Records in the exchanges table have these characteristics:
amount It's negative for credits (representing an outflow from
Gratipay to you) and positive for charges.
Liberapay to you) and positive for charges.
The sign is how we differentiate the two in, e.g., the
history page.

View File

@ -1,7 +1,7 @@
"""This is Gratipay's payday algorithm.
"""This is Liberapay's payday algorithm.
Exchanges (moving money between Gratipay and the outside world) and transfers
(moving money amongst Gratipay users) happen within an isolated event called
Exchanges (moving money between Liberapay and the outside world) and transfers
(moving money amongst Liberapay users) happen within an isolated event called
payday. This event has duration (it's not punctiliar).
Payday is designed to be crash-resistant. Everything that can be rolled back
@ -14,16 +14,16 @@ from __future__ import unicode_literals
import itertools
from multiprocessing.dummy import Pool as ThreadPool
from balanced import CardHold
import aspen.utils
from aspen import log
from gratipay.billing.exchanges import (
from balanced import CardHold
from psycopg2 import IntegrityError
from liberapay.billing.exchanges import (
ach_credit, cancel_card_hold, capture_card_hold, create_card_hold, upcharge
)
from gratipay.exceptions import NegativeBalance
from gratipay.models import check_db
from psycopg2 import IntegrityError
from liberapay.exceptions import NegativeBalance
from liberapay.models import check_db
with open('sql/fake_payday.sql') as f:
@ -57,31 +57,6 @@ class NoPayday(Exception):
class Payday(object):
"""Represent an abstract event during which money is moved.
On Payday, we want to use a participant's Gratipay balance to settle their
tips due (pulling in more money via credit card as needed), but we only
want to use their balance at the start of Payday. Balance changes should be
atomic globally per-Payday.
Here's the call structure of the Payday.run method:
run
payin
prepare
create_card_holds
transfer_tips
transfer_takes
settle_card_holds
update_balances
take_over_balances
payout
update_stats
update_cached_amounts
end
"""
@classmethod
def start(cls):
@ -115,7 +90,6 @@ class Payday(object):
payday.__dict__.update(d)
return payday
def run(self):
"""This is the starting point for payday.
@ -148,7 +122,6 @@ class Payday(object):
fmt_past = "Script ran for %%(age)s (%s)." % _delta
log(aspen.utils.to_age(_start, fmt_past=fmt_past))
def payin(self):
"""The first stage of payday where we charge credit cards and transfer
money internally between participants.
@ -181,7 +154,6 @@ class Payday(object):
DROP FUNCTION transfer(text, text, numeric, context_type);
""")
@staticmethod
def prepare(cursor, ts_start):
"""Prepare the DB: we need temporary tables with indexes and triggers.
@ -385,7 +357,6 @@ class Payday(object):
""", dict(ts_start=ts_start))
log('Prepared the DB.')
@staticmethod
def fetch_card_holds(participant_ids):
holds = {}
@ -408,7 +379,6 @@ class Payday(object):
cancel_card_hold(hold)
return holds
def create_card_holds(self, cursor):
# Get the list of participants to create card holds for
@ -464,7 +434,6 @@ class Payday(object):
return holds
@staticmethod
def transfer_tips(cursor):
cursor.run("""
@ -479,7 +448,6 @@ class Payday(object):
""")
@staticmethod
def transfer_takes(cursor, ts_start):
cursor.run("""
@ -507,7 +475,6 @@ class Payday(object):
""", dict(ts_start=ts_start))
def settle_card_holds(self, cursor, holds):
participants = cursor.all("""
SELECT *
@ -527,7 +494,6 @@ class Payday(object):
threaded_map(cancel_card_hold, holds.values())
log("Canceled %i card holds." % len(holds))
@staticmethod
def update_balances(cursor):
participants = cursor.all("""
@ -557,7 +523,6 @@ class Payday(object):
""")
log("Updated the balances of %i participants." % len(participants))
def take_over_balances(self):
"""If an account that receives money is taken over during payin we need
to transfer the balance to the absorbing account.
@ -597,7 +562,6 @@ class Payday(object):
""")
def payout(self):
"""This is the second stage of payday in which we send money out to the
bank accounts of participants.
@ -626,7 +590,6 @@ class Payday(object):
self.db.self_check()
log("Checked the DB.")
def update_stats(self):
self.db.run("""\
@ -689,24 +652,19 @@ class Payday(object):
""", {'ts_start': self.ts_start})
log("Updated payday stats.")
def update_cached_amounts(self):
with self.db.get_cursor() as cursor:
cursor.execute(FAKE_PAYDAY)
log("Updated receiving amounts.")
def end(self):
self.ts_end = self.db.one("""\
self.ts_end = self.db.one("""
UPDATE paydays
SET ts_end=now()
WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
RETURNING ts_end AT TIME ZONE 'UTC'
""", default=NoPayday).replace(tzinfo=aspen.utils.utc)
def notify_participants(self):
ts_start, ts_end = self.ts_start, self.ts_end
exchanges = self.db.all("""
@ -756,27 +714,21 @@ class Payday(object):
top_tippee=top_tippee,
)
# Record-keeping.
# ===============
# Record-keeping
# ==============
def mark_ach_failed(self):
self.db.one("""\
self.db.one("""
UPDATE paydays
SET nach_failing = nach_failing + 1
WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
RETURNING id
""", default=NoPayday)
def mark_stage_done(self):
self.db.one("""\
self.db.one("""
UPDATE paydays
SET stage = stage + 1
WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
RETURNING id
""", default=NoPayday)

View File

@ -1,6 +1,4 @@
"""This is installed as `payday`.
"""
from gratipay import wireup
from liberapay import wireup
def payday():
@ -16,11 +14,9 @@ def payday():
# Lazily import the billing module.
# =================================
# This dodges a problem where db in billing is None if we import it from
# gratipay before calling wireup.billing.
from gratipay.billing.exchanges import sync_with_balanced
from gratipay.billing.payday import Payday
from liberapay.billing.exchanges import sync_with_balanced
from liberapay.billing.payday import Payday
try:
sync_with_balanced(db)

View File

@ -15,8 +15,8 @@ from aspen.utils import to_age, utc
from oauthlib.oauth2 import TokenExpiredError
from requests_oauthlib import OAuth1Session, OAuth2Session
from gratipay.elsewhere._extractors import not_available
from gratipay.utils import LazyResponse
from liberapay.elsewhere._extractors import not_available
from liberapay.utils import LazyResponse
ACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}
@ -27,7 +27,7 @@ class UnknownAccountElsewhere(Exception): pass
class PlatformRegistry(object):
"""Registry of platforms we support connecting to Gratipay accounts.
"""Registry of platforms we support.
"""
def __init__(self, platforms):
self.__dict__ = OrderedDict((p.name, p) for p in platforms)

View File

@ -1,9 +1,9 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from aspen import Response
from gratipay.elsewhere import PlatformOAuth1
from gratipay.elsewhere._extractors import any_key, key, not_available
from gratipay.elsewhere._paginators import keys_paginator
from liberapay.elsewhere import PlatformOAuth1
from liberapay.elsewhere._extractors import any_key, key, not_available
from liberapay.elsewhere._paginators import keys_paginator
class Bitbucket(PlatformOAuth1):

View File

@ -10,8 +10,8 @@ from urlparse import parse_qs, urlparse
import requests
from aspen import Response
from gratipay.elsewhere import Platform
from gratipay.elsewhere._extractors import key, not_available
from liberapay.elsewhere import Platform
from liberapay.elsewhere._extractors import key, not_available
class Bountysource(Platform):
@ -49,7 +49,7 @@ class Bountysource(Platform):
redirect_url=self.callback_url+'?query_id='+query_id,
external_access_token=token
)
url = self.auth_url+'/auth/gratipay/confirm?'+urlencode(params)
url = self.auth_url+'/auth/liberapay/confirm?'+urlencode(params)
return url, query_id, ''
def get_query_id(self, querystring):

View File

@ -1,8 +1,8 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from gratipay.elsewhere import PlatformOAuth2
from gratipay.elsewhere._extractors import key
from gratipay.elsewhere._paginators import keys_paginator
from liberapay.elsewhere import PlatformOAuth2
from liberapay.elsewhere._extractors import key
from liberapay.elsewhere._paginators import keys_paginator
class Facebook(PlatformOAuth2):

View File

@ -1,8 +1,8 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from gratipay.elsewhere import PlatformOAuth2
from gratipay.elsewhere._extractors import key
from gratipay.elsewhere._paginators import header_links_paginator
from liberapay.elsewhere import PlatformOAuth2
from liberapay.elsewhere._extractors import key
from liberapay.elsewhere._paginators import header_links_paginator
class GitHub(PlatformOAuth2):

View File

@ -1,8 +1,8 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from gratipay.elsewhere import PlatformOAuth2
from gratipay.elsewhere._extractors import any_key, key
from gratipay.elsewhere._paginators import query_param_paginator
from liberapay.elsewhere import PlatformOAuth2
from liberapay.elsewhere._extractors import any_key, key
from liberapay.elsewhere._paginators import query_param_paginator
class Google(PlatformOAuth2):

View File

@ -1,7 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from gratipay.elsewhere import PlatformOAuth1
from gratipay.elsewhere._extractors import not_available, xpath
from liberapay.elsewhere import PlatformOAuth1
from liberapay.elsewhere._extractors import not_available, xpath
class OpenStreetMap(PlatformOAuth1):

View File

@ -1,8 +1,8 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from gratipay.elsewhere import PlatformOAuth1
from gratipay.elsewhere._extractors import key, not_available
from gratipay.elsewhere._paginators import query_param_paginator
from liberapay.elsewhere import PlatformOAuth1
from liberapay.elsewhere._extractors import key, not_available
from liberapay.elsewhere._paginators import query_param_paginator
class Twitter(PlatformOAuth1):

View File

@ -1,7 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from gratipay.elsewhere import PlatformOAuth2
from gratipay.elsewhere._extractors import key
from liberapay.elsewhere import PlatformOAuth2
from liberapay.elsewhere._extractors import key
class Venmo(PlatformOAuth2):

View File

@ -32,7 +32,7 @@ class ProblemChangingEmail(Response):
Response.__init__(self, 400, self.msg.format(*args))
class EmailAlreadyTaken(ProblemChangingEmail):
msg = "{} is already connected to a different Gratipay account."
msg = "{} is already connected to a different Liberapay account."
class CannotRemovePrimaryEmail(ProblemChangingEmail):
msg = "You cannot remove your primary email address."
@ -44,14 +44,6 @@ class TooManyEmailAddresses(ProblemChangingEmail):
msg = "You've reached the maximum number of email addresses we allow."
class ProblemChangingNumber(Exception):
def __str__(self):
return self.msg
class HasBigTips(ProblemChangingNumber):
msg = "You receive tips too large for an individual. Please contact support@gratipay.com."
class TooGreedy(Exception): pass
class NoSelfTipping(Exception): pass
class NoTippee(Exception): pass

View File

@ -2,15 +2,12 @@ from __future__ import division
import base64
import gratipay
import gratipay.wireup
from gratipay import canonize, utils
from gratipay.cron import Cron
from gratipay.models.participant import Participant
from gratipay.security import authentication, csrf, x_frame_options
from gratipay.utils import erase_cookie, http_caching, i18n, set_cookie, timer
from gratipay.version import get_version
from gratipay.renderers import csv_dump, jinja2_htmlescaped
from liberapay import canonize, utils, wireup
from liberapay.cron import Cron
from liberapay.models.participant import Participant
from liberapay.security import authentication, csrf, x_frame_options
from liberapay.utils import erase_cookie, http_caching, i18n, set_cookie, timer
from liberapay.renderers import csv_dump, jinja2_htmlescaped
import aspen
from aspen.website import Website
@ -49,28 +46,17 @@ website.renderer_factories['jinja2'].Renderer.global_context = {
# Wireup Algorithm
# ================
exc = None
try:
website.version = get_version()
except Exception, e:
exc = e
website.version = 'x'
env = website.env = gratipay.wireup.env()
tell_sentry = website.tell_sentry = gratipay.wireup.make_sentry_teller(env)
gratipay.wireup.canonical(env)
website.db = gratipay.wireup.db(env)
website.mailer = gratipay.wireup.mail(env, website.project_root)
gratipay.wireup.billing(env)
gratipay.wireup.username_restrictions(website)
gratipay.wireup.load_i18n(website.project_root, tell_sentry)
gratipay.wireup.other_stuff(website, env)
gratipay.wireup.accounts_elsewhere(website, env)
gratipay.wireup.cryptocoin_networks(website)
if exc:
tell_sentry(exc, {})
env = website.env = wireup.env()
tell_sentry = website.tell_sentry = wireup.make_sentry_teller(env)
wireup.canonical(env)
website.db = wireup.db(env)
website.mailer = wireup.mail(env, website.project_root)
wireup.billing(env)
wireup.username_restrictions(website)
wireup.load_i18n(website.project_root, tell_sentry)
wireup.other_stuff(website, env)
wireup.accounts_elsewhere(website, env)
wireup.cryptocoin_networks(website)
# Periodic jobs
@ -102,8 +88,8 @@ algorithm.functions = [
algorithm['dispatch_request_to_filesystem'],
http_caching.get_etag_for_file if website.cache_static else noop,
http_caching.try_to_serve_304 if website.cache_static else noop,
http_caching.get_etag_for_file if env.cache_static else noop,
http_caching.try_to_serve_304 if env.cache_static else noop,
algorithm['apply_typecasters_to_path'],
algorithm['get_resource_for_request'],
@ -113,7 +99,6 @@ algorithm.functions = [
tell_sentry,
algorithm['get_response_for_exception'],
gratipay.set_version_header,
authentication.add_auth_to_response,
csrf.add_token_to_response,
http_caching.add_caching_to_response,

View File

@ -1,11 +1,3 @@
"""
The most important object in the Gratipay object model is Participant, and the
second most important one is Ccommunity. There are a few others, but those are
the most important two. Participant, in particular, is at the center of
everything on Gratipay.
"""
from contextlib import contextmanager
from postgres import Postgres
@ -17,14 +9,14 @@ def just_yield(obj):
yield obj
class GratipayDB(Postgres):
class DB(Postgres):
def get_cursor(self, cursor=None, **kw):
if cursor:
if kw:
raise ValueError('cannot change options when reusing a cursor')
return just_yield(cursor)
return super(GratipayDB, self).get_cursor(**kw)
return super(DB, self).get_cursor(**kw)
def self_check(self):
with self.get_cursor() as cursor:

View File

@ -1,4 +1,4 @@
"""Teams on Gratipay are plural participants with members.
"""Teams are plural participants with members.
"""
from collections import OrderedDict
from decimal import Decimal
@ -8,19 +8,11 @@ from aspen.utils import typecheck
class MemberLimitReached(Exception): pass
class StubParticipantAdded(Exception): pass
class MixinTeam(object):
"""This class provides methods for working with a Participant as a Team.
:param Participant participant: the underlying :py:class:`~gratipay.participant.Participant` object for this team
"""
# XXX These were all written with the ORM and need to be converted.
def __init__(self, participant):
self.participant = participant
def show_as_team(self, user):
"""Return a boolean, whether to show this participant as a team.
@ -112,8 +104,8 @@ class MixinTeam(object):
assert self.IS_PLURAL
# lazy import to avoid circular import
from gratipay.security.user import User
from gratipay.models.participant import Participant
from liberapay.security.user import User
from liberapay.models.participant import Participant
typecheck( member, Participant
, take, Decimal

View File

@ -12,10 +12,10 @@ from postgres.orm import Model
from psycopg2 import IntegrityError
import xmltodict
import gratipay
from gratipay.exceptions import ProblemChangingUsername
from gratipay.security.crypto import constant_time_compare
from gratipay.utils.username import safely_reserve_a_username
import liberapay
from liberapay.exceptions import ProblemChangingUsername
from liberapay.security.crypto import constant_time_compare
from liberapay.utils.username import safely_reserve_a_username
CONNECT_TOKEN_TIMEOUT = timedelta(hours=24)
@ -185,15 +185,15 @@ class AccountElsewhere(Model):
return self.platform_data.get_auth_session(**params)
@property
def gratipay_slug(self):
def liberapay_slug(self):
return self.user_name or ('~' + self.user_id)
@property
def gratipay_url(self):
scheme = gratipay.canonical_scheme
host = gratipay.canonical_host
def liberapay_url(self):
scheme = liberapay.canonical_scheme
host = liberapay.canonical_host
platform = self.platform
slug = self.gratipay_slug
slug = self.liberapay_slug
return "{scheme}://{host}/on/{platform}/{slug}/".format(**locals())
@property
@ -225,7 +225,7 @@ class AccountElsewhere(Model):
def opt_in(self, desired_username):
"""Given a desired username, return a User object.
"""
from gratipay.security.user import User
from liberapay.security.user import User
user = User.from_username(self.participant.username)
assert not user.ANON, self.participant # sanity check
if self.participant.is_claimed:

View File

@ -40,8 +40,6 @@ def get_list_for(db, participant_id):
""", (participant_id,))
class Community(Model):
"""Model a community on Gratipay.
"""
typname = "communities"

View File

@ -1,11 +1,9 @@
"""*Participant* is the name Gratipay gives to people and groups that are known
to Gratipay. We've got a ``participants`` table in the database, and a
:py:class:`Participant` class that we define here. We distinguish several kinds
of participant, based on certain properties.
"""*Participant* is the name we give to people and groups that are known to us.
We've got a `participants` table in the database, and a `Participant` class
that we define here. We distinguish two kinds of participants:
- *Stub* participants
- *Organizations* are plural participants
- *Teams* are plural participants with members
- "stub" participants who haven't actually joined yet
- "real" participants who have signed in
"""
from __future__ import print_function, unicode_literals
@ -24,10 +22,8 @@ from markupsafe import escape as htmlescape
from postgres.orm import Model
from psycopg2 import IntegrityError
import gratipay
from gratipay import NotSane
from gratipay.exceptions import (
HasBigTips,
import liberapay
from liberapay.exceptions import (
UsernameIsEmpty,
UsernameTooLong,
UsernameContainsInvalidCharacters,
@ -43,28 +39,26 @@ from gratipay.exceptions import (
TooManyEmailAddresses,
)
from gratipay.models import add_event
from gratipay.models._mixin_team import MixinTeam
from gratipay.models.account_elsewhere import AccountElsewhere
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.security.crypto import constant_time_compare
from gratipay.utils import i18n, is_card_expiring, emails, notifications, pricing
from gratipay.utils.username import safely_reserve_a_username
from liberapay.models import add_event
from liberapay.models._mixin_team import MixinTeam
from liberapay.models.account_elsewhere import AccountElsewhere
from liberapay.models.exchange_route import ExchangeRoute
from liberapay.security.crypto import constant_time_compare
from liberapay.utils import i18n, is_card_expiring, emails, notifications
from liberapay.utils.username import safely_reserve_a_username
ASCII_ALLOWED_IN_USERNAME = set("0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
".,-_:@ ")
# We use | in Sentry logging, so don't make that allowable. :-)
"-_")
EMAIL_HASH_TIMEOUT = timedelta(hours=24)
USERNAME_MAX_SIZE = 32
class Participant(Model, MixinTeam):
"""Represent a Gratipay participant.
"""
typname = 'participants'
@ -199,12 +193,6 @@ class Participant(Model, MixinTeam):
def update_number(self, number):
assert number in ('singular', 'plural')
if number == 'singular':
nbigtips = self.db.one("""\
SELECT count(*) FROM current_tips WHERE tippee=%s AND amount > %s
""", (self.username, gratipay.MAX_TIP_SINGULAR))
if nbigtips > 0:
raise HasBigTips
with self.db.get_cursor() as c:
add_event(c, 'participant', dict(action='set', id=self.id, values=dict(number=number)))
self.remove_all_members(c)
@ -270,11 +258,7 @@ class Participant(Model, MixinTeam):
@property
def suggested_payment(self):
return pricing.suggested_payment(self.usage)
@property
def suggested_payment_low_high(self):
return pricing.suggested_payment_low_high(self.usage)
return (self.usage * Decimal('0.05')).quantize(Decimal('.01'))
# API Key
@ -295,8 +279,6 @@ class Participant(Model, MixinTeam):
# Claiming
# ========
# An unclaimed Participant is a stub that's created when someone pledges to
# give to an AccountElsewhere that's not been connected on Gratipay yet.
def resolve_unclaimed(self):
"""Given a username, return an URL path.
@ -353,7 +335,7 @@ class Participant(Model, MixinTeam):
class BankWithdrawalFailed(Exception): pass
def withdraw_balance_to_bank_account(self):
from gratipay.billing.exchanges import ach_credit
from liberapay.billing.exchanges import ach_credit
error = ach_credit( self.db
, self
, Decimal('0.00') # don't withhold anything
@ -541,8 +523,8 @@ class Participant(Model, MixinTeam):
if not nonce:
return self.add_email(email)
scheme = gratipay.canonical_scheme
host = gratipay.canonical_host
scheme = liberapay.canonical_scheme
host = liberapay.canonical_host
username = self.username_lower
quoted_email = quote(email)
link = "{scheme}://{host}/{username}/emails/verify.html?email={quoted_email}&nonce={nonce}"
@ -648,8 +630,8 @@ class Participant(Model, MixinTeam):
b = base_spt[t].render(context).strip()
return b.replace('$body', spt[t].render(context).strip())
message = {}
message['from_email'] = 'support@gratipay.com'
message['from_name'] = 'Gratipay Support'
message['from_email'] = 'support@liberapay.com'
message['from_name'] = 'Liberapay Support'
message['to'] = [{'email': email, 'name': self.username}]
message['subject'] = spt['subject'].render(context)
message['html'] = render('text/html', context_html)
@ -668,7 +650,7 @@ class Participant(Model, MixinTeam):
user_name=elsewhere.user_name,
platform=elsewhere.platform_data.display_name,
amount=t.amount,
profile_url=elsewhere.gratipay_url,
profile_url=elsewhere.liberapay_url,
)
def queue_email(self, spt_name, **context):
@ -792,8 +774,8 @@ class Participant(Model, MixinTeam):
@property
def profile_url(self):
scheme = gratipay.canonical_scheme
host = gratipay.canonical_host
scheme = liberapay.canonical_scheme
host = liberapay.canonical_host
username = self.username
return '{scheme}://{host}/{username}/'.format(**locals())
@ -856,7 +838,7 @@ class Participant(Model, MixinTeam):
lowercased = suggested.lower()
if lowercased in gratipay.RESTRICTED_USERNAMES:
if lowercased in liberapay.RESTRICTED_USERNAMES:
raise UsernameIsRestricted(suggested)
if suggested != self.username:
@ -1017,18 +999,6 @@ class Participant(Model, MixinTeam):
new_takes = self.compute_actual_takes(cursor=cursor)
self.update_taking(old_takes, new_takes, cursor=cursor)
def update_is_free_rider(self, is_free_rider, cursor=None):
with self.db.get_cursor(cursor) as cursor:
cursor.run( "UPDATE participants SET is_free_rider=%(is_free_rider)s "
"WHERE username=%(username)s"
, dict(username=self.username, is_free_rider=is_free_rider)
)
add_event( cursor
, 'participant'
, dict(id=self.id, action='set', values=dict(is_free_rider=is_free_rider))
)
self.set_attributes(is_free_rider=is_free_rider)
def set_tip_to(self, tippee, amount, update_self=True, update_tippee=True, cursor=None):
"""Given a Participant or username, and amount as str, returns a dict.
@ -1056,8 +1026,7 @@ class Participant(Model, MixinTeam):
raise NoSelfTipping
amount = Decimal(amount) # May raise InvalidOperation
max_tip = gratipay.MAX_TIP_PLURAL if tippee.IS_PLURAL else gratipay.MAX_TIP_SINGULAR
if (amount < gratipay.MIN_TIP) or (amount > max_tip):
if amount != 0 and amount < liberapay.MIN_TIP or amount > liberapay.MAX_TIP:
raise BadAmount
if not tippee.accepts_tips and amount != 0:
@ -1088,9 +1057,6 @@ class Participant(Model, MixinTeam):
if update_tippee:
# Update receiving amount of tippee
tippee.update_receiving(cursor)
if tippee.username == 'Gratipay':
# Update whether the tipper is using Gratipay for free
self.update_is_free_rider(None if amount == 0 else False, cursor)
return t._asdict()
@ -1173,12 +1139,6 @@ class Participant(Model, MixinTeam):
def get_giving_for_profile(self):
"""Given a participant id and a date, return a list and a Decimal.
This function is used to populate a participant's page for their own
viewing pleasure.
"""
TIPS = """\
@ -1232,13 +1192,7 @@ class Participant(Model, MixinTeam):
unclaimed_tips = self.db.all(UNCLAIMED_TIPS, (self.username,))
# Compute the total.
# ==================
# For payday we only want to process payments to tippees who have
# themselves opted into Gratipay. For the tipper's profile page we want
# to show the total amount they've pledged (so they're not surprised
# when someone *does* start accepting tips and all of a sudden they're
# hit with bigger charges.
# Compute the total
total = sum([t.amount for t in tips])
if not total:
@ -1282,19 +1236,6 @@ class Participant(Model, MixinTeam):
return self.db.all(TIPS, (self.username,), back_as=dict)
def get_og_title(self):
out = self.username
receiving = self.receiving
giving = self.giving
if (giving > receiving) and not self.anonymous_giving:
out += " gives $%.2f/wk" % giving
elif receiving > 0 and not self.anonymous_receiving:
out += " receives $%.2f/wk" % receiving
else:
out += " is"
return out + " on Gratipay"
def get_age_in_seconds(self):
out = -1
if self.claimed_time is not None:
@ -1406,7 +1347,7 @@ class Participant(Model, MixinTeam):
""", ( username
, username.lower()
, self.username
), default=NotSane)
), default=Exception)
return check
archived_as = safely_reserve_a_username(cursor, reserve=reserve)
@ -1426,16 +1367,16 @@ class Participant(Model, MixinTeam):
Returns None or raises NeedConfirmation.
This method associates an account on another platform (GitHub, Twitter,
etc.) with the given Gratipay participant. Every account elsewhere has an
associated Gratipay participant account, even if its only a stub
etc.) with the given Liberapay participant. Every account elsewhere has an
associated Liberapay participant account, even if its only a stub
participant (it allows us to track pledges to that account should they
ever decide to join Gratipay).
ever decide to join Liberapay).
In certain circumstances, we want to present the user with a
confirmation before proceeding to transfer the account elsewhere to
the new Gratipay account; NeedConfirmation is the signal to request
the new Liberapay account; NeedConfirmation is the signal to request
confirmation. If it was the last account elsewhere connected to the old
Gratipay account, then we absorb the old Gratipay account into the new one,
Liberapay account, then we absorb the old Liberapay account into the new one,
effectively archiving the old account.
Here's what absorbing means:
@ -1596,7 +1537,7 @@ class Participant(Model, MixinTeam):
# Load the existing connection.
# =============================
# Every account elsewhere has at least a stub participant account
# on Gratipay.
# on Liberapay.
elsewhere = cursor.one("""
@ -1605,7 +1546,7 @@ class Participant(Model, MixinTeam):
JOIN participants ON participant=participants.username
WHERE elsewhere.platform=%s AND elsewhere.user_id=%s
""", (platform, user_id), default=NotSane)
""", (platform, user_id), default=Exception)
other = elsewhere.participant
@ -1622,13 +1563,12 @@ class Participant(Model, MixinTeam):
# three cases:
#
# - the other participant is not a stub; we are taking the
# account elsewhere away from another viable Gratipay
# participant
# account elsewhere away from another viable participant
#
# - the other participant has no other accounts elsewhere; taking
# away the account elsewhere will leave the other Gratipay
# participant without any means of logging in, and it will be
# archived and its tips absorbed by us
# away the account elsewhere will leave the other participant
# without any means of logging in, and it will be archived
# and its tips absorbed by us
#
# - we already have an account elsewhere connected from the given
# platform, and it will be handed off to a new stub
@ -1785,7 +1725,6 @@ class Participant(Model, MixinTeam):
, 'username': self.username
, 'avatar': self.avatar_url
, 'number': self.number
, 'on': 'gratipay'
}
if not details:

View File

@ -4,10 +4,10 @@ import binascii
from datetime import date
from aspen import Response
from gratipay.models.participant import Participant
from gratipay.security import csrf
from gratipay.security.crypto import constant_time_compare
from gratipay.security.user import User, SESSION
from liberapay.models.participant import Participant
from liberapay.security import csrf
from liberapay.security.crypto import constant_time_compare
from liberapay.security.user import User, SESSION
ANON = User()

View File

@ -3,8 +3,8 @@ from datetime import timedelta
import uuid
from aspen.utils import utcnow
from gratipay.models.participant import Participant
from gratipay.utils import erase_cookie, set_cookie
from liberapay.models.participant import Participant
from liberapay.utils import erase_cookie, set_cookie
SESSION = b'session'

View File

@ -1,4 +1,4 @@
"""Helpers for testing Gratipay.
"""Helpers for testing.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
@ -9,13 +9,13 @@ from os.path import dirname, join, realpath
from aspen import resources
from aspen.utils import utcnow
from aspen.testing.client import Client
from gratipay.billing.exchanges import record_exchange, record_exchange_result
from gratipay.elsewhere import UserInfo
from gratipay.main import website
from gratipay.models.account_elsewhere import AccountElsewhere
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.security.user import User
from gratipay.testing.vcr import use_cassette
from liberapay.billing.exchanges import record_exchange, record_exchange_result
from liberapay.elsewhere import UserInfo
from liberapay.main import website
from liberapay.models.account_elsewhere import AccountElsewhere
from liberapay.models.exchange_route import ExchangeRoute
from liberapay.security.user import User
from liberapay.testing.vcr import use_cassette
from psycopg2 import IntegrityError, InternalError

View File

@ -4,9 +4,9 @@ import itertools
import balanced
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.testing import Harness
from gratipay.testing.vcr import use_cassette
from liberapay.models.exchange_route import ExchangeRoute
from liberapay.testing import Harness
from liberapay.testing.vcr import use_cassette
class BalancedHarness(Harness):
@ -55,13 +55,6 @@ with use_cassette('BalancedHarness'):
'state': 'Confusion',
'postal_code': '90210',
},
# gratipay stores some of the address data in the meta fields,
# continue using them to support backwards compatibility
meta={
'address_2': 'Box 2',
'city_town': '',
'region': 'Confusion',
}
).save()
cls.card.associate_to_customer(cls.janet_href)
cls.card_href = unicode(cls.card.href)

View File

@ -1,7 +1,7 @@
import mock
from gratipay.models.participant import Participant
from gratipay.testing import Harness
from liberapay.models.participant import Participant
from liberapay.testing import Harness
class EmailHarness(Harness):
@ -11,7 +11,7 @@ class EmailHarness(Harness):
self.mailer_patcher = mock.patch.object(Participant._mailer.messages, 'send')
self.mailer = self.mailer_patcher.start()
self.addCleanup(self.mailer_patcher.stop)
sleep_patcher = mock.patch('gratipay.models.participant.sleep')
sleep_patcher = mock.patch('liberapay.models.participant.sleep')
sleep_patcher.start()
self.addCleanup(sleep_patcher.stop)

View File

@ -9,7 +9,7 @@ from aspen.utils import to_rfc822, utcnow
from dependency_injection import resolve_dependencies
from postgres.cursors import SimpleCursorBase
import gratipay
import liberapay
BEGINNING_OF_EPOCH = to_rfc822(datetime(1970, 1, 1)).encode('ascii')
@ -59,7 +59,7 @@ def get_participant(state, restrict=True, resolve_unclaimed=True):
if user.ANON:
raise Response(403, _("You need to log in to access this page."))
from gratipay.models.participant import Participant # avoid circular import
from liberapay.models.participant import Participant # avoid circular import
participant = Participant.from_username(slug)
if participant is None:
@ -76,7 +76,7 @@ def get_participant(state, restrict=True, resolve_unclaimed=True):
to = participant.resolve_unclaimed()
if to:
# This is a stub account (someone on another platform who hasn't
# actually registered with Gratipay yet)
# actually registered with Liberapay yet)
request.redirect(to)
else:
# This is an archived account (result of take_over)
@ -100,22 +100,6 @@ def update_global_stats(website):
website.gnactive = stats[0]
website.gtransfer_volume = stats[1]
nbackers = website.db.one("""
SELECT npatrons
FROM participants
WHERE username = 'Gratipay'
""", default=0)
website.support_current = cur = int(round(nbackers / stats[0] * 100)) if stats[0] else 0
if cur < 10: goal = 20
elif cur < 15: goal = 30
elif cur < 25: goal = 40
elif cur < 35: goal = 50
elif cur < 45: goal = 60
elif cur < 55: goal = 70
elif cur < 65: goal = 80
elif cur > 70: goal = None
website.support_goal = goal
def _execute(this, sql, params=[]):
print(sql.strip(), params)
@ -166,7 +150,7 @@ def set_cookie(cookies, key, value, expires=None, httponly=True, path=b'/'):
cookie[b'httponly'] = True
if path:
cookie[b'path'] = path
if gratipay.canonical_scheme == 'https':
if liberapay.canonical_scheme == 'https':
cookie[b'secure'] = True

View File

@ -7,11 +7,11 @@ import sys
from faker import Factory
from psycopg2 import IntegrityError
from gratipay import wireup, MAX_TIP_SINGULAR, MIN_TIP
from gratipay.elsewhere import PLATFORMS
from gratipay.models.participant import Participant
from gratipay.models import community
from gratipay.models import check_db
from liberapay import wireup, MAX_TIP, MIN_TIP
from liberapay.elsewhere import PLATFORMS
from liberapay.models.participant import Participant
from liberapay.models import community
from liberapay.models import check_db
faker = Factory.create()
@ -88,8 +88,7 @@ def fake_community(db, creator):
def fake_tip_amount():
amount = ((D(random.random()) * (MAX_TIP_SINGULAR - MIN_TIP))
+ MIN_TIP)
amount = (D(random.random()) * (MAX_TIP - MIN_TIP) + MIN_TIP)
decimal_amount = D(amount).quantize(D('.01'))
while decimal_amount == D('0.00'):

View File

@ -72,9 +72,9 @@ def add_caching_to_response(response, request=None, etag=None):
# https://developers.google.com/speed/docs/best-practices/caching
response.headers['Etag'] = etag
# Set CORS header for https://assets.gratipay.com (see issue #2970)
# Set CORS header for https://static.liberapay.com
if 'Access-Control-Allow-Origin' not in response.headers:
response.headers['Access-Control-Allow-Origin'] = 'https://gratipay.com'
response.headers['Access-Control-Allow-Origin'] = 'https://liberapay.com'
if request.line.uri.querystring.get('etag'):
# We can cache "indefinitely" when the querystring contains the etag.

View File

@ -1,6 +1,7 @@
# encoding: utf8
from __future__ import print_function, unicode_literals
from datetime import date, datetime
from io import BytesIO
import re
from unicodedata import combining, normalize
@ -147,7 +148,9 @@ def n_get_text(tell_sentry, state, loc, s, p, n, *a, **kw):
def to_age(dt, loc, **kw):
if isinstance(dt, datetime):
return format_timedelta(dt - utcnow(), locale=loc, **kw)
return format_timedelta(dt - date.today(), locale=loc, granularity='day', **kw)
def regularize_locale(loc):

View File

@ -1,52 +0,0 @@
from decimal import Decimal as D, ROUND_HALF_EVEN
def suggested_payment(usage):
if usage >= 500:
percentage = D('0.02')
elif usage >= 20:
percentage = D('0.05')
else:
percentage = D('0.10')
suggestion = usage * percentage
if suggestion == 0:
rounded = suggestion
elif suggestion < 0.25:
rounded = D('0.25')
elif suggestion < 0.50:
rounded = D('0.50')
elif suggestion < 1:
rounded = D('1.00')
else:
rounded = suggestion.quantize(D('0'), ROUND_HALF_EVEN)
return rounded
def suggested_payment_low_high(usage):
# Above $500/wk we suggest 2%.
if usage >= 5000:
low = D('100.00')
high = D('1000.00')
elif usage >= 500:
low = D('10.00')
high = D('100.00')
# From $20 to $499 we suggest 5%.
elif usage >= 100:
low = D('5.00')
high = D('25.00')
elif usage >= 20:
low = D('1.00')
high = D('5.00')
# Below $20 we suggest 10%.
elif usage >= 5:
low = D('0.50')
high = D('2.00')
else:
low = D('0.10')
high = D('1.00')
return low, high

View File

@ -1,10 +0,0 @@
from os.path import abspath, dirname, join
def get_version():
root = dirname(dirname(abspath(__file__)))
with open(join(root, 'www/version.txt')) as f:
version = f.read().strip()
return version
if __name__ == '__main__':
print(get_version())

View File

@ -13,45 +13,45 @@ from babel.core import Locale
from babel.messages.pofile import read_po
from babel.numbers import parse_pattern
import balanced
import gratipay
import gratipay.billing.payday
import liberapay
import liberapay.billing.payday
import raven
import mandrill
from environment import Environment, is_yesish
from gratipay.elsewhere import PlatformRegistry
from gratipay.elsewhere.bitbucket import Bitbucket
from gratipay.elsewhere.bountysource import Bountysource
from gratipay.elsewhere.github import GitHub
from gratipay.elsewhere.facebook import Facebook
from gratipay.elsewhere.google import Google
from gratipay.elsewhere.openstreetmap import OpenStreetMap
from gratipay.elsewhere.twitter import Twitter
from gratipay.elsewhere.venmo import Venmo
from gratipay.models.account_elsewhere import AccountElsewhere
from gratipay.models.community import Community
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.models.participant import Participant
from gratipay.models import GratipayDB
from gratipay.utils.emails import compile_email_spt
from gratipay.utils.http_caching import asset_etag
from gratipay.utils.i18n import (
from liberapay.elsewhere import PlatformRegistry
from liberapay.elsewhere.bitbucket import Bitbucket
from liberapay.elsewhere.bountysource import Bountysource
from liberapay.elsewhere.github import GitHub
from liberapay.elsewhere.facebook import Facebook
from liberapay.elsewhere.google import Google
from liberapay.elsewhere.openstreetmap import OpenStreetMap
from liberapay.elsewhere.twitter import Twitter
from liberapay.elsewhere.venmo import Venmo
from liberapay.models.account_elsewhere import AccountElsewhere
from liberapay.models.community import Community
from liberapay.models.exchange_route import ExchangeRoute
from liberapay.models.participant import Participant
from liberapay.models import DB
from liberapay.utils.emails import compile_email_spt
from liberapay.utils.http_caching import asset_etag
from liberapay.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
gratipay.canonical_host = env.canonical_host
liberapay.canonical_scheme = env.canonical_scheme
liberapay.canonical_host = env.canonical_host
def db(env):
dburl = env.database_url
maxconn = env.database_maxconn
db = GratipayDB(dburl, maxconn=maxconn)
db = DB(dburl, maxconn=maxconn)
for model in (AccountElsewhere, Community, ExchangeRoute, Participant):
db.register_model(model)
gratipay.billing.payday.Payday.db = db
liberapay.billing.payday.Payday.db = db
return db
@ -70,7 +70,7 @@ def billing(env):
def username_restrictions(website):
gratipay.RESTRICTED_USERNAMES = os.listdir(website.www_root)
liberapay.RESTRICTED_USERNAMES = os.listdir(website.www_root)
def make_sentry_teller(env):
@ -95,7 +95,7 @@ def make_sentry_teller(env):
# Only log server errors to Sentry. For responses < 500 we use
# stream-/line-based access logging. See discussion on:
# https://github.com/gratipay/gratipay.com/pull/1560.
# https://github.com/liberapay/liberapay.com/pull/1560.
return
@ -130,7 +130,7 @@ def make_sentry_teller(env):
, 'is_admin': user.participant.is_admin
, 'is_suspicious': user.participant.is_suspicious
, 'claimed_time': user.participant.claimed_time.isoformat()
, 'url': 'https://gratipay.com/{}/'.format(username)
, 'url': 'https://liberapay.com/{}/'.format(username)
}
@ -310,10 +310,7 @@ def load_i18n(project_root, tell_sentry):
def other_stuff(website, env):
website.cache_static = env.gratipay_cache_static
website.compress_assets = env.gratipay_compress_assets
if website.cache_static:
if env.cache_static:
def asset(path):
fspath = website.www_root+'/assets/'+path
etag = ''
@ -321,16 +318,13 @@ def other_stuff(website, env):
etag = asset_etag(fspath)
except Exception as e:
website.tell_sentry(e, {})
return env.gratipay_asset_url+path+(etag and '?etag='+etag)
return env.asset_url+path+(etag and '?etag='+etag)
website.asset = asset
compile_assets(website)
else:
website.asset = lambda path: env.gratipay_asset_url+path
website.asset = lambda path: env.asset_url+path
clean_assets(website.www_root)
website.optimizely_id = env.optimizely_id
website.include_piwik = env.include_piwik
website.log_metrics = env.log_metrics
@ -340,9 +334,9 @@ def env():
CANONICAL_HOST = unicode,
CANONICAL_SCHEME = unicode,
DATABASE_MAXCONN = int,
GRATIPAY_ASSET_URL = unicode,
GRATIPAY_CACHE_STATIC = is_yesish,
GRATIPAY_COMPRESS_ASSETS = is_yesish,
ASSET_URL = unicode,
CACHE_STATIC = is_yesish,
COMPRESS_ASSETS = is_yesish,
BALANCED_API_SECRET = unicode,
GITHUB_CLIENT_ID = unicode,
GITHUB_CLIENT_SECRET = unicode,
@ -374,10 +368,8 @@ def env():
UPDATE_GLOBAL_STATS_EVERY = int,
CHECK_DB_EVERY = int,
DEQUEUE_EMAILS_EVERY = int,
OPTIMIZELY_ID = unicode,
SENTRY_DSN = unicode,
LOG_METRICS = is_yesish,
INCLUDE_PIWIK = is_yesish,
MANDRILL_KEY = unicode,
RAISE_SIGNIN_NOTIFICATIONS = is_yesish,
@ -391,43 +383,24 @@ def env():
# ==============
if env.malformed:
these = len(env.malformed) != 1 and 'these' or 'this'
plural = len(env.malformed) != 1 and 's' or ''
aspen.log_dammit("=" * 42)
aspen.log_dammit( "Oh no! Gratipay.com couldn't understand %s " % these
, "environment variable%s:" % plural
)
aspen.log_dammit("Malformed environment variable%s:" % plural)
aspen.log_dammit(" ")
for key, err in env.malformed:
aspen.log_dammit(" {} ({})".format(key, err))
aspen.log_dammit(" ")
aspen.log_dammit("See ./default_local.env for hints.")
aspen.log_dammit("=" * 42)
keys = ', '.join([key for key in env.malformed])
raise BadEnvironment("Malformed envvar{}: {}.".format(plural, keys))
if env.missing:
these = len(env.missing) != 1 and 'these' or 'this'
plural = len(env.missing) != 1 and 's' or ''
aspen.log_dammit("=" * 42)
aspen.log_dammit( "Oh no! Gratipay.com needs %s missing " % these
, "environment variable%s:" % plural
)
aspen.log_dammit("Missing environment variable%s:" % plural)
aspen.log_dammit(" ")
for key in env.missing:
aspen.log_dammit(" " + key)
aspen.log_dammit(" ")
aspen.log_dammit( "(Sorry, we must've started looking for "
, "%s since you last updated Gratipay!)" % these
)
aspen.log_dammit(" ")
aspen.log_dammit("Running Gratipay locally? Edit ./local.env.")
aspen.log_dammit("Running the test suite? Edit ./tests/env.")
aspen.log_dammit(" ")
aspen.log_dammit("See ./default_local.env for hints.")
aspen.log_dammit("=" * 42)
keys = ', '.join([key for key in env.missing])
raise BadEnvironment("Missing envvar{}: {}.".format(plural, keys))

View File

@ -1,3 +0,0 @@
#!/bin/sh
heroku config -s | honcho run -e /dev/stdin ./env/bin/python ./bin/masspay.py -i
./bin/masspay.py -o

1741
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
{
"name": "gratipay",
"version": "0.0.0",
"private": true,
"dependencies": {},
"devDependencies": {
"grunt": "~0.4.2",
"grunt-cli": "~0.1.9",
"grunt-contrib-jshint": "~0.9.2",
"grunt-contrib-watch": "~0.6.0",
"grunt-webdriver": "^0.4.4",
"ini": "~1.1.0",
"js-yaml": "~3.0.2",
"phantomjs": "^1.9.12",
"sync-exec": "^0.4.0"
},
"engines": {
"node": ">= 0.10.0"
}
}

103
payday.sh
View File

@ -1,103 +0,0 @@
#!/bin/bash
# Fail on error.
# ==============
set -e
# Be somewhere predictable.
# =========================
cd "`dirname $0`"
# --help
# ======
if [ $# = 0 -o "$1" = "" ]; then
echo
echo "Usage: $0 <number> [\"for_real_please\"]"
echo
echo " This is a payday wrapper script for Gratipay. It runs payday, logging to a file."
echo " You must pass at least one argument, a small integer indicating which week of "
echo " Gratipay you are running (it's only used to decide where to log). If you pass a"
echo " second arg then it must be the string \"for_real_please\", and in that case we"
echo " try to run against the production database. Without that string we run using "
echo " your local.env configuration."
echo
echo " Payday is designed such that you can rerun it if it fails. In particular, it "
echo " will append to the log (not overwrite it)."
echo
exit
fi
# Helpers
# =======
confirm () {
proceed=""
while [ "$proceed" != "y" ]; do
read -p"$1 (y/N) " proceed
if [ "$proceed" == "n" -o "$proceed" == "N" -o "$proceed" == "" ]
then
return 1
fi
done
return 0
}
require () {
if [ ! `which $1` ]; then
echo "The '$1' command was not found."
return 1
fi
return 0
}
start () {
echo "Logging to $LOG."
echo >> $LOG
date -u >> $LOG
}
# Work
# ====
if [ "$2" == "for_real_please" ]; then
LOG="../paydays/gratipay-$1.log"
else
LOG="../paydays/test-$1.log"
fi
if [ -f $LOG ]; then
RUN="Rerun"
else
# If the path is bad the next line will fail and we'll exit.
touch $LOG
RUN="Run"
fi
export PATH="./env/bin:$PATH"
require honcho
confirm "$RUN payday #$1?" || exit 0
case "$2" in
"")
start
honcho run -e defaults.env,local.env ./env/bin/payday >>$LOG 2>&1 &
;;
"for_real_please")
confirm "$RUN payday #$1 FOR REAL?!?!?!??!?!?" || exit 0
start
heroku config -s | honcho run -e /dev/stdin ./env/bin/payday >>$LOG 2>&1 &
;;
*)
echo "Your second arg was $2. Wazzat mean?"
exit 1
;;
esac
disown -a
tail -f $LOG

1
pytest.ini Normal file
View File

@ -0,0 +1 @@
# the existence of this file sets pytest's `rootdir`

View File

@ -1,12 +1,10 @@
#!/bin/sh
# usage: DATABASE_URL=postgres://foo:bar@baz:5234/buz recreate-schema.sh
# Exit if any subcommands or pipeline returns a non-zero status.
set -e
# Make a database for Gratipay.
#
# usage: DATABASE_URL=postgres://foo:bar@baz:5234/buz recreate-schema.sh
echo "=============================================================================="
# I got the idea for dropping the schema as a way to clear out the db from
@ -23,6 +21,15 @@ echo "==========================================================================
echo "Applying sql/schema.sql ..."
echo
if [ "$1" = "test" ]; then
psql $DATABASE_URL <<EOF
DO \$$
BEGIN
EXECUTE 'ALTER DATABASE '||current_database()||' SET synchronous_commit TO off';
END
\$$
EOF
fi
psql $DATABASE_URL < sql/schema.sql

View File

@ -42,19 +42,19 @@ git pull
# Compute the next version number
prev="$(git describe --tags --match '[0-9]*' | cut -d- -f1 | sed 's/\.//g')"
prev="$(git describe --tags --match '[0-9]*' | cut -d- -f1)"
version="$((prev + 1))"
# Check that the environment contains all required variables
heroku config -sa gratipay | ./env/bin/honcho run -e /dev/stdin \
./env/bin/python gratipay/wireup.py
heroku config -sa liberapay | ./env/bin/honcho run -e /dev/stdin \
./env/bin/python liberapay/wireup.py
# Sync the translations
echo "Syncing translations..."
if [ ! -e .transifexrc -a ! -e ~/.transifexrc ]; then
heroku config -sa gratipay | ./env/bin/honcho run -e /dev/stdin make transifexrc
heroku config -sa liberapay | ./env/bin/honcho run -e /dev/stdin make transifexrc
fi
make i18n_upload
make i18n_download
@ -86,17 +86,15 @@ fi
# Ask confirmation and bump the version
yesno "Tag and deploy version $version?" || exit
echo $version >www/version.txt
git commit www/version.txt -m "Bump version to $version"
git tag $version
# Deploy to Heroku
[ "$maintenance" = "yes" ] && heroku maintenance:on -a gratipay
[ "$run_sql" = "before" ] && heroku pg:psql -a gratipay <sql/branch.sql
[ "$maintenance" = "yes" ] && heroku maintenance:on -a liberapay
[ "$run_sql" = "before" ] && heroku pg:psql -a liberapay <sql/branch.sql
git push --force heroku master
[ "$maintenance" = "yes" ] && heroku maintenance:off -a gratipay
[ "$run_sql" = "after" ] && heroku pg:psql -a gratipay <sql/branch.sql
[ "$maintenance" = "yes" ] && heroku maintenance:off -a liberapay
[ "$run_sql" = "after" ] && heroku pg:psql -a liberapay <sql/branch.sql
rm -f sql/branch.sql

View File

@ -35,7 +35,7 @@ fake-factory==0.3.2
six==1.8.0
libsass==0.3.0
honcho==cfcc7ebb
honcho==0.5.0
environment==1.0.0

View File

@ -1,7 +0,0 @@
DROP DATABASE IF EXISTS "gratipay-test";
DROP DATABASE IF EXISTS gratipay;
DROP ROLE IF EXISTS gratipay;
CREATE ROLE gratipay LOGIN PASSWORD 'gratipay' SUPERUSER;
CREATE DATABASE gratipay WITH OWNER = gratipay;
CREATE DATABASE "gratipay-test" WITH OWNER = gratipay;

View File

@ -1,10 +0,0 @@
#!/usr/bin/env bash
# installs Python-independent project dependencies
apt-get -y install postgresql
apt-get -y install libpq-dev
apt-get -y install python-dev # for building psycopg2
apt-get -y install g++ # for libsass
apt-get -y install git # release.sh and commit process
apt-get -y install npm # for jstests

View File

@ -1,3 +0,0 @@
DROP ROLE IF EXISTS vagrant;
CREATE ROLE vagrant LOGIN SUPERUSER;

View File

@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -e
# Set login directory to root vagrant share
sudo sh -c "echo 'cd /vagrant' > /etc/profile.d/login-directory.sh"
# Configure Postgres (using system user 'postgres' run command
# 'psql' with PostreSQL user 'postgres` to quietly execute scripts)
sudo -u postgres psql -U postgres -qf /vagrant/scripts/reset-db.sql
sudo -u postgres psql -U postgres -qf /vagrant/scripts/vagrant-postgre.sql
# Set up the environment, the database, and run Gratipay
cd /vagrant
sudo -u vagrant make clean env schema data
# Output helper text
cat <<EOF
Gratipay installed! To run,
$ vagrant ssh --command "make run"
EOF

View File

@ -1,5 +1,5 @@
/*
This is the top of the Gratipay CSS tree.
This is the top of the CSS tree.
Our code organization here was inspired by Brad Frost's Atomic Web Design:
@ -38,10 +38,8 @@
@import "components/payments-by";
@import "components/profile-statement";
@import "components/sign_in";
@import "components/support_gratipay";
@import "components/table";
@import "components/tip-distribution";
@import "components/tipr";
@import "components/upgrade";
@import "fragments/accounts";

View File

@ -1,60 +0,0 @@
.support-gratipay {
a {
color: $brown ! important;
&:hover { color: $green ! important; }
text-decoration: underline;
}
padding: 3px 20px;
border-top: 1px solid $brown;
background: transparentize($gold, 0.05);
text-align: left;
clear: both;
.cta {
padding-top: 4px;
padding-right: 20px;
}
.knobs {
width: 133px;
button {
box-sizing: border-box;
width: 64px;
border: 1px solid $green;
background: $green;
color: $white;
margin: 0;
&:hover {
border: 1px solid $brown;
background: $brown;
color: $white;
}
&.low {
margin: 0 5px 0 0;
}
}
a {
box-sizing: border-box;
font-weight: normal;
font-size: 11px;
display: inline-block;
width: 64px;
margin: 0;
padding: 0;
text-align: center;
&.other {
margin-right: 5px;
}
}
}
.help {
margin-top: 2px;
font-size: 13px;
font-style: italic;
a {
font-weight: normal;
}
}
}

Some files were not shown because too many files have changed in this diff Show More