diff --git a/ccvpn/common.py b/ccvpn/common.py index 34a980c..48b16f3 100644 --- a/ccvpn/common.py +++ b/ccvpn/common.py @@ -1,4 +1,6 @@ from django.conf import settings +from constance import config +from datetime import timedelta def get_client_ip(request): @@ -11,3 +13,24 @@ def get_client_ip(request): return value.split(',', 1)[0] return request.META.get('REMOTE_ADDR') + + +def get_price(): + return config.MONTHLY_PRICE_EUR + + +def get_price_float(): + return get_price() / 100 + + +def get_trial_period_duration(): + return config.TRIAL_PERIOD_HOURS * timedelta(hours=1) + + +def parse_integer_list(ls): + l = ls.split(',') + l = [p.strip() for p in l] + l = [p for p in l if p] + l = [int(p) for p in l] + return l + diff --git a/ccvpn/settings.py b/ccvpn/settings.py index a4a4e58..abf4dfe 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -13,7 +13,8 @@ https://docs.djangoproject.com/en/1.8/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -from datetime import timedelta +from django.core.validators import RegexValidator + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -43,6 +44,8 @@ INSTALLED_APPS = ( 'lambdainst', 'payments', 'tickets', + 'constance', + 'constance.backends.database', ) MIDDLEWARE_CLASSES = ( @@ -229,10 +232,24 @@ PAYMENTS_BACKENDS = { } PAYMENTS_CURRENCY = ('eur', '€') -PAYMENTS_MONTHLY_PRICE = 300 # in currency*100 -TRIAL_PERIOD = timedelta(hours=2) # Time added on each trial awarded -TRIAL_PERIOD_LIMIT = 2 # 2 * 2h = 4h, still has to push the button twice -NOTIFY_DAYS_BEFORE = (3, 1) # notify twice: 3 and 1 days before expiration + +CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' +CONSTANCE_CONFIG = { + 'MOTD': ('', "Public site message, displayed on homepage"), + 'MOTD_USER': ('', "Message for users, displayed on account home"), + 'MONTHLY_PRICE_EUR': (300, "Base subscription price per month (x0.01€)"), + 'BTC_EUR_VALUE': (300000, "Current value of a bitcoin (x0.01€/btc)"), + 'TRIAL_PERIOD_HOURS': (2, "Hours given for each trial period"), + 'TRIAL_PERIOD_MAX': (84, "Maximum number of trial periods to give (84*2h=1w)"), + 'NOTIFY_DAYS_BEFORE': ("3, 1", "When to send account expiration notifications. In number of days before, separated y commas", + 'integer_list'), +} + +CONSTANCE_ADDITIONAL_FIELDS = { + 'integer_list': ['django.forms.fields.CharField', { + 'validators': [RegexValidator(r'^([0-9]+[ ,]+)*([0-9]+)?$')], + }], +} # Local settings diff --git a/ccvpn/views.py b/ccvpn/views.py index ff969bc..e847b1f 100644 --- a/ccvpn/views.py +++ b/ccvpn/views.py @@ -10,14 +10,17 @@ from django.utils.http import is_safe_url from django.utils.translation import ( LANGUAGE_SESSION_KEY, check_for_language, ) +from constance import config + +from .common import get_price_float md = markdown.Markdown(extensions=['toc', 'meta', 'codehilite(noclasses=True)']) def index(request): - eur = '%.2f' % (settings.PAYMENTS_MONTHLY_PRICE / 100) - return render(request, 'ccvpn/index.html', dict(eur_price=eur)) + eur = '%.2f' % get_price_float() + return render(request, 'ccvpn/index.html', dict(eur_price=eur, motd=config.MOTD)) def chat(request): diff --git a/lambdainst/management/commands/expire_notify.py b/lambdainst/management/commands/expire_notify.py index 8c41880..cf52462 100644 --- a/lambdainst/management/commands/expire_notify.py +++ b/lambdainst/management/commands/expire_notify.py @@ -7,15 +7,14 @@ from django.conf import settings from django.utils import timezone from django.template.loader import get_template from django.core.mail import send_mass_mail +from constance import config as site_config +from ccvpn.common import parse_integer_list from lambdainst.models import VPNUser ROOT_URL = settings.ROOT_URL SITE_NAME = settings.TICKETS_SITE_NAME -NOTIFY_DAYS_BEFORE = settings.NOTIFY_DAYS_BEFORE -assert isinstance(NOTIFY_DAYS_BEFORE, (list, tuple, set)) - def get_next_expirations(days=3): """ Gets users whose subscription will expire in some days """ @@ -40,7 +39,7 @@ class Command(BaseCommand): def handle(self, *args, **options): from_email = settings.DEFAULT_FROM_EMAIL - for v in NOTIFY_DAYS_BEFORE: + for v in parse_integer_list(site_config.NOTIFY_DAYS_BEFORE): emails = [] qs = get_next_expirations(v) users = list(qs) diff --git a/lambdainst/models.py b/lambdainst/models.py index 2badd17..a1b2fd1 100644 --- a/lambdainst/models.py +++ b/lambdainst/models.py @@ -4,16 +4,14 @@ from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext as _ from django.utils import timezone -from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver -from . import core +from constance import config as site_config +from . import core +from ccvpn.common import get_trial_period_duration from payments.models import Subscription -assert isinstance(settings.TRIAL_PERIOD, timedelta) -assert isinstance(settings.TRIAL_PERIOD_LIMIT, int) - prng = random.SystemRandom() @@ -66,12 +64,12 @@ class VPNUser(models.Model): core.update_user_expiration(self.user) def give_trial_period(self): - self.add_paid_time(settings.TRIAL_PERIOD) + self.add_paid_time(get_trial_period_duration()) self.trial_periods_given += 1 @property def can_have_trial(self): - if self.trial_periods_given >= settings.TRIAL_PERIOD_LIMIT: + if self.trial_periods_given >= site_config.TRIAL_PERIOD_MAX: return False if self.user.payment_set.filter(status='confirmed').count() > 0: return False @@ -79,7 +77,7 @@ class VPNUser(models.Model): @property def remaining_trial_periods(self): - return settings.TRIAL_PERIOD_LIMIT - self.trial_periods_given + return site_config.TRIAL_PERIOD_MAX - self.trial_periods_given def on_payment_confirmed(self, payment): if self.referrer and not self.referrer_used: diff --git a/lambdainst/tests.py b/lambdainst/tests.py index f26d530..4b5f0b2 100644 --- a/lambdainst/tests.py +++ b/lambdainst/tests.py @@ -4,6 +4,8 @@ from django.utils import timezone from django.core.management import call_command from django.core import mail from django.utils.six import StringIO +from constance import config as site_config +from constance.test import override_config from .forms import SignupForm from .models import VPNUser, User, random_gift_code, GiftCode, GiftCodeUser @@ -80,7 +82,7 @@ class UserModelTest(TestCase, UserTestMixin): u = User.objects.get(username='aaa') vu = u.vpnuser - with self.settings(TRIAL_PERIOD=p, TRIAL_PERIOD_LIMIT=2): + with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): self.assertEqual(vu.remaining_trial_periods, 2) self.assertTrue(vu.can_have_trial) vu.give_trial_period() @@ -103,7 +105,7 @@ class UserModelTest(TestCase, UserTestMixin): vu = u.vpnuser - with self.settings(TRIAL_PERIOD=p, TRIAL_PERIOD_LIMIT=2): + with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): self.assertEqual(vu.remaining_trial_periods, 2) self.assertFalse(vu.can_have_trial) @@ -186,25 +188,27 @@ class AccountViewsTest(TestCase, UserTestMixin): def test_trial(self): p = timedelta(days=1) - with self.settings(RECAPTCHA_API='TEST', TRIAL_PERIOD=p): - good_data = {'g-recaptcha-response': 'TEST-TOKEN'} + with self.settings(RECAPTCHA_API='TEST'): + with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): + good_data = {'g-recaptcha-response': 'TEST-TOKEN'} - response = self.client.post('/account/trial', good_data) - self.assertRedirects(response, '/account/') + response = self.client.post('/account/trial', good_data) + self.assertRedirects(response, '/account/') - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, p) + user = User.objects.get(username='test') + self.assertRemaining(user.vpnuser, p) def test_trial_fail(self): p = timedelta(days=1) - with self.settings(RECAPTCHA_API='TEST', TRIAL_PERIOD=p): - bad_data = {'g-recaptcha-response': 'TOTALLY-NOT-TEST-TOKEN'} + with self.settings(RECAPTCHA_API='TEST'): + with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): + bad_data = {'g-recaptcha-response': 'TOTALLY-NOT-TEST-TOKEN'} - response = self.client.post('/account/trial', bad_data) - self.assertRedirects(response, '/account/') + response = self.client.post('/account/trial', bad_data) + self.assertRedirects(response, '/account/') - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, timedelta()) + user = User.objects.get(username='test') + self.assertRemaining(user.vpnuser, timedelta()) def test_settings_form(self): response = self.client.get('/account/settings') diff --git a/lambdainst/views.py b/lambdainst/views.py index 0252cd2..5a4a01c 100644 --- a/lambdainst/views.py +++ b/lambdainst/views.py @@ -25,9 +25,10 @@ from django.db.models import Count from django.contrib import auth from django.contrib.auth.models import User from django_countries import countries +from constance import config as site_config import lcoreapi -from ccvpn.common import get_client_ip +from ccvpn.common import get_client_ip, get_price_float from payments.models import ACTIVE_BACKENDS from .forms import SignupForm, ReqEmailForm from .models import GiftCode, VPNUser @@ -164,7 +165,7 @@ def index(request): 3 an arbitrary number of months """ def __getitem__(self, months): - n = int(months) * project_settings.PAYMENTS_MONTHLY_PRICE / 100 + n = int(months) * get_price_float() c = project_settings.PAYMENTS_CURRENCY[1] return '%.2f %s' % (n, c) @@ -180,6 +181,7 @@ def index(request): default_backend='paypal', recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY, price=price_fn(), + user_motd=site_config.MOTD_USER, ) return render(request, 'lambdainst/account.html', context) diff --git a/payments/management/commands/update_stripe_plans.py b/payments/management/commands/update_stripe_plans.py index ccade31..db46842 100644 --- a/payments/management/commands/update_stripe_plans.py +++ b/payments/management/commands/update_stripe_plans.py @@ -1,10 +1,10 @@ from django.core.management.base import BaseCommand, CommandError from django.conf import settings +from ccvpn.common import get_price from payments.models import ACTIVE_BACKENDS, SUBSCR_PERIOD_CHOICES, period_months CURRENCY_CODE, CURRENCY_NAME = settings.PAYMENTS_CURRENCY -MONTHLY_PRICE = settings.PAYMENTS_MONTHLY_PRICE class Command(BaseCommand): @@ -26,11 +26,11 @@ class Command(BaseCommand): for period_id, period_name in SUBSCR_PERIOD_CHOICES: plan_id = backend.get_plan_id(period_id) months = period_months(period_id) - amount = months * MONTHLY_PRICE + amount = months * get_price() kwargs = dict( id=plan_id, - amount=months * MONTHLY_PRICE, + amount=amount, interval='month', interval_count=months, name=backend.name + " (%s)" % period_id, diff --git a/payments/models.py b/payments/models.py index 93879ff..52bfbac 100644 --- a/payments/models.py +++ b/payments/models.py @@ -4,13 +4,13 @@ from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField from datetime import timedelta +from ccvpn.common import get_price from .backends import BackendBase -backend_settings = settings.PAYMENTS_BACKENDS -assert isinstance(backend_settings, dict) +backends_settings = settings.PAYMENTS_BACKENDS +assert isinstance(backends_settings, dict) CURRENCY_CODE, CURRENCY_NAME = settings.PAYMENTS_CURRENCY -MONTHLY_PRICE = settings.PAYMENTS_MONTHLY_PRICE STATUS_CHOICES = ( ('new', _("Waiting for payment")), @@ -51,12 +51,17 @@ for cls in BackendBase.__subclasses__(): name = cls.backend_id assert isinstance(name, str) - if name not in backend_settings: + if name not in backends_settings: continue - obj = cls(backend_settings.get(name, {})) + backend_settings = backends_settings.get(name, {}) + for k, v in backend_settings.items(): + if hasattr(v, '__call__'): + backend_settings[k] = v() + + obj = cls(backend_settings) if not obj.backend_enabled: - if name in backend_settings: + if name in backends_settings: raise Exception("Invalid settings for payment backend %r" % name) BACKENDS[name] = obj @@ -131,7 +136,7 @@ class Payment(models.Model): backend_id=backend_id, status='new', time=timedelta(days=30 * months), - amount=MONTHLY_PRICE * months + amount=get_price() * months ) return payment @@ -161,7 +166,7 @@ class Subscription(models.Model): @property def period_amount(self): - return self.months * MONTHLY_PRICE + return self.months * get_price() @property def next_renew(self): @@ -172,7 +177,7 @@ class Subscription(models.Model): @property def monthly_amount(self): - return MONTHLY_PRICE + return get_price() def create_payment(self): payment = Payment( @@ -180,7 +185,7 @@ class Subscription(models.Model): backend_id=self.backend_id, status='new', time=timedelta(days=30 * self.months), - amount=MONTHLY_PRICE * self.months, + amount=get_price() * self.months, subscription=self, ) return payment diff --git a/payments/views.py b/payments/views.py index 2cebd74..6293480 100644 --- a/payments/views.py +++ b/payments/views.py @@ -13,9 +13,6 @@ from .forms import NewPaymentForm from .models import Payment, Subscription, BACKENDS, ACTIVE_BACKENDS -monthly_price = settings.PAYMENTS_MONTHLY_PRICE - - @login_required def new(request): if request.method != 'POST': diff --git a/requirements.txt b/requirements.txt index bcfae59..2638357 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ django django-jsonfield +django-constance[database] django_countries markdown requests diff --git a/static/css/style.css b/static/css/style.css index 1cb9ea2..51c40a5 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -235,6 +235,9 @@ footer p { margin-top: 0.6em; margin-bottom: 0.5em; } margin: 0; color: white; } +.message.motd p { + color: black; +} .message p.info, .message p.success { background-color: #062D4D; } @@ -492,6 +495,15 @@ a.home-signup-button { padding: 0.6em 2em; margin: 2em 0 0 0; } +.account-motd { + background: #E6F5FF; + border-radius: 4px; + border: 1px solid #72B6ED; + box-shadow: 1px 1px 3px #aaa; + padding: 0.3em 2em; + text-align: center; + margin: 2em 0 0 0; +} .account-payment-box form label, .account-giftcode-box form label { width: 8em; diff --git a/templates/lambdainst/account.html b/templates/lambdainst/account.html index ebef4f9..83eaeac 100644 --- a/templates/lambdainst/account.html +++ b/templates/lambdainst/account.html @@ -7,6 +7,13 @@ {% block account_content %}
+ + {% if user_motd %} +
+

{{ user_motd | safe }}

+
+ {% endif %} +

{% trans 'Account' %} : {{user.username}}

diff --git a/templates/layout.html b/templates/layout.html index 9110bcc..d34e2c3 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -65,6 +65,11 @@ {% block wrap %} + {% if motd %} +
+

{{ motd | safe }}

+
+ {% endif %} {% for message in messages %}

{{ message }}