Selaa lähdekoodia

Merge pull request #16 from simple-login/lifetime

Lifetime coupon
Son Nguyen Kim 5 vuotta sitten
vanhempi
commit
b8ca2d0158

+ 20 - 0
app/dashboard/templates/dashboard/pricing.html

@@ -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>
 

+ 4 - 4
app/dashboard/views/billing.py

@@ -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)

+ 37 - 0
app/dashboard/views/pricing.py

@@ -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,
     )
 
 

+ 22 - 6
app/models.py

@@ -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)

+ 39 - 0
migrations/versions/2020_010120_d29cca963221_.py

@@ -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 ###

+ 1 - 1
requirements.txt

@@ -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

+ 4 - 0
server.py

@@ -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,

+ 1 - 1
templates/header.html

@@ -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>