User.plan with an enum instead of User.upgraded

This commit is contained in:
fiatjaf 2018-09-12 21:53:26 +00:00 committed by Cole
parent cb145b9fd0
commit 0156b05d4d
17 changed files with 95 additions and 66 deletions

View File

@ -19,9 +19,9 @@ hashids = "==1.2.0"
psycopg2 = "==2.7.4"
redis = "==2.10.5"
requests = "==2.1.0"
SQLAlchemy = "==1.0.13"
stripe = "==1.55.0"
structlog = "==16.1.0"
SQLAlchemy = "==1.0.13"
Flask = "==1"
Flask-CDN = "==1.5.3"
Flask-Cors = "==3.0.2"

View File

@ -14,7 +14,7 @@ from .models import Form, Submission
@login_required
def list():
# grab all the forms this user controls
if current_user.upgraded:
if current_user.has_feature('dashboard'):
forms = current_user.forms.order_by(Form.id.desc()).all()
else:
forms = []
@ -22,8 +22,8 @@ def list():
return jsonify({
'ok': True,
'user': {
'upgraded': current_user.upgraded,
'email': current_user.email
'features': {f: True for f in current_user.features},
'email': current_user.email
},
'forms': [f.serialize() for f in forms]
})
@ -37,8 +37,8 @@ def create():
if referrer != service:
return jsonerror(400, {'error': 'Improper request.'})
if not current_user.upgraded:
g.log.info('Failed to create form from dashboard. User is not upgraded.')
if not current_user.has_feature('dashboard'):
g.log.info('Failed to create form from dashboard. Forbidden.')
return jsonerror(402, {'error': "Please upgrade your account."})
email = request.get_json().get('email')
@ -97,7 +97,7 @@ def create():
@login_required
def get(hashid):
if not current_user.upgraded:
if not current_user.has_feature('dashboard'):
return jsonerror(402, {'error': "Please upgrade your account."})
form = Form.get_with_hashid(hashid)

View File

@ -6,6 +6,8 @@ import datetime
from flask import url_for, render_template, render_template_string, g
from sqlalchemy.sql import table
from sqlalchemy.sql.expression import delete
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import func
from werkzeug.datastructures import ImmutableMultiDict, \
ImmutableOrderedMultiDict
@ -14,6 +16,7 @@ from formspree import settings
from formspree.stuff import DB, redis_store, TEMPLATES
from formspree.utils import send_email, unix_time_for_12_months_from_now, \
next_url, IS_VALID_EMAIL, request_wants_json
from formspree.users.models import Plan
from .helpers import HASH, HASHIDS_CODEC, REDIS_COUNTER_KEY, \
http_form_to_dict, referrer_to_path, \
store_first_submission, fetch_first_submission, \
@ -103,10 +106,9 @@ class Form(DB.Model):
.filter(Form.id == self.id)
return by_email.union(by_creation)
@property
def upgraded(self):
upgraded_controllers = [i for i in self.controllers if i.upgraded]
return len(upgraded_controllers) > 0
def has_feature(self, feature):
c = [user for user in self.controllers if user.has_feature(feature)]
return len(c) > 0
@classmethod
def get_with_hashid(cls, hashid):
@ -207,8 +209,8 @@ class Form(DB.Model):
# increment the forms counter
self.counter = Form.counter + 1
# if submission storage is disabled and form is upgraded, don't store submission
if self.disable_storage and self.upgraded:
# if submission storage is disabled, don't store submission
if self.disable_storage and self.has_feature('dashboard'):
pass
else:
DB.session.add(self)
@ -239,17 +241,18 @@ class Form(DB.Model):
# url to request_unconfirm_form page
unconfirm = url_for('request_unconfirm_form', form_id=self.id, _external=True)
# check if the forms are over the counter and the user is not upgraded
# check if the forms are over the counter and the user has unlimited submissions
overlimit = False
monthly_counter = self.get_monthly_counter()
monthly_limit = settings.MONTHLY_SUBMISSIONS_LIMIT \
if self.id > settings.FORM_LIMIT_DECREASE_ACTIVATION_SEQUENCE \
else settings.GRANDFATHER_MONTHLY_LIMIT
if monthly_counter > monthly_limit and not self.upgraded:
if monthly_counter > monthly_limit and not self.has_feature('unlimited'):
overlimit = True
if monthly_counter == int(monthly_limit * 0.9) and not self.upgraded:
if monthly_counter == int(monthly_limit * 0.9) and \
not self.has_feature('unlimited'):
# send email notification
send_email(
to=self.email,
@ -295,8 +298,8 @@ class Form(DB.Model):
else:
return {'code': Form.STATUS_OVERLIMIT}
# if emails are disabled and form is upgraded, don't send email notification
if self.disable_email and self.upgraded:
# if emails are disabled, don't send email notification
if self.disable_email and self.has_feature('dashboard'):
return {'code': Form.STATUS_NO_EMAIL, 'next': next}
else:
result = send_email(
@ -483,15 +486,12 @@ class Form(DB.Model):
return True
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.ext.mutable import MutableDict
class Submission(DB.Model):
__tablename__ = 'submissions'
id = DB.Column(DB.Integer, primary_key=True)
submitted_at = DB.Column(DB.DateTime)
form_id = DB.Column(DB.Integer, DB.ForeignKey('forms.id'))
form_id = DB.Column(DB.Integer, DB.ForeignKey('forms.id'), nullable=False)
data = DB.Column(MutableDict.as_mutable(JSON))
def __init__(self, form_id):

View File

@ -195,8 +195,8 @@ def send(email_or_string):
captcha_verified or
settings.TESTING)
# if form is upgraded check if captcha is disabled
if form.upgraded:
# check if captcha is disabled
if form.has_feature('dashboard'):
needs_captcha = needs_captcha and not form.captcha_disabled
if needs_captcha:
@ -492,7 +492,7 @@ def serve_dashboard(hashid=None, s=None):
@login_required
def export_submissions(hashid, format=None):
if not current_user.upgraded:
if not current_user.has_feature('dashboard'):
return jsonerror(402, {'error': "Please upgrade your account."})
form = Form.get_with_hashid(hashid)

View File

@ -36,7 +36,7 @@ module.exports = class CreateForm extends React.Component {
}
render() {
if (!this.props.user.upgraded) {
if (!this.props.user.features.dashboard) {
return (
<div className="col-1-1 create-form">
<h6 className="light">

View File

@ -109,7 +109,7 @@ module.exports = class FormList extends React.Component {
{this.state.enabled_forms.length === 0 &&
this.state.disabled_forms.length === 0 &&
this.state.user.upgraded ? (
this.state.user.features.dashboard ? (
<h6 className="light">
You don't have any forms associated with this account, maybe you
should <a href="/account">verify your email</a>.

View File

@ -64,7 +64,7 @@
<div class="menu">
{% if request.path != '/' %}<span class="item"><a href="/">Home</a></span>{% endif %}
<span class="item"><a href="{{ url_for('dashboard') }}">Your forms</a></span>
<span class="item"><a href="{{ url_for('account') }}">{% if current_user.upgraded %}Your{% else %}Upgrade{% endif %} account</a></span>
<span class="item"><a href="{{ url_for('account') }}">{% if current_user.has_feature('dashboard') %}Your{% else %}Upgrade{% endif %} account</a></span>
{% else %}
<div class="greetings">
<h4 class="light">

View File

@ -43,7 +43,7 @@
<div class="card">
<h3>Plan</h3>
{% if current_user.upgraded %}
{% if current_user.has_feature('dashboard') %}
<p>You are a {{ config.SERVICE_NAME }} {{ config.UPGRADED_PLAN_NAME }} user.</p>
{% if sub.cancel_at_period_end %}
<p>You've cancelled your subscription and it is ending on {{ sub.current_period_end }}.</p>

View File

@ -11,7 +11,7 @@
<div class="card">
<h3>Plan</h3>
{% if current_user.upgraded %}
{% if current_user.has_feature('dashboard') %}
<p>You are a {{ config.SERVICE_NAME }} {{ config.UPGRADED_PLAN_NAME }} user.</p>
{% if sub.cancel_at_period_end %}
<p>You've cancelled your subscription and it is ending on {{ sub.current_period_end }}.</p>

View File

@ -18,7 +18,7 @@
<div class="row section grey">
<div class="container narrow block">
{% if current_user.upgraded %}
{% if current_user.has_feature('dashboard') %}
<div class="col-1-2">
<p>{{config.SERVICE_NAME}} is a tool <a href="https://github.com/formspree/formspree">managed on GitHub</a>. For fastest support check out our <a href="https://help.formspree.io">knowledge base</a> or use the form on the right.</p>
</div>

View File

@ -1,6 +1,7 @@
import hmac
import hashlib
from datetime import datetime
from flask import url_for, render_template, render_template_string, g
from formspree import settings
@ -8,13 +9,30 @@ from formspree.stuff import DB, TEMPLATES
from formspree.utils import send_email, IS_VALID_EMAIL
from .helpers import hash_pwd
class Plan(DB.Enum):
free = 'v1_free'
gold = 'v1_gold'
platinum = 'v1_platinum'
plan_features = {
'v1_free': set(),
'v1_gold': {'dashboard', 'unlimited'},
'v1_platinum': {'dashboard', 'unlimited', 'whitelabel'}
}
@classmethod
def has_feature(cls, plan, feature_id):
return feature_id in cls.plan_features[plan]
class User(DB.Model):
__tablename__ = 'users'
id = DB.Column(DB.Integer, primary_key=True)
email = DB.Column(DB.Text, unique=True, index=True)
password = DB.Column(DB.String(100))
upgraded = DB.Column(DB.Boolean)
plan = DB.Column(DB.Enum(*Plan.plan_features.keys(), name='plans'), nullable=False)
stripe_id = DB.Column(DB.String(50))
registered_on = DB.Column(DB.DateTime)
invoice_address = DB.Column(DB.Text)
@ -40,7 +58,7 @@ class User(DB.Model):
self.email = email
self.password = hash_pwd(password)
self.upgraded = False
self.plan = Plan.free
self.registered_on = datetime.utcnow()
@property
@ -55,6 +73,13 @@ class User(DB.Model):
def is_anonymous(self):
return False
@property
def features(self):
return Plan.plan_features[self.plan]
def has_feature(self, feature_id):
return Plan.has_feature(self.plan, feature_id)
def get_id(self):
return self.id

View File

@ -10,7 +10,7 @@ from sqlalchemy.exc import IntegrityError
from formspree import settings
from formspree.stuff import DB, TEMPLATES
from formspree.utils import send_email
from .models import User, Email
from .models import User, Email, Plan
from .helpers import check_password, hash_pwd, send_downgrade_email, \
send_downgrade_reason_email
@ -231,7 +231,7 @@ def upgrade():
"error")
return redirect(url_for('dashboard'))
current_user.upgraded = True
current_user.plan = Plan.gold
DB.session.add(current_user)
DB.session.commit()
flash(u"Congratulations! You are now a {SERVICE_NAME} "
@ -278,7 +278,8 @@ def downgrade():
if reason:
send_downgrade_reason_email.delay(current_user.email, reason)
sub = sub.delete(at_period_end=True)
sub.cancel_at_period_end = True
sub.save()
flash(u"You were unregistered from the {SERVICE_NAME} "
"{UPGRADED_PLAN_NAME} plan.".format(**settings.__dict__),
'success')
@ -310,7 +311,7 @@ def stripe_webhook():
customer = stripe.Customer.retrieve(customer_id)
if len(customer.subscriptions.data) == 0:
user = User.query.filter_by(stripe_id=customer_id).first()
user.upgraded = False
user.plan = Plan.free
DB.session.add(user)
DB.session.commit()
g.log.info('Downgraded user from webhook.', account=user.email)
@ -320,8 +321,10 @@ def stripe_webhook():
customer = stripe.Customer.retrieve(customer_id)
g.log.info('User payment failed', account=customer.email)
send_email(to=customer.email,
subject='[ACTION REQUIRED] Failed Payment for {} {}'.format(settings.SERVICE_NAME,
settings.UPGRADED_PLAN_NAME),
subject='[ACTION REQUIRED] Failed Payment for {} {}'.format(
settings.SERVICE_NAME,
settings.UPGRADED_PLAN_NAME
),
text=render_template('email/payment-failed.txt'),
html=render_template_string(TEMPLATES.get('payment-failed.html')),
sender=settings.DEFAULT_SENDER)
@ -433,6 +436,7 @@ def account():
sub.current_period_end = datetime.datetime.fromtimestamp(sub.current_period_end).strftime('%A, %B %d, %Y')
except stripe.error.StripeError:
return render_template('error.html', title='Unable to connect', text="We're unable to make a secure connection to verify your account details. Please try again in a little bit. If this problem persists, please contact <strong>%s</strong>" % settings.CONTACT_EMAIL)
return render_template('users/account.html', emails=emails, cards=cards, sub=sub)
@login_required

View File

@ -3,7 +3,7 @@ import json
from formspree import settings
from formspree.stuff import DB
from formspree.forms.helpers import HASH
from formspree.users.models import User
from formspree.users.models import User, Plan
from formspree.forms.models import Form, Submission
def test_automatically_created_forms(client, msend):
@ -147,7 +147,7 @@ def test_automatically_created_forms(client, msend):
assert newest.data['name'] == 'husserl'
assert last.data['name'] == 'schelling'
def test_upgraded_user_access(client, msend):
def test_gold_user_access(client, msend):
# register user
r = client.post('/register',
data={'email': 'colorado@springs.com',
@ -156,7 +156,7 @@ def test_upgraded_user_access(client, msend):
# upgrade user manually
user = User.query.filter_by(email='colorado@springs.com').first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -209,7 +209,7 @@ def test_upgraded_user_access(client, msend):
assert '"hi in my name is bruce!"', lines[1]
# test submissions endpoint with the user downgraded
user.upgraded = False
user.plan = Plan.free
DB.session.add(user)
DB.session.commit()
r = client.get("/api-int/forms/" + form_endpoint)

View File

@ -2,7 +2,7 @@ import json
from formspree import settings
from formspree.stuff import DB
from formspree.users.models import User, Email
from formspree.users.models import User, Email, Plan
from formspree.forms.models import Form
from .conftest import parse_confirmation_link_sent
@ -61,7 +61,7 @@ def test_user_gets_previous_forms_assigned_to_him(client, msend):
link, qs = parse_confirmation_link_sent(msend.call_args[1]['text'])
client.get(link, query_string=qs)
# confirm that the user has no access to the form since he is not upgraded
# confirm that the user has no access to the form since he is not gold
r = client.get(
"/api-int/forms",
headers={"Accept": "application/json", "Referer": settings.SERVICE_URL},
@ -71,7 +71,7 @@ def test_user_gets_previous_forms_assigned_to_him(client, msend):
# upgrade user
user = User.query.filter_by(email=u'márkö@example.com').first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()

View File

@ -3,7 +3,7 @@ import json
from formspree import settings
from formspree.stuff import DB
from formspree.forms.helpers import HASH
from formspree.users.models import User, Email
from formspree.users.models import User, Email, Plan
from formspree.forms.models import Form, Submission
def test_form_creation(client, msend):
@ -27,7 +27,7 @@ def test_form_creation(client, msend):
# upgrade user manually
user = User.query.filter_by(email='colorado@springs.com').first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -67,7 +67,7 @@ def test_form_creation(client, msend):
# Make sure that it marks the first form as AJAX
assert Form.query.first().uses_ajax
# send 5 forms (monthly limits should not apply to the upgraded user)
# send 5 forms (monthly limits should not apply to the gold user)
assert settings.MONTHLY_SUBMISSIONS_LIMIT == 2
for i in range(5):
r = client.post(
@ -98,7 +98,7 @@ def test_form_creation_with_a_registered_email(client, msend):
)
# upgrade user manually
user = User.query.filter_by(email="user@testsite.com").first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -180,7 +180,7 @@ def test_sitewide_forms(client, msend, mocker):
)
# upgrade user manually
user = User.query.filter_by(email="user@testsite.com").first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -330,7 +330,7 @@ def test_form_settings(client, msend):
# register and upgrade user
client.post("/register", data={"email": "texas@springs.com", "password": "water"})
user = User.query.filter_by(email="texas@springs.com").first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()

View File

@ -1,7 +1,7 @@
from formspree import settings
from formspree.stuff import DB
from formspree.forms.models import Form
from formspree.users.models import User, Email
from formspree.users.models import User, Email, Plan
http_headers = {
'Referer': 'testwebsite.com'
@ -233,13 +233,13 @@ def test_monthly_limits(client, msend):
assert f.counter == 3
assert f.get_monthly_counter() == 3 # the counters mark 4
# the user pays and becomes upgraded
# the user pays and becomes gold
r = client.post('/register',
data={'email': 'luke@testwebsite.com',
'password': 'banana'}
)
user = User.query.filter_by(email='luke@testwebsite.com').first()
user.upgraded = True
user.plan = Plan.gold
user.emails = [Email(address='luke@testwebsite.com')]
DB.session.add(user)
DB.session.commit()

View File

@ -5,7 +5,7 @@ import stripe
from formspree import settings
from formspree.stuff import DB
from formspree.forms.helpers import HASH
from formspree.users.models import User, Email
from formspree.users.models import User, Email, Plan
from formspree.forms.models import Form, Submission
from .conftest import parse_confirmation_link_sent
@ -123,7 +123,7 @@ def test_form_creation(client, msend):
# upgrade user manually
user = User.query.filter_by(email='colorado@springs.com').first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -156,7 +156,7 @@ def test_form_creation(client, msend):
client.get('/confirm/%s:%s' % (HASH(form.email, str(form.id)), form.hashid))
assert Form.query.first().confirmed
# send 5 forms (monthly limits should not apply to the upgraded user)
# send 5 forms (monthly limits should not apply to the gold user)
assert settings.MONTHLY_SUBMISSIONS_LIMIT == 2
for i in range(5):
r = client.post('/' + form_endpoint,
@ -192,7 +192,7 @@ def test_form_toggle(client, msend):
# upgrade user
user = User.query.filter_by(email='hello@world.com').first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -281,7 +281,7 @@ def test_form_and_submission_deletion(client, msend):
# upgrade user
user = User.query.filter_by(email='hello@world.com').first()
user.upgraded = True
user.plan = Plan.gold
DB.session.add(user)
DB.session.commit()
@ -399,7 +399,7 @@ def test_user_upgrade_and_downgrade(client, msend, mocker):
msend.reset_mock()
user = User.query.filter_by(email='maria@example.com').first()
assert user.upgraded == False
assert user.plan == Plan.free
# subscribe with card through stripe
token = stripe.Token.create(card={
@ -414,7 +414,7 @@ def test_user_upgrade_and_downgrade(client, msend, mocker):
})
user = User.query.filter_by(email='maria@example.com').first()
assert user.upgraded == True
assert user.plan == Plan.gold
# downgrade back to the free plan
r = client.post('/account/downgrade', follow_redirects=True)
@ -424,7 +424,7 @@ def test_user_upgrade_and_downgrade(client, msend, mocker):
assert "You've cancelled your subscription and it is ending on" in r.data.decode('utf-8')
user = User.query.filter_by(email='maria@example.com').first()
assert user.upgraded == True
assert user.plan == Plan.gold
customer = stripe.Customer.retrieve(user.stripe_id)
assert customer.subscriptions.data[0].cancel_at_period_end == True
@ -445,7 +445,7 @@ def test_user_upgrade_and_downgrade(client, msend, mocker):
}), headers={'Content-type': 'application/json'})
user = User.query.filter_by(email='maria@example.com').first()
assert user.upgraded == False
assert user.plan == Plan.free
assert m_senddowngraded.called
# delete the stripe customer
@ -465,7 +465,7 @@ def test_user_card_management(client, msend):
assert r.status_code == 302
user = User.query.filter_by(email='maria@example.com').first()
assert user.upgraded == False
assert user.plan == Plan.free
# subscribe with card through stripe
token = stripe.Token.create(card={
@ -479,7 +479,7 @@ def test_user_card_management(client, msend):
})
user = User.query.filter_by(email='maria@example.com').first()
assert user.upgraded == True
assert user.plan == Plan.gold
# add another card
token = stripe.Token.create(card={