diff --git a/digitalglarus/forms.py b/digitalglarus/forms.py index 4084ced5..6982037f 100644 --- a/digitalglarus/forms.py +++ b/digitalglarus/forms.py @@ -57,7 +57,7 @@ class MembershipOrderForm(forms.ModelForm): class BookingBillingForm(BillingAddressForm): - token = forms.CharField(widget=forms.HiddenInput()) + token = forms.CharField(widget=forms.HiddenInput(), required=False) start_date = forms.DateField(widget=forms.HiddenInput()) end_date = forms.DateField(widget=forms.HiddenInput()) price = forms.FloatField(widget=forms.HiddenInput()) diff --git a/digitalglarus/management/commands/make_membership_charge.py b/digitalglarus/management/commands/make_membership_charge.py index c727c734..4dedc1ff 100644 --- a/digitalglarus/management/commands/make_membership_charge.py +++ b/digitalglarus/management/commands/make_membership_charge.py @@ -97,70 +97,3 @@ class Command(BaseCommand): print(e) print("-------------------------") continue - # for donator_status in donators: - # donator = donator_status.user.stripecustomer - # try: - # Donation.objects.get(created_at__month=current_month, - # created_at__year=current_year, - # donator=donator) - # except Donation.DoesNotExist: - # try: - # # Get donator last donation amount - # last_donation = Donation.objects.filter(donator=donator).last() - # donation_amount = last_donation.donation - - # # Make stripe charge to a customer - # stripe_utils = StripeUtils() - # stripe_utils.CURRENCY = self.CURRENCY - # charge_response = stripe_utils.make_charge(amount=donation_amount, - # customer=donator.stripe_id) - # charge = charge_response.get('response_object') - - # # Check if the payment was approved - # if not charge: - # # There is an error trying to creating the stripe charge - # context = { - # 'paymentError': charge_response.get('error'), - # } - # print("--------- STRIPE PAYMENT ERROR ---------") - # print(context) - # print("-------------------------") - # continue - # # Create a donation - # charge = charge_response.get('response_object') - # donation_data = { - # 'cc_brand': charge.source.brand, - # 'stripe_charge_id': charge.id, - # 'last4': charge.source.last4, - # 'billing_address': last_donation.billing_address.id, - # 'donator': donator.id, - # 'donation': donation_amount - # } - # donation_form = DonationForm(donation_data) - # if donation_form.is_valid(): - # donation = donation_form.save() - - # context = { - # 'donation': donation, - # 'base_url': "{0}://{1}".format('https', 'dynamicweb.ungleich.ch') - - # } - # email_data = { - # 'subject': 'Your donation have been charged', - # 'to': donation.donator.user.email, - # 'context': context, - # 'template_name': 'donation_charge', - # 'template_path': 'nosystemd/emails/' - # } - # email = BaseEmail(**email_data) - # email.send() - - # print("--------- PAYMENT DONATION SUCCESSFULL ---------") - # print("Donator: %s" % donation.donator.user.email) - # print("Amount: %s %s" % (donation.donation, self.CURRENCY)) - # print("-----------------------------------------------") - # except Exception as e: - # print("--------- ERROR ---------") - # print(e) - # print("-------------------------") - # continue diff --git a/digitalglarus/migrations/0020_auto_20161013_0253.py b/digitalglarus/migrations/0020_auto_20161013_0253.py new file mode 100644 index 00000000..f9d49868 --- /dev/null +++ b/digitalglarus/migrations/0020_auto_20161013_0253.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-13 02:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('digitalglarus', '0019_auto_20160929_0324'), + ] + + operations = [ + migrations.AlterField( + model_name='bookingorder', + name='cc_brand', + field=models.CharField(blank=True, max_length=10), + ), + migrations.AlterField( + model_name='bookingorder', + name='last4', + field=models.CharField(blank=True, max_length=4), + ), + migrations.AlterField( + model_name='membershiporder', + name='cc_brand', + field=models.CharField(blank=True, max_length=10), + ), + migrations.AlterField( + model_name='membershiporder', + name='last4', + field=models.CharField(blank=True, max_length=4), + ), + ] diff --git a/digitalglarus/mixins.py b/digitalglarus/mixins.py index 013cdd78..5392ad37 100644 --- a/digitalglarus/mixins.py +++ b/digitalglarus/mixins.py @@ -32,8 +32,8 @@ class Ordereable(models.Model): billing_address = models.ForeignKey(BillingAddress) created_at = models.DateTimeField(auto_now_add=True) approved = models.BooleanField(default=False) - last4 = models.CharField(max_length=4) - cc_brand = models.CharField(max_length=10) + last4 = models.CharField(max_length=4, blank=True) + cc_brand = models.CharField(max_length=10, blank=True) stripe_charge_id = models.CharField(max_length=100, null=True) class Meta: @@ -43,6 +43,8 @@ class Ordereable(models.Model): def create(cls, data): stripe_charge = data.pop('stripe_charge', None) instance = cls.objects.create(**data) + if not stripe_charge: + return instance instance.stripe_charge_id = stripe_charge.id instance.last4 = stripe_charge.source.last4 instance.cc_brand = stripe_charge.source.brand diff --git a/digitalglarus/models.py b/digitalglarus/models.py index 17621bfd..9d423f3d 100644 --- a/digitalglarus/models.py +++ b/digitalglarus/models.py @@ -71,12 +71,12 @@ class Membership(models.Model): @classmethod def is_digitalglarus_active_member(cls, user): past_month = (datetime.today() - relativedelta(months=1)).month - has_booking_current_month = Q(membershiporder__customer__user=user, - membershiporder__created_at__month=datetime.today().month) - has_booking_past_month = Q(membershiporder__customer__user=user, - membershiporder__created_at__month=past_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_booking_past_month | has_booking_current_month).\ + return cls.objects.filter(has_order_past_month | has_order_current_month).\ filter(active_membership).exists() def deactivate(self): @@ -147,7 +147,7 @@ class Booking(models.Model): def get_ramaining_free_days(cls, user, start_date, end_date): TWO_DAYS = 2 - + ONE_DAY = 1 start_month = start_date.month end_month = end_date.month months = abs(start_month - (end_month + 12) if end_month < start_month @@ -156,6 +156,10 @@ class Booking(models.Model): current_month_bookings = cls.objects.filter(bookingorder__customer__user=user, start_date__month=current_date.month) free_days_this_month = TWO_DAYS - sum(map(lambda x: x.free_days, current_month_bookings)) + + if start_date == end_date and free_days_this_month == TWO_DAYS: + free_days_this_month = ONE_DAY + total_free_days = months * TWO_DAYS + free_days_this_month return total_free_days @@ -190,11 +194,11 @@ class Booking(models.Model): # Calculating membership required months price for booking required_membership_months = 0 membership_booking_price = 0.0 - if BookingOrder.user_has_not_bookings(user): - today = datetime.today().date() - membership_price = MembershipType.objects.get(name=MembershipType.STANDARD).price - required_membership_months = cls.membership_required_booking_months(today, end_date) - membership_booking_price = membership_price * required_membership_months + # if not BookingOrder.user_has_not_bookings(user): + today = datetime.today().date() + membership_price = MembershipType.objects.get(name=MembershipType.STANDARD).price + required_membership_months = cls.membership_required_booking_months(today, end_date) + membership_booking_price = membership_price * required_membership_months # Add required membership months to final prices final_booking_price += membership_booking_price @@ -210,11 +214,15 @@ class BookingOrder(Ordereable, models.Model): membership_required_months = models.IntegerField(default=0) membership_required_months_price = models.FloatField(default=0) - @classmethod def user_has_not_bookings(cls, user): return cls.objects.filter(customer__user=user).exists() + def get_booking_cc_data(self): + return { + 'last4': self.last4, + 'cc_brand': self.cc_brand, + } def booking_days(self): return (self.booking.end_date - self.booking.start_date).days + 1 diff --git a/digitalglarus/static/digitalglarus/css/agency.css b/digitalglarus/static/digitalglarus/css/agency.css index 7be17307..39289c6e 100755 --- a/digitalglarus/static/digitalglarus/css/agency.css +++ b/digitalglarus/static/digitalglarus/css/agency.css @@ -241,6 +241,8 @@ fieldset[disabled] .btn-xl.active { .navbar-default .navbar-collapse { border-color: rgba(255,255,255,.02); + padding-right: 100px; + text-align: right; } .navbar-default .navbar-toggle { @@ -259,7 +261,7 @@ fieldset[disabled] .btn-xl.active { .navbar-default .nav li a { text-transform: uppercase; - font-family: Montserrat,"Helvetica Neue",Helvetica,Arial,sans-serif; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-weight: 400; letter-spacing: 1px; color: #fff; diff --git a/digitalglarus/static/digitalglarus/css/price.css b/digitalglarus/static/digitalglarus/css/price.css index 22d37b99..359b47fe 100644 --- a/digitalglarus/static/digitalglarus/css/price.css +++ b/digitalglarus/static/digitalglarus/css/price.css @@ -197,7 +197,7 @@ } .glyphicon-ok { - font-size: 42px; + font-size: 28px; display: block; text-align: center; margin-bottom: 20px; @@ -364,7 +364,7 @@ } .form-control { - color: #999; + color: #000; border-radius: 0px; box-shadow: none; } diff --git a/digitalglarus/static/digitalglarus/js/payment.js b/digitalglarus/static/digitalglarus/js/payment.js index dd6c64d4..d9c6e0f7 100644 --- a/digitalglarus/static/digitalglarus/js/payment.js +++ b/digitalglarus/static/digitalglarus/js/payment.js @@ -24,12 +24,21 @@ $( document ).ready(function() { } }); + var submit_form_btn = $('#payment_button'); + submit_form_btn.on('click', submit_payment); + + function submit_payment(e){ + $('#billing-form').submit(); + // $form.submit(); + } + var $form = $('#payment-form'); $form.submit(payWithStripe); /* If you're using Stripe for payments */ function payWithStripe(e) { + console.log("submiting"); e.preventDefault(); /* Visual feedback */ diff --git a/digitalglarus/templates/digitalglarus/booking_orders_list.html b/digitalglarus/templates/digitalglarus/booking_orders_list.html index bff0e231..9fa3d793 100644 --- a/digitalglarus/templates/digitalglarus/booking_orders_list.html +++ b/digitalglarus/templates/digitalglarus/booking_orders_list.html @@ -39,11 +39,15 @@ -

Billing AdressEdit

-

Nico Schottelius
- In der Au 7 8762 Schwanden
- Switzerland +

Billing AdressEdit

+

+ {{request.user.name}}

+

+ {{billing_address.street_address}},{{billing_address.postal_code}}
+ {{billing_address.city}}, {{billing_address.country}}. +

+ diff --git a/digitalglarus/templates/digitalglarus/booking_payment.html b/digitalglarus/templates/digitalglarus/booking_payment.html index fda2a0e2..8613dcab 100644 --- a/digitalglarus/templates/digitalglarus/booking_payment.html +++ b/digitalglarus/templates/digitalglarus/booking_payment.html @@ -27,7 +27,7 @@

Booking


-

Billing Adress

+

Your Digital Glarus Membership enables you to use our coworking space and it includes @@ -37,7 +37,25 @@ 15CHF per day. More than 17 days a month it will charge only 290CHF/month.

+ {% if is_free %} +

Billing Adress

+
+
+ {% for field in form %} + {% csrf_token %} + {% bootstrap_field field show_label=False type='fields'%} + {% endfor %} + {% bootstrap_form_errors form type='non_fields'%} +
+
+
+
+
+

Your Booking is TOTALLY FREE

+

+ {% else %} +

Billing Adress

{% for field in form %} @@ -48,18 +66,26 @@
-

Credit Card

+ {% if credit_card_data %} +
+

Credit Card

+

Last 4: {{credit_card_data.last4}}

+

Type: {{credit_card_data.cc_brand}}

+ +
+ {% else %} +

Credit Card (Last used)

-
+
-
+
@@ -86,11 +112,6 @@
-
-
- -
-
@@ -136,18 +159,20 @@

Total

{{final_price|floatformat}}CHF

- +
+
+
+
- -
-
-
+
+
+
diff --git a/digitalglarus/templates/digitalglarus/membership_activated.html b/digitalglarus/templates/digitalglarus/membership_activated.html index f899f6ec..db641555 100644 --- a/digitalglarus/templates/digitalglarus/membership_activated.html +++ b/digitalglarus/templates/digitalglarus/membership_activated.html @@ -24,7 +24,7 @@ Go to Booking
-

Your membership will be automatically renewed each month. For deactivating go tomy page

+

Your membership will be automatically renewed each month. For deactivating go todeactivate page

diff --git a/digitalglarus/templates/digitalglarus/membership_deactivated.html b/digitalglarus/templates/digitalglarus/membership_deactivated.html index 3bfe3809..7ce1137f 100644 --- a/digitalglarus/templates/digitalglarus/membership_deactivated.html +++ b/digitalglarus/templates/digitalglarus/membership_deactivated.html @@ -22,7 +22,28 @@
{% csrf_token %} - + + + + + +
@@ -32,6 +53,9 @@

+ + +
diff --git a/digitalglarus/templates/digitalglarus/membership_orders_detail.html b/digitalglarus/templates/digitalglarus/membership_orders_detail.html index 817c818a..2f1ef140 100644 --- a/digitalglarus/templates/digitalglarus/membership_orders_detail.html +++ b/digitalglarus/templates/digitalglarus/membership_orders_detail.html @@ -60,6 +60,7 @@

Membership month {{order.created_at|date:"F"}}

{{order.amount|floatformat}}CHF

+

Total

{{order.amount|floatformat}}CHF

diff --git a/digitalglarus/templates/digitalglarus/membership_orders_list.html b/digitalglarus/templates/digitalglarus/membership_orders_list.html index 159e1379..8247835a 100644 --- a/digitalglarus/templates/digitalglarus/membership_orders_list.html +++ b/digitalglarus/templates/digitalglarus/membership_orders_list.html @@ -38,11 +38,14 @@ {% endfor %} - -

Billing AdressEdit

-

Nico Schottelius
- In der Au 7 8762 Schwanden
- Switzerland + +

Billing AdressEdit

+

+ {{request.user.name}} +

+

+ {{billing_address.street_address}},{{billing_address.postal_code}}
+ {{billing_address.city}}, {{billing_address.country}}.


@@ -50,14 +53,15 @@

Dates: {{membership_start_date|date}} - {{membership_end_date|date}}

-

+
- Deactivate -

+ Deactivate + +
-
- You will be charged on the first of the month until you - cancel your subscription. Previous charges won't be refunded. +
+

You will be charged on the first of the month until you + cancel your subscription. Previous charges won't be refunded.

@@ -66,19 +70,38 @@
+
-

Thank You!

+ + {% if messages %} +
+

Message

+
+ {% for message in messages %} + + {% endfor %} +
+ {% else %} + +

Thank You!

+ + +
+

This box is here just to thank you

+ + {% endif %} + + + -

This box is here just to thank you

- -
+
diff --git a/digitalglarus/templates/digitalglarus/membership_payment.html b/digitalglarus/templates/digitalglarus/membership_payment.html index bde934de..824d46a9 100644 --- a/digitalglarus/templates/digitalglarus/membership_payment.html +++ b/digitalglarus/templates/digitalglarus/membership_payment.html @@ -56,14 +56,14 @@
-
+
-
+
@@ -92,7 +92,7 @@
- +
+
+
+ + + + +
+
+
+
+
+ Digital Glarus
+ In der Au 7 Schwanden 8762 Switzerland +
info@digitalglarus.ch +
+ (044) 534-66-22 +

 

+
+
+

 

+
+
+
+
+ + +{% if stripe_key %} + + +{%endif%} + +{% endblock %} \ No newline at end of file diff --git a/digitalglarus/templates/new_base_glarus.html b/digitalglarus/templates/new_base_glarus.html index b8162c34..b156dd45 100644 --- a/digitalglarus/templates/new_base_glarus.html +++ b/digitalglarus/templates/new_base_glarus.html @@ -107,7 +107,7 @@
  • - booking & price + booking & price
  • history diff --git a/digitalglarus/test_views.py b/digitalglarus/test_views.py index 5658a73c..63b2cc06 100644 --- a/digitalglarus/test_views.py +++ b/digitalglarus/test_views.py @@ -1,11 +1,25 @@ import json +from model_mommy import mommy +from unittest import mock from django.test import TestCase +from django.conf import settings from django.core.urlresolvers import reverse from django.core.urlresolvers import resolve from cms.test_utils.testcases import CMSTestCase +from django.contrib.auth.tokens import default_token_generator +from django.utils.http import urlsafe_base64_encode +from django.utils.encoding import force_bytes from cms.api import create_page +from membership.models import CustomUser, StripeCustomer +from utils.tests import BaseTestCase + + +from .views import LoginView, SignupView, PasswordResetView, PasswordResetConfirmView,\ + MembershipPricingView, MembershipPaymentView +from .models import MembershipType, MembershipOrder + class ContactViewTest(TestCase): def setUp(self): @@ -38,14 +52,243 @@ class ViewsTest(CMSTestCase): self.assertEqual(res2.status_code, 200) -class CalendarApiTestCase(TestCase): - def test_api_response(self): - calendar_api_url_1 = reverse('digitalglarus:calendar_api_1', kwargs={'month': '3', 'year': '2016'}) - res1 = self.client.get(calendar_api_url_1) - pd = json.loads(res1.content.decode('utf-8')) - self.assertEqual(pd['month'], '3') - self.assertEqual(pd['year'], '2016') +class MembershipPricingViewTest(BaseTestCase): - # TODO:check post - # calendar_api_url = reverse('digitalglarus:calendar_api') - # res = self.client.get(calendar_api_url) + def setUp(self): + super(MembershipPricingViewTest, self).setUp() + + self.membership_type = mommy.make(MembershipType) + self.url = reverse('digitalglarus:membership_pricing') + self.view = MembershipPricingView + self.expected_template = 'digitalglarus/membership_pricing.html' + + def test_url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + # Anonymous user should get data + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['membership_type'], self.membership_type) + self.assertTemplateUsed(response, self.expected_template) + + +class MembershipPaymentViewTest(BaseTestCase): + + def setUp(self): + super(MembershipPaymentViewTest, self).setUp() + + self.membership_type = mommy.make(MembershipType) + self.url = reverse('digitalglarus:membership_payment') + self.view = MembershipPaymentView + self.expected_template = 'digitalglarus/membership_payment.html' + + # post data + self.billing_address = { + 'street_address': 'street name', + 'city': 'MyCity', + 'postal_code': '32123123123123', + 'country': 'VE', + 'token': 'a23kfmslwxhkwis', + 'membership_type': self.membership_type.id + } + + def test_url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + + # Anonymous user should get redirect to login + response = self.client.get(self.url) + expected_url = "%s?next=%s" % (reverse('digitalglarus:signup'), + reverse('digitalglarus:membership_payment')) + self.assertRedirects(response, expected_url=expected_url, + status_code=302, target_status_code=200) + + # Logged user should get the page + response = self.customer_client.get(self.url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['stripe_key'], + settings.STRIPE_API_PUBLIC_KEY) + self.assertEqual(response.context['membership_type'], + self.membership_type) + + @mock.patch('utils.stripe_utils.StripeUtils.create_customer') + def test_post(self, stripe_mocked_call): + + # Anonymous user should get redirect to login + # response = self.client.post(self.url) + # expected_url = "%s?next=%s" % (reverse('digitalglarus:signup'), + # reverse('digitalglarus:membership_payment')) + # self.assertRedirects(response, expected_url=expected_url, + # status_code=302, target_status_code=200) + + # Customer user should be able to pay + stripe_mocked_call.return_value = { + 'paid': True, + 'response_object': self.stripe_mocked_customer, + 'error': None + } + response = self.customer_client.post(self.url, self.billing_address) + self.assertEqual(response.status_code, 200) + self.assertTrue(StripeCustomer.objects.filter(user__email=self.customer.email).exists()) + stripe_customer = StripeCustomer.objects.get(user__email=self.customer.email) + self.assertEqual(stripe_customer.user, self.customer) + self.assertTrue(MembershipOrder.objects.filter(customer=stripe_customer).exists()) + membership_order = MembershipOrder.objects.filter(customer=stripe_customer).first() + session_data = { + 'membership_price': membership_order.membership.type.first_month_price, + 'membership_dates': membership_order.membership.type.first_month_formated_range + } + self.assertEqual(session_data.get('membership_price'), + self.session_data.get('membership_price')) + self.assertEqual(session_data.get('membership_dates'), + self.session_data.get('membership_dates')) + + # self.assertTrue(HostingOrder.objects.filter(customer=stripe_customer).exists()) + # hosting_order = HostingOrder.objects.filter(customer=stripe_customer)[0] + # vm_plan = { + # 'cores': hosting_order.vm_plan.cores, + # 'memory': hosting_order.vm_plan.memory, + # 'disk_size': hosting_order.vm_plan.disk_size, + # 'price': hosting_order.vm_plan.price, + # 'hosting_company': hosting_order.vm_plan.vm_type.hosting_company, + # 'configuration': hosting_order.vm_plan.configuration + # } + # self.assertEqual(vm_plan, self.session_data.get('vm_specs')) + + +class LoginViewTest(TestCase): + + def setUp(self): + self.url = reverse('digitalglarus:login') + self.view = LoginView + self.expected_template = 'digitalglarus/login.html' + self.user = mommy.make('membership.CustomUser') + self.password = 'fake_password' + self.user.set_password(self.password) + self.user.save() + + def test_url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, self.expected_template) + + def test_anonymous_user_can_login(self): + data = { + 'email': self.user.email, + 'password': self.password + } + response = self.client.post(self.url, data=data, follow=True) + self.assertEqual(response.context['user'], self.user) + self.assertEqual(response.status_code, 200) + + +class SignupViewTest(TestCase): + + def setUp(self): + self.url = reverse('digitalglarus:signup') + self.expected_template = 'digitalglarus/signup.html' + self.view = SignupView + self.signup_data = { + 'name': 'ungleich', + 'email': 'test@ungleich.com', + 'password': 'fake_password', + 'confirm_password': 'fake_password', + } + + def test_url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, self.expected_template) + + def test_anonymous_user_can_signup(self): + response = self.client.post(self.url, data=self.signup_data, follow=True) + self.user = CustomUser.objects.get(email=self.signup_data.get('email')) + self.assertEqual(response.context['user'], self.user) + self.assertEqual(response.status_code, 200) + + +class PasswordResetViewTest(BaseTestCase): + + def setUp(self): + super(PasswordResetViewTest, self).setUp() + + self.url = reverse('digitalglarus:reset_password') + self.view = PasswordResetView + self.expected_template = 'digitalglarus/reset_password.html' + self.user = mommy.make('membership.CustomUser') + self.password = 'fake_password' + self.user.set_password(self.password) + self.user.save() + + self.post_data = { + 'email': self.user.email + } + + def test_url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, self.expected_template) + + def test_post(self): + response = self.client.post(self.url, data=self.post_data, follow=True) + self.assertEqual(response.status_code, 200) + + def test_test_generate_email_context(self): + context = self.setup_view(self.view()).\ + test_generate_email_context(self.user) + self.assertEqual(context.get('user'), self.user) + self.assertEqual(context.get('site_name'), 'ungleich') + self.assertEqual(len(context.get('token')), 24) + + +class PasswordResetConfirmViewTest(BaseTestCase): + + def setUp(self): + super(PasswordResetConfirmViewTest, self).setUp() + + self.view = PasswordResetConfirmView + self.expected_template = 'digitalglarus/confirm_reset_password.html' + self.user = mommy.make('membership.CustomUser') + self.password = 'fake_password' + self.user.set_password(self.password) + self.user.save() + + self.token = default_token_generator.make_token(self.user) + self.uid = urlsafe_base64_encode(force_bytes(self.user.pk)) + self.url = reverse('digitalglarus:reset_password_confirm', + kwargs={'token': self.token, 'uidb64': self.uid}) + + self.post_data = { + 'new_password1': 'new_password', + 'new_password2': 'new_password' + } + + def test_url_resolve_to_view_correctly(self): + found = resolve(self.url) + self.assertEqual(found.func.__name__, self.view.__name__) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, self.expected_template) + + def test_post(self): + response = self.client.post(self.url, data=self.post_data, follow=True) + self.assertEqual(response.status_code, 200) + self.assertTrue(not response.context['form'].errors) diff --git a/digitalglarus/urls.py b/digitalglarus/urls.py index 8e3960df..6ed5271a 100644 --- a/digitalglarus/urls.py +++ b/digitalglarus/urls.py @@ -6,7 +6,7 @@ from .views import ContactView, IndexView, AboutView, HistoryView, LoginView, Si PasswordResetView, PasswordResetConfirmView, MembershipPaymentView, MembershipActivatedView,\ MembershipPricingView, BookingSelectDatesView, BookingPaymentView, OrdersBookingDetailView,\ BookingOrdersListView, MembershipOrdersListView, OrdersMembershipDetailView, \ - MembershipDeactivateView, MembershipDeactivateSuccessView + MembershipDeactivateView, MembershipDeactivateSuccessView, UserBillingAddressView # from membership.views import LoginRegistrationView urlpatterns = [ @@ -20,6 +20,7 @@ urlpatterns = [ url(r'reset-password-confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', PasswordResetConfirmView.as_view(), name='reset_password_confirm'), url(_(r'history/?$'), HistoryView.as_view(), name='history'), + url(_(r'users/billing_address/?$'), UserBillingAddressView.as_view(), name='user_billing_address'), url(_(r'booking/?$'), BookingSelectDatesView.as_view(), name='booking'), url(_(r'booking/payment/?$'), BookingPaymentView.as_view(), name='booking_payment'), url(_(r'booking/orders/(?P\d+)/?$'), OrdersBookingDetailView.as_view(), diff --git a/digitalglarus/views.py b/digitalglarus/views.py index 0148620c..f2dfb4ab 100644 --- a/digitalglarus/views.py +++ b/digitalglarus/views.py @@ -27,8 +27,9 @@ from membership.models import Calendar as CalendarModel, StripeCustomer from utils.views import LoginViewMixin, SignupViewMixin, \ PasswordResetViewMixin, PasswordResetConfirmViewMixin -from utils.forms import PasswordResetRequestForm +from utils.forms import PasswordResetRequestForm, UserBillingAddressForm from utils.stripe_utils import StripeUtils +from utils.models import UserBillingAddress from .forms import LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,\ @@ -120,6 +121,7 @@ class BookingSelectDatesView(LoginRequiredMixin, MembershipRequiredMixin, FormVi 'free_days': free_days, 'start_date': start_date.strftime('%m/%d/%Y'), 'end_date': end_date.strftime('%m/%d/%Y'), + 'is_free': final_price == 0 }) return super(BookingSelectDatesView, self).form_valid(form) @@ -132,7 +134,7 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView): booking_needed_fields = ['original_price', 'final_price', 'booking_days', 'free_days', 'start_date', 'end_date', 'membership_required_months_price', 'membership_required_months', 'booking_price_per_day', - 'total_discount'] + 'total_discount', 'is_free'] def dispatch(self, request, *args, **kwargs): from_booking = all(field in request.session.keys() @@ -146,12 +148,17 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView): return reverse('digitalglarus:booking_orders_detail', kwargs={'pk': order_id}) def get_form_kwargs(self): + current_billing_address = self.request.user.billing_addresses.first() form_kwargs = super(BookingPaymentView, self).get_form_kwargs() form_kwargs.update({ 'initial': { 'start_date': self.request.session.get('start_date'), 'end_date': self.request.session.get('end_date'), 'price': self.request.session.get('final_price'), + 'street_address': current_billing_address.street_address, + 'city': current_billing_address.city, + 'postal_code': current_billing_address.postal_code, + 'country': current_billing_address.country, } }) return form_kwargs @@ -161,11 +168,15 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView): booking_data = {key: self.request.session.get(key) for key in self.booking_needed_fields} + user = self.request.user + last_booking_order = BookingOrder.objects.filter(customer__user=user).last() # booking_price_per_day = BookingPrice.objects.get().price_per_day # total_discount = booking_price_per_day * booking_data.get('free_days') booking_data.update({ # 'booking_price_per_day': booking_price_per_day, - # 'total_discount': total_discount, + # 'current_billing_address': self.request.user.billing_addresses.first().to_dict(), + 'credit_card_data': last_booking_order.get_booking_cc_data() if last_booking_order + else None, 'stripe_key': settings.STRIPE_API_PUBLIC_KEY }) context.update(booking_data) @@ -177,11 +188,12 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView): token = data.get('token') start_date = data.get('start_date') end_date = data.get('end_date') - + is_free = context.get('is_free') normal_price, final_price, free_days, membership_required_months,\ membership_required_months_price = Booking.\ booking_price(self.request.user, start_date, end_date) + # if not credit_card_needed: # Get or create stripe customer customer = StripeCustomer.get_or_create(email=self.request.user.email, token=token) @@ -189,6 +201,44 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView): form.add_error("__all__", "Invalid credit card") return self.render_to_response(self.get_context_data(form=form)) + if is_free: + billing_address = form.save() + + # Create Billing Address for User if he does not have one + if not customer.user.billing_addresses.count(): + data.update({ + 'user': customer.user.id + }) + billing_address_user_form = UserBillingAddressForm(data) + billing_address_user_form.is_valid() + billing_address_user_form.save() + + # Create membership plan + 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, + } + booking = Booking.create(booking_data) + + # Create membership order + order_data = { + 'booking': booking, + 'customer': customer, + 'billing_address': billing_address, + 'amount': final_price, + 'original_price': normal_price, + 'special_month_price': BookingPrice.objects.last().special_month_price, + 'membership_required_months': membership_required_months, + 'membership_required_months_price': membership_required_months_price, + } + order = BookingOrder.create(order_data) + + return HttpResponseRedirect(self.get_success_url(order.id)) + # Make stripe charge to a customer stripe_utils = StripeUtils() charge_response = stripe_utils.make_charge(amount=final_price, @@ -205,9 +255,18 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView): charge = charge_response.get('response_object') - # Create Billing Address + # Create Billing Address for Membership Order billing_address = form.save() + # Create Billing Address for User if he does not have one + if not customer.user.billing_addresses.count(): + data.update({ + 'user': customer.user.id + }) + billing_address_user_form = UserBillingAddressForm(data) + billing_address_user_form.is_valid() + billing_address_user_form.save() + # Create membership plan booking_data = { 'start_date': start_date, @@ -319,6 +378,7 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView): 'stripe_charge': charge, 'amount': membership_type.first_month_price } + membership_order = MembershipOrder.create(order_data) request.session.update({ @@ -369,6 +429,7 @@ class MembershipActivatedView(TemplateView): class MembershipDeactivateView(LoginRequiredMixin, UpdateView): template_name = "digitalglarus/membership_deactivated.html" model = Membership + success_message = "Your membership has been deactivated :(" success_url = reverse_lazy('digitalglarus:membership_orders_list') login_url = reverse_lazy('digitalglarus:login') fields = '__all__' @@ -384,9 +445,38 @@ 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) + return HttpResponseRedirect(self.success_url) +class UserBillingAddressView(LoginRequiredMixin, UpdateView): + model = UserBillingAddress + form_class = UserBillingAddressForm + template_name = "digitalglarus/user_billing_address.html" + success_url = reverse_lazy('digitalglarus:user_billing_address') + + def get_form_kwargs(self): + current_billing_address = self.request.user.billing_addresses.first() + form_kwargs = super(UserBillingAddressView, self).get_form_kwargs() + form_kwargs.update({ + 'initial': { + 'street_address': current_billing_address.street_address, + 'city': current_billing_address.city, + 'postal_code': current_billing_address.postal_code, + 'country': current_billing_address.country, + } + }) + return form_kwargs + + def get_object(self): + current_billing_address = self.request.user.billing_addresses.filter(current=True).last() + if not current_billing_address: + raise AttributeError("Billing Address does not exists") + return current_billing_address + + class MembershipDeactivateSuccessView(LoginRequiredMixin, TemplateView): template_name = "digitalglarus/membership_deactivated_success.html" @@ -401,9 +491,11 @@ class MembershipOrdersListView(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super(MembershipOrdersListView, self).get_context_data(**kwargs) start_date, end_date = MembershipOrder.current_membership(self.request.user) + current_billing_address = self.request.user.billing_addresses.filter(current=True).last() context.update({ 'membership_start_date': start_date, 'membership_end_date': end_date, + 'billing_address': current_billing_address }) return context @@ -477,6 +569,14 @@ class BookingOrdersListView(LoginRequiredMixin, ListView): model = BookingOrder paginate_by = 10 + def get_context_data(self, **kwargs): + context = super(BookingOrdersListView, self).get_context_data(**kwargs) + current_billing_address = self.request.user.billing_addresses.filter(current=True).last() + context.update({ + 'billing_address': current_billing_address + }) + return context + def get_queryset(self): queryset = super(BookingOrdersListView, self).get_queryset() queryset = queryset.filter(customer__user=self.request.user) diff --git a/utils/forms.py b/utils/forms.py index 10208265..80b6186b 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import ContactMessage, BillingAddress +from .models import ContactMessage, BillingAddress, UserBillingAddress from django.template.loader import render_to_string from django.core.mail import EmailMultiAlternatives from django.utils.translation import ugettext_lazy as _ @@ -109,6 +109,19 @@ class BillingAddressForm(forms.ModelForm): } +class UserBillingAddressForm(forms.ModelForm): + user = forms.ModelChoiceField(queryset=CustomUser.objects.all(), + widget=forms.HiddenInput()) + class Meta: + model = UserBillingAddress + fields = ['street_address', 'city', 'postal_code', 'country', 'user'] + labels = { + 'street_address': _('Street Address'), + 'city': _('City'), + 'postal_code': _('Postal Code'), + 'Country': _('Country'), + } + class ContactUsForm(forms.ModelForm): error_css_class = 'autofocus' diff --git a/utils/migrations/0003_userbillingaddress.py b/utils/migrations/0003_userbillingaddress.py new file mode 100644 index 00000000..f65faef4 --- /dev/null +++ b/utils/migrations/0003_userbillingaddress.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-08 20:00 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('utils', '0002_billingaddress'), + ] + + operations = [ + migrations.CreateModel( + name='UserBillingAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('street_address', models.CharField(max_length=100)), + ('city', models.CharField(max_length=50)), + ('postal_code', models.CharField(max_length=50)), + ('country', utils.fields.CountryField(choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AN', 'Netherlands Antilles'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'American Samoa'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia and Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BN', 'Brunei Darussalam'), ('BO', 'Bolivia'), ('BR', 'Brazil'), ('BS', 'Bahama'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CF', 'Central African Republic'), ('CG', 'Congo'), ('CH', 'Switzerland'), ('CI', 'Ivory Coast'), ('CK', 'Cook Iislands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands (Malvinas)'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('FX', 'France, Metropolitan'), ('GA', 'Gabon'), ('GB', 'United Kingdom (Great Britain)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia and the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Islamic Republic of Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St. Kitts and Nevis'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', "Lao People's Democratic Republic"), ('LB', 'Lebanon'), ('LC', 'Saint Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libyan Arab Jamahiriya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova, Republic of'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('ML', 'Mali'), ('MN', 'Mongolia'), ('MM', 'Myanmar'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Monserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St. Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St. Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen Islands'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SY', 'Syrian Arab Republic'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Islands'), ('TD', 'Chad'), ('TF', 'French Southern Territories'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TP', 'East Timor'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan, Province of China'), ('TZ', 'Tanzania, United Republic of'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'United States Minor Outlying Islands'), ('US', 'United States of America'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City State (Holy See)'), ('VC', 'St. Vincent & the Grenadines'), ('VE', 'Venezuela'), ('VG', 'British Virgin Islands'), ('VI', 'United States Virgin Islands'), ('VN', 'Viet Nam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna Islands'), ('WS', 'Samoa'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('YU', 'Yugoslavia'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZR', 'Zaire'), ('ZW', 'Zimbabwe'), ('ZZ', 'Unknown or unspecified country')], default='CH', max_length=2)), + ('current', models.BooleanField(default=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/utils/migrations/0004_auto_20161013_0253.py b/utils/migrations/0004_auto_20161013_0253.py new file mode 100644 index 00000000..f8a7017c --- /dev/null +++ b/utils/migrations/0004_auto_20161013_0253.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-10-13 02:53 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('utils', '0003_userbillingaddress'), + ] + + operations = [ + migrations.AlterField( + model_name='userbillingaddress', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='billing_addresses', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/utils/models.py b/utils/models.py index 1720e1b6..aadb4e72 100644 --- a/utils/models.py +++ b/utils/models.py @@ -1,20 +1,45 @@ from django.db import models +from django.core import serializers +from django.forms.models import model_to_dict + +from membership.models import CustomUser from .fields import CountryField + # Create your models here. - -class BillingAddress(models.Model): +class BaseBillingAddress(models.Model): street_address = models.CharField(max_length=100) city = models.CharField(max_length=50) postal_code = models.CharField(max_length=50) country = CountryField() + class Meta: + abstract = True + +class BillingAddress(BaseBillingAddress): + def __str__(self): return self.street_address +class UserBillingAddress(BaseBillingAddress): + user = models.ForeignKey(CustomUser, related_name='billing_addresses') + current = models.BooleanField(default=True) + + def __str__(self): + return self.street_address + + def to_dict(self): + return { + 'Street Address': self.street_address, + 'City': self.city, + 'Postal Code': self.postal_code, + 'Country': self.country, + } + + class ContactMessage(models.Model): name = models.CharField(max_length=200) email = models.EmailField()