Merge branch 'master' into feature/6561/invoices-webhook

This commit is contained in:
PCoder 2019-12-25 11:13:41 +05:30
commit b9f9c7b00e
37 changed files with 855 additions and 104 deletions

3
.gitignore vendored
View file

@ -10,7 +10,7 @@ __pycache__/
.ropeproject/
#django
local_settings.py
Pipfile
media/
!media/keep
/CACHE/
@ -43,3 +43,4 @@ secret-key
# to keep empty dirs
!.gitkeep
*.orig
.vscode/settings.json

View file

@ -1,3 +1,35 @@
2.8.2: 2019-12-24
* Bugfix: [dcl calculator plugin] Set the POST action url explicitly
2.8.1: 2019-12-24
* [dcl cms navbar plugin]: Provide an option to show non transparent navar always
2.8: 2019-12-20
* ldap_migration: Migrate django users to Ldap
Notes for deployment:
```
1. Git Pull
2. Ensure the newly dependencies in requirements.txt are installed
3. Put new values in .env
4. Run migrations
5. Restart uwsgi
```
2.7.3: 2019-12-18
* Bugfix: Swiss VAT being wrongly added to non-EU customers
2.7.2: 2019-12-17
* Add vat rates for AD, TK and IS
* Improve billing address' string representation
Notes for deployment:
- Import the newly added vat rates into db
```
./manage.py import_vat_rates vat_rates.csv
```
2.7.1: 2019-12-14
* feature: Add management command to list active VM customers (MR!723)
2.7: 2019-12-9
* feature: EU VAT for new subscriptions (MR!721)
Notes for deployment:
- Add the following to .env file
- FIRST_VM_ID_AFTER_EU_VAT=<to VM_ID from which we begin EU VAT>
- PRE_EU_VAT_RATE=whatever the rate was before introduction of EU VAT (7.7 for example)
2.6.10: 2019-11-16
* translation: Add DE translations for features in 2.6.{8,9} by moep (MR!719)
2.6.9: 2019-11-15

View file

@ -10,13 +10,35 @@ Requirements
Install
=======
.. note::
lxml that is one of the dependency of dynamicweb couldn't
get build on Python 3.7 so, please use Python 3.5.
First install packages from requirements.archlinux.txt or
requirements.debian.txt based on your distribution.
The quick way:
``pip install -r requirements.txt``
Next find the dump.db file on stagging server. Path for the file is under the base application folder.
or you can create one for yourself by running the following commands on dynamicweb server
.. code:: sh
sudo su - postgres
pg_dump app > /tmp/postgres_db.bak
exit
cp /tmp/postgres_db.bak /root/postgres_db.bak
Now, you can download this using sftp.
Install the postgresql server and import the database::
``psql -d app < dump.db``
``psql -d app -U root < dump.db``
**No migration is needed after a clean install, and You are ready to start developing.**
@ -25,9 +47,9 @@ Development
Project is separated in master branch and development branch, and feature branches.
Master branch is currently used on `Digital Glarus <https://digitalglarus.ungleich.ch/en-us/digitalglarus/>`_ and `Ungleich blog <https://digitalglarus.ungleich.ch/en-us/blog/>`_.
If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch.
If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch.
After You have complited the task create a pull request and ask someone to review the code from other developers.
After You have completed the task, create a pull request and ask someone to review the code from other developers.
**Cheat sheet for branching and forking**:

View file

@ -184,6 +184,11 @@ class DCLNavbarPluginModel(CMSPlugin):
default=True,
help_text='Uncheck this if you do not want to show login/dashboard.'
)
show_non_transparent_navbar_always = models.BooleanField(
default=False,
help_text='Check this if you want to show non transparent navbar only.'
'(Useful when we want to setup a simple page)'
)
def get_logo_dark(self):
# used only if atleast one logo exists

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-15 17:33+0000\n"
"POT-Creation-Date: 2019-12-09 12:13+0000\n"
"PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -144,8 +144,9 @@ msgid ""
"the heart of Switzerland."
msgstr "Bei uns findest Du die günstiges VMs aus der Schweiz."
msgid "Try now, order a VM. VM price starts from only 15CHF per month."
msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
msgid "Try now, order a VM. VM price starts from only 10.5 CHF per month."
msgstr "Unser Angebot beginnt bei 10.5 CHF pro Monat. Probier's jetzt aus!"
msgid "ORDER VM"
msgstr "VM BESTELLEN"
@ -214,16 +215,16 @@ msgid ""
"Is creative, using a modern and alternative design for a data center in "
"order to make it more sustainable and affordable at the same time."
msgstr ""
"Es ist kreativ, da es sich ein modernes und alternatives Layout zu Nutze"
"macht um Nachhaltigkeit zu fördern und somit erschwingliche Preise bieten zu "
"können."
"Es ist kreativ, da es sich ein modernes und alternatives Layout zu "
"Nutzemacht um Nachhaltigkeit zu fördern und somit erschwingliche Preise "
"bieten zu können."
msgid ""
"Cuts down the costs for you by using FOSS (Free Open Source Software) "
"exclusively, wherefore we can save money from paying licenses."
msgstr ""
"Um unnötige Kosten zu sparen werden, wird ausschliesslich Software auf"
"Basis von FOSS (Free Open Source Software) eingesetzt und dadurch können auf "
"Um unnötige Kosten zu sparen werden, wird ausschliesslich Software aufBasis "
"von FOSS (Free Open Source Software) eingesetzt und dadurch können auf "
"Lizenzgebühren verzichtet werden."
msgid "Scale out"
@ -439,9 +440,6 @@ msgstr "Wiederholend"
msgid "Subtotal"
msgstr "Zwischensumme"
msgid "VAT"
msgstr "Mehrwertsteuer"
#, fuzzy, python-format
#| msgid ""
#| "By clicking \"Place order\" this plan will charge your credit card "
@ -453,7 +451,7 @@ msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s "
"CHF pro Jahr belastet"
#, python-format
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(total_price)s CHF/month"
@ -591,7 +589,8 @@ msgstr "Ungültige Speicher-Grösse"
#, python-brace-format
msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr "Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
msgstr ""
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
#, python-brace-format
msgid "{user} does not have permission to access the card"
@ -636,8 +635,15 @@ msgid ""
"\n"
"Cheers,\n"
"Your Data Center Light team"
msgstr "Hallo {name},\n" "\n" "vielen Dank für deine Bestellung!\n" "Wir haben deine Bezahlung in Höhe von {amount:.2f} CHF erhalten. {recurring}\n" "\n" "Grüsse\n"
"Dein Data Center Light Team"
msgstr ""
"Hallo {name},\n"
"\n"
"vielen Dank für deine Bestellung!\n"
"Wir haben deine Bezahlung in Höhe von {amount:.2f} CHF erhalten. "
"{recurring}\n"
"\n"
"Grüsse\n"
"Dein Data Center Light Team"
msgid "Thank you for the payment."
msgstr "Danke für Deine Bestellung."
@ -645,7 +651,9 @@ msgstr "Danke für Deine Bestellung."
msgid ""
"You will soon receive a confirmation email of the payment. You can always "
"contact us at info@ungleich.ch for any question that you may have."
msgstr "Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst jederzeit unter info@ungleich.ch kontaktieren."
msgstr ""
"Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst "
"jederzeit unter info@ungleich.ch kontaktieren."
msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung."
@ -657,6 +665,9 @@ msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst."
#~ msgid "VAT"
#~ msgstr "Mehrwertsteuer"
#~ msgid ""
#~ "You are not making any payment yet. After submitting your card "
#~ "information, you will be taken to the Confirm Order Page."

View file

@ -0,0 +1,41 @@
import logging
from django.core.management.base import BaseCommand
from hosting.models import (
HostingOrder, VMDetail
)
from membership.models import CustomUser
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''Dumps the email addresses of all customers who have a VM'''
def add_arguments(self, parser):
parser.add_argument('-a', '--all_registered', action='store_true',
help='All registered users')
def handle(self, *args, **options):
all_registered = options['all_registered']
all_customers_set = set()
if all_registered:
all_customers = CustomUser.objects.filter(
is_admin=False, validated=True
)
for customer in all_customers:
all_customers_set.add(customer.email)
else:
all_hosting_orders = HostingOrder.objects.filter()
running_vm_details = VMDetail.objects.filter(terminated_at=None)
running_vm_ids = [rvm.vm_id for rvm in running_vm_details]
for order in all_hosting_orders:
if order.vm_id in running_vm_ids:
all_customers_set.add(order.customer.user.email)
for cu in all_customers_set:
print(cu)
if all_registered:
print("All registered users = %s" % len(all_customers_set))
else:
print("Total active customers = %s" % len(all_customers_set))

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-12-24 03:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0029_auto_20190420_1022'),
]
operations = [
migrations.AddField(
model_name='dclnavbarpluginmodel',
name='show_non_transparent_navbar_always',
field=models.BooleanField(default=False, help_text='Check this if you want to show non transparent navbar only.(Useful when we want to setup a simple page)'),
),
]

View file

@ -191,3 +191,9 @@ footer .dcl-link-separator::before {
font-weight: bold;
font-size: 14px;
}
@media(max-width:767px) {
.vspace-top {
margin-top: 35px;
}
}

View file

@ -77,16 +77,18 @@
}
function _navScroll() {
if ($(window).scrollTop() > 10) {
$(".navbar").removeClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#777");
$(".dropdown-menu").removeClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#777");
} else {
$(".navbar").addClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#fff");
$(".dropdown-menu").addClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#fff");
if (!window.non_transparent_navbar_always) {
if ($(window).scrollTop() > 10) {
$(".navbar").removeClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#777");
$(".dropdown-menu").removeClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#777");
} else {
$(".navbar").addClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#fff");
$(".dropdown-menu").addClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#fff");
}
}
}

View file

@ -1,7 +1,9 @@
{% load static i18n custom_tags cms_tags %}
{% get_current_language as LANGUAGE_CODE %}
<nav class="navbar navbar-default navbar-fixed-top topnav navbar-transparent">
{% if instance.show_non_transparent_navbar_always %}
<script>window.non_transparent_navbar_always=true;</script>
{% endif %}
<nav class="navbar navbar-default navbar-fixed-top topnav {% if instance.show_non_transparent_navbar_always != True %}navbar-transparent{% endif %}">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#dcl-topnav">

View file

@ -28,7 +28,7 @@
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
</p>
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}Try now, order a VM. VM price starts from only 15CHF per month.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %}
</p>
</td>
</tr>

View file

@ -3,7 +3,7 @@
{% trans "Welcome to Data Center Light!" %}
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 15CHF per month.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %}
{{ base_url }}{% url 'hosting:create_virtual_machine' %}

View file

@ -14,7 +14,7 @@
</script>
{% endif %}
<form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form">
<form id="order_form" method="POST" action="{% url 'datacenterlight:index' %}" data-toggle="validator" role="form">
{% csrf_token %}
<input type="hidden" name="pid" value="{{instance.id}}">
<div class="title">
@ -102,4 +102,4 @@
</div>
<input type="hidden" name="pricing_name" value="{% if vm_pricing.name %}{{vm_pricing.name}}{% else %}unknown{% endif%}"></input>
<input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input>
</form>
</form>

View file

@ -1,7 +1,6 @@
{% load staticfiles i18n custom_tags %}
{% get_current_language as LANGUAGE_CODE %}
<nav class="navbar navbar-default navbar-fixed-top topnav navbar-transparent">
<nav class="navbar navbar-default navbar-fixed-top topnav {% if instance.show_non_transparent_navbar_always is False %}navbar-transparent{% endif %}">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">

View file

@ -120,7 +120,7 @@
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
<p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}

View file

@ -28,7 +28,8 @@ from utils.forms import (
BillingAddress
)
from utils.hosting_utils import (
get_vm_price_with_vat, get_all_public_keys, get_vat_rate_for_country
get_vm_price_with_vat, get_all_public_keys, get_vat_rate_for_country,
get_vm_price_for_given_vat
)
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
@ -601,8 +602,28 @@ class OrderConfirmationView(DetailView, FormView):
request.session['generic_payment_details'],
})
else:
vm_specs = request.session.get('specs')
user_vat_country = (
request.session.get('billing_address_data').get("country")
)
user_country_vat_rate = get_vat_rate_for_country(user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=vm_specs['cpu'],
memory=vm_specs['memory'],
ssd_size=vm_specs['disk_size'],
pricing_name=vm_specs['pricing_name'],
vat_rate=user_country_vat_rate * 100
)
vm_specs["price"] = price
vm_specs["vat"] = vat
vm_specs["vat_percent"] = vat_percent
vm_specs["vat_country"] = user_vat_country
vm_specs["discount"] = discount
vm_specs["total_price"] = round(price + vat - discount['amount'], 2)
request.session['specs'] = vm_specs
context.update({
'vm': request.session.get('specs'),
'vm': vm_specs,
'form': UserHostingKeyForm(request=self.request),
'keys': get_all_public_keys(self.request.user)
})

View file

@ -52,7 +52,7 @@ PROJECT_DIR = os.path.abspath(
)
# load .env file
dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR))
dotenv.load_dotenv("{0}/.env".format(PROJECT_DIR))
from multisite import SiteID
@ -245,8 +245,9 @@ DATABASES = {
}
AUTHENTICATION_BACKENDS = (
'utils.backend.MyLDAPBackend',
'guardian.backends.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend',
)
# Internationalization
@ -726,6 +727,31 @@ INVOICE_WEBHOOK_SECRET = env('INVOICE_WEBHOOK_SECRET')
DEBUG = bool_env('DEBUG')
ADD_TRIAL_PERIOD_TO_SUBSCRIPTION = bool_env('ADD_TRIAL_PERIOD_TO_SUBSCRIPTION')
# LDAP setup
LDAP_ADMIN_DN = env('LDAP_ADMIN_DN')
LDAP_ADMIN_PASSWORD = env('LDAP_ADMIN_PASSWORD')
AUTH_LDAP_SERVER = env('LDAPSERVER')
LDAP_CUSTOMER_DN = env('LDAP_CUSTOMER_DN')
LDAP_CUSTOMER_GROUP_ID = int(env('LDAP_CUSTOMER_GROUP_ID'))
LDAP_MAX_UID_FILE_PATH = os.environ.get('LDAP_MAX_UID_FILE_PATH',
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ldap_max_uid_file')
)
LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID'))
# Search union over OUs
AUTH_LDAP_START_TLS = bool(os.environ.get('LDAP_USE_TLS', False))
ENTIRE_SEARCH_BASE = env("ENTIRE_SEARCH_BASE")
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
READ_VM_REALM = env('READ_VM_REALM')
AUTH_NAME = env('AUTH_NAME')
AUTH_SEED = env('AUTH_SEED')
@ -733,6 +759,9 @@ AUTH_REALM = env('AUTH_REALM')
OTP_SERVER = env('OTP_SERVER')
OTP_VERIFY_ENDPOINT = env('OTP_VERIFY_ENDPOINT')
FIRST_VM_ID_AFTER_EU_VAT = int_env('FIRST_VM_ID_AFTER_EU_VAT')
PRE_EU_VAT_RATE = float(env('PRE_EU_VAT_RATE'))
if DEBUG:
from .local import * # flake8: noqa

View file

@ -23,7 +23,6 @@
.hosting-dashboard .dashboard-container-head {
color: #fff;
margin-bottom: 60px;
}
.hosting-dashboard-item {

View file

@ -248,6 +248,9 @@
.dashboard-title-thin {
font-size: 22px;
}
.dashboard-greetings-thin {
font-size: 16px;
}
}
.btn-vm-invoice {
@ -315,6 +318,11 @@
font-size: 32px;
}
.dashboard-greetings-thin {
font-weight: 300;
font-size: 24px;
}
.dashboard-title-thin .un-icon {
height: 34px;
margin-right: 5px;
@ -411,6 +419,9 @@
.dashboard-title-thin {
font-size: 22px;
}
.dashboard-greetings-thin {
font-size: 16px;
}
.dashboard-title-thin .un-icon {
height: 22px;
width: 22px;

View file

@ -7,6 +7,9 @@
<div class="dashboard-container-head">
<h1 class="dashboard-title-thin">{% trans "My Dashboard" %}</h1>
</div>
<div style="color:#fff; font-size: 18px; font-weight:300; padding: 0 8px; margin-top: 30px; margin-bottom: 30px;">
{% trans "Welcome" %} {{request.user.name}}
</div>
<div class="hosting-dashboard-content">
<a href="{% url 'hosting:create_virtual_machine' %}" class="hosting-dashboard-item">
<h2>{% trans "Create VM" %}</h2>

View file

@ -26,7 +26,7 @@
</li>
<li class="dropdown highlights-dropdown">
<a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#">
<i class="fa fa-fw fa-user"></i>&nbsp;&nbsp;{{request.user.name}}&nbsp;<span class="fa fa-fw fa-caret-down"></span>
<i class="fa fa-fw fa-user"></i>&nbsp;&nbsp;{{request.user.username}}&nbsp;<span class="fa fa-fw fa-caret-down"></span>
</a>
<ul id="g-account-menu" class="dropdown-menu" role="menu">
<li><a href="{% url 'hosting:logout' %}">{% trans "Logout"%}</a></li>

View file

@ -147,8 +147,12 @@
CHF</strong>
</p>
<p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
{% if vm.after_eu_vat_intro %}
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
{% else %}
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
</small>
{% endif %}
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}

View file

@ -142,7 +142,12 @@
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
<p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
{% if vm.after_eu_vat_intro %}
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
{% else %}
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
</small>
{% endif %}
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}

View file

@ -15,6 +15,10 @@
<div class="settings-container">
<div class="row">
<div class="col-sm-5 col-md-6 billing dcl-billing">
<h3><b>{%trans "My Username"%}</b></h3>
<hr class="top-hr">
<p>{{request.user.username}}</p>
<br>
<h3>{%trans "Billing Address" %}</h3>
<hr>
<form role="form" id="billing-form" method="post" action="" novalidate>

View file

@ -45,8 +45,8 @@
<h2 class="vm-detail-title">{% trans "Billing" %} <img src="{% static 'hosting/img/billing.svg' %}" class="un-icon"></h2>
<div class="vm-vmid">
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% trans "Month" %}</div>
<a class="btn btn-vm-invoice" href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' order.pk %}{% endif %}">{% trans "See Invoice" %}</a>
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% if order.generic_product %}{% trans order.generic_product.product_subscription_interval %}{% else %}{% trans "Month" %}{% endif %}</div>
<a class="btn btn-vm-invoice" href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' order.pk %}{% endif %}">{% trans "See Invoice" %}</a>
</div>
</div>
<div class="vm-detail-item">

View file

@ -50,10 +50,15 @@ from utils.forms import (
ResendActivationEmailForm
)
from utils.hosting_utils import get_all_public_keys
from utils.hosting_utils import get_vm_price_with_vat, HostingUtils
from utils.hosting_utils import (
get_vm_price_with_vat, get_vm_price_for_given_vat, HostingUtils,
get_vat_rate_for_country
)
from utils.mailer import BaseEmail
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
from utils.ldap_manager import LdapManager
from utils.views import (
PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin,
ResendActivationLinkViewMixin
@ -391,21 +396,30 @@ class PasswordResetConfirmView(HostingContextMixin,
if user is not None and default_token_generator.check_token(user,
token):
if form.is_valid():
ldap_manager = LdapManager()
new_password = form.cleaned_data['new_password2']
user.set_password(new_password)
user.save()
messages.success(request, _('Password has been reset.'))
# Change opennebula password
opennebula_client.change_user_password(user.password)
# Make sure the user have an ldap account already
user.create_ldap_account(new_password)
return self.form_valid(form)
else:
messages.error(
request, _('Password reset has not been successful.'))
form.add_error(None,
_('Password reset has not been successful.'))
return self.form_invalid(form)
# We are changing password in ldap before changing in database because
# ldap have more chances of failure than local database
if ldap_manager.change_password(user.username, new_password):
user.set_password(new_password)
user.save()
messages.success(request, _('Password has been reset.'))
# Change opennebula password
opennebula_client.change_user_password(user.password)
return self.form_valid(form)
messages.error(
request, _('Password reset has not been successful.'))
form.add_error(None,
_('Password reset has not been successful.'))
return self.form_invalid(form)
else:
error_msg = _('The reset password link is no longer valid.')
@ -845,18 +859,32 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
price, vat, vat_percent, discount = get_vm_price_with_vat(
user_vat_country = obj.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
memory=context['vm']['memory'],
pricing_name=(obj.vm_pricing.name
if obj.vm_pricing else 'default')
if obj.vm_pricing else 'default'),
vat_rate= (
user_country_vat_rate * 100
if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = price + vat - discount['amount']
context['vm']["after_eu_vat_intro"] = (
True if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
)
context['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
context['subscription_end_date'] = vm_detail.end_date()
except VMDetail.DoesNotExist:
try:
@ -865,20 +893,32 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
price, vat, vat_percent, discount = get_vm_price_with_vat(
user_vat_country = obj.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
memory=context['vm']['memory'],
pricing_name=(obj.vm_pricing.name
if obj.vm_pricing else 'default')
if obj.vm_pricing else 'default'),
vat_rate=(
user_country_vat_rate * 100
if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = (
price + vat - discount['amount']
context['vm']["after_eu_vat_intro"] = (
True if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
)
context['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
except WrongIdError:
messages.error(
self.request,
@ -916,7 +956,27 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
context['cc_exp_year'] = card_detail.exp_year
context['cc_exp_month'] = '{:02d}'.format(card_detail.exp_month)
context['site_url'] = reverse('hosting:create_virtual_machine')
context['vm'] = self.request.session.get('specs')
vm_specs = self.request.session.get('specs')
user_vat_country = (
self.request.session.get('billing_address_data').get("country")
)
user_country_vat_rate = get_vat_rate_for_country(user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=vm_specs['cpu'],
memory=vm_specs['memory'],
ssd_size=vm_specs['disk_size'],
pricing_name=vm_specs['pricing_name'],
vat_rate=user_country_vat_rate * 100
)
vm_specs["price"] = price
vm_specs["vat"] = vat
vm_specs["vat_percent"] = vat_percent
vm_specs["vat_country"] = user_vat_country
vm_specs["discount"] = discount
vm_specs["total_price"] = round(price + vat - discount['amount'],
2)
vm_specs["after_eu_vat_intro"] = True
context['vm'] = vm_specs
return context
@method_decorator(decorators)
@ -1247,18 +1307,32 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
price, vat, vat_percent, discount = get_vm_price_with_vat(
user_vat_country = obj.order.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
memory=context['vm']['memory'],
pricing_name=(obj.order.vm_pricing.name
if obj.order.vm_pricing else 'default')
if obj.order.vm_pricing else 'default'),
vat_rate=(
user_country_vat_rate * 100
if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = price + vat - discount['amount']
context['vm']["after_eu_vat_intro"] = (
True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
)
context['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
except VMDetail.DoesNotExist:
# fallback to get it from the infrastructure
try:
@ -1268,20 +1342,32 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
)
vm = manager.get_vm(vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
price, vat, vat_percent, discount = get_vm_price_with_vat(
user_vat_country = obj.order.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
memory=context['vm']['memory'],
pricing_name=(obj.order.vm_pricing.name
if obj.order.vm_pricing else 'default')
if obj.order.vm_pricing else 'default'),
vat_rate=(
user_country_vat_rate * 100
if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = (
price + vat - discount['amount']
context['vm']["after_eu_vat_intro"] = (
True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
)
context['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
except TypeError:
logger.error("Type error. Probably we "
"came from a generic product. "
@ -1297,8 +1383,8 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
self.kwargs['error'] = 'WrongIdError'
context['error'] = 'WrongIdError'
return context
else:
logger.debug("No VM_ID. So, no details available.")
else:
logger.debug("No VM_ID. So, no details available.")
# add context params from monthly hosting bill
context['period_start'] = obj.get_period_start()

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-12-18 10:50
from __future__ import unicode_literals
from django.db import migrations, models
import membership.models
class Migration(migrations.Migration):
dependencies = [
('membership', '0010_customuser_import_stripe_bill_remark'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='in_ldap',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customuser',
name='username',
field=models.CharField(max_length=60, null=True, unique=True),
),
migrations.AlterField(
model_name='customuser',
name='name',
field=models.CharField(max_length=50, validators=[membership.models.validate_name]),
),
]

View file

@ -1,5 +1,8 @@
from datetime import datetime
import logging
import random
import unicodedata
from datetime import datetime
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
@ -7,13 +10,17 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.core.validators import RegexValidator
from django.db import models
from django.db import models, IntegrityError
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from utils.mailer import BaseEmail
from utils.mailer import DigitalGlarusRegistrationMailer
from utils.stripe_utils import StripeUtils
from utils.ldap_manager import LdapManager
logger = logging.getLogger(__name__)
REGISTRATION_MESSAGE = {'subject': "Validation mail",
'message': 'Please validate Your account under this link '
@ -42,6 +49,7 @@ class MyUserManager(BaseUserManager):
user.is_admin = False
user.set_password(password)
user.save(using=self._db)
user.create_ldap_account(password)
return user
def create_superuser(self, email, name, password):
@ -63,13 +71,54 @@ def get_validation_slug():
return make_password(None)
def get_first_and_last_name(full_name):
first_name, *last_name = full_name.split(" ")
last_name = " ".join(last_name)
return first_name, last_name
def assign_username(user):
if not user.username:
ldap_manager = LdapManager()
# Try to come up with a username
first_name, last_name = get_first_and_last_name(user.name)
user.username = unicodedata.normalize('NFKD', first_name + last_name)
user.username = "".join([char for char in user.username if char.isalnum()]).lower()
exist = True
while exist:
# Check if it exists
exist, entries = ldap_manager.check_user_exists(user.username)
if exist:
# If username exists in ldap, come up with a new user name and check it again
user.username = user.username + str(random.randint(0, 2 ** 10))
else:
# If username does not exists in ldap, try to save it in database
try:
user.save()
except IntegrityError:
# If username exists in database then come up with a new username
user.username = user.username + str(random.randint(0, 2 ** 10))
exist = True
def validate_name(value):
valid_chars = [char for char in value if (char.isalpha() or char == "-" or char == " ")]
if len(valid_chars) < len(value):
raise ValidationError(
_('%(value)s is not a valid name. A valid name can only include letters, spaces or -'),
params={'value': value},
)
class CustomUser(AbstractBaseUser, PermissionsMixin):
VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
site = models.ForeignKey(Site, default=1)
name = models.CharField(max_length=50)
name = models.CharField(max_length=50, validators=[validate_name])
email = models.EmailField(unique=True)
username = models.CharField(max_length=60, unique=True, null=True)
validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0)
in_ldap = models.BooleanField(default=False)
# By default, we initialize the validation_slug with appropriate value
# This is required for User(page) admin
validation_slug = models.CharField(
@ -164,6 +213,29 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
# The user is identified by their email address
return self.email
def create_ldap_account(self, password):
# create ldap account for user if it does not exists already.
if self.in_ldap:
return
assign_username(self)
ldap_manager = LdapManager()
try:
user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.username)
except Exception:
logger.exception("Exception occur while searching for user in LDAP")
else:
if not user_exists_in_ldap:
# IF no ldap account
first_name, last_name = get_first_and_last_name(self.name)
if not last_name:
last_name = first_name
ldap_manager.create_user(self.username, password=password,
firstname=first_name, lastname=last_name,
email=self.email)
self.in_ldap = True
self.save()
def __str__(self): # __unicode__ on Python 2
return self.email

View file

@ -485,9 +485,15 @@ class OpenNebulaManager():
)
def change_user_password(self, passwd_hash):
if type(self.opennebula_user) == int:
logger.debug("opennebula_user is int and has value = %s" %
self.opennebula_user)
else:
logger.debug("opennebula_user is object and corresponding id is %s"
% self.opennebula_user.id)
self.oneadmin_client.call(
oca.User.METHODS['passwd'],
self.opennebula_user.id,
self.opennebula_user if type(self.opennebula_user) == int else self.opennebula_user.id,
passwd_hash
)

View file

@ -1 +1,2 @@
base-devel
libmemcached

View file

@ -23,7 +23,7 @@ django-classy-tags==0.7.2
django-cms==3.2.5
django-compressor==2.0
django-debug-toolbar==1.4
django-dotenv==1.4.1
python-dotenv==0.10.3
django-extensions==1.6.7
django-filer==1.2.0
django-filter==0.13.0
@ -63,6 +63,7 @@ djangocms-text-ckeditor==2.9.3
djangocms-video==1.0.0
easy-thumbnails==2.3
html5lib==0.9999999
ldap3==2.6.1
lxml==3.6.0
model-mommy==1.2.6
phonenumbers==7.4.0

13
utils/backend.py Normal file
View file

@ -0,0 +1,13 @@
import logging
from django.contrib.auth.backends import ModelBackend
logger = logging.getLogger(__name__)
class MyLDAPBackend(ModelBackend):
def authenticate(self, username=None, password=None, **kwargs):
user = super().authenticate(username, password, **kwargs)
if user:
user.create_ldap_account(password)
return user

View file

@ -241,7 +241,6 @@ COUNTRIES = (
('ZM', _('Zambia')),
('ZR', _('Zaire')),
('ZW', _('Zimbabwe')),
('ZZ', _('Unknown or unspecified country')),
)

View file

@ -84,6 +84,41 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
return round(float(price), 2)
def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0,
pricing_name='default', vat_rate=0):
try:
pricing = VMPricing.objects.get(name=pricing_name)
except Exception as ex:
logger.error(
"Error getting VMPricing object for {pricing_name}."
"Details: {details}".format(
pricing_name=pricing_name, details=str(ex)
)
)
return None
price = (
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
)
vat = price * decimal.Decimal(vat_rate) * decimal.Decimal(0.01)
vat_percent = vat_rate
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
'name': pricing.discount_name,
'amount': round(float(pricing.discount_amount), 2)
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
pricing_name='default'):
"""

281
utils/ldap_manager.py Normal file
View file

@ -0,0 +1,281 @@
import base64
import hashlib
import random
import ldap3
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
class LdapManager:
__instance = None
def __new__(cls):
if LdapManager.__instance is None:
LdapManager.__instance = object.__new__(cls)
return LdapManager.__instance
def __init__(self):
"""
Initialize the LDAP subsystem.
"""
self.rng = random.SystemRandom()
self.server = ldap3.Server(settings.AUTH_LDAP_SERVER)
def get_admin_conn(self):
"""
Return a bound :class:`ldap3.Connection` instance which has write
permissions on the dn in which the user accounts reside.
"""
conn = self.get_conn(user=settings.LDAP_ADMIN_DN,
password=settings.LDAP_ADMIN_PASSWORD,
raise_exceptions=True)
conn.bind()
return conn
def get_conn(self, **kwargs):
"""
Return an unbound :class:`ldap3.Connection` which talks to the configured
LDAP server.
The *kwargs* are passed to the constructor of :class:`ldap3.Connection` and
can be used to set *user*, *password* and other useful arguments.
"""
return ldap3.Connection(self.server, **kwargs)
def _ssha_password(self, password):
"""
Apply the SSHA password hashing scheme to the given *password*.
*password* must be a :class:`bytes` object, containing the utf-8
encoded password.
Return a :class:`bytes` object containing ``ascii``-compatible data
which can be used as LDAP value, e.g. after armoring it once more using
base64 or decoding it to unicode from ``ascii``.
"""
SALT_BYTES = 15
sha1 = hashlib.sha1()
salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, "little")
sha1.update(password)
sha1.update(salt)
digest = sha1.digest()
passwd = b"{SSHA}" + base64.b64encode(digest + salt)
return passwd
def create_user(self, user, password, firstname, lastname, email):
conn = self.get_admin_conn()
uidNumber = self._get_max_uid() + 1
logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber))
user_exists = True
while user_exists:
user_exists, _ = self.check_user_exists(
"",
'(&(objectClass=inetOrgPerson)(objectClass=posixAccount)'
'(objectClass=top)(uidNumber={uidNumber}))'.format(
uidNumber=uidNumber
)
)
if user_exists:
logger.debug(
"{uid} exists. Trying next.".format(uid=uidNumber)
)
uidNumber += 1
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
self._set_max_uid(uidNumber)
try:
uid = user.encode("utf-8")
conn.add("uid={uid},{customer_dn}".format(
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
),
["inetOrgPerson", "posixAccount", "ldapPublickey"],
{
"uid": [uid],
"sn": [lastname.encode("utf-8")],
"givenName": [firstname.encode("utf-8")],
"cn": [uid],
"displayName": ["{} {}".format(firstname, lastname).encode("utf-8")],
"uidNumber": [str(uidNumber)],
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
"loginShell": ["/bin/bash"],
"homeDirectory": ["/home/{}".format(user).encode("utf-8")],
"mail": email.encode("utf-8"),
"userPassword": [self._ssha_password(
password.encode("utf-8")
)]
}
)
logger.debug('Created user %s %s' % (user.encode('utf-8'),
uidNumber))
except Exception as ex:
logger.debug('Could not create user %s' % user.encode('utf-8'))
logger.error("Exception: " + str(ex))
raise Exception(ex)
finally:
conn.unbind()
def change_password(self, uid, new_password):
"""
Changes the password of the user identified by user_dn
:param uid: str The uid that identifies the user
:param new_password: str The new password string
:return: True if password was changed successfully False otherwise
"""
conn = self.get_admin_conn()
# Make sure the user exists first to change his/her details
user_exists, entries = self.check_user_exists(
uid=uid,
search_base=settings.ENTIRE_SEARCH_BASE
)
return_val = False
if user_exists:
try:
return_val = conn.modify(
entries[0].entry_dn,
{
"userpassword": (
ldap3.MODIFY_REPLACE,
[self._ssha_password(new_password.encode("utf-8"))]
)
}
)
except Exception as ex:
logger.error("Exception: " + str(ex))
else:
logger.error("User {} not found".format(uid))
conn.unbind()
return return_val
def change_user_details(self, uid, details):
"""
Updates the user details as per given values in kwargs of the user
identified by user_dn.
Assumes that all attributes passed in kwargs are valid.
:param uid: str The uid that identifies the user
:param details: dict A dictionary containing the new values
:return: True if user details were updated successfully False otherwise
"""
conn = self.get_admin_conn()
# Make sure the user exists first to change his/her details
user_exists, entries = self.check_user_exists(
uid=uid,
search_base=settings.ENTIRE_SEARCH_BASE
)
return_val = False
if user_exists:
details_dict = {k: (ldap3.MODIFY_REPLACE, [v.encode("utf-8")]) for
k, v in details.items()}
try:
return_val = conn.modify(entries[0].entry_dn, details_dict)
msg = "success"
except Exception as ex:
msg = str(ex)
logger.error("Exception: " + msg)
finally:
conn.unbind()
else:
msg = "User {} not found".format(uid)
logger.error(msg)
conn.unbind()
return return_val, msg
def check_user_exists(self, uid, search_filter="", attributes=None,
search_base=settings.LDAP_CUSTOMER_DN, search_attr="uid"):
"""
Check if the user with the given uid exists in the customer group.
:param uid: str representing the user
:param search_filter: str representing the filter condition to find
users. If its empty, the search finds the user with
the given uid.
:param attributes: list A list of str representing all the attributes
to be obtained in the result entries
:param search_base: str
:return: tuple (bool, [ldap3.abstract.entry.Entry ..])
A bool indicating if the user exists
A list of all entries obtained in the search
"""
conn = self.get_admin_conn()
entries = []
try:
result = conn.search(
search_base=search_base,
search_filter=search_filter if len(search_filter) > 0 else
'(uid={uid})'.format(uid=uid),
attributes=attributes
)
entries = conn.entries
finally:
conn.unbind()
return result, entries
def delete_user(self, uid):
"""
Deletes the user with the given uid from ldap
:param uid: str representing the user
:return: True if the delete was successful False otherwise
"""
conn = self.get_admin_conn()
try:
return_val = conn.delete(
("uid={uid}," + settings.LDAP_CUSTOMER_DN).format(uid=uid),
)
msg = "success"
except Exception as ex:
msg = str(ex)
logger.error("Exception: " + msg)
return_val = False
finally:
conn.unbind()
return return_val, msg
def _set_max_uid(self, max_uid):
"""
a utility function to save max_uid value to a file
:param max_uid: an integer representing the max uid
:return:
"""
with open(settings.LDAP_MAX_UID_FILE_PATH, 'w+') as handler:
handler.write(str(max_uid))
def _get_max_uid(self):
"""
A utility function to read the max uid value that was previously set
:return: An integer representing the max uid value that was previously
set
"""
try:
with open(settings.LDAP_MAX_UID_FILE_PATH, 'r+') as handler:
try:
return_value = int(handler.read())
except ValueError as ve:
logger.error(
"Error reading int value from {}. {}"
"Returning default value {} instead".format(
settings.LDAP_MAX_UID_PATH,
str(ve),
settings.LDAP_DEFAULT_START_UID
)
)
return_value = settings.LDAP_DEFAULT_START_UID
return return_value
except FileNotFoundError as fnfe:
logger.error("File not found : " + str(fnfe))
return_value = settings.LDAP_DEFAULT_START_UID
logger.error("So, returning UID={}".format(return_value))
return return_value

View file

@ -20,7 +20,10 @@ class BaseBillingAddress(models.Model):
class BillingAddress(BaseBillingAddress):
def __str__(self):
return self.street_address
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
class UserBillingAddress(BaseBillingAddress):
@ -28,7 +31,10 @@ class UserBillingAddress(BaseBillingAddress):
current = models.BooleanField(default=True)
def __str__(self):
return self.street_address
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
def to_dict(self):
return {

View file

@ -316,3 +316,6 @@ IM",GBP,0.1,standard,
2019-11-15,,FR,EUR,0.2,standard,France standard VAT (added manually)
2019-11-15,,GR,EUR,0.24,standard,Greece standard VAT (added manually)
2019-11-15,,GB,EUR,0.2,standard,UK standard VAT (added manually)
2019-12-17,,AD,EUR,0.045,standard,Andorra standard VAT (added manually)
2019-12-17,,TK,EUR,0.18,standard,Turkey standard VAT (added manually)
2019-12-17,,IS,EUR,0.24,standard,Iceland standard VAT (added manually)

1 start_date stop_date territory_codes currency_code rate rate_type description
316
317
318
319
320
321