migrate to python3, flask1 and pipenv/pipfiles.

This commit is contained in:
fiatjaf 2018-05-12 03:12:38 +00:00
parent 882224d741
commit f1a7ba9908
31 changed files with 705 additions and 339 deletions

39
Pipfile Normal file
View File

@ -0,0 +1,39 @@
[[source]]
verify_ssl = true
name = "pypi"
url = "https://pypi.org/simple"
[dev-packages]
fakeredis = { version = "*" }
httpretty = { version = "*" }
python-dotenv = { version = "*" }
mock = { version = "*" }
[requires]
python_version = "3.6"
[packages]
celery = "==4.1.0"
gunicorn = "==19.6.0"
hashids = "==1.2.0"
"psycopg2" = "==2.7"
redis = "==2.10.5"
requests = "==2.1.0"
stripe = "==1.55.0"
structlog = "==16.1.0"
pyaml = "==17.12.1"
SQLAlchemy = "==1.0.13"
Flask = "==1"
Flask-CDN = "==1.5.3"
Flask-Cors = "==3.0.2"
Flask-Limiter = "==0.9.3"
Flask-Login = "==0.3.0"
Flask-Migrate = "==2.0.3"
Flask-Redis = "==0.0.6"
Flask-Script = "==2.0.5"
Flask-SQLAlchemy = "==2.1"
Flask-Testing = "==0.6.1"
unicodecsv = "==0.14.1"
[pipenv]
allow_prereleases = true

390
Pipfile.lock generated Normal file
View File

@ -0,0 +1,390 @@
{
"_meta": {
"hash": {
"sha256": "cc2b2db95dc7263c3ea29d7abb715712854572e32a6c17aa1dfa418a604cd8cc"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"alembic": {
"hashes": [
"sha256:85bd3ea7633024e4930900bc64fb58f9742dedbc6ebb6ecf25be2ea9a3c1b32e"
],
"version": "==0.9.9"
},
"amqp": {
"hashes": [
"sha256:4e28d3ea61a64ae61830000c909662cb053642efddbe96503db0e7783a6ee85b",
"sha256:cba1ace9d4ff6049b190d8b7991f9c1006b443a5238021aca96dd6ad2ac9da22"
],
"version": "==2.2.2"
},
"billiard": {
"hashes": [
"sha256:1d7b22bdc47aa52841120fcd22a74ae4fc8c13e9d3935643098184f5788c3ce6",
"sha256:abd9ce008c9a71ccde2c816f8daa36246e92a21e6a799831b887d88277187ecd"
],
"version": "==3.5.0.3"
},
"celery": {
"hashes": [
"sha256:77ff3730198d6a17b3c1f05579ebe570b579efb35f6d7e13dba3b1368d068b35",
"sha256:81a67f0d53a688ec2bc8557bd5d6d7218f925a6f2e6df80e01560de9e28997ec"
],
"index": "pypi",
"version": "==4.1.0"
},
"click": {
"hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
],
"version": "==6.7"
},
"flask": {
"hashes": [
"sha256:7fab1062d11dd0038434e790d18c5b9133fd9e6b7257d707c4578ccc1e38b67c",
"sha256:b1883637bbee4dc7bc98d900792d0a304d609fce0f5bd9ca91d1b6457e5918dd"
],
"index": "pypi",
"version": "==1.0"
},
"flask-cdn": {
"hashes": [
"sha256:2f1c55dbd42a5f2d7ceb110064819befb4eaf87ed1b353286971f8a72322a2cd"
],
"index": "pypi",
"version": "==1.5.3"
},
"flask-cors": {
"hashes": [
"sha256:0a09f3559ded4759387dfa2a355de59bc161f67269a1f4b7b0712a64b1f7dad6",
"sha256:149a2cca7bb5785324ed12afc0f327050f4e5e397113359b1390e4430b55b3dd",
"sha256:66a78dd308157f17296a254f793eba4cda1f75bfc96d9c5e33ad12bf9a411287"
],
"index": "pypi",
"version": "==3.0.2"
},
"flask-limiter": {
"hashes": [
"sha256:8fe5cb813fc3887dcebc53b0952b2116a4fe557b80aa4e008063800448bbc4d9",
"sha256:e909793a6d52138aef026c2b40ffae8609889db11c3bfd4dfe1b906d9a06a8b8"
],
"index": "pypi",
"version": "==0.9.3"
},
"flask-login": {
"hashes": [
"sha256:f2f144cdadfd03ca84cb65c3a5162a21b57262bebdd83e6db20077d460c45d52"
],
"index": "pypi",
"version": "==0.3.0"
},
"flask-migrate": {
"hashes": [
"sha256:331f1facf93712b6a3067eac382e645b1ef09e9a6d34da447acb6a3c293afd80"
],
"index": "pypi",
"version": "==2.0.3"
},
"flask-redis": {
"hashes": [
"sha256:f0d49837581bfb9985f9af73620d3eebec5ca24d7cfe3578a3ae33e698a56488"
],
"index": "pypi",
"version": "==0.0.6"
},
"flask-script": {
"hashes": [
"sha256:cef76eac751396355429a14c38967bb14d4973c53e07dec94af5cc8fb017107f"
],
"index": "pypi",
"version": "==2.0.5"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:c5244de44cc85d2267115624d83faef3f9e8f088756788694f305a5d5ad137c5"
],
"index": "pypi",
"version": "==2.1"
},
"flask-testing": {
"hashes": [
"sha256:abf539332c013aee5301cbb720d2c6a78bb69fe9bcf854697b6f62f1e7f175b2"
],
"index": "pypi",
"version": "==0.6.1"
},
"gunicorn": {
"hashes": [
"sha256:723234ea1fa8dff370ab69830ba8bc37469a7cba13fd66055faeef24085e6530",
"sha256:813f6916d18a4c8e90efde72f419308b357692f81333cb1125f80013d22fb618"
],
"index": "pypi",
"version": "==19.6.0"
},
"hashids": {
"hashes": [
"sha256:6539b892a426e75747a9c0ad69409e9566f9c21b79310fc3424b5b6726f28da6"
],
"index": "pypi",
"version": "==1.2.0"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"kombu": {
"hashes": [
"sha256:01f0da9fe222a2183345004243d1518c0fbe5875955f1b24842f2d9c65709ade",
"sha256:4249d9dd9dbf1fcec471d1c2def20653c9310dd1a217272d77e4844f9d5273cb"
],
"version": "==4.1.0"
},
"limits": {
"hashes": [
"sha256:9df578f4161017d79f5188609f1d65f6b639f8aad2914c3960c9252e56a0ff95",
"sha256:a017b8d9e9da6761f4574642149c337f8f540d4edfe573fb91ad2c4001a2bc76"
],
"version": "==1.3"
},
"mako": {
"hashes": [
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
],
"version": "==1.0.7"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"psycopg2": {
"hashes": [
"sha256:076351ed3c3b36ea147d456b3eb568be0ec89a29b000349d5b6d8e3e3026f49e",
"sha256:11b5e4f9d1e7e742d6a953e70d19ac5a12db5dc539db929657df676ec475b435",
"sha256:23198892998282d06884897dcb7561c043371c2b33118f1d12c0abbe1d7fd1d5",
"sha256:25c843b5ba383820a027603f3ef7b4d9854602d325295cd82bded15b33b762c9",
"sha256:3474c77a657fc2344f415ac0b5dc053ee0b3cdc84bbcc7cfc8047604b981febc",
"sha256:4460dc9d0132db3483fcedd4f2fb30475fd89a98b9cd563f6ad1dabd59247845",
"sha256:45dd9b3f08e327f23f5e70075738a69f4851c83e91b90b9fe3fe873de8c4e953",
"sha256:58041e8372f077ed4dd37029e6988b2f0859e520d3949a90c4d6456ec47f7c75",
"sha256:5e1d71b33e1712ab691988cd8d8866196e981fd3e353e01ff4604c437b338c75",
"sha256:6f3b5bf74396800738f8c15e6cecf7ce64d0e59f80e1ca5fe5ceedbcdeaac5ed",
"sha256:6ff610d8a5f44f28c27191cbc9025e4a6f8922dbb99b9d3ef0779b9c5ca164d6",
"sha256:707b40dc972223880364fce6eaa42e8fd7946f636e91fc7ccf65beb149e9a489",
"sha256:72448b57b82ece42a7f6c1c50a5ca27c0521f9e31b5d3621d19b2d9434debb32",
"sha256:7d8e4c2639a693a3b87942596a68b2cfc6cfbd66c0997b1adb7b8828bde6c1b7",
"sha256:84cb4591cd8a1238ddabcf8b066d3179fb2e2296679b720e8272f267d9137783",
"sha256:868a251a32f46c2615e034541bf61944b810ba66a2a8efeef7fbeb35d20ba246",
"sha256:87c950f463a11341af2228bc4173d4c47dce2e4d4b9282715b923c8d90b7547d",
"sha256:8cdfef6ca3359f66418c84778506e431fd02a0a4cee10c8f54affef141d9a37f",
"sha256:8e875e864876bf69879221746bacc3479cd5db26245bac4c3b2f9622ee6ac52f",
"sha256:96c08e0d72a1dd7a9314636351a3ed8bdcdf332ec8414da93e81c2cb747ca62d",
"sha256:99b02d34029d163344490212ebd7987b75a18349f5e1925ea77a1e77dd700c12",
"sha256:ab4f2a7d9c43c65ea62f28f549079d3f5f5d5ca54eafb552c765537718a8dbd4",
"sha256:ac8d1231aae6b6849e732e8d021a5f93fa9d8d8bac02c663797a9e6c5ac1607d",
"sha256:c946dd53fd39427051e72707146643660f017388641b9daa2d316fe45af06551",
"sha256:ceadecf660ad4f7a31ea5baef30a7351add8626f9fd3daaafabb9a9e549f3f9a",
"sha256:da8475270687816c500a5874e14b25e120c2e9711dc6f7cf697af6bda31ab741",
"sha256:e0313fb08c883ebc772d11c9ecf0b4ecf8946a37ed6fb3d2b6ad96f833a1af2b",
"sha256:e452672f74f8d4540da0b9df4d50cd26109f05e2ed35015bb0ffe6a0f08e730e",
"sha256:ebabc4ab0bd3fb4e4644cfe5c11c3ba22991ce1c6d40b4755a5f3f144e97e822",
"sha256:ec26d70bc9e5be78334f3df474bb852e078b80d2ac44a069a925f4333a432cc9",
"sha256:f50d0df01a1fe2c0b8e8d74c9eb8f636d09ccea4c7f50eb267eb117bd33b8516"
],
"index": "pypi",
"version": "==2.7"
},
"pyaml": {
"hashes": [
"sha256:66623c52f34d83a2c0fc963e08e8b9d0c13d88404e3b43b1852ef71eda19afa3",
"sha256:f83fc302c52c6b83a15345792693ae0b5bc07ad19f59e318b7617d7123d62990"
],
"index": "pypi",
"version": "==17.12.1"
},
"python-dateutil": {
"hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
],
"version": "==2.7.3"
},
"python-editor": {
"hashes": [
"sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565"
],
"version": "==1.0.3"
},
"pytz": {
"hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
],
"version": "==2018.4"
},
"pyyaml": {
"hashes": [
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
],
"version": "==3.12"
},
"redis": {
"hashes": [
"sha256:5dfbae6acfc54edf0a7a415b99e0b21c0a3c27a7f787b292eea727b1facc5533",
"sha256:97156b37d7cda4e7d8658be1148c983984e1a975090ba458cc7e244025191dbd"
],
"index": "pypi",
"version": "==2.10.5"
},
"requests": {
"hashes": [
"sha256:a57307f3a5f35ec9e1254aaf3e0484063ee3ee6b5f123fb35c5b2673492efa71",
"sha256:fcef306d62b1c061eb00b8402cf136ff0ea1daf7a53b60cdef9563a22850072c"
],
"index": "pypi",
"version": "==2.1.0"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"sqlalchemy": {
"hashes": [
"sha256:e755fd23b8bd574163d392ae85f41f6cd32eca8fe5bd7b5692de77265bb220cf"
],
"index": "pypi",
"version": "==1.0.13"
},
"stripe": {
"hashes": [
"sha256:74ef15145feba5fdfad2311195bf500971f04397756d4dcb0715173e928ccf1e",
"sha256:7545301c65459ab65546ef1f3d58d2a0500d2820214af2e8a21f89d1517047bf",
"sha256:d395e13ded4ae0c13872e5dc4402e20b46f8c2eafa9ec035756347dac53a0a45"
],
"index": "pypi",
"version": "==1.55.0"
},
"structlog": {
"hashes": [
"sha256:a8cc45c208c6b251031ec223940552eccd97dbdb322474e0c4ec9e50f76a225a",
"sha256:b44dfaadcbab84e6bb97bd9b263f61534a79611014679757cd93e2359ee7be01"
],
"index": "pypi",
"version": "==16.1.0"
},
"unicodecsv": {
"hashes": [
"sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc"
],
"index": "pypi",
"version": "==0.14.1"
},
"vine": {
"hashes": [
"sha256:52116d59bc45392af9fdd3b75ed98ae48a93e822cee21e5fda249105c59a7a72",
"sha256:6849544be74ec3638e84d90bc1cf2e1e9224cc10d96cd4383ec3f69e9bce077b"
],
"version": "==1.1.4"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
}
},
"develop": {
"fakeredis": {
"hashes": [
"sha256:26631f046ecc327140176af595835fb8b5cae27b0bc0adcc4a84ba286fcad487",
"sha256:488e6654ec57ef96f7f24227196602bac227d838b4a28407f113525ec0449310"
],
"index": "pypi",
"version": "==0.10.3"
},
"httpretty": {
"hashes": [
"sha256:69259e22addf5ab5f25bd00c6568e16fc2e54efdd4da69eb0950718dba3b2dab"
],
"index": "pypi",
"version": "==0.9.4"
},
"mock": {
"hashes": [
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
],
"index": "pypi",
"version": "==2.0.0"
},
"pbr": {
"hashes": [
"sha256:4e8a0ed6a8705a26768f4c3da26026013b157821fe5f95881599556ea9d91c19",
"sha256:dae4aaa78eafcad10ce2581fc34d694faa616727837fd8e55c1a00951ad6744f"
],
"version": "==4.0.2"
},
"python-dotenv": {
"hashes": [
"sha256:4965ed170bf51c347a89820e8050655e9c25db3837db6602e906b6d850fad85c",
"sha256:509736185257111613009974e666568a1b031b028b61b500ef1ab4ee780089d5"
],
"index": "pypi",
"version": "==0.8.2"
},
"redis": {
"hashes": [
"sha256:5dfbae6acfc54edf0a7a415b99e0b21c0a3c27a7f787b292eea727b1facc5533",
"sha256:97156b37d7cda4e7d8658be1148c983984e1a975090ba458cc7e244025191dbd"
],
"index": "pypi",
"version": "==2.10.5"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
}
}
}

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from .create_app import create_app
app = create_app()
from app import create_app
forms_app = create_app()
from . import manage

View File

@ -1,26 +1,14 @@
import json
import stripe
import structlog
from flask import Flask, g, request, redirect
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user
from flask_cdn import CDN
from flask_redis import Redis
from flask_limiter import Limiter
from flask_limiter.util import get_ipaddr
from celery import Celery
import settings
DB = SQLAlchemy()
redis_store = Redis()
stripe.api_key = settings.STRIPE_SECRET_KEY
cdn = CDN()
celery = Celery(__name__, broker=settings.CELERY_BROKER_URL)
import routes
from users.models import User
from . import routes, settings
from .stuff import DB, redis_store, cdn, celery
from .users.models import User
def configure_login(app):
login_manager = LoginManager()
@ -62,7 +50,7 @@ def configure_logger(app):
rest = []
for k, v in event.items():
if type(v) is unicode:
if type(v) is str:
v = v.encode('utf-8', 'ignore')
rest.append('\x1b[{}m{}\x1b[0m={}'.format(
levelcolor,

View File

@ -1 +0,0 @@
import views

View File

@ -1,15 +1,14 @@
import werkzeug.datastructures
import urlparse
import requests
import hashlib
import hashids
import uuid
import json
from urlparse import urljoin
from urllib.parse import urljoin, urlparse
from flask import request, g
from formspree import settings
from formspree.app import redis_store, DB
from formspree.stuff import redis_store, DB
from flask import jsonify
from flask_login import current_user
@ -44,7 +43,7 @@ def ordered_storage(f):
def referrer_to_path(r):
if not r:
return ''
parsed = urlparse.urlparse(r)
parsed = urlparse(r)
n = parsed.netloc + parsed.path
return n
@ -52,7 +51,7 @@ def referrer_to_path(r):
def referrer_to_baseurl(r):
if not r:
return ''
parsed = urlparse.urlparse(r)
parsed = urlparse(r)
n = parsed.netloc
return n
@ -66,7 +65,7 @@ def http_form_to_dict(data):
ret = {}
ordered_keys = []
for elem in data.iteritems(multi=True):
for elem in data.items(multi=True):
if not elem[0] in ret.keys():
ret[elem[0]] = []
ordered_keys.append(elem[0])
@ -181,4 +180,4 @@ def check_valid_form_settings_request(form):
if not form:
return jsonify(error='That form does not exist. Please check the link and try again.'), 400
return True
return True

View File

@ -3,16 +3,17 @@ import random
import hashlib
import datetime
from formspree.app import DB, redis_store
from formspree import settings
from formspree.utils import send_email, unix_time_for_12_months_from_now, \
next_url, IS_VALID_EMAIL, request_wants_json
from flask import url_for, render_template, g
from sqlalchemy.sql.expression import delete
from sqlalchemy import func
from werkzeug.datastructures import ImmutableMultiDict, \
ImmutableOrderedMultiDict
from helpers import HASH, HASHIDS_CODEC, REDIS_COUNTER_KEY, \
from formspree import settings
from formspree.stuff import DB, redis_store
from formspree.utils import send_email, unix_time_for_12_months_from_now, \
next_url, IS_VALID_EMAIL, request_wants_json
from .helpers import HASH, HASHIDS_CODEC, REDIS_COUNTER_KEY, \
http_form_to_dict, referrer_to_path, \
store_first_submission, fetch_first_submission, \
KEYS_NOT_STORED
@ -390,14 +391,14 @@ class Form(DB.Model):
def unconfirm_digest(self):
return hmac.new(
settings.NONCE_SECRET,
'id={}'.format(self.id),
'id={}'.format(self.id).encode('utf-8'),
hashlib.sha256
).hexdigest()
def unconfirm_with_digest(self, digest):
if hmac.new(
settings.NONCE_SECRET,
'id={}'.format(self.id),
'id={}'.format(self.id).encode('utf-8'),
hashlib.sha256
).hexdigest() != digest:
return False

View File

@ -10,20 +10,20 @@ from flask import request, url_for, render_template, redirect, \
abort
from flask_login import current_user, login_required
from flask_cors import cross_origin
from urlparse import urljoin
from jinja2.exceptions import TemplateNotFound
from urllib.parse import urljoin
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.utils import request_wants_json, jsonerror, IS_VALID_EMAIL, \
url_domain, valid_url
from helpers import http_form_to_dict, ordered_storage, referrer_to_path, \
from .helpers import http_form_to_dict, ordered_storage, referrer_to_path, \
remove_www, referrer_to_baseurl, sitewide_file_check, \
verify_captcha, temp_store_hostname, get_temp_hostname, \
HASH, assign_ajax, valid_domain_request, \
KEYS_NOT_STORED, KEYS_EXCLUDED_FROM_EMAIL, check_valid_form_settings_request
from models import Form, Submission
from jinja2.exceptions import TemplateNotFound
KEYS_NOT_STORED, KEYS_EXCLUDED_FROM_EMAIL, \
check_valid_form_settings_request
from .models import Form, Submission
def thanks():

66
formspree/manage.py Normal file
View File

@ -0,0 +1,66 @@
import os
import datetime
import click
from flask_script import prompt_bool
from flask_migrate import Migrate
from formspree import app, settings
from formspree.stuff import redis_store, DB
from formspree.forms.helpers import REDIS_COUNTER_KEY
from formspree.forms.models import Form
from celery.bin.celery import main as celery_main
# add flask-migrate commands
migrate = Migrate(app, DB)
@app.cli.command()
def run_debug(port=os.getenv('PORT', 5000)):
'''runs the app with debug flag set to true'''
app.run(host='0.0.0.0', debug=True, port=int(port))
@app.cli.command()
@click.option('-i', '--id', default=None, help='form id')
@click.option('-H', '--host', default=None, help='referer hostname')
@click.option('-e', '--email', default=None, help='form email')
def monthly_counters(email=None, host=None, id=None, month=datetime.date.today().month):
if id:
query = [Form.query.get(id)]
elif email and host:
query = Form.query.filter_by(email=email, host=host)
elif email and not host:
query = Form.query.filter_by(email=email)
elif host and not email:
query = Form.query.filter_by(host=host)
else:
print('supply each --email or --form or both (or --id).')
return 1
for form in query:
nsubmissions = redis_store.get(REDIS_COUNTER_KEY(form_id=form.id, month=month)) \
or 0
print('%s submissions for %s' % (nsubmissions, form))
@app.cli.command()
@click.option('-t', '--testname', 'testname', default=None, help='name of test')
def test(testname=None):
import unittest
test_loader = unittest.defaultTestLoader
if testname:
test_suite = test_loader.loadTestsFromName(testname)
else:
test_suite = test_loader.discover('.')
test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)
@app.cli.command()
def celery():
celery_args = ['celery', 'worker', '-B', '-s', '/tmp/celery.db', '--concurrency=5']
return celery_main(celery_args)
if __name__ == "__main__":
app.run()

View File

@ -1,55 +1,55 @@
import forms
import users
import static_pages
import formspree.forms.views as fv
import formspree.users.views as uv
import formspree.static_pages.views as sv
def configure_routes(app):
app.add_url_rule('/', 'index', view_func=static_pages.views.default, methods=['GET'])
app.add_url_rule('/favicon.ico', view_func=static_pages.views.favicon)
app.add_url_rule('/formspree-verify.txt', view_func=static_pages.views.formspree_verify)
app.add_url_rule('/', 'index', view_func=sv.default, methods=['GET'])
app.add_url_rule('/favicon.ico', view_func=sv.favicon)
app.add_url_rule('/formspree-verify.txt', view_func=sv.formspree_verify)
# Public forms
app.add_url_rule('/<email_or_string>', 'send', view_func=forms.views.send, methods=['GET', 'POST'])
app.add_url_rule('/unblock/<email>', 'unblock_email', view_func=forms.views.unblock_email, methods=['GET', 'POST'])
app.add_url_rule('/resend/<email>', 'resend_confirmation', view_func=forms.views.resend_confirmation, methods=['POST'])
app.add_url_rule('/confirm/<nonce>', 'confirm_email', view_func=forms.views.confirm_email, methods=['GET'])
app.add_url_rule('/unconfirm/<digest>/<form_id>', 'unconfirm_form', view_func=forms.views.unconfirm_form, methods=['POST'])
app.add_url_rule('/thanks', 'thanks', view_func=forms.views.thanks, methods=['GET'])
app.add_url_rule('/<email_or_string>', 'send', view_func=fv.send, methods=['GET', 'POST'])
app.add_url_rule('/unblock/<email>', 'unblock_email', view_func=fv.unblock_email, methods=['GET', 'POST'])
app.add_url_rule('/resend/<email>', 'resend_confirmation', view_func=fv.resend_confirmation, methods=['POST'])
app.add_url_rule('/confirm/<nonce>', 'confirm_email', view_func=fv.confirm_email, methods=['GET'])
app.add_url_rule('/unconfirm/<digest>/<form_id>', 'unconfirm_form', view_func=fv.unconfirm_form, methods=['POST'])
app.add_url_rule('/thanks', 'thanks', view_func=fv.thanks, methods=['GET'])
# Users
app.add_url_rule('/account', 'account', view_func=users.views.account, methods=['GET'])
app.add_url_rule('/account/upgrade', 'account-upgrade', view_func=users.views.upgrade, methods=['POST'])
app.add_url_rule('/account/downgrade', view_func=users.views.downgrade, methods=['POST'])
app.add_url_rule('/account/resubscribe', view_func=users.views.resubscribe, methods=['POST'])
app.add_url_rule('/card/add', 'add-card', view_func=users.views.add_card, methods=['POST'])
app.add_url_rule('/card/<cardid>/default', 'change-default-card', view_func=users.views.change_default_card, methods=['POST'])
app.add_url_rule('/card/<cardid>/delete', 'delete-card', view_func=users.views.delete_card, methods=['POST'])
app.add_url_rule('/account/billing', 'billing-dashboard', view_func=users.views.billing, methods=['GET'])
app.add_url_rule('/account/billing/invoice/update-invoice-address', 'update-invoice-address', view_func=users.views.update_invoice_address, methods=['POST'])
app.add_url_rule('/account/billing/invoice/<invoice_id>', view_func=users.views.invoice, methods=['GET'])
app.add_url_rule('/account/add-email', 'add-account-email', view_func=users.views.add_email, methods=['POST'])
app.add_url_rule('/account/confirm/<digest>', 'confirm-account-email', view_func=users.views.confirm_email, methods=['GET'])
app.add_url_rule('/register', 'register', view_func=users.views.register, methods=['GET', 'POST'])
app.add_url_rule('/login', 'login', view_func=users.views.login, methods= ['GET', 'POST'])
app.add_url_rule('/login/reset', 'forgot-password', view_func=users.views.forgot_password, methods=['GET', 'POST'])
app.add_url_rule('/login/reset/<digest>', 'reset-password', view_func=users.views.reset_password, methods=['GET', 'POST'])
app.add_url_rule('/logout', 'logout', view_func=users.views.logout, methods=['GET'])
app.add_url_rule('/account', 'account', view_func=uv.account, methods=['GET'])
app.add_url_rule('/account/upgrade', 'account-upgrade', view_func=uv.upgrade, methods=['POST'])
app.add_url_rule('/account/downgrade', view_func=uv.downgrade, methods=['POST'])
app.add_url_rule('/account/resubscribe', view_func=uv.resubscribe, methods=['POST'])
app.add_url_rule('/card/add', 'add-card', view_func=uv.add_card, methods=['POST'])
app.add_url_rule('/card/<cardid>/default', 'change-default-card', view_func=uv.change_default_card, methods=['POST'])
app.add_url_rule('/card/<cardid>/delete', 'delete-card', view_func=uv.delete_card, methods=['POST'])
app.add_url_rule('/account/billing', 'billing-dashboard', view_func=uv.billing, methods=['GET'])
app.add_url_rule('/account/billing/invoice/update-invoice-address', 'update-invoice-address', view_func=uv.update_invoice_address, methods=['POST'])
app.add_url_rule('/account/billing/invoice/<invoice_id>', view_func=uv.invoice, methods=['GET'])
app.add_url_rule('/account/add-email', 'add-account-email', view_func=uv.add_email, methods=['POST'])
app.add_url_rule('/account/confirm/<digest>', 'confirm-account-email', view_func=uv.confirm_email, methods=['GET'])
app.add_url_rule('/register', 'register', view_func=uv.register, methods=['GET', 'POST'])
app.add_url_rule('/login', 'login', view_func=uv.login, methods= ['GET', 'POST'])
app.add_url_rule('/login/reset', 'forgot-password', view_func=uv.forgot_password, methods=['GET', 'POST'])
app.add_url_rule('/login/reset/<digest>', 'reset-password', view_func=uv.reset_password, methods=['GET', 'POST'])
app.add_url_rule('/logout', 'logout', view_func=uv.logout, methods=['GET'])
# Users' forms
app.add_url_rule('/dashboard', 'dashboard', view_func=forms.views.forms, methods=['GET'])
app.add_url_rule('/forms', 'forms', view_func=forms.views.forms, methods=['GET'])
app.add_url_rule('/forms', 'create-form', view_func=forms.views.create_form, methods=['POST'])
app.add_url_rule('/forms/sitewide-check', view_func=forms.views.sitewide_check, methods=['GET'])
app.add_url_rule('/forms/<hashid>/', 'form-submissions', view_func=forms.views.form_submissions, methods=['GET'])
app.add_url_rule('/forms/<hashid>.<format>', 'form-submissions', view_func=forms.views.form_submissions, methods=['GET'])
app.add_url_rule('/forms/<hashid>/toggle-recaptcha', 'toggle-recaptcha', view_func=forms.views.form_recaptcha_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/toggle-emails', 'toggle-emails', view_func=forms.views.form_email_notification_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/toggle-storage', 'toggle-storage', view_func=forms.views.form_archive_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/toggle', 'form-toggle', view_func=forms.views.form_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/delete', 'form-deletion', view_func=forms.views.form_deletion, methods=['POST'])
app.add_url_rule('/forms/<hashid>/delete/<submissionid>', 'submission-deletion', view_func=forms.views.submission_deletion, methods=['POST'])
app.add_url_rule('/dashboard', 'dashboard', view_func=fv.forms, methods=['GET'])
app.add_url_rule('/forms', 'forms', view_func=fv.forms, methods=['GET'])
app.add_url_rule('/forms', 'create-form', view_func=fv.create_form, methods=['POST'])
app.add_url_rule('/forms/sitewide-check', view_func=fv.sitewide_check, methods=['GET'])
app.add_url_rule('/forms/<hashid>/', 'form-submissions', view_func=fv.form_submissions, methods=['GET'])
app.add_url_rule('/forms/<hashid>.<format>', 'form-submissions', view_func=fv.form_submissions, methods=['GET'])
app.add_url_rule('/forms/<hashid>/toggle-recaptcha', 'toggle-recaptcha', view_func=fv.form_recaptcha_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/toggle-emails', 'toggle-emails', view_func=fv.form_email_notification_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/toggle-storage', 'toggle-storage', view_func=fv.form_archive_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/toggle', 'form-toggle', view_func=fv.form_toggle, methods=['POST'])
app.add_url_rule('/forms/<hashid>/delete', 'form-deletion', view_func=fv.form_deletion, methods=['POST'])
app.add_url_rule('/forms/<hashid>/delete/<submissionid>', 'submission-deletion', view_func=fv.submission_deletion, methods=['POST'])
# Webhooks
app.add_url_rule('/webhooks/stripe', view_func=users.views.stripe_webhook, methods=['POST'])
app.add_url_rule('/webhooks/stripe', view_func=uv.stripe_webhook, methods=['POST'])
# Any other static pages and 404
app.add_url_rule('/<path:template>', 'default', view_func=static_pages.views.default, methods=['GET'])
app.add_url_rule('/<path:template>', 'default', view_func=sv.default, methods=['GET'])

View File

@ -13,9 +13,9 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False
LOG_LEVEL = os.getenv('LOG_LEVEL') or 'debug'
SECRET_KEY = os.getenv('SECRET_KEY')
NONCE_SECRET = os.getenv('NONCE_SECRET')
HASHIDS_SALT = os.getenv('HASHIDS_SALT')
SECRET_KEY = os.getenv('SECRET_KEY') or ''
HASHIDS_SALT = os.getenv('HASHIDS_SALT') or ''
NONCE_SECRET = (os.getenv('NONCE_SECRET') or '').encode('utf-8')
MONTHLY_SUBMISSIONS_LIMIT = int(os.getenv('MONTHLY_SUBMISSIONS_LIMIT') or 1000)
ARCHIVED_SUBMISSIONS_LIMIT = int(os.getenv('ARCHIVED_SUBMISSIONS_LIMIT') or 100)
@ -54,4 +54,4 @@ CONTACT_FORM_HASHID = os.getenv('CONTACT_FORM_HASHID', CONTACT_EMAIL)
TYPEKIT_KEY = os.getenv('TYPEKIT_KEY', '1234567')
CELERY_BROKER_URL = os.getenv('REDIS_URL')
CELERY_BROKER_URL = os.getenv('REDIS_URL')

View File

@ -1 +0,0 @@
import views

13
formspree/stuff.py Normal file
View File

@ -0,0 +1,13 @@
import stripe
from flask_sqlalchemy import SQLAlchemy
from flask_cdn import CDN
from flask_redis import Redis
from celery import Celery
from . import settings
DB = SQLAlchemy()
redis_store = Redis()
stripe.api_key = settings.STRIPE_SECRET_KEY
cdn = CDN()
celery = Celery(__name__, broker=settings.CELERY_BROKER_URL)

View File

@ -1 +0,0 @@
import views

View File

@ -4,9 +4,9 @@ from datetime import datetime
from flask import url_for, render_template, g
from formspree import settings
from formspree.stuff import DB
from formspree.utils import send_email, IS_VALID_EMAIL
from formspree.app import DB
from helpers import hash_pwd
from .helpers import hash_pwd
class User(DB.Model):
__tablename__ = 'users'
@ -56,12 +56,12 @@ class User(DB.Model):
return False
def get_id(self):
return unicode(self.id)
return self.id
def reset_password_digest(self):
return hmac.new(
settings.NONCE_SECRET,
'id={0}&password={1}'.format(self.id, self.password),
'id={0}&password={1}'.format(self.id, self.password).encode('utf-8'),
hashlib.sha256
).hexdigest()
@ -121,7 +121,9 @@ class Email(DB.Model):
email=addr,
user_id=user_id)
digest = hmac.new(
settings.NONCE_SECRET, message.encode('utf-8'), hashlib.sha256
settings.NONCE_SECRET,
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
link = url_for('confirm-account-email',
digest=digest, email=addr, _external=True)
@ -145,7 +147,9 @@ class Email(DB.Model):
email=addr,
user_id=user_id)
what_should_be = hmac.new(
settings.NONCE_SECRET, message.encode('utf-8'), hashlib.sha256
settings.NONCE_SECRET,
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
if digest == what_should_be:
return cls(address=addr, owner_id=user_id)

View File

@ -5,11 +5,12 @@ from flask import request, flash, url_for, render_template, redirect, g
from flask_login import login_user, logout_user, \
current_user, login_required
from sqlalchemy.exc import IntegrityError
from helpers import check_password, hash_pwd
from formspree.app import DB, celery
from formspree import settings
from models import User, Email
from formspree.stuff import DB, celery
from formspree.utils import send_email
from .models import User, Email
from .helpers import check_password, hash_pwd
def register():

View File

@ -1,7 +1,7 @@
import requests
import datetime
import calendar
import urlparse
from urllib.parse import urlparse, urlunparse
import uuid
import json
import re
@ -12,7 +12,7 @@ from formspree import settings
IS_VALID_EMAIL = lambda x: re.match(r"[^@]+@[^@]+\.[^@]+", x)
def valid_url(url):
parsed = urlparse.urlparse(url)
parsed = urlparse(url)
return len(parsed.scheme) > 0 and len(parsed.netloc) > 0 and not 'javascript:' in url
def request_wants_json():
@ -75,14 +75,14 @@ def get_url(endpoint, secure=False, **values):
def url_domain(url):
parsed = urlparse.urlparse(url)
parsed = urlparse(url)
return '.'.join(parsed.netloc.split('.')[-2:])
def unix_time_for_12_months_from_now(now=None):
now = now or datetime.date.today()
month = now.month - 1 + 12
next_year = now.year + month / 12
next_year = now.year + int(month / 12)
next_month = month % 12 + 1
start_of_next_month = datetime.datetime(next_year, next_month, 1, 0, 0)
return calendar.timegm(start_of_next_month.utctimetuple())
@ -102,10 +102,10 @@ def next_url(referrer=None, next=None):
# parts from _next. so, if _next is only a path it will just use
# that path. if it is a netloc without a scheme, will use that
# netloc, but reuse the scheme from base and so on.
parsed_next = urlparse.urlparse(next)
base = urlparse.urlparse(referrer)
parsed_next = urlparse(next)
base = urlparse(referrer)
return urlparse.urlunparse([
return urlunparse([
parsed_next.scheme or base.scheme,
parsed_next.netloc or base.netloc,
parsed_next.path or base.path,

104
manage.py
View File

@ -1,104 +0,0 @@
import os
import datetime
import click
from flask_script import prompt_bool
from flask_migrate import Migrate
from formspree import create_app, app, settings
from formspree.app import redis_store
from formspree.forms.helpers import REDIS_COUNTER_KEY
from formspree.forms.models import Form
from celery.bin.celery import main as celery_main
forms_app = create_app()
# add flask-migrate commands
migrate = Migrate(forms_app, app.DB)
@forms_app.cli.command()
def run_debug(port=os.getenv('PORT', 5000)):
'''runs the app with debug flag set to true'''
forms_app.run(host='0.0.0.0', debug=True, port=int(port))
@forms_app.cli.command()
@click.option('-H', '--host', default=None, help='referer hostname')
@click.option('-e', '--email', default=None, help='form email')
def unsubscribe(email, host):
''' Unsubscribes an email by resetting the form to unconfirmed. User may get
one more confirmation email, but if she doesn't confirm that will be it.'''
form = None
if email and host:
form = Form.query.filter_by(email=email, host=host).first()
elif email and not host:
query = Form.query.filter_by(email=email)
if query.count() == 1:
form = query.first()
elif query.count() > 1:
for f in query.all():
print '-', f.host
print 'More than one result for this email, specify the host.'
elif host and not email:
query = Form.query.filter_by(host=host)
if query.count() == 1:
form = query.first()
elif query.count() > 1:
for f in query.all():
print '-', f.email
print 'More than one result for this host, specify the email.'
if form:
print 'unsubscribing the email %s from the form at %s' % (form.email, form.host)
if prompt_bool('are you sure?'):
form.confirmed = False
form.confirm_sent = False
app.DB.session.add(form)
app.DB.session.commit()
print 'success.'
@forms_app.cli.command()
@click.option('-i', '--id', default=None, help='form id')
@click.option('-H', '--host', default=None, help='referer hostname')
@click.option('-e', '--email', default=None, help='form email')
def monthly_counters(email=None, host=None, id=None, month=datetime.date.today().month):
if id:
query = [Form.query.get(id)]
elif email and host:
query = Form.query.filter_by(email=email, host=host)
elif email and not host:
query = Form.query.filter_by(email=email)
elif host and not email:
query = Form.query.filter_by(host=host)
else:
print 'supply each --email or --form or both (or --id).'
return 1
for form in query:
nsubmissions = redis_store.get(REDIS_COUNTER_KEY(form_id=form.id, month=month)) or 0
print '%s submissions for %s' % (nsubmissions, form)
@forms_app.cli.command()
@click.option('-t', '--testname', 'testname', default=None, help='name of test')
def test(testname=None):
import unittest
test_loader = unittest.defaultTestLoader
if testname:
test_suite = test_loader.loadTestsFromName(testname)
else:
test_suite = test_loader.discover('.')
test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)
@forms_app.cli.command()
def celery():
celery_args = ['celery', 'worker', '-B', '-s', '/tmp/celery.db', '--concurrency=5']
return celery_main(celery_args)
if __name__ == "__main__":
forms_app.run()

View File

@ -1,36 +0,0 @@
sqlalchemy==1.0.13
alembic==0.8.10
celery==4.1.0
click==6.7
fakeredis==0.8.2
Flask==0.12
Flask-CDN==1.5.3
Flask-Cors==3.0.2
Flask-Limiter==0.9.3
Flask-Login==0.3.0
Flask-Migrate==2.0.3
Flask-Redis==0.0.6
Flask-Script==2.0.5
Flask-SQLAlchemy==2.1
Flask-Testing==0.6.1
funcsigs==1.0.2
future==0.16.0
gunicorn==19.6.0
hashids==1.0.2
httpretty==0.8.14
itsdangerous==0.24
git+https://github.com/mitsuhiko/jinja2@84f39ff5afd89760e1eff387ba4ce38e4a982977
limits==1.2.1
Mako==1.0.6
MarkupSafe==0.23
mock==2.0.0
Paste==2.0.3
pbr==1.10.0
psycopg2==2.7
python-dotenv==0.3.0
redis==2.10.5
requests==2.1.0
stripe==1.55.0
structlog==16.1.0
unicodecsv==0.14.1
pyaml==17.12.1

View File

@ -1 +0,0 @@
python-2.7.11

View File

@ -2,12 +2,11 @@
import os
import redis
import fakeredis
from flask_testing import TestCase
import mock
from formspree import create_app
from formspree import settings
from formspree.app import DB, redis_store
from formspree import create_app, settings
from formspree.stuff import DB, redis_store
# the different redis database only accessed by flask-limiter

View File

@ -2,12 +2,12 @@ import httpretty
import json
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.forms.helpers import HASH
from formspree.users.models import User
from formspree.forms.models import Form, Submission
from formspree_test_case import FormspreeTestCase
from .formspree_test_case import FormspreeTestCase
class ArchiveSubmissionsTestCase(FormspreeTestCase):
@httpretty.activate
@ -176,7 +176,7 @@ class ArchiveSubmissionsTestCase(FormspreeTestCase):
'Content-type': 'application/json'},
data=json.dumps({'email': 'hope@springs.com'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
form_endpoint = resp['hashid']
# manually confirm the form
@ -195,20 +195,20 @@ class ArchiveSubmissionsTestCase(FormspreeTestCase):
r = self.client.get('/forms/' + form_endpoint + '/',
headers={'Accept': 'application/json'}
)
submissions = json.loads(r.data)['submissions']
submissions = json.loads(r.data.decode('utf-8'))['submissions']
self.assertEqual(len(submissions), 1)
self.assertEqual(submissions[0]['name'], 'bruce')
self.assertEqual(submissions[0]['message'], 'hi, my name is bruce!')
# test exporting feature (both json and csv file downloads)
r = self.client.get('/forms/' + form_endpoint + '.json')
submissions = json.loads(r.data)['submissions']
submissions = json.loads(r.data.decode('utf-8'))['submissions']
self.assertEqual(len(submissions), 1)
self.assertEqual(submissions[0]['name'], 'bruce')
self.assertEqual(submissions[0]['message'], 'hi, my name is bruce!')
r = self.client.get('/forms/' + form_endpoint + '.csv')
lines = r.data.splitlines()
lines = r.data.decode('utf-8').splitlines()
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], 'date,message,name')
self.assertIn('"hi, my name is bruce!"', lines[1])

View File

@ -3,9 +3,9 @@ import json
from formspree.forms.models import Form
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree_test_case import FormspreeTestCase
from .formspree_test_case import FormspreeTestCase
class ContentTypeTestCase(FormspreeTestCase):
@ -24,16 +24,16 @@ class ContentTypeTestCase(FormspreeTestCase):
def isjson(res):
try:
d = json.loads(res.get_data())
d = json.loads(res.data.decode('utf-8'))
self.assertIsInstance(d, dict)
self.assertIn('success', d)
self.assertEqual(res.mimetype, 'application/json')
except ValueError, e:
except ValueError as e:
self.assertFalse(e)
def ishtml(res):
try:
d = json.loads(res.get_data())
d = json.loads(res.data.decode('utf-8'))
self.assertNotIsInstance(d, dict)
except ValueError:
self.assertEqual(res.mimetype, 'text/html')

View File

@ -1,15 +1,13 @@
# encoding: utf-8
import httpretty
import json
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.users.models import User, Email
from formspree.forms.models import Form
from formspree_test_case import FormspreeTestCase
from utils import parse_confirmation_link_sent
from .formspree_test_case import FormspreeTestCase
from .utils import parse_confirmation_link_sent
class EmailConfirmationsTestCase(FormspreeTestCase):
@ -32,7 +30,7 @@ class EmailConfirmationsTestCase(FormspreeTestCase):
for i, addr in enumerate(emails):
self.client.post('/account/add-email', data={'address': addr})
link, qs = parse_confirmation_link_sent(httpretty.last_request().body)
link, qs = parse_confirmation_link_sent(httpretty.last_request().body.decode('utf-8'))
self.client.get(link, query_string=qs)
email = Email.query.get([addr, user.id])
@ -65,18 +63,18 @@ class EmailConfirmationsTestCase(FormspreeTestCase):
r = self.client.get('/forms',
headers={'Accept': 'application/json'},
)
forms = json.loads(r.data)['forms']
forms = json.loads(r.data.decode('utf-8'))['forms']
self.assertEqual(0, len(forms))
# verify user email
link, qs = parse_confirmation_link_sent(httpretty.last_request().body)
link, qs = parse_confirmation_link_sent(httpretty.last_request().body.decode('utf-8'))
self.client.get(link, query_string=qs)
# confirm that the user has no access to the form since he is not upgraded
r = self.client.get('/forms',
headers={'Accept': 'application/json'},
)
forms = json.loads(r.data)['forms']
forms = json.loads(r.data.decode('utf-8'))['forms']
self.assertEqual(0, len(forms))
# upgrade user
@ -89,7 +87,7 @@ class EmailConfirmationsTestCase(FormspreeTestCase):
r = self.client.get('/forms',
headers={'Accept': 'application/json'},
)
forms = json.loads(r.data)['forms']
forms = json.loads(r.data.decode('utf-8'))['forms']
self.assertEqual(1, len(forms))
self.assertEqual(forms[0]['email'], u'márkö@example.com')
self.assertEqual(forms[0]['host'], 'tomatoes.com')
@ -109,20 +107,20 @@ class EmailConfirmationsTestCase(FormspreeTestCase):
r = self.client.get('/forms',
headers={'Accept': 'application/json'},
)
forms = json.loads(r.data)['forms']
forms = json.loads(r.data.decode('utf-8'))['forms']
self.assertEqual(1, len(forms))
# add this other email address to user account
self.client.post('/account/add-email', data={'address': 'contact@mark.com'})
link, qs = parse_confirmation_link_sent(httpretty.last_request().body)
link, qs = parse_confirmation_link_sent(httpretty.last_request().body.decode('utf-8'))
self.client.get(link, query_string=qs)
# confirm that the user account now has access to the form
r = self.client.get('/forms',
headers={'Accept': 'application/json'},
)
forms = json.loads(r.data)['forms']
forms = json.loads(r.data.decode('utf-8'))['forms']
self.assertEqual(2, len(forms))
self.assertEqual(forms[0]['email'], 'contact@mark.com') # forms are sorted by -id, so the newer comes first
self.assertEqual(forms[0]['host'], 'mark.com')
@ -142,7 +140,7 @@ class EmailConfirmationsTestCase(FormspreeTestCase):
r = self.client.get('/forms',
headers={'Accept': 'application/json'},
)
forms = json.loads(r.data)['forms']
forms = json.loads(r.data.decode('utf-8'))['forms']
self.assertEqual(3, len(forms))
self.assertEqual(forms[0]['email'], u'márkö@example.com')
self.assertEqual(forms[0]['host'], 'elsewhere.com')

View File

@ -1,16 +1,14 @@
# encoding: utf-8
import httpretty
import json
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.forms.helpers import HASH
from formspree.users.models import User, Email
from formspree.forms.models import Form, Submission
from formspree_test_case import FormspreeTestCase
from utils import parse_confirmation_link_sent
from .formspree_test_case import FormspreeTestCase
from .utils import parse_confirmation_link_sent
class TestFormCreationFromDashboard(FormspreeTestCase):
@ -32,7 +30,7 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
data={'email': 'hope@springs.com'}
)
self.assertEqual(r.status_code, 402)
self.assertIn('error', json.loads(r.data))
self.assertIn('error', json.loads(r.data.decode('utf-8')))
self.assertEqual(0, Form.query.count())
# upgrade user manually
@ -46,7 +44,7 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
headers={'Accept': 'application/json', 'Content-type': 'application/json'},
data=json.dumps({'email': 'hope@springs.com'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(r.status_code, 200)
self.assertIn('submission_url', resp)
self.assertIn('hashid', resp)
@ -60,8 +58,8 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
headers={'Referer': 'http://testsite.com'},
data={'name': 'bruce'}
)
self.assertIn("sent an email confirmation", r.data)
self.assertIn('confirm+your+email', httpretty.last_request().body)
self.assertIn("sent an email confirmation", r.data.decode('utf-8'))
self.assertIn('confirm+your+email', httpretty.last_request().body.decode('utf-8'))
self.assertEqual(1, Form.query.count())
# confirm form
@ -83,9 +81,9 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
form = Form.query.first()
self.assertEqual(form.counter, 5)
self.assertEqual(form.get_monthly_counter(), 5)
self.assertIn('ana', httpretty.last_request().body)
self.assertIn('__4__', httpretty.last_request().body)
self.assertNotIn('You+are+past+our+limit', httpretty.last_request().body)
self.assertIn('ana', httpretty.last_request().body.decode('utf-8'))
self.assertIn('__4__', httpretty.last_request().body.decode('utf-8'))
self.assertNotIn('You+are+past+our+limit', httpretty.last_request().body.decode('utf-8'))
# try (and fail) to submit from a different host
r = self.client.post('/' + form_endpoint,
@ -93,8 +91,8 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
data={'name': 'usurper'}
)
self.assertEqual(r.status_code, 403)
self.assertIn('ana', httpretty.last_request().body) # no more data is sent to sendgrid
self.assertIn('__4__', httpretty.last_request().body)
self.assertIn('ana', httpretty.last_request().body.decode('utf-8')) # no more data is sent to sendgrid
self.assertIn('__4__', httpretty.last_request().body.decode('utf-8'))
@httpretty.activate
def test_form_creation_with_a_registered_email(self):
@ -127,11 +125,11 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
data=json.dumps({'email': 'email@testsite.com',
'url': 'https://www.testsite.com/contact.html'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(resp['confirmed'], False)
self.assertEqual(httpretty.has_request(), True)
self.assertIn('Confirm+email', httpretty.last_request().body)
self.assertIn('www.testsite.com%2Fcontact.html', httpretty.last_request().body)
self.assertIn('Confirm+email', httpretty.last_request().body.decode('utf-8'))
self.assertIn('www.testsite.com%2Fcontact.html', httpretty.last_request().body.decode('utf-8'))
# manually verify an email
email = Email()
@ -146,15 +144,16 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
data=json.dumps({'email': 'owned-by@testsite.com',
'url': 'https://www.testsite.com/about.html'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(resp['confirmed'], True)
self.assertIn('www.testsite.com%2Fcontact.html', httpretty.last_request().body) # same as the last, means no new request was made
self.assertIn('www.testsite.com%2Fcontact.html', httpretty.last_request().body.decode('utf-8')) # same as the last, means no new request was made
# should have three created forms in the end
self.assertEqual(Form.query.count(), 3)
@httpretty.activate
def test_sitewide_forms(self):
httpretty.register_uri(httpretty.POST, 'https://api.sendgrid.com/api/mail.send.json')
httpretty.register_uri(httpretty.GET,
'http://mysite.com/formspree-verify.txt',
body=u'other_email@forms.com\nmyüñìćõð€email@email.com',
@ -189,7 +188,7 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
'url': 'http://mysite.com',
'sitewide': 'true'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(httpretty.has_request(), True)
self.assertEqual(resp['confirmed'], True)
@ -207,19 +206,19 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
headers = {'Referer': 'http://www.mysite.com/hipopotamo', 'content-type': 'application/json'},
data=json.dumps({'name': 'alice'})
)
self.assertIn('alice', httpretty.last_request().body)
self.assertIn('alice', httpretty.last_request().body.decode('utf-8'))
self.client.post('/' + form.hashid,
headers = {'Referer': 'http://mysite.com/baleia/urso?w=2', 'content-type': 'application/json'},
data=json.dumps({'name': 'maria'})
)
self.assertIn('maria', httpretty.last_request().body)
self.assertIn('maria', httpretty.last_request().body.decode('utf-8'))
self.client.post('/' + form.hashid,
headers = {'Referer': 'http://mysite.com/', 'content-type': 'application/json'},
data=json.dumps({'name': 'laura'})
)
self.assertIn('laura', httpretty.last_request().body)
self.assertIn('laura', httpretty.last_request().body.decode('utf-8'))
# another form, now with a www prefix that will be stripped
r = self.client.post('/forms',
@ -228,7 +227,7 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
'url': 'http://www.naive.com',
'sitewide': 'true'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(httpretty.has_request(), True)
self.assertEqual(resp['confirmed'], True)
@ -246,19 +245,19 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
headers={'Referer': 'http://naive.com/hipopotamo', 'content-type': 'application/json'},
data=json.dumps({'name': 'alice'})
)
self.assertIn('alice', httpretty.last_request().body)
self.assertIn('alice', httpretty.last_request().body.decode('utf-8'))
self.client.post('/' + form.hashid,
headers={'Referer': 'http://www.naive.com/baleia/urso?w=2', 'content-type': 'application/json'},
data=json.dumps({'name': 'maria'})
)
self.assertIn('maria', httpretty.last_request().body)
self.assertIn('maria', httpretty.last_request().body.decode('utf-8'))
self.client.post('/' + form.hashid,
headers={'Referer': 'http://www.naive.com/', 'content-type': 'application/json'},
data=json.dumps({'name': 'laura'})
)
self.assertIn('laura', httpretty.last_request().body)
self.assertIn('laura', httpretty.last_request().body.decode('utf-8'))
# create a different form with the same email address, now using unprefixed url
r = self.client.post('/forms',
@ -267,7 +266,7 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
'url': 'mysite.com',
'sitewide': 'true'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
@httpretty.activate
def test_form_settings(self):
@ -288,7 +287,7 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
headers={'Accept': 'application/json', 'Content-type': 'application/json'},
data=json.dumps({'email': 'texas@springs.com'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
form = Form.query.first()
form.confirmed = True
DB.session.add(form)
@ -308,7 +307,10 @@ class TestFormCreationFromDashboard(FormspreeTestCase):
data={'name': 'bruce'}
)
# make sure it doesn't send the email
self.assertNotIn('Someone+just+submitted+your+form', httpretty.last_request().body)
self.assertNotIn(
'Someone+just+submitted+your+form',
httpretty.last_request().body.decode('utf-8')
)
# disable archive storage on this form
self.client.post('/forms/' + form_endpoint + '/toggle-storage',

View File

@ -1,11 +1,11 @@
import httpretty
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.forms.models import Form
from formspree.users.models import User, Email
from formspree_test_case import FormspreeTestCase
from .formspree_test_case import FormspreeTestCase
http_headers = {
'Referer': 'testwebsite.com'
@ -46,10 +46,10 @@ class FormPostsTestCase(FormspreeTestCase):
'_subject': 'my-nice-subject',
'_format': 'plain'}
)
self.assertIn('my-nice-subject', httpretty.last_request().body)
self.assertNotIn('_subject', httpretty.last_request().body)
self.assertNotIn('_format', httpretty.last_request().body)
self.assertNotIn('plain', httpretty.last_request().body)
self.assertIn('my-nice-subject', httpretty.last_request().body.decode('utf-8'))
self.assertNotIn('_subject', httpretty.last_request().body.decode('utf-8'))
self.assertNotIn('_format', httpretty.last_request().body.decode('utf-8'))
self.assertNotIn('plain', httpretty.last_request().body.decode('utf-8'))
@httpretty.activate
def test_fail_form_without_header(self):
@ -73,7 +73,7 @@ class FormPostsTestCase(FormspreeTestCase):
headers={'Referer': settings.SERVICE_URL},
data={'name': 'alice', '_subject': 'my-nice-subject'}
)
self.assertIn("Unable to submit form", r.data)
self.assertIn("Unable to submit form", r.data.decode('utf-8'))
self.assertNotEqual(200, r.status_code)
self.assertFalse(httpretty.has_request())
self.assertEqual(0, Form.query.count())
@ -231,7 +231,7 @@ class FormPostsTestCase(FormspreeTestCase):
data={'name': 'peter'}
)
self.assertEqual(r.status_code, 302)
self.assertIn('peter', httpretty.last_request().body)
self.assertIn('peter', httpretty.last_request().body.decode('utf-8'))
# second submission
httpretty.register_uri(httpretty.POST, 'https://api.sendgrid.com/api/mail.send.json')
@ -240,7 +240,7 @@ class FormPostsTestCase(FormspreeTestCase):
data={'name': 'ana'}
)
self.assertEqual(r.status_code, 302)
self.assertIn('ana', httpretty.last_request().body)
self.assertIn('ana', httpretty.last_request().body.decode('utf-8'))
# third submission, now we're over the limit
httpretty.register_uri(httpretty.POST, 'https://api.sendgrid.com/api/mail.send.json')
@ -252,8 +252,8 @@ class FormPostsTestCase(FormspreeTestCase):
# being the form over the limits or not
# but the mocked sendgrid should never receive this last form
self.assertNotIn('maria', httpretty.last_request().body)
self.assertIn('You+are+past+our+limit', httpretty.last_request().body)
self.assertNotIn('maria', httpretty.last_request().body.decode('utf-8'))
self.assertIn('You+are+past+our+limit', httpretty.last_request().body.decode('utf-8'))
# all the other variables are ok:
self.assertEqual(1, Form.query.count())
@ -279,7 +279,7 @@ class FormPostsTestCase(FormspreeTestCase):
data={'name': 'noah'}
)
self.assertEqual(r.status_code, 302)
self.assertIn('noah', httpretty.last_request().body)
self.assertIn('noah', httpretty.last_request().body.decode('utf-8'))
@httpretty.activate
def test_first_submission_is_stored(self):
@ -295,7 +295,7 @@ class FormPostsTestCase(FormspreeTestCase):
self.assertEqual(f.get_monthly_counter(), 0) # monthly submissions also 0
# got a confirmation email
self.assertIn('one+step+away', httpretty.last_request().body)
self.assertIn('one+step+away', httpretty.last_request().body.decode('utf-8'))
# clicking of activation link
self.client.get('/confirm/%s' % (f.hash,))
@ -306,4 +306,4 @@ class FormPostsTestCase(FormspreeTestCase):
self.assertEqual(f.get_monthly_counter(), 1) # monthly submissions also
# got the first (missed) submission
self.assertIn('this+was+important', httpretty.last_request().body)
self.assertIn('this+was+important', httpretty.last_request().body.decode('utf-8'))

View File

@ -1,13 +1,13 @@
import httpretty
import urllib2
import json
import re
from urllib.parse import unquote
from formspree.forms.models import Form
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree_test_case import FormspreeTestCase
from .formspree_test_case import FormspreeTestCase
class ListUnsubscribeTestCase(FormspreeTestCase):
@httpretty.activate
@ -21,7 +21,7 @@ class ListUnsubscribeTestCase(FormspreeTestCase):
f = Form.query.first()
# List-Unsubscribe is present on confirmation email
body = urllib2.unquote(httpretty.last_request().body)
body = unquote(httpretty.last_request().body)
res = re.search('"List-Unsubscribe":[^"]*"<([^>]+)>"', body)
self.assertTrue(res is not None)
list_unsubscribe_url = res.group(1)
@ -38,7 +38,7 @@ class ListUnsubscribeTestCase(FormspreeTestCase):
self.assertEqual(r.status_code, 302)
# List-Unsubscribe is present on normal submission
body = urllib2.unquote(httpretty.last_request().body)
body = unquote(httpretty.last_request().body)
res = re.search('"List-Unsubscribe":[^"]*"<([^>]+)>"', body)
self.assertTrue(res is not None)
url = res.group(1)

View File

@ -1,9 +1,10 @@
import httpretty
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.forms.models import Form
from formspree_test_case import FormspreeTestCase
from .formspree_test_case import FormspreeTestCase
class RateLimitingTestCase(FormspreeTestCase):

View File

@ -3,13 +3,13 @@ import json
import stripe
from formspree import settings
from formspree.app import DB
from formspree.stuff import DB
from formspree.forms.helpers import HASH
from formspree.users.models import User, Email
from formspree.forms.models import Form, Submission
from formspree_test_case import FormspreeTestCase
from utils import parse_confirmation_link_sent
from .formspree_test_case import FormspreeTestCase
from .utils import parse_confirmation_link_sent
class UserAccountsTestCase(FormspreeTestCase):
@ -128,7 +128,7 @@ class UserAccountsTestCase(FormspreeTestCase):
data={'email': 'hope@springs.com'}
)
self.assertEqual(r.status_code, 402)
self.assertIn('error', json.loads(r.data))
self.assertIn('error', json.loads(r.data.decode('utf-8')))
self.assertEqual(0, Form.query.count())
# upgrade user manually
@ -142,7 +142,7 @@ class UserAccountsTestCase(FormspreeTestCase):
headers={'Accept': 'application/json', 'Content-type': 'application/json'},
data=json.dumps({'email': 'hope@springs.com'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(r.status_code, 200)
self.assertIn('submission_url', resp)
self.assertIn('hashid', resp)
@ -156,7 +156,7 @@ class UserAccountsTestCase(FormspreeTestCase):
headers={'Referer': 'formspree.io'},
data={'name': 'bruce'}
)
self.assertIn("We've sent a link to your email", r.data)
self.assertIn("We've sent a link to your email", r.data.decode('utf-8'))
self.assertIn('confirm+your+email', httpretty.last_request().body)
self.assertEqual(1, Form.query.count())
@ -209,7 +209,7 @@ class UserAccountsTestCase(FormspreeTestCase):
headers={'Accept': 'application/json', 'Content-type': 'application/json'},
data=json.dumps({'email': 'hope@springs.com'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(r.status_code, 200)
self.assertIn('submission_url', resp)
self.assertIn('hashid', resp)
@ -292,7 +292,7 @@ class UserAccountsTestCase(FormspreeTestCase):
headers={'Accept': 'application/json', 'Content-type': 'application/json'},
data=json.dumps({'email': 'hope@springs.com'})
)
resp = json.loads(r.data)
resp = json.loads(r.data.decode('utf-8'))
self.assertEqual(r.status_code, 200)
self.assertIn('submission_url', resp)
self.assertIn('hashid', resp)
@ -409,7 +409,10 @@ class UserAccountsTestCase(FormspreeTestCase):
# redirect back to /account, the HTML shows that the user is not yet
# in the free plan, since it will be valid for the next 30 days
self.assertIn("You've cancelled your subscription and it is ending on", r.data)
self.assertIn(
"You've cancelled your subscription and it is ending on",
r.data.decode('utf-8')
)
user = User.query.filter_by(email='maria@example.com').first()
self.assertEqual(user.upgraded, True)
@ -492,7 +495,10 @@ class UserAccountsTestCase(FormspreeTestCase):
r = self.client.post('/card/add', data={
'stripeToken': token
}, follow_redirects=True)
self.assertIn('That card already exists in your wallet', r.data)
self.assertIn(
'That card already exists in your wallet',
r.data.decode('utf-8')
)
# delete a card
r = self.client.post('/card/%s/delete' % cards[1].id)

View File

@ -1,4 +1,4 @@
from formspree_test_case import FormspreeTestCase
from .formspree_test_case import FormspreeTestCase
from formspree.utils import next_url

View File

@ -1,7 +1,10 @@
import re
from urllib import unquote
from urllib.parse import unquote
def parse_confirmation_link_sent(request_body):
if type(request_body) != str:
request_body = request_body.decode('utf-8')
request_body = unquote(request_body)
matchlink = re.search('Link:\+([^?]+)\?(\S+)', request_body)
if not matchlink: