@@ -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 | |||
@@ -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 | |||
@@ -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): | |||
@@ -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) | |||
@@ -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: | |||
@@ -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') | |||
@@ -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) | |||
@@ -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, | |||
@@ -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 | |||
@@ -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': | |||
@@ -1,5 +1,6 @@ | |||
django | |||
django-jsonfield | |||
django-constance[database] | |||
django_countries | |||
markdown | |||
requests | |||
@@ -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; | |||
@@ -7,6 +7,13 @@ | |||
{% block account_content %} | |||
<div> | |||
{% if user_motd %} | |||
<div class="account-motd"> | |||
<p> {{ user_motd | safe }} </p> | |||
</div> | |||
{% endif %} | |||
<h1>{% trans 'Account' %} : {{user.username}}</h1> | |||
<div class="account-status"> | |||
@@ -65,6 +65,11 @@ | |||
</header> | |||
{% block wrap %} | |||
{% if motd %} | |||
<div class="message motd"> | |||
<p>{{ motd | safe }}</p> | |||
</div> | |||
{% endif %} | |||
{% for message in messages %} | |||
<div class="message"> | |||
<p class="{{message.tags}}">{{ message }}</p> | |||