Merge remote-tracking master into task/3530/upgrade_to_django_1.11

This commit is contained in:
PCoder 2018-01-03 09:39:00 +01:00
commit d20cb5c8f7
11 changed files with 203 additions and 45 deletions

View file

@ -1,19 +1,25 @@
Next:
1.3.1: 2017-12-31
* feature: [all] Load email configurations host, port and use_tls from env
* bugfix: [all] Use ungleich's smtp as relayhost for sending emails
1.3: 2017-12-27
* #3911: [dcl] Integrate resend activation link into dcl landing payment page
* #3972: [hosting] Add ungleich company info to invoice footer
* #3974: [hosting] Improve invoice number: Show 404 for invoice resources that do not belong to the user
* [ungleich] Add video cover to the header on ungleich.ch landing page and add corresponding cms plugin
* #3961: [ungleich] Add video cover to the header on ungleich.ch landing page and add corresponding cms plugin
* #3774: [hosting] Update Stripe subscription on vm delete
* [ungleich] Update text on landing page
* #3601: [dcl, hosting] Change minimum required RAM from 2GB to 1GB
* #3973: [dcl] Update datacenterlight and glasfaser contact address to Linthal and company name to "ungleich glarus ag"
* #3993: [dg] Fix new user membership payment by setting cardholder_name field for UserBillingAddressForm
* #3799: [dg] Make digital glarus billing work as monthly subscription
* #3994: [dg] Add a line on signup for clarifying dcl users can login without new signup
1.2.13: 2017-12-09
* [cms] Introduce UngleichHeaderBackgroundImageAndTextSliderPlugin that allows to have scrolling images and texts
* [cms] Remove <p> tag for ungleich cms customer item template
1.2.12: 2017-12-09
* #3594: [digitalglarus] Remove white scroll bar on the right in mobile
* #3905: [ungleich] Update ungleich.ch header into a slider
* #3968: [ungleich] Fix navbar logo alignment
* [all] Enable logging custom modules
1.2.11: 2017-11-30
* [all] TravisCI: Test against python 3.4.2 only

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2017-12-23 22:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('digitalglarus', '0024_bookingcancellation'),
]
operations = [
migrations.AddField(
model_name='membershiporder',
name='stripe_subscription_id',
field=models.CharField(max_length=100, null=True),
),
]

View file

@ -1,6 +1,6 @@
import calendar
import time
from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta
from django.db import models
@ -59,6 +59,17 @@ class MembershipType(models.Model):
return "{} - {}".format(datetime.strftime(start_date, "%b, %d %Y"),
datetime.strftime(end_date, "%b, %d %Y"))
@cached_property
def next_month_in_sec_since_epoch(self):
"""
First day of the next month expressed in seconds since the epoch time
:return: Time in seconds
"""
start_date, end_date = self.first_month_range
first_day_next_month = end_date + timedelta(days=1)
epoch_time = int(time.mktime(first_day_next_month.timetuple()))
return epoch_time
class Membership(models.Model):
type = models.ForeignKey(MembershipType)
@ -71,12 +82,12 @@ class Membership(models.Model):
@classmethod
def get_current_membership(cls, user):
has_order_current_month = Q(membershiporder__customer__user=user,
membershiporder__created_at__month=datetime.today().month)
has_order_current_month = Q(
membershiporder__customer__user=user,
membershiporder__created_at__month=datetime.today().month
)
# import pdb;pdb.set_trace()
return cls.objects.\
filter(has_order_current_month).last()
return cls.objects.filter(has_order_current_month).last()
# def get_current_active_membership(cls, user):
# membership = cls.get_current_membership(user)
@ -84,8 +95,7 @@ class Membership(models.Model):
@classmethod
def get_by_user(cls, user):
return cls.objects.\
filter(membershiporder__customer__user=user).last()
return cls.objects.filter(membershiporder__customer__user=user).last()
@classmethod
def create(cls, data):
@ -96,18 +106,23 @@ class Membership(models.Model):
def activate_or_crete(cls, data, user):
membership = cls.get_by_user(user)
membership_id = membership.id if membership else None
obj, created = cls.objects.update_or_create(id=membership_id, defaults=data)
obj, created = cls.objects.update_or_create(
id=membership_id, defaults=data
)
return obj
@classmethod
def is_digitalglarus_active_member(cls, user):
# past_month = (datetime.today() - relativedelta(months=1)).month
has_order_current_month = Q(membershiporder__customer__user=user,
membershiporder__created_at__month=datetime.today().month)
has_order_current_month = Q(
membershiporder__customer__user=user,
membershiporder__created_at__month=datetime.today().month
)
# has_order_past_month = Q(membershiporder__customer__user=user,
# membershiporder__created_at__month=past_month)
active_membership = Q(active=True)
# return cls.objects.filter(has_order_past_month | has_order_current_month).\
# return cls.objects.filter(
# has_order_past_month | has_order_current_month).\
return cls.objects.filter(has_order_current_month).\
filter(active_membership).exists()
@ -129,6 +144,7 @@ class MembershipOrder(Ordereable, models.Model):
membership = models.ForeignKey(Membership)
start_date = models.DateField()
end_date = models.DateField()
stripe_subscription_id = models.CharField(max_length=100, null=True)
@classmethod
def current_membership_dates(cls, user):
@ -172,10 +188,12 @@ class MembershipOrder(Ordereable, models.Model):
@classmethod
def create(cls, data):
stripe_charge = data.pop('stripe_charge', None)
stripe_subscription_id = data.pop('stripe_subscription_id', None)
instance = cls.objects.create(**data)
instance.stripe_charge_id = stripe_charge.id
instance.last4 = stripe_charge.source.last4
instance.cc_brand = stripe_charge.source.brand
instance.stripe_subscription_id = stripe_subscription_id
instance.save()
return instance

View file

@ -95,12 +95,9 @@
<a class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_deactivate' %}">Deactivate</a>
</div>
{% elif not current_membership.active %}
<form method="POST" action="{% url 'digitalglarus:membership_reactivate' %}">
{% csrf_token %}
<div class="edit-button">
<button type="submit" class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_reactivate' %}">Reactivate</button>
<a class="btn btn-primary btn-grey btn-deactivate" href="{% url 'digitalglarus:membership_pricing' %}">Reactivate</a>
</div>
</form>
{% endif %}
{% else %}
<div class="edit-button">

View file

@ -29,6 +29,9 @@
<br>
<div class="notice-box">
<p class="signup-text">Already a member?<a href="{% url 'digitalglarus:login' %}">Log in</a></p>
<p class="signup-text">If you are a user of <a href="{% url 'datacenterlight:index' %}" style="margin:0;">Data Center Light</a>,
you can <a href="{% url 'digitalglarus:login' %}" style="margin:0;">login</a> on Digital Glarus without a new signup.
</p>
</div>
</div>
@ -56,4 +59,4 @@
</div>
</div>
</section>
{% endblock %}
{% endblock %}

View file

@ -1,5 +1,5 @@
from model_mommy import mommy
from unittest import mock
from unittest import mock, skipIf
from django.test import TestCase
from django.conf import settings
@ -126,6 +126,11 @@ class MembershipPaymentViewTest(BaseTestCase):
self.assertEqual(response.context['membership_type'],
self.membership_type)
@skipIf(
settings.STRIPE_API_PRIVATE_KEY_TEST is None or
settings.STRIPE_API_PRIVATE_KEY_TEST is "",
"""Stripe details unavailable, so skipping CeleryTaskTestCase"""
)
@mock.patch('utils.stripe_utils.StripeUtils.create_customer')
def test_post(self, stripe_mocked_call):

View file

@ -1,3 +1,5 @@
import logging
from django.conf import settings
from django.shortcuts import render
from django.http import HttpResponseRedirect
@ -9,25 +11,34 @@ from django.utils.translation import get_language
from djangocms_blog.models import Post
from django.contrib import messages
from django.views.generic import DetailView, ListView
from .models import Supporter
from .mixins import ChangeMembershipStatusMixin
from utils.forms import ContactUsForm
from utils.mailer import BaseEmail
from django.views.generic.edit import FormView
from membership.models import StripeCustomer
from utils.views import LoginViewMixin, SignupViewMixin, \
PasswordResetViewMixin, PasswordResetConfirmViewMixin
from utils.forms import PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
from utils.views import (
LoginViewMixin, SignupViewMixin, PasswordResetViewMixin,
PasswordResetConfirmViewMixin
)
from utils.forms import (
PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
)
from utils.stripe_utils import StripeUtils
from utils.models import UserBillingAddress
from utils.tasks import send_plain_email_task
from .forms import LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,\
from .forms import (
LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,
BookingBillingForm, CancelBookingForm
)
from .models import (
MembershipType, Membership, MembershipOrder, Booking, BookingPrice,
BookingOrder, BookingCancellation, Supporter
)
from .mixins import (
MembershipRequiredMixin, IsNotMemberMixin, ChangeMembershipStatusMixin
)
from .models import MembershipType, Membership, MembershipOrder, Booking, BookingPrice,\
BookingOrder, BookingCancellation
from .mixins import MembershipRequiredMixin, IsNotMemberMixin
logger = logging.getLogger(__name__)
class IndexView(TemplateView):
@ -271,7 +282,6 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView):
booking_data = {
'start_date': start_date,
'end_date': end_date,
'start_date': start_date,
'free_days': free_days,
'price': normal_price,
'final_price': final_price,
@ -355,16 +365,21 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
membership_type = data.get('membership_type')
# Get or create stripe customer
customer = StripeCustomer.get_or_create(email=self.request.user.email,
token=token)
customer = StripeCustomer.get_or_create(
email=self.request.user.email, token=token
)
if not customer:
form.add_error("__all__", "Invalid credit card")
return self.render_to_response(self.get_context_data(form=form))
return self.render_to_response(
self.get_context_data(form=form)
)
# Make stripe charge to a customer
stripe_utils = StripeUtils()
charge_response = stripe_utils.make_charge(amount=membership_type.first_month_price,
customer=customer.stripe_id)
charge_response = stripe_utils.make_charge(
amount=membership_type.first_month_price,
customer=customer.stripe_id
)
charge = charge_response.get('response_object')
# Check if the payment was approved
@ -373,6 +388,58 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
'paymentError': charge_response.get('error'),
'form': form
})
email_to_admin_data = {
'subject': "Could not create charge for Digital Glarus "
"user: {user}".format(
user=self.request.user.email
),
'from_email': 'info@digitalglarus.ch',
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
charge_response.items()]),
}
send_plain_email_task.delay(email_to_admin_data)
return render(request, self.template_name, context)
# Subscribe the customer to dg plan from the next month onwards
stripe_plan = stripe_utils.get_or_create_stripe_plan(
amount=membership_type.price,
name='Digital Glarus {sub_type_name} Subscription'.format(
sub_type_name=membership_type.name
),
stripe_plan_id='dg-{sub_type_name}'.format(
sub_type_name=membership_type.name
)
)
subscription_result = stripe_utils.subscribe_customer_to_plan(
customer.stripe_id,
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
trial_end=membership_type.next_month_in_sec_since_epoch
)
stripe_subscription_obj = subscription_result.get(
'response_object'
)
# Check if call to create subscription was ok
if (stripe_subscription_obj is None or
(stripe_subscription_obj.status != 'active' and
stripe_subscription_obj.status != 'trialing')):
context.update({
'paymentError': subscription_result.get('error'),
'form': form
})
email_to_admin_data = {
'subject': "Could not create Stripe subscription for "
"Digital Glarus user: {user}".format(
user=self.request.user.email
),
'from_email': 'info@digitalglarus.ch',
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
subscription_result.items()]),
}
send_plain_email_task.delay(email_to_admin_data)
return render(request, self.template_name, context)
charge = charge_response.get('response_object')
@ -412,6 +479,7 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
'customer': customer,
'billing_address': billing_address,
'stripe_charge': charge,
'stripe_subscription_id': stripe_subscription_obj.id,
'amount': membership_type.first_month_price,
'start_date': membership_start_date,
'end_date': membership_end_date
@ -481,8 +549,43 @@ class MembershipDeactivateView(LoginRequiredMixin, UpdateView):
def post(self, *args, **kwargs):
membership = self.get_object()
membership.deactivate()
messages.add_message(self.request, messages.SUCCESS, self.success_message)
messages.add_message(
self.request, messages.SUCCESS, self.success_message
)
# cancel Stripe subscription
stripe_utils = StripeUtils()
membership_order = MembershipOrder.objects.filter(
customer__user=self.request.user
).last()
if membership_order:
if membership_order.stripe_subscription_id:
result = stripe_utils.unsubscribe_customer(
subscription_id=membership_order.stripe_subscription_id
)
stripe_subscription_obj = result.get('response_object')
# Check if the subscription was canceled
if (stripe_subscription_obj is None or
stripe_subscription_obj.status != 'canceled'):
error_msg = result.get('error')
logger.error(
"Could not cancel Digital Glarus subscription. "
"Reason: {reason}".format(
reason=error_msg
)
)
else:
logger.error(
"User {user} may have Stripe subscriptions created "
"manually. Please check.".format(
user=self.request.user.name
)
)
else:
logger.error(
"MembershipOrder for {user} not found".format(
user=self.request.user.name
)
)
return HttpResponseRedirect(self.success_url)

View file

@ -63,8 +63,12 @@ LOGIN_URL = None
LOGOUT_URL = None
LOGIN_REDIRECT_URL = None
EMAIL_HOST = "localhost"
EMAIL_PORT = 25
EMAIL_HOST = env("EMAIL_HOST")
if not EMAIL_HOST:
EMAIL_HOST = "localhost"
EMAIL_PORT = int_env("EMAIL_PORT", 25)
EMAIL_USE_TLS = bool_env("EMAIL_USE_TLS")
SECRET_KEY = env('DJANGO_SECRET_KEY')
# Application definition

View file

@ -155,7 +155,7 @@
{% endblock submit_btn %}
{% else %}
<div class="order_detail_footer">
<strong>ungleich glarus ag</strong>&nbsp;&nbsp;Bahnhotstrasse 1, 8783 Linthal, Switzerland<br>
<strong>ungleich glarus ag</strong>&nbsp;&nbsp;Bahnhofstrasse 1, 8783 Linthal, Switzerland<br>
www.datacenterlight.ch&nbsp;&nbsp;|&nbsp;&nbsp;info@datacenterlight.ch
</div>
{% endif %}

View file

@ -1,3 +1,4 @@
{% load i18n %}
{% if instance.image %}
<div class="bg_img" style="background-image:url({{ instance.image.url }})"></div>
{% endif %}

View file

@ -210,12 +210,14 @@ class StripeUtils(object):
return return_value
@handleStripeError
def subscribe_customer_to_plan(self, customer, plans):
def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
"""
Subscribes the given customer to the list of given plans
:param customer: The stripe customer identifier
:param plans: A list of stripe plans.
:param trial_end: An integer representing when the Stripe subscription
is supposed to end
Ref: https://stripe.com/docs/api/python#create_subscription-items
e.g.
plans = [
@ -227,8 +229,7 @@ class StripeUtils(object):
"""
subscription_result = self.stripe.Subscription.create(
customer=customer,
items=plans,
customer=customer, items=plans, trial_end=trial_end
)
return subscription_result