Merge pull request #16 from simple-login/lifetime

Lifetime coupon
This commit is contained in:
Son Nguyen Kim 2020-01-01 23:24:15 +01:00 committed by GitHub
commit b8ca2d0158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 128 additions and 12 deletions

View File

@ -52,6 +52,26 @@
Yearly <br>
$29.99/year
</button>
<hr class="my-6">
<form method="post">
{{ coupon_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<div class="font-weight-bold mb-2">Coupon</div>
<div class="small-text">
If you have a lifetime coupon, please paste it here. <br>
For information, we offer free premium account for education (student, professor or technical staff working at
an educational institute). <br>
Drop us an email at hi@simplelogin.io with your student ID or certificate to get the coupon.
</div>
{{ coupon_form.code(class="form-control", placeholder="Coupon") }}
{{ render_field_errors(coupon_form.code) }}
<button class="btn btn-lg btn-success mt-2">Apply</button>
</form>
</div>
</div>

View File

@ -8,10 +8,10 @@ from app.dashboard.base import dashboard_bp
@login_required
def billing():
# sanity check: make sure this page is only for user who has paddle subscription
if not current_user.is_premium():
flash("This page is for paid customer only", "warning")
return redirect(url_for("dashboard.index"))
sub = current_user.get_subscription()
if not sub:
flash("You don't have any active subscription", "warning")
return redirect(url_for("dashboard.index"))
return render_template("dashboard/billing.html", sub=sub)

View File

@ -1,13 +1,23 @@
from flask import render_template, flash, redirect, url_for
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from app.config import (
PADDLE_VENDOR_ID,
PADDLE_MONTHLY_PRODUCT_ID,
PADDLE_YEARLY_PRODUCT_ID,
URL,
ADMIN_EMAIL,
)
from app.dashboard.base import dashboard_bp
from app.email_utils import send_email
from app.extensions import db
from app.models import LifetimeCoupon
class CouponForm(FlaskForm):
code = StringField("Coupon Code", validators=[validators.DataRequired()])
@dashboard_bp.route("/pricing", methods=["GET", "POST"])
@ -18,12 +28,39 @@ def pricing():
flash("You are already a premium user", "warning")
return redirect(url_for("dashboard.index"))
coupon_form = CouponForm()
if coupon_form.validate_on_submit():
code = coupon_form.code.data
coupon = LifetimeCoupon.get_by(code=code)
if coupon and coupon.nb_used > 0:
coupon.nb_used -= 1
current_user.lifetime = True
db.session.commit()
# notify admin
send_email(
ADMIN_EMAIL,
subject=f"User {current_user.id} used lifetime coupon. Coupon nb_used: {coupon.nb_used}",
plaintext="",
html="",
)
flash("You are upgraded to lifetime premium!", "success")
return redirect(url_for("dashboard.index"))
else:
flash(f"Coupon *{code}* expired or invalid", "warning")
return render_template(
"dashboard/pricing.html",
PADDLE_VENDOR_ID=PADDLE_VENDOR_ID,
PADDLE_MONTHLY_PRODUCT_ID=PADDLE_MONTHLY_PRODUCT_ID,
PADDLE_YEARLY_PRODUCT_ID=PADDLE_YEARLY_PRODUCT_ID,
success_url=URL + "/dashboard/subscription_success",
coupon_form=coupon_form,
)

View File

@ -119,6 +119,9 @@ class User(db.Model, ModelMixin, UserMixin):
db.Boolean, nullable=False, default=False, server_default="0"
)
# some users could have lifetime premium
lifetime = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
profile_picture = db.relationship(File)
@classmethod
@ -143,13 +146,11 @@ class User(db.Model, ModelMixin, UserMixin):
def is_premium(self):
"""user is premium if they have a active subscription"""
if self.lifetime:
return True
sub: Subscription = self.get_subscription()
if sub:
if sub.cancelled:
# user is premium until the next billing_date + 1
return sub.next_bill_date >= arrow.now().shift(days=-1).date()
# subscription active, ie not cancelled
return True
return False
@ -217,8 +218,18 @@ class User(db.Model, ModelMixin, UserMixin):
return "Free Plan"
def get_subscription(self):
"""return *active* subscription
TODO: support user unsubscribe and re-subscribe
"""
sub = Subscription.get_by(user_id=self.id)
return sub
if sub and sub.cancelled:
# sub is active until the next billing_date + 1
if sub.next_bill_date >= arrow.now().shift(days=-1).date():
return sub
else: # past subscription, user is considered not having a subscription
return None
else:
return sub
def verified_custom_domains(self):
return CustomDomain.query.filter_by(user_id=self.id, verified=True).all()
@ -709,3 +720,8 @@ class CustomDomain(db.Model, ModelMixin):
def __repr__(self):
return f"<Custom Domain {self.domain}>"
class LifetimeCoupon(db.Model, ModelMixin):
code = db.Column(db.String(128), nullable=False, unique=True)
nb_used = db.Column(db.Integer, nullable=False)

View File

@ -0,0 +1,39 @@
"""empty message
Revision ID: d29cca963221
Revises: 01f808f15b2e
Create Date: 2020-01-01 20:01:51.861329
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd29cca963221'
down_revision = '01f808f15b2e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('lifetime_coupon',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
sa.Column('code', sa.String(length=128), nullable=False),
sa.Column('nb_used', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.add_column('users', sa.Column('lifetime', sa.Boolean(), server_default='0', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'lifetime')
op.drop_table('lifetime_coupon')
# ### end Alembic commands ###

View File

@ -87,7 +87,7 @@ s3transfer==0.2.1 # via boto3
sentry-sdk==0.13.5
simplejson==3.17.0 # via flask-profiler
six==1.12.0 # via bcrypt, cryptography, flask-cors, packaging, pip-tools, prompt-toolkit, pyopenssl, pytest, python-dateutil, sqlalchemy-utils, traitlets
sqlalchemy-utils==0.33.11
sqlalchemy-utils==0.36.1
sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils
strictyaml==1.0.2 # via yacron
traitlets==4.3.2 # via ipython

View File

@ -41,6 +41,7 @@ from app.models import (
PlanEnum,
ApiKey,
CustomDomain,
LifetimeCoupon,
)
from app.monitor.base import monitor_bp
from app.oauth.base import oauth_bp
@ -117,6 +118,9 @@ def fake_data():
)
db.session.commit()
LifetimeCoupon.create(code="coupon", nb_used=10)
db.session.commit()
# Create a subscription for user
Subscription.create(
user_id=user.id,

View File

@ -42,7 +42,7 @@
</a>
<div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow">
{% if current_user.is_premium() %}
{% if current_user.get_subscription() %}
<a class="dropdown-item" href="{{ url_for('dashboard.billing') }}">
<i class="dropdown-icon fe fe-dollar-sign"></i> Billing
</a>