Commit 7b387e0b authored by David Seaward's avatar David Seaward

Merge fixes from production.

Signed-off-by: David Seaward's avatarDavid Seaward <david.seaward@puri.sm>
parents 33e57f53 00d8495f
Pipeline #54245 failed with stages
in 1 minute and 30 seconds
......@@ -65,7 +65,7 @@ nosetests.xml
coverage.xml
*,cover
.hypothesis/
test_database
# Translations
*.mo
*.pot
......
image: pureos/amber:latest
stages:
- deb
- test
- deb
test:
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
key: liberty-cache-key
paths:
- .cache/pip
- venv/
tags:
- libremone
stage: test
script:
# this configures Django application to use attached postgres database that is run on `postgres` host
- export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app
- apt-get update -qy
- apt-get install -y python3-dev python-pip pipenv libldap2-dev libpq-dev libsasl2-dev
- pipenv install --verbose --three --dev --skip-lock
- pipenv run ./run_tests
- coverage xml
- flake8 .
coverage: "/TOTAL.+ ([0-9]{1,3}%)/"
artifacts:
paths:
- htmlcov
- docs/_build
deb:
tags:
- liberty
stage: deb
script:
- rbenv global 2.4.0
- rbenv exec gem install fpm
- pip3 install --user setuptools
- make clean
- make debsource
artifacts:
name: "ldh-middleware.deb"
paths:
- ./*.deb
expire_in: 26 weeks
tags:
- libremone
stage: deb
script:
- rbenv global 2.4.0
- rbenv exec gem install fpm
- pip3 install --user setuptools
- make clean
- make debsource
artifacts:
name: "ldh-middleware.deb"
paths:
- ./*.deb
expire_in: 26 weeks
......@@ -28,12 +28,16 @@ django-simple-captcha = "==0.5.10"
djangorestframework = "==3.9.2"
django-password-reset = "==2.0"
psycopg2-binary = "==2.8.2"
matplotlib = "==3.1.2"
[dev-packages]
twine = "==1.12.1"
pylint = "*"
django_extensions = "*"
pydotplus = "*"
flake8 = "*"
volatildap = "*"
httmock = "*"
[requires]
python_version = "3"
......@@ -125,6 +125,21 @@ unzip ldh-middleware_master.zip
dpkg -c ldh-middleware_0.0.3_amd64.deb
```
Runing tests
------------
The testing libraries require access to the files provided by the `slapd` package.
```bash
apt install slapd
```
You might want to disable the sldap service if you run the tests on your local development machine:
```bash
sudo systemctl stop slapd.service && systemctl status slapd.service
```
Sharing and contributions
-------------------------
......
......@@ -10,7 +10,7 @@ class ChooseBundleForm(ConfirmAddressForm):
super(ChooseBundleForm, self).__init__(*args, **kwargs)
self.fields['bundle'] = forms.ChoiceField(
label=_('You can change to the following bundles'),
widget=forms.RadioSelect,
widget=forms.RadioSelect(attrs={'class': 'no-bullets'}),
choices=choices
)
self.fields['address'].label = _(
......
......@@ -3,7 +3,7 @@ from django.conf import settings
from django.utils import timezone
from limitmonitor.models import get_available_bundles
class BundleChange(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
from_bdl = models.CharField(max_length=100)
......
......@@ -80,12 +80,12 @@ class CartRegistrationForm(RegistrationForm):
full_bundle = forms.ChoiceField(
label=_('Bundle type'),
widget=forms.RadioSelect(attrs={'onclick': 'update_desc()'}),
widget=forms.RadioSelect(attrs={'onclick': 'update_desc()', 'class': 'no-bullets'}),
choices=list(zip(bundles, bundles))
)
full_period = forms.ChoiceField(
label=_('Billing period'),
widget=forms.RadioSelect(attrs={'onclick': 'update_desc()'}),
widget=forms.RadioSelect(attrs={'onclick': 'update_desc()', 'class': 'no-bullets'}),
choices=list(zip(periods, periods))
)
hidden_bundle = forms.CharField(
......
import json
import logging
from django.conf import settings
from django.contrib.auth import logout
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.debug import sensitive_post_parameters
from registration.backends.simple.views import RegistrationView
from cart.models import ChosenReward
from purist.models import AccountType
from .forms import CartRegistrationFormWithCaptcha, CartRegistrationForm
from limitmonitor.common import forced_update
from limitmonitor.subscription import SubscriptionManager, SubscriptionStatus
import json
import logging
from purist.models import AccountType
from .forms import CartRegistrationFormWithCaptcha, CartRegistrationForm
log = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
class CartRegistrationView(RegistrationView):
......@@ -80,6 +82,7 @@ class CartRegistrationView(RegistrationView):
if self.bundle == 'Basic':
user.account_type = AccountType.BASIC
user.quota = settings.STORAGE_QUOTA_BASIC
try:
new_sub = SubscriptionManager.create_subscription(
......@@ -95,13 +98,15 @@ class CartRegistrationView(RegistrationView):
except Exception as e:
message = "Failed to create subscription for basic user {}.".format(user)
logging.exception(message)
logger.exception(message, e)
elif self.bundle == 'Complete':
user.account_type = AccountType.COMPLETE
user.quota = settings.STORAGE_QUOTA_OTHERS
if self.bundle == 'Family':
user.account_type = AccountType.GROUP
user.quota = settings.STORAGE_QUOTA_OTHERS
user.save()
reward.save()
......
......@@ -42,7 +42,8 @@ WOO_PARENT_PROD_ID = 34
WOO_UPGRADE_PAY_URL = http://wp.example.com/checkout/order-pay/{}/?pay_for_order=true&key={}
WOO_BUNDLES_INFO_FILE = /etc/opt/purist/middleware/upgrades_downgrades.yml
WOO_REG_OPTIONS_INFO_FILE = /etc/opt/purist/middleware/registration_options.yml
WOO_STORAGE_PAYG_PROD_ID = 5658
WOO_STORAGE_PAYG_PROD_URL = https://wp.example.com/shop/pay-as-you-go-storage/
TUNNEL_HOST=https://example.com
PASSWORD_RESET_TOKEN_EXPIRES=1800
......@@ -53,7 +54,7 @@ PASSWORD_RESET_TOKEN_EXPIRES=1800
# EMAIL_USE_TLS = False
# Email configuration sample for production/staging
# DEFAULT_FROM_EMAIL = "Do not reply <noreply@example.com>"
DEFAULT_FROM_EMAIL = admin@example.com
SERVER_EMAIL = noreply@example.com
EMAIL_HOST = smtp.example.com
......@@ -61,7 +62,6 @@ EMAIL_HOST_USER = jhon.doe@example.com
EMAIL_PORT = 465
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
DEFAULT_FROM_EMAIL = "Do not reply <noreply@example.com>"
ADMINS_EMAIL = The Admins, theadmins@example.com; The Ops, theops@example.com
# This is the link to the subscribe page, used in different parts of the middleware
......
......@@ -39,3 +39,11 @@
ids:
- 62887
- 54393
-
name: PLUS
services:
- Chat
- Social
- Mail
ids:
- 3331
......@@ -76,3 +76,43 @@
- 12350
- 12347
- 12351
12360:
title: Example Invitee (monthly)
short_title: Invitee (monthly)
type: INVITED
period: monthly
domain: example.com
upgrades:
downgrades:
12361:
title: Example Invitee (annual)
short_title: Invitee (annual)
type: INVITED
period: annual
domain: example.com
upgrades:
downgrades:
12370:
title: Pay As You Go Storage (monthly)
short_title: PAYG Storage (monthly)
type: PAYGS
period: monthly
domain: example.com
upgrades:
downgrades:
12371:
title: Librem One Plus (monthly)
short_title: Plus (monthly)
type: PLUS
period: monthly
domain: example.com
upgrades:
- 12351
- 12347
- 12348
- 12349
downgrades:
import logging
import matplotlib.dates as mdates
import base64
from matplotlib.figure import Figure
from io import BytesIO
from decimal import Decimal
from django.conf import settings
from django.utils import timezone
from django.contrib.auth.hashers import make_password
......@@ -84,6 +89,13 @@ def forced_update(user):
srv_to_enable | srv_to_reenable,
sub.next_payment_date)
user.account_type = sub.account_type
if (
sub.account_type == AccountType.BASIC
or sub.account_type == AccountType.PLUS
):
user.quota = settings.STORAGE_QUOTA_BASIC
else:
user.quota = settings.STORAGE_QUOTA_OTHERS
user.save()
if hasattr(user, 'chosenreward'):
user.chosenreward.is_pending = False
......@@ -242,13 +254,14 @@ def can_change_bundle(user):
return len(choices) > 0
def active_subscription(user):
def active_subscription(user, product_id=settings.WOO_PARENT_PROD_ID):
"""Returns currently active subscription for the user or None if the
user does not have an active subscription.
user does not have an active subscription. Passing product_id you
can specify to search for subscriptions with that specific product.
"""
subs = SubscriptionManager.search_subscriptions(
user=user, status=SubscriptionStatus.ACTIVE
user=user, status=SubscriptionStatus.ACTIVE, product_id=product_id
)
if len(subs) >= 1:
return subs[0]
......@@ -277,3 +290,159 @@ def invitees_clean(user):
):
return False
return True
def calculate_rma(user):
limits = user.limit_set.filter(is_active=True).filter(service__in=settings.MEASURED_SERVICES)
sum_total = Decimal(0)
for l in limits:
dates, sizes = maximums_per_day(l)
sum_total = sum(sizes, sum_total)
return round(sum_total/30, 2)
def current_used_storage(user):
now = timezone.now()
limits = user.limit_set.filter(is_active=True).filter(service__in=settings.MEASURED_SERVICES)
sum = Decimal(0)
for l in limits:
lm = l.limitmeasure_set.filter(date__lte=now).order_by('date').last()
if lm:
sum += lm.size
return sum
def maximums_per_day(limit, start_time=None, days=30):
"""Given a limit it returns two lists, one with dates and another one
with maximum_size for that date.
"""
if start_time is None:
# set start_time to a month ago at 00:00:00
start_time = (
timezone.now() - timezone.timedelta(days=30)
).replace(hour=0, minute=0, second=0, microsecond=0)
dates = []
sizes = []
prev_size = 0
for d in range(days):
end_time = start_time + timezone.timedelta(days=1)
dmax = limit.limitmeasure_set.filter(date__gte=start_time).filter(
date__lt=end_time).order_by('size').last()
if dmax:
# print('Max is:', start_time, end_time, dmax.date, dmax.size)
# input('Intro to continue.')
dates.append(dmax.date)
sizes.append(dmax.size)
prev_size = dmax.size
else:
dates.append(end_time)
sizes.append(prev_size)
start_time = end_time
return dates, sizes
def storage_used_chart(user):
"""Given a user returns a base64 encoded .svg data which is a
stackplotchart showing the storage used by this user's
services.
"""
limits = user.limit_set.filter(is_active=True).filter(service__in=settings.MEASURED_SERVICES)
if not limits.exists():
return None
serv_sizes = []
labels = []
for lm in limits:
dates, sizes = maximums_per_day(lm)
serv_sizes.append([float(x) for x in sizes])
labels.append(lm.service_label())
# Create matplotlib graph
fig = Figure()
ax = fig.subplots()
ax.stackplot(dates, serv_sizes)
locator = mdates.AutoDateLocator(minticks=4, maxticks=8)
formatter = mdates.ConciseDateFormatter(locator)
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)
ax.set_title('Storage used last month')
ax.set_ylabel('GiB')
ax.legend(labels=labels, loc='best')
# Save it to a temporary buffer.
buf = BytesIO()
fig.savefig(buf, format="svg")
# return the svg base64 encoded to embed ti in the html output.
return base64.b64encode(buf.getbuffer()).decode("ascii")
def storage_used_chart2(user):
"""Given a user returns a base64 encoded .svg data which is a chart
showing the storage used by this user's services."""
limits = user.limit_set.filter(is_active=True).filter(service__in=settings.MEASURED_SERVICES)
fig = Figure()
ax = fig.subplots()
labels = []
for lm in limits:
dates, sizes = maximums_per_day(lm)
ax.plot(dates, sizes)
labels.append(lm.service_label())
locator = mdates.AutoDateLocator(minticks=4, maxticks=8)
formatter = mdates.ConciseDateFormatter(locator)
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)
ax.set_title('Storage used last month')
ax.set_ylabel('GiB')
ax.legend(labels=labels, loc='best')
# Save it to a temporary buffer.
buf = BytesIO()
fig.savefig(buf, format="svg")
# return the svg base64 encoded to embed ti in the html output.
return base64.b64encode(buf.getbuffer()).decode("ascii")
def storage_paid(user, sub=None):
"""Returns True if the user is up to date with pay as you go payments
"""
if sub is None:
sub = active_subscription(user=user,
product_id=settings.WOO_STORAGE_PAYG_PROD_ID)
return sub is not None
else:
return sub.status == SubscriptionStatus.ACTIVE
def offends_storage_policy(user, sub=None):
"""Returns True if the user offends the storage policy. Pay as you go
users offend the storage policy when they are not up to date with
payments. Non payg users offend the policy when their RMA is above
quota.
"""
if user.payg:
return not storage_paid(user, sub)
else:
return user.rma > user.quota
def update_storage_subscription(user, sub=None):
if sub is None:
sub = active_subscription(user=user,
product_id=settings.WOO_STORAGE_PAYG_PROD_ID)
if user.payg and sub is not None:
if user.rma > user.quota:
cost = settings.STORAGE_COST * (user.rma - user.quota)
else:
cost = 0
sub.price = cost
sub.save()
user.storage_cost_cache = cost
user.save()
return sub
Hi {{ username }},
Your account has been permanently purged. We're sorry to see you go.
Thanks for using {{ brand }} services.
Hi {{ username }},
Your account has been locked, to unlock it reduce the amount of
storage used by it.
Locked accounts are permanently deleted after 30 days.
If you have any questions please contact support.
Hi {{ username }},
Your rolling month average (RMA) storage usage ({{ rma|round(2) }} GiB) is now below
your free qouta ({{ quota }}) GiB.
Your account will not be locked.
If you have any questions please contact support.
Hi {{ username }},
Your account will soon be locked!
Locked accounts are permanently purged after 30 days.
Please free up some storage or purchase a paid subscription to prevent
locking.
If you need further help please contact support.
Hi {{ username }},
Your rolling month average (RMA) storage usage is {{ rma|round(2) }} GiB which is
above your free qouta of {{ quota }} GiB.
Please reduce the storage used or purchase a paid subscription which
offers higher free quota plus Pay As You Go storage option.
If your RMA is still above quota in {{ days }} days, your account will be
locked. Locked accounts are permanently deleted after 30 days.
If you have any questions please contact support.
BASIC_account_deleted_email.txt
\ No newline at end of file
Hi {{ username }},
Your account has been locked, to unlock it reduce the amount of
storage used or enable Pay As You Go subscription from your profile's
storage page:
{{ url }}
Locked accounts are permanently deleted after 30 days.
If you have any questions please contact support.
BASIC_back_to_normal_email.txt
\ No newline at end of file
Hi {{ username }},
Your account will soon be locked!
Locked accounts are permanently purged after 30 days.
Please free up some storage or enable Pay As You Go subscription in
your profile's storage page to prevent locking.
If you need further help please contact support.
Hi {{ username }},
Your rolling month average (RMA) storage usage is {{ rma|round(2) }} GiB which is
above your free qouta of {{ quota }} GiB.
Please reduce the storage used or enable Pay As You Go subscription in
your profile's storage page.
If your RMA is still above quota in {{ days }} days, your account will be
locked. Locked accounts are permanently deleted after 30 days.
If you have any questions please contact support.
BASIC_account_deleted_email.txt
\ No newline at end of file
Hi {{ username }},
Your account has been locked, to unlock it pay the overdue Pay As You
Go subscription by using the link provided in your profile's storage
details page:
{{ url }}
Locked accounts are permanently deleted after 30 days.
If you have any questions please contact support.
Hi {{ username }},
You are now up to date with your Pay As You Go stoarge subscription.
If you have any questions please contact support.
Hi {{ username }},
Your account will soon be locked!
Locked accounts are permanently purged after 30 days.
Please pay the overdue Pay As You Go subscription to prevent
locking. You can pay using the link in your profile's storage details
page:
{{ url }}
If you need further help please contact support.
Hi {{ username }},
You have a pending payment of ${{ cost|round(2) }} for your Pay As You Go storage
subscription.
You can pay using the link in your profile's storage details page:
{{ url }}
If the payemnt is still pending in {{ days }} days your account will be
locked. Locked accounts are permanently deleted after 30 days.
If you have any questions please contact support.
......@@ -165,8 +165,23 @@ SPDX-License-Identifier: AGPL-3.0
{% if can_upgrade or pending_upgrade %}
<li><a href="{% url 'upgrade' %}">{% trans "Manage Subscription" %}</a></li>
{% endif %}
{% if beta_access %}
<li><a href="{% url 'storage' %}">{% trans "Storage" %}</a></li>
{% endif %}
</ul>
{% if beta_access %}
<h2>{% trans "Profile summary" %}</h2>
<ul>
<li>{% trans "Rolling Month Average (RMA): "%} {{ rma }} GiB</li>
<li>{% trans "Limit: "%} {{ request.user.quota }} GiB</li>
{% if request.user.payg %}
<li>{% trans "RMA cost: " %} ${{ request.user.storage_cost_cache }}</li>
{% endif %}
<li>{% trans "Status: " %} {% if locked %} {% trans "Locked" %} {% else %} {% trans "Active "%}{% endif %}</li>
</ul>
{% endif %}
</article>
<nav class="col-2">
<ul>
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2020-01-15 22:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('limitmonitor', '0008_delete_externalcredit'),
]
operations = [
migrations.CreateModel(
name='LimitMeasure',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(default=django.utils.timezone.now)),
('size', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
('limit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='limitmonitor.Limit')),
],
),
]
......@@ -190,6 +190,12 @@ class Limit(models.Model):
return self.is_active
class LimitMeasure(models.Model):
limit = models.ForeignKey(Limit, on_delete=models.CASCADE)
date = models.DateTimeField(default=timezone.now)
size = models.DecimalField(default=0, decimal_places=2, max_digits=6)
class ExternalBundle(models.Model):
"""Class used to represent external bundles that exist in external
entities like WooCommerce
......
from enum import Enum
import strictyaml
from django.conf import settings
from django.utils import timezone
from purist.models import AccountType, User
from middleware.common import get_woo_connection
from invitation.models import Invitation
import strictyaml
from middleware.common import get_woo_connection
from purist.models import AccountType, User
def get_available_bundles():
with open(settings.WOO_BUNDLES_INFO_FILE, 'r') as stream: