From fb681b8bca4596d8d1c1c82e9ff98a372724b535 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Fri, 10 May 2019 11:52:06 -0500 Subject: [PATCH 01/18] First commit --- .env.sample | 2 +- .vscode/launch.json | 20 ++ .vscode/settings.json | 1 + Pipfile | 1 + Pipfile.lock | 113 +++++---- README.md | 7 + coderdojochi/old_views.py | 106 +++------ coderdojochi/settings.py | 11 +- coderdojochi/templates/donate.html | 306 ++++++++++-------------- coderdojochi/urls.py | 110 +++++---- weallcode/static/weallcode/css/app.css | 307 ++++++++++++++++++++----- 11 files changed, 576 insertions(+), 408 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.env.sample b/.env.sample index 27d86709..c2e819b7 100644 --- a/.env.sample +++ b/.env.sample @@ -3,7 +3,7 @@ ENABLE_DEV_FIXTURES=True SITE_NAME=We All Code SITE_URL=http://localhost:8000 -SECRET_KEY= +SECRET_KEY=8@azpb5u(t-udum_bsiwv(o$m7f^*=0qbga!27-%mf3y6ckv_7 SECURE_SSL_REDIRECT=False # Database diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..a761c1e7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}\\manage.py", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "django": true + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 70fc2471..142da964 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,4 +46,5 @@ "editor.quickSuggestions": false, "editor.trimAutoWhitespace": false, }, + "python.pythonPath": "C:\\Users\\Lokman Musliu\\AppData\\Local\\Programs\\Python\\Python36-32\\python.exe", } diff --git a/Pipfile b/Pipfile index a50f7ea4..8750ca84 100644 --- a/Pipfile +++ b/Pipfile @@ -32,6 +32,7 @@ icalendar = "*" json-logging-py = "*" django-anymail = "*" raven = "*" +dj-stripe = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 42f96d4d..28b7db16 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2f08e6e5f687eef606e91a2e6f74e97929ac231a311de6d5700883680791b361" + "sha256": "143432609575656a89c803e765f121e355065db3c34a1c0eb38266d3e96612bf" }, "pipfile-spec": 6, "requires": { @@ -33,18 +33,18 @@ }, "boto3": { "hashes": [ - "sha256:883f7143bcb081a834f7c09659524059b66745ea043fffd40420e88ef0143feb", - "sha256:9c789a775f0499743b083ffd63e0e87dae9a727511bb37f2529da52ccd25a360" + "sha256:484650b86ea843587f484a8f9cc9629465ad805aff0ffaabf95345960168f569", + "sha256:635e1864cd35d78d33fd7ce325f9baa15c93a932403953b2b4801567a791b869" ], "index": "pypi", - "version": "==1.9.134" + "version": "==1.9.143" }, "botocore": { "hashes": [ - "sha256:5c4d9ea1b0fbb1dc98b6a06ed8780096fca981a1c3599bf8f03f338e6aa389ae", - "sha256:c59a74539eb081f4b3a307fc5c3d69d8459e30bfaf4b94aa78e74a9a05583764" + "sha256:0247ad0da9fdbf4e8025b0dafb3982b945d335bcd7043518fdabe9d99f704e17", + "sha256:94846e90fc4dbe91a9e70f6a24ca823b4f3acc9a4047b497266d003fe12c80ce" ], - "version": "==1.12.134" + "version": "==1.12.143" }, "certifi": { "hashes": [ @@ -80,13 +80,21 @@ ], "version": "==0.5.0" }, + "dj-stripe": { + "hashes": [ + "sha256:603b307683557e1e5291efebabf5e080844053f50c3abc41d00b0738fe097f57", + "sha256:8e39fdbef0dda4d82597e2d1b56b674d520dd233abc916db569cb58433b82067" + ], + "index": "pypi", + "version": "==2.0.1" + }, "django": { "hashes": [ - "sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119", - "sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b" + "sha256:6fcc3cbd55b16f9a01f37de8bcbe286e0ea22e87096557f1511051780338eaea", + "sha256:bb407d0bb46395ca1241f829f5bd03f7e482f97f7d1936e26e98dacb201ed4ec" ], "index": "pypi", - "version": "==2.2" + "version": "==2.2.1" }, "django-allauth": { "hashes": [ @@ -221,8 +229,8 @@ }, "et-xmlfile": { "hashes": [ - "sha256:2b3fd7c36b5d9be5ae8f964a16429eb6f68bd36d836dc5b76309d3a51f1dd3d5", - "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b" + "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b", + "sha256:87d0c1be722fb61688e7f167aece7cae527faa158e2fe269a4bd75484ddda8e2" ], "version": "==1.0.1" }, @@ -294,13 +302,20 @@ "index": "pypi", "version": "==0.2" }, + "jsonfield": { + "hashes": [ + "sha256:a0a7fdee736ff049059409752b045281a225610fecbda9b9bd588ba976493c12", + "sha256:beb1cd4850d6d6351c32daefcb826c01757744e9c863228a642f87a1a4acb834" + ], + "version": "==2.0.2" + }, "mock": { "hashes": [ - "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", - "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" + "sha256:21a2c07af3bbc4a77f9d14ac18fcc1782e8e7ea363df718740cdeaf61995b5e7", + "sha256:7868db2825a1563578869d4a011a036503a2f1d60f9ff9dd1e3205cd6e25fcec" ], "index": "pypi", - "version": "==2.0.0" + "version": "==3.0.4" }, "nose": { "hashes": [ @@ -329,13 +344,6 @@ ], "version": "==2.6.2" }, - "pbr": { - "hashes": [ - "sha256:8257baf496c8522437e8a6cfe0f15e00aedc6c0e0e7c9d55eeeeab31e0853843", - "sha256:8c361cc353d988e4f5b998555c88098b9d5964c2e11acf7b0d21925a66bb5824" - ], - "version": "==5.1.3" - }, "pillow": { "hashes": [ "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55", @@ -483,6 +491,7 @@ "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], + "markers": "python_version >= '3.0'", "version": "==2.21.0" }, "requests-oauthlib": { @@ -513,6 +522,13 @@ ], "version": "==0.3.0" }, + "stripe": { + "hashes": [ + "sha256:43cf1addbd5685d166c483f29f2b13e514304b091086561c2e22a3c7664043a2", + "sha256:814e6a87fdb679cf2ddca9a12099a93aaf437dac0b09edac4851c6ba9ebd7e5f" + ], + "version": "==2.27.0" + }, "tablib": { "hashes": [ "sha256:0f88a9cebdaa1a2cc29ae57387082ee81015d1149ecd34e48a8c8d3b4dd21670", @@ -529,11 +545,11 @@ }, "urllib3": { "hashes": [ - "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", - "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" ], "markers": "python_version >= '3.4'", - "version": "==1.24.2" + "version": "==1.24.3" }, "whitenoise": { "hashes": [ @@ -574,10 +590,10 @@ }, "isort": { "hashes": [ - "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43", - "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a" + "sha256:1349c6f7c2a0f7539f5f2ace51a9a8e4a37086ce4de6f78f5f53fb041d0a3cd5", + "sha256:f09911f6eb114e5592abe635aded8bf3d2c3144ebcfcaf81ee32e7af7b7d1870" ], - "version": "==4.3.17" + "version": "==4.3.18" }, "lazy-object-proxy": { "hashes": [ @@ -644,29 +660,28 @@ }, "typed-ast": { "hashes": [ - "sha256:04894d268ba6eab7e093d43107869ad49e7b5ef40d1a94243ea49b352061b200", - "sha256:16616ece19daddc586e499a3d2f560302c11f122b9c692bc216e821ae32aa0d0", - "sha256:252fdae740964b2d3cdfb3f84dcb4d6247a48a6abe2579e8029ab3be3cdc026c", - "sha256:2af80a373af123d0b9f44941a46df67ef0ff7a60f95872412a145f4500a7fc99", - "sha256:2c88d0a913229a06282b285f42a31e063c3bf9071ff65c5ea4c12acb6977c6a7", - "sha256:2ea99c029ebd4b5a308d915cc7fb95b8e1201d60b065450d5d26deb65d3f2bc1", - "sha256:3d2e3ab175fc097d2a51c7a0d3fda442f35ebcc93bb1d7bd9b95ad893e44c04d", - "sha256:4766dd695548a15ee766927bf883fb90c6ac8321be5a60c141f18628fb7f8da8", - "sha256:56b6978798502ef66625a2e0f80cf923da64e328da8bbe16c1ff928c70c873de", - "sha256:5cddb6f8bce14325b2863f9d5ac5c51e07b71b462361fd815d1d7706d3a9d682", - "sha256:644ee788222d81555af543b70a1098f2025db38eaa99226f3a75a6854924d4db", - "sha256:64cf762049fc4775efe6b27161467e76d0ba145862802a65eefc8879086fc6f8", - "sha256:68c362848d9fb71d3c3e5f43c09974a0ae319144634e7a47db62f0f2a54a7fa7", - "sha256:6c1f3c6f6635e611d58e467bf4371883568f0de9ccc4606f17048142dec14a1f", - "sha256:b213d4a02eec4ddf622f4d2fbc539f062af3788d1f332f028a2e19c42da53f15", - "sha256:bb27d4e7805a7de0e35bd0cb1411bc85f807968b2b0539597a49a23b00a622ae", - "sha256:c9d414512eaa417aadae7758bc118868cd2396b0e6138c1dd4fda96679c079d3", - "sha256:f0937165d1e25477b01081c4763d2d9cdc3b18af69cb259dd4f640c9b900fe5e", - "sha256:fb96a6e2c11059ecf84e6741a319f93f683e440e341d4489c9b161eca251cf2a", - "sha256:fc71d2d6ae56a091a8d94f33ec9d0f2001d1cb1db423d8b4355debfe9ce689b7" + "sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b", + "sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d", + "sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a", + "sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462", + "sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee", + "sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a", + "sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4", + "sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649", + "sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a", + "sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f", + "sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7", + "sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760", + "sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18", + "sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616", + "sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd", + "sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21", + "sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93", + "sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb", + "sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7" ], "markers": "implementation_name == 'cpython'", - "version": "==1.3.4" + "version": "==1.3.5" }, "wrapt": { "hashes": [ diff --git a/README.md b/README.md index 5b7b625a..1bc8e0f2 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,13 @@ docker kill $(docker ps -q); docker-compose rm -f; docker volume rm $(docker vol docker-compose build ``` +### Adding a new django vendor app +1. Update the pipfile +2. run pipenv lock +3. run docker-compose build + + + [docker-mac]: https://www.docker.com/docker-mac [docker-windows]: https://www.docker.com/docker-windows [docker-toolbox]: https://www.docker.com/products/docker-toolbox diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index 9ddf2af4..81c59f0a 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -3,8 +3,18 @@ import operator from collections import Counter from datetime import date, timedelta +from decimal import * from functools import reduce +import arrow +from coderdojochi.forms import (CDCModelForm, ContactForm, DonationForm, + GuardianForm, MentorForm, StudentForm) +from coderdojochi.models import (Donation, Equipment, EquipmentType, Guardian, + Meeting, MeetingOrder, Mentor, MentorOrder, + Order, PartnerPasswordAccess, Session, + Student) +from coderdojochi.util import email +from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model @@ -20,29 +30,11 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView - -import arrow -from dateutil.relativedelta import relativedelta +# Stripe specific imports +from djstripe.models import Charge from icalendar import Calendar, Event, vText from paypal.standard.forms import PayPalPaymentsForm -from coderdojochi.forms import CDCModelForm, ContactForm, DonationForm, GuardianForm, MentorForm, StudentForm -from coderdojochi.models import ( - Donation, - Equipment, - EquipmentType, - Guardian, - Meeting, - MeetingOrder, - Mentor, - MentorOrder, - Order, - PartnerPasswordAccess, - Session, - Student, -) -from coderdojochi.util import email - logger = logging.getLogger(__name__) # this will assign User to our custom CDCUser @@ -467,67 +459,23 @@ def student_detail( def donate(request, template_name="donate.html"): - if request.method == 'POST': - - # if new donation form submit - if ( - 'first_name' in request.POST and - 'last_name' in request.POST and - 'email' in request.POST and - 'amount' in request.POST - ): - donation = Donation( - first_name=request.POST['first_name'], - last_name=request.POST['last_name'], - email=request.POST['email'], - amount=request.POST['amount'], - ) - if 'referral_code' in request.POST and request.POST['referral_code']: - donation.referral_code = request.POST['referral_code'] + return render( + request, + template_name + ) - donation.save() - return HttpResponse(donation.id) +@csrf_exempt +def donate_charge(request): - else: - return HttpResponse('fail') - - referral_heading = None - referral_code = None - referral_disclaimer = None - - if 'ref' in request.GET: - referral_code = request.GET['ref'] - - paypal_dict = { - 'business': settings.PAYPAL_BUSINESS_ID, - 'amount': '25', - 'item_name': 'We All Code Donation', - 'cmd': '_donations', - 'lc': 'US', - 'invoice': '', - 'currency_code': 'USD', - 'no_note': '0', - 'cn': 'Add a message for We All Code to read:', - 'no_shipping': '1', - 'address_override': '1', - 'first_name': '', - 'last_name': '', - 'notify_url': request.build_absolute_uri(reverse('paypal-ipn')), - 'return_url': request.build_absolute_uri('return'), - 'cancel_return': request.build_absolute_uri('cancel'), - 'bn': 'PP-DonationsBF:btn_donateCC_LG.gif:NonHosted' - } - - form = PayPalPaymentsForm(initial=paypal_dict) + charge = Charge( + amount=Decimal(10.00), + currency='usd', + description='Donation Charge' + ) - return render(request, template_name, { - 'form': form, - 'referral_heading': referral_heading, - 'referral_code': referral_code, - 'referral_disclaimer': referral_disclaimer - }) + return redirect('donate') @csrf_exempt @@ -567,7 +515,8 @@ def contact(request, template_name="contact.html"): email( subject=f"{request.POST['name']} | We All Code Contact Form", recipients=[settings.CONTACT_EMAIL], - reply_to=[f"{request.POST['name']}<{request.POST['email']}>"], + reply_to=[ + f"{request.POST['name']}<{request.POST['email']}>"], template_name='contact-email', merge_global_data={ 'message': request.POST['message'], @@ -788,7 +737,8 @@ def session_stats(request, pk, template_name="session-stats.html"): if current_orders_checked_in: student_ages = [] for order in current_orders_checked_in: - student_ages.append(order.student.get_age(order.session.start_date)) + student_ages.append( + order.student.get_age(order.session.start_date)) average_age = ( reduce( diff --git a/coderdojochi/settings.py b/coderdojochi/settings.py index e4ba5d75..2088eb9c 100644 --- a/coderdojochi/settings.py +++ b/coderdojochi/settings.py @@ -94,7 +94,8 @@ 'stdimage', 'import_export', 'django_nose', - + 'djstripe', + # apps 'accounts', 'coderdojochi', @@ -306,6 +307,14 @@ def MediaRootS3BotoStorage(): return S3Boto3Storage(location='media', file_overw PAYPAL_BUSINESS_ID = os.environ.get('PAYPAL_BUSINESS_ID') PAYPAL_TEST = os.environ.get('PAYPAL_TEST') == 'True' +# Stripe +STRIPE_LIVE_PUBLIC_KEY = os.environ.get('STRIPE_LIVE_PUBLIC_KEY', '') +STRIPE_LIVE_SECRET_KEY = os.environ.get('STRIPE_LIVE_SECRET_KEY', '') +STRIPE_TEST_PUBLIC_KEY = os.environ.get('STRIPE_TEST_PUBLIC_KEY') +STRIPE_TEST_SECRET_KEY = os.environ.get('STRIPE_TEST_SECRET_KEY') +STRIPE_LIVE_MODE = os.environ.get('STRIPE_LIVE_MODE') +DJSTRIPE_WEBHOOK_SECRET = os.environ.get('DJSTRIPE_WEBHOOK_SECRET') + # django allauth LOGIN_REDIRECT_URL = '/account' diff --git a/coderdojochi/templates/donate.html b/coderdojochi/templates/donate.html index b6ddb70a..fbcc861d 100644 --- a/coderdojochi/templates/donate.html +++ b/coderdojochi/templates/donate.html @@ -8,199 +8,147 @@ {% block contained_content %} -
-
-

- {% if referral_heading %}{{ referral_heading }}{% else %}Donate Today!{% endif %} -

-
    -
  • -
  • -
  • -
  • -
  • -
-
- $ - +
+

{% trans "Donate" %}

+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ $ +
-
-

Please tell us a little about yourself

-
- -
-
- -
-
- -
-
- -
-

We will email you a receipt
(includes tax exempt information)

- - +
+
+
+ +
+ +
+ + +
+
+ +
+
+
-

{% trans "You can help!" %}

-

We All Code is a registered non-profit organization supported entirely by...you! Please help us continue our mission by donating a small amount to our cause. Thank you!

+

{% trans "You can help!" %}

+

We All Code is a registered non-profit organization supported entirely by...you! Please help us continue our + mission by donating a small amount to our cause. Thank you!

- {% if referral_disclaimer %} -

{{ referral_disclaimer }}

- {% endif %} + {% if referral_disclaimer %} +

{{ referral_disclaimer }}

+ {% endif %} - {{ form.render }} + {{ form.render }}
{% endblock %} {% block extra_script %} - + {% endblock %} diff --git a/coderdojochi/urls.py b/coderdojochi/urls.py index a8810352..e72deb17 100644 --- a/coderdojochi/urls.py +++ b/coderdojochi/urls.py @@ -7,33 +7,19 @@ from django.urls import path from django.views import defaults from django.views.generic import RedirectView - from loginas import views as loginas_views from . import old_views from .views.general import AboutView, HomeView, PrivacyView, WelcomeView -from .views.meetings import ( - MeetingCalendarRedirectView, - MeetingCalendarView, - MeetingDetailRedirectView, - MeetingDetailView, - MeetingsView, - meeting_announce, - meeting_sign_up, -) +from .views.meetings import (MeetingCalendarRedirectView, MeetingCalendarView, + MeetingDetailRedirectView, MeetingDetailView, + MeetingsView, meeting_announce, meeting_sign_up) from .views.profile import DojoMentorView -from .views.sessions import ( - PasswordSessionRedirectView, - PasswordSessionView, - SessionCalendarRedirectView, - SessionCalendarView, - SessionDetailRedirectView, - SessionDetailView, - SessionSignUpRedirectView, - SessionSignUpView, - SessionsRedirectView, - SessionsView, -) +from .views.sessions import (PasswordSessionRedirectView, PasswordSessionView, + SessionCalendarRedirectView, SessionCalendarView, + SessionDetailRedirectView, SessionDetailView, + SessionSignUpRedirectView, SessionSignUpView, + SessionsRedirectView, SessionsView) from .views.volunteer import VolunteerView admin.autodiscover() @@ -75,6 +61,9 @@ # /donate/ path('', old_views.donate, name='donate'), + # /donate/charge + path('charge/', old_views.donate_charge, name='donate-charge'), + # /donate/cancel/ path('cancel/', old_views.donate_cancel, name='donate-cancel'), @@ -109,18 +98,22 @@ path('/stats/', old_views.session_stats, name='stats'), # /admin/classes/ID/check-in/ - path('/check-in/', old_views.session_check_in, name='student-check-in'), + path('/check-in/', old_views.session_check_in, + name='student-check-in'), # /admin/classes/ID/check-in-mentors/ - path('/check-in-mentors/', old_views.session_check_in_mentors, name='mentor-check-in'), + path('/check-in-mentors/', + old_views.session_check_in_mentors, name='mentor-check-in'), # /admin/classes/ID/donations/ - path('/donations/', old_views.session_donations, name='donations'), + path('/donations/', + old_views.session_donations, name='donations'), ])), path('meetings/', include([ # /admin/meeting/ID/check-in/ - path('/check-in/', old_views.meeting_check_in, name='meeting-check-in'), + path('/check-in/', + old_views.meeting_check_in, name='meeting-check-in'), ])), # Admin Check System @@ -143,25 +136,31 @@ # Password # /classes/ID/password/ - path('/password/', PasswordSessionView.as_view(), name='session-password'), + path('/password/', PasswordSessionView.as_view(), + name='session-password'), # Announce # /classes/ID/announce/mentors/ - path('/announce/mentors/', old_views.session_announce_mentors, name='session-announce-mentors'), + path('/announce/mentors/', old_views.session_announce_mentors, + name='session-announce-mentors'), # /classes/ID/announce/guardians/ - path('/announce/guardians/', old_views.session_announce_guardians, name='session-announce-guardians'), + path('/announce/guardians/', old_views.session_announce_guardians, + name='session-announce-guardians'), # Calendar # /classes/ID/calendar/ - path('/calendar/', SessionCalendarView.as_view(), name='session-calendar'), + path('/calendar/', SessionCalendarView.as_view(), + name='session-calendar'), # Sign up # /classes/ID/sign-up/ - path('/sign-up/', SessionSignUpView.as_view(), name='session-sign-up'), + path('/sign-up/', SessionSignUpView.as_view(), + name='session-sign-up'), # /classes/ID/sign-up/STUDENT-ID/ - path('/sign-up//', SessionSignUpView.as_view(), name='session-sign-up'), + path('/sign-up//', + SessionSignUpView.as_view(), name='session-sign-up'), ])), # TODO: Old redirects. Remove by July 2018 @@ -173,16 +172,20 @@ path('/', SessionDetailRedirectView.as_view()), # Redirect /class/YYYY/MM/DD/SLUG/ID/ -> /classes/ID/ - path('/////', SessionDetailRedirectView.as_view()), + path('/////', + SessionDetailRedirectView.as_view()), # Redirect /class/YYYY/MM/DD/SLUG/ID/password/ -> /classes/ID/password/ - path('/////password/', PasswordSessionRedirectView.as_view()), + path('/////password/', + PasswordSessionRedirectView.as_view()), # Redirect /class/YYYY/MM/DD/SLUG/ID/calendar/ -> /classes/ID/calendar/ - path('/////calendar/', SessionCalendarRedirectView.as_view()), + path('/////calendar/', + SessionCalendarRedirectView.as_view()), # Redirect /class/YYYY/MM/DD/SLUG/ID/sign-up/ -> /classes/ID/sign-up/ - path('/////sign-up/', SessionSignUpRedirectView.as_view()), + path('/////sign-up/', + SessionSignUpRedirectView.as_view()), # Redirect /class/YYYY/MM/DD/SLUG/ID/sign-up/STUDENT-ID/ -> /classes/ID/sign-up/STUDENT-ID/ path( @@ -212,10 +215,12 @@ path('/', old_views.mentor_detail, name='mentor-detail'), # /ID/reject-avatar/ - path('/reject-avatar/', old_views.mentor_reject_avatar, name='mentor-reject-avatar'), + path('/reject-avatar/', old_views.mentor_reject_avatar, + name='mentor-reject-avatar'), # /ID/approve-avatar/ - path('/approve-avatar/', old_views.mentor_approve_avatar, name='mentor-approve-avatar'), + path('/approve-avatar/', old_views.mentor_approve_avatar, + name='mentor-approve-avatar'), ])), # TODO: Old redirects. Remove by July 2018 @@ -223,8 +228,10 @@ path('mentor/', include([ path('', RedirectView.as_view(pattern_name='mentors')), path('/', RedirectView.as_view(pattern_name='mentor-detail')), - path('/reject-avatar/', RedirectView.as_view(pattern_name='mentor-reject-avatar')), - path('/approve-avatar/', RedirectView.as_view(pattern_name='mentor-approve-avatar')), + path('/reject-avatar/', + RedirectView.as_view(pattern_name='mentor-reject-avatar')), + path('/approve-avatar/', + RedirectView.as_view(pattern_name='mentor-approve-avatar')), ])), ] @@ -232,7 +239,8 @@ urlpatterns += [ # Student # /student/ID/ - path('students//', old_views.student_detail, name='student-detail'), + path('students//', + old_views.student_detail, name='student-detail'), ] # Dojo @@ -266,7 +274,8 @@ # Meeting Calendar # /meeting/ID/calendar/ - path('/calendar/', MeetingCalendarView.as_view(), name='meeting-calendar'), + path('/calendar/', MeetingCalendarView.as_view(), + name='meeting-calendar'), ])), # TODO: Old redirects. Remove by July 2018 @@ -279,7 +288,8 @@ path('/', MeetingDetailRedirectView.as_view()), # Redirect /meeting/YYYY/MM/DD/SLUG/ID/ -> /meeting/ID/ - path('/////', MeetingDetailRedirectView.as_view()), + path('/////', + MeetingDetailRedirectView.as_view()), # Redirect /meeting/ID/calendar/ -> /classes/ID/calendar/ path('/calendar/', MeetingCalendarRedirectView.as_view()), @@ -289,7 +299,8 @@ # robots.txt urlpatterns += [ - path('robots.txt', lambda r: HttpResponse('User-agent: *\nDisallow:', content_type='text/plain')) + path('robots.txt', lambda r: HttpResponse( + 'User-agent: *\nDisallow:', content_type='text/plain')) ] # Anymail @@ -297,6 +308,11 @@ path('anymail/', include('anymail.urls')), ] +# Stripe +urlpatterns += [ + path('stripe/', include('djstripe.urls', namespace='djstripe')), +] + # Media urlpatterns += static( settings.MEDIA_URL, @@ -331,7 +347,8 @@ if 'debug_toolbar' in settings.INSTALLED_APPS: import debug_toolbar - urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns + urlpatterns = [ + path('__debug__/', include(debug_toolbar.urls))] + urlpatterns else: from django.contrib.staticfiles.storage import staticfiles_storage @@ -358,7 +375,8 @@ urlpatterns += [ path( favicon, - RedirectView.as_view(url=staticfiles_storage.url(favicon), permanent=False), + RedirectView.as_view( + url=staticfiles_storage.url(favicon), permanent=False), name=favicon ), ] diff --git a/weallcode/static/weallcode/css/app.css b/weallcode/static/weallcode/css/app.css index 437f4943..a1727e80 100644 --- a/weallcode/static/weallcode/css/app.css +++ b/weallcode/static/weallcode/css/app.css @@ -22,51 +22,179 @@ body { font-family: 'Open Sans', sans-serif; } -.text-primary { color: var(--we-blue); } -.text-secondary { color: var(--all-yellow); } -.text-tertiary { color: var(--code-red); } -.text-dark-blue { color: var(--dark-blue); } -.text-black { color: var(--black); } -.text-white { color: var(--white); } -.text-light-gray { color: var(--light-gray); } -.text-grey-for-white { color: var(--gray-4-white); } -.text-grey-for-blue { color: var(--gray-4-blue); } - -.fill-primary { fill: var(--we-blue); } -.fill-secondary { fill: var(--all-yellow); } -.fill-tertiary { fill: var(--code-red); } -.fill-dark-blue { fill: var(--dark-blue); } -.fill-black { fill: var(--black); } -.fill-white { fill: var(--white); } -.fill-light-gray { fill: var(--light-gray); } -.fill-gray-for-white { fill: var(--gray-4-white); } -.fill-gray-for-blue { fill: var(--gray-4-blue); } - -.bg-primary { background-color: var(--we-blue) !important; } -.bg-secondary { background-color: var(--all-yellow) !important; } -.bg-tertiary { background-color: var(--code-red) !important; } -.bg-dark-blue { background-color: var(--dark-blue) !important; } -.bg-black { background-color: var(--black) !important; } -.bg-white { background-color: var(--white) !important; } -.bg-light-gray { background-color: var(--light-gray) !important; } -.bg-gray-for-white { background-color: var(--gray-4-white) !important; } -.bg-gray-for-blue { background-color: var(--gray-4-blue) !important; } - -.margin-half { margin-top: 0.5rem; margin-bottom: 0.5rem; margin-left: 0.5rem; margin-right: 0.5rem; } -.margin-vertical-half { margin-top: 0.5rem; margin-bottom: 0.5rem; } -.margin-horizontal-half { margin-left: 0.5rem; margin-right: 0.5rem; } -.margin-top-half { margin-top: 0.5rem; } -.margin-right-half { margin-right: 0.5rem; } -.margin-bottom-half { margin-bottom: 0.5rem; } -.margin-left-half { margin-left: 0.5rem; } - -.padding-half { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 0.5rem; padding-right: 0.5rem; } -.padding-vertical-half { padding-top: 0.5rem; padding-bottom: 0.5rem; } -.padding-horizontal-half { padding-left: 0.5rem; padding-right: 0.5rem; } -.padding-top-half { padding-top: 0.5rem; } -.padding-right-half { padding-right: 0.5rem; } -.padding-bottom-half { padding-bottom: 0.5rem; } -.padding-left-half { padding-left: 0.5rem; } +.text-primary { + color: var(--we-blue); +} + +.text-secondary { + color: var(--all-yellow); +} + +.text-tertiary { + color: var(--code-red); +} + +.text-dark-blue { + color: var(--dark-blue); +} + +.text-black { + color: var(--black); +} + +.text-white { + color: var(--white); +} + +.text-light-gray { + color: var(--light-gray); +} + +.text-grey-for-white { + color: var(--gray-4-white); +} + +.text-grey-for-blue { + color: var(--gray-4-blue); +} + +.fill-primary { + fill: var(--we-blue); +} + +.fill-secondary { + fill: var(--all-yellow); +} + +.fill-tertiary { + fill: var(--code-red); +} + +.fill-dark-blue { + fill: var(--dark-blue); +} + +.fill-black { + fill: var(--black); +} + +.fill-white { + fill: var(--white); +} + +.fill-light-gray { + fill: var(--light-gray); +} + +.fill-gray-for-white { + fill: var(--gray-4-white); +} + +.fill-gray-for-blue { + fill: var(--gray-4-blue); +} + +.bg-primary { + background-color: var(--we-blue) !important; +} + +.bg-secondary { + background-color: var(--all-yellow) !important; +} + +.bg-tertiary { + background-color: var(--code-red) !important; +} + +.bg-dark-blue { + background-color: var(--dark-blue) !important; +} + +.bg-black { + background-color: var(--black) !important; +} + +.bg-white { + background-color: var(--white) !important; +} + +.bg-light-gray { + background-color: var(--light-gray) !important; +} + +.bg-gray-for-white { + background-color: var(--gray-4-white) !important; +} + +.bg-gray-for-blue { + background-color: var(--gray-4-blue) !important; +} + +.margin-half { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.margin-vertical-half { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.margin-horizontal-half { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.margin-top-half { + margin-top: 0.5rem; +} + +.margin-right-half { + margin-right: 0.5rem; +} + +.margin-bottom-half { + margin-bottom: 0.5rem; +} + +.margin-left-half { + margin-left: 0.5rem; +} + +.padding-half { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.padding-vertical-half { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.padding-horizontal-half { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.padding-top-half { + padding-top: 0.5rem; +} + +.padding-right-half { + padding-right: 0.5rem; +} + +.padding-bottom-half { + padding-bottom: 0.5rem; +} + +.padding-left-half { + padding-left: 0.5rem; +} small { font-size: 75%; @@ -149,9 +277,17 @@ ul.list-plus li { font-weight: bold; } -.text-gigantic { font-size: 4.5em; } -.text-huge { font-size: 2.5em; } -.text-large { font-size: 1.25em; } +.text-gigantic { + font-size: 4.5em; +} + +.text-huge { + font-size: 2.5em; +} + +.text-large { + font-size: 1.25em; +} .line-height-1 { line-height: 1.1em; @@ -193,7 +329,7 @@ ul.list-plus li { z-index: 0; } -.bg-kid-overlay > * { +.bg-kid-overlay>* { z-index: 1; position: relative; } @@ -205,11 +341,22 @@ ul.list-plus li { } @media screen and (min-width: 40em) { - .medium-padding-horizontal-3 { padding-left: 3rem; padding-right: 3rem; } - .medium-padding-vertical-3 { padding-top: 3rem; padding-bottom: 3rem; } + .medium-padding-horizontal-3 { + padding-left: 3rem; + padding-right: 3rem; + } + + .medium-padding-vertical-3 { + padding-top: 3rem; + padding-bottom: 3rem; + } } + @media screen and (min-width: 64em) { - .large-padding-vertical-4 { padding-top: 4rem; padding-bottom: 4rem; } + .large-padding-vertical-4 { + padding-top: 4rem; + padding-bottom: 4rem; + } } .aside-title { @@ -236,6 +383,7 @@ ul.list-plus li { /* Binary Drop (only visible on larger screens) */ @media screen and (min-width: 40em) { + .binary-drop-left, .binary-drop-right { position: relative; @@ -319,7 +467,7 @@ ul.list-plus li { } .reveal-overlay { - background-color: rgba(10,10,10,.85); + background-color: rgba(10, 10, 10, .85); outline: none; } @@ -350,7 +498,10 @@ ul.list-plus li { /* Kill Debug on Mobile */ @media screen and (max-width: 48em) { - #djDebug, #djDebugToolbar, #djDebugToolbarHandle { + + #djDebug, + #djDebugToolbar, + #djDebugToolbarHandle { display: none !important; } } @@ -377,6 +528,7 @@ ul.list-plus li { @media screen and (min-width: 48em) { + .main-logo, .mobile-nav-trigger { height: 1.5em; @@ -397,6 +549,7 @@ ul.list-plus li { } @media screen and (min-width: 48em) { + .main-menu a, .sub-menu a { font-size: 0.83em; @@ -404,6 +557,7 @@ ul.list-plus li { } @media screen and (min-width: 64em) { + .main-menu a, .sub-menu a { font-size: 1em; @@ -559,6 +713,51 @@ body.home .free-programs { padding: 2rem 1rem; } +/* +* +* +* +* +* +Stripe only CSS +*/ + +.StripeElement { + box-sizing: border-box; + + height: 40px; + + padding: 10px 12px; + + border: 1px solid transparent; + border-radius: 4px; + background-color: white; + + box-shadow: 0 1px 3px 0 #e6ebf1; + -webkit-transition: box-shadow 150ms ease; + transition: box-shadow 150ms ease; +} + +.StripeElement--focus { + box-shadow: 0 1px 3px 0 #cfd7df; +} + +.StripeElement--invalid { + border-color: #fa755a; +} + +.StripeElement--webkit-autofill { + background-color: #fefde5 !important; +} + +/* +* +* +* +* +* +*/ + /* Media Queries */ /* Small only */ From 54517eb9e9a47b5f35987165a4f40ada90507ab8 Mon Sep 17 00:00:00 2001 From: Ali Karbassi Date: Wed, 15 May 2019 17:35:50 -0500 Subject: [PATCH 02/18] Charge works. --- .env.sample | 11 +++-- .vscode/settings.json | 1 - app.json | 10 ++++- coderdojochi/old_views.py | 72 +++++++++++++++++++++++------- coderdojochi/settings.py | 9 ++-- coderdojochi/templates/donate.html | 8 ++-- 6 files changed, 79 insertions(+), 32 deletions(-) diff --git a/.env.sample b/.env.sample index c2e819b7..c3a7b52f 100644 --- a/.env.sample +++ b/.env.sample @@ -23,10 +23,13 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_STORAGE_BUCKET_NAME= -# PayPal -PAYPAL_RECEIVER_EMAIL= -PAYPAL_BUSINESS_ID= -PAYPAL_TEST=False +# Stripe +STRIPE_LIVE_PUBLIC_KEY= +STRIPE_LIVE_SECRET_KEY= +STRIPE_TEST_PUBLIC_KEY= +STRIPE_TEST_SECRET_KEY= +STRIPE_LIVE_MODE=False +DJSTRIPE_WEBHOOK_SECRET= # Sentry SENTRY_DSN= diff --git a/.vscode/settings.json b/.vscode/settings.json index 142da964..70fc2471 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,5 +46,4 @@ "editor.quickSuggestions": false, "editor.trimAutoWhitespace": false, }, - "python.pythonPath": "C:\\Users\\Lokman Musliu\\AppData\\Local\\Programs\\Python\\Python36-32\\python.exe", } diff --git a/app.json b/app.json index 8dcf9c58..e6d11440 100644 --- a/app.json +++ b/app.json @@ -22,10 +22,16 @@ "ENABLE_DEV_FIXTURES": { "required": true }, - "PAYPAL_BUSINESS_ID": { + "STRIPE_LIVE_PUBLIC_KEY": { "required": true }, - "PAYPAL_RECEIVER_EMAIL": { + "STRIPE_LIVE_SECRET_KEY": { + "required": true + }, + "STRIPE_LIVE_MODE": { + "required": true + }, + "DJSTRIPE_WEBHOOK_SECRET": { "required": true }, "SECRET_KEY": { diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index 81c59f0a..aea0e1b8 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -3,18 +3,8 @@ import operator from collections import Counter from datetime import date, timedelta -from decimal import * from functools import reduce -import arrow -from coderdojochi.forms import (CDCModelForm, ContactForm, DonationForm, - GuardianForm, MentorForm, StudentForm) -from coderdojochi.models import (Donation, Equipment, EquipmentType, Guardian, - Meeting, MeetingOrder, Mentor, MentorOrder, - Order, PartnerPasswordAccess, Session, - Student) -from coderdojochi.util import email -from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model @@ -30,11 +20,32 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView + +import arrow +import stripe +from dateutil.relativedelta import relativedelta # Stripe specific imports from djstripe.models import Charge from icalendar import Calendar, Event, vText from paypal.standard.forms import PayPalPaymentsForm +from coderdojochi.forms import CDCModelForm, ContactForm, DonationForm, GuardianForm, MentorForm, StudentForm +from coderdojochi.models import ( + Donation, + Equipment, + EquipmentType, + Guardian, + Meeting, + MeetingOrder, + Mentor, + MentorOrder, + Order, + PartnerPasswordAccess, + Session, + Student, +) +from coderdojochi.util import email + logger = logging.getLogger(__name__) # this will assign User to our custom CDCUser @@ -469,11 +480,42 @@ def donate(request, template_name="donate.html"): @csrf_exempt def donate_charge(request): - charge = Charge( - amount=Decimal(10.00), - currency='usd', - description='Donation Charge' - ) + donor_name = request.POST['donor-name'] + donor_email = request.POST['donor-email'] + donor_phone = request.POST['donor-phone'] + donor_amount = request.POST['donor-amount'] + + stripe_token = request.POST['stripeToken'] + stripe.api_key = settings.STRIPE_SECRET_KEY + + try: + charge = stripe.Charge.create( + amount=int(float(donor_amount) * 100), + currency='usd', + card=stripe_token, + description='Donation to We All Code', + metadata={ + 'name': donor_name, + 'email': donor_email, + 'receipt_email': donor_email, + 'statement_descriptor': 'Donation to We All Code', + } + ) + messages.success(request, f"Thank you for your donation of ${donor_amount}!") + + except stripe.error.StripeError as e: + messages.error( + request, + f"There was an error processing your payment: {e}. " + f" Please try again, or contact us if you're having trouble." + ) + + else: + # TODO: Save to model? Send Receipt? Something... + messages.success( + request, + "Else command worked" + ) return redirect('donate') diff --git a/coderdojochi/settings.py b/coderdojochi/settings.py index 2088eb9c..63ef2e86 100644 --- a/coderdojochi/settings.py +++ b/coderdojochi/settings.py @@ -95,7 +95,7 @@ 'import_export', 'django_nose', 'djstripe', - + # apps 'accounts', 'coderdojochi', @@ -302,11 +302,6 @@ def MediaRootS3BotoStorage(): return S3Boto3Storage(location='media', file_overw AUTH_USER_MODEL = 'coderdojochi.CDCUser' -# Paypal -PAYPAL_RECEIVER_EMAIL = os.environ.get('PAYPAL_RECEIVER_EMAIL') -PAYPAL_BUSINESS_ID = os.environ.get('PAYPAL_BUSINESS_ID') -PAYPAL_TEST = os.environ.get('PAYPAL_TEST') == 'True' - # Stripe STRIPE_LIVE_PUBLIC_KEY = os.environ.get('STRIPE_LIVE_PUBLIC_KEY', '') STRIPE_LIVE_SECRET_KEY = os.environ.get('STRIPE_LIVE_SECRET_KEY', '') @@ -315,6 +310,8 @@ def MediaRootS3BotoStorage(): return S3Boto3Storage(location='media', file_overw STRIPE_LIVE_MODE = os.environ.get('STRIPE_LIVE_MODE') DJSTRIPE_WEBHOOK_SECRET = os.environ.get('DJSTRIPE_WEBHOOK_SECRET') +STRIPE_PUBLIC_KEY = STRIPE_TEST_PUBLIC_KEY if STRIPE_TEST_PUBLIC_KEY != '' else STRIPE_LIVE_PUBLIC_KEY +STRIPE_SECRET_KEY = STRIPE_TEST_SECRET_KEY if STRIPE_TEST_SECRET_KEY != '' else STRIPE_LIVE_SECRET_KEY # django allauth LOGIN_REDIRECT_URL = '/account' diff --git a/coderdojochi/templates/donate.html b/coderdojochi/templates/donate.html index fbcc861d..cb26ad9a 100644 --- a/coderdojochi/templates/donate.html +++ b/coderdojochi/templates/donate.html @@ -15,19 +15,19 @@

{% trans "Donate" %}

@@ -37,7 +37,7 @@

{% trans "Donate" %}

$ - +
From 425c7d4f118b10d8e75460d3b1cac02e30933995 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Mon, 20 May 2019 16:26:17 -0500 Subject: [PATCH 03/18] Payment is working Made a new Payment App with a Donation model. Currently it works if you are not logged and you are anonymous. --- coderdojochi/old_views.py | 37 +++---- coderdojochi/settings.py | 4 +- coderdojochi/signals_handlers.py | 2 +- coderdojochi/templates/donate.html | 41 ++----- coderdojochi/urls.py | 39 ++++--- payments/__init__.py | 0 payments/admin.py | 29 +++++ payments/apps.py | 5 + payments/forms.py | 9 ++ payments/migrations/0001_initial.py | 30 +++++ payments/migrations/__init__.py | 0 payments/models.py | 36 ++++++ payments/templates/contact.html | 4 + payments/templates/donate.html | 154 ++++++++++++++++++++++++++ payments/tests.py | 3 + payments/urls.py | 45 ++++++++ payments/views.py | 163 ++++++++++++++++++++++++++++ payments/views_old.py | 87 +++++++++++++++ 18 files changed, 610 insertions(+), 78 deletions(-) create mode 100644 payments/__init__.py create mode 100644 payments/admin.py create mode 100644 payments/apps.py create mode 100644 payments/forms.py create mode 100644 payments/migrations/0001_initial.py create mode 100644 payments/migrations/__init__.py create mode 100644 payments/models.py create mode 100644 payments/templates/contact.html create mode 100644 payments/templates/donate.html create mode 100644 payments/tests.py create mode 100644 payments/urls.py create mode 100644 payments/views.py create mode 100644 payments/views_old.py diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index aea0e1b8..a573d8a2 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -5,6 +5,16 @@ from datetime import date, timedelta from functools import reduce +import arrow +import stripe +from coderdojochi.forms import (CDCModelForm, ContactForm, DonationForm, + GuardianForm, MentorForm, StudentForm) +from coderdojochi.models import (Donation, Equipment, EquipmentType, Guardian, + Meeting, MeetingOrder, Mentor, MentorOrder, + Order, PartnerPasswordAccess, Session, + Student) +from coderdojochi.util import email +from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model @@ -20,32 +30,11 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView - -import arrow -import stripe -from dateutil.relativedelta import relativedelta # Stripe specific imports from djstripe.models import Charge from icalendar import Calendar, Event, vText from paypal.standard.forms import PayPalPaymentsForm -from coderdojochi.forms import CDCModelForm, ContactForm, DonationForm, GuardianForm, MentorForm, StudentForm -from coderdojochi.models import ( - Donation, - Equipment, - EquipmentType, - Guardian, - Meeting, - MeetingOrder, - Mentor, - MentorOrder, - Order, - PartnerPasswordAccess, - Session, - Student, -) -from coderdojochi.util import email - logger = logging.getLogger(__name__) # this will assign User to our custom CDCUser @@ -484,7 +473,7 @@ def donate_charge(request): donor_email = request.POST['donor-email'] donor_phone = request.POST['donor-phone'] donor_amount = request.POST['donor-amount'] - + donor_statement = f"Donation to We All Code from {donor_name}" stripe_token = request.POST['stripeToken'] stripe.api_key = settings.STRIPE_SECRET_KEY @@ -493,12 +482,12 @@ def donate_charge(request): amount=int(float(donor_amount) * 100), currency='usd', card=stripe_token, - description='Donation to We All Code', + description=donor_statement, metadata={ 'name': donor_name, 'email': donor_email, 'receipt_email': donor_email, - 'statement_descriptor': 'Donation to We All Code', + 'statement_descriptor': donor_statement, } ) messages.success(request, f"Thank you for your donation of ${donor_amount}!") diff --git a/coderdojochi/settings.py b/coderdojochi/settings.py index 63ef2e86..d771a372 100644 --- a/coderdojochi/settings.py +++ b/coderdojochi/settings.py @@ -12,11 +12,10 @@ import os -from django.urls import reverse_lazy - import dj_database_url import django_heroku import raven +from django.urls import reverse_lazy # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -100,6 +99,7 @@ 'accounts', 'coderdojochi', 'weallcode', + 'payments', ] MIDDLEWARE = [ diff --git a/coderdojochi/signals_handlers.py b/coderdojochi/signals_handlers.py index feab2e55..06371a36 100644 --- a/coderdojochi/signals_handlers.py +++ b/coderdojochi/signals_handlers.py @@ -10,7 +10,7 @@ from paypal.standard.ipn.signals import valid_ipn_received from paypal.standard.models import ST_PP_COMPLETED -from coderdojochi.models import Donation, Mentor +from coderdojochi.models import Mentor from coderdojochi.util import email diff --git a/coderdojochi/templates/donate.html b/coderdojochi/templates/donate.html index cb26ad9a..9ef0d9a1 100644 --- a/coderdojochi/templates/donate.html +++ b/coderdojochi/templates/donate.html @@ -9,37 +9,11 @@ {% block contained_content %}
-

{% trans "Donate" %}

-
-
-
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- $ - -
-
+
+

{% trans "Donate" %}

+ + {% csrf_token %} + {{ form.as_p }}
-
- +
+
+
diff --git a/coderdojochi/urls.py b/coderdojochi/urls.py index e72deb17..9386b633 100644 --- a/coderdojochi/urls.py +++ b/coderdojochi/urls.py @@ -33,6 +33,9 @@ # Accounts urlpatterns += [path('account/', include('accounts.urls'))] +# Payments +urlpatterns += [path('donate/', include('payments.urls'))] + # Old General urlpatterns += [ # / @@ -55,25 +58,25 @@ ] # Donate / Donations -urlpatterns += [ - path('donate/', include([ - # Donation - # /donate/ - path('', old_views.donate, name='donate'), +# urlpatterns += [ +# path('donate/', include([ +# # Donation +# # /donate/ +# path('', old_views.donate, name='donate'), - # /donate/charge - path('charge/', old_views.donate_charge, name='donate-charge'), +# # /donate/charge +# path('charge/', old_views.donate_charge, name='donate-charge'), - # /donate/cancel/ - path('cancel/', old_views.donate_cancel, name='donate-cancel'), +# # /donate/cancel/ +# path('cancel/', old_views.donate_cancel, name='donate-cancel'), - # /donate/return/ - path('return/', old_views.donate_return, name='donate-return'), +# # /donate/return/ +# path('return/', old_views.donate_return, name='donate-return'), - # /donate/paypal/ - path('paypal/', include('paypal.standard.ipn.urls')), - ])), -] +# # /donate/paypal/ +# path('paypal/', include('paypal.standard.ipn.urls')), +# ])), +# ] # Login As urlpatterns += [ @@ -309,9 +312,9 @@ ] # Stripe -urlpatterns += [ - path('stripe/', include('djstripe.urls', namespace='djstripe')), -] +# urlpatterns += [ +# path('stripe/', include('djstripe.urls', namespace='djstripe')), +# ] # Media urlpatterns += static( diff --git a/payments/__init__.py b/payments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/admin.py b/payments/admin.py new file mode 100644 index 00000000..2c183dea --- /dev/null +++ b/payments/admin.py @@ -0,0 +1,29 @@ +from django.contrib import admin +from import_export.admin import ImportExportActionModelAdmin, ImportExportMixin + +from .models import Donation + + +@admin.register(Donation) +class DonationAdmin(ImportExportMixin, ImportExportActionModelAdmin): + + list_per_page = 10 + + list_display = [ + 'id', + 'email', + 'formatted_amount', + ] + + ordering = [ + 'created_at', + ] + + search_fields = [ + 'email' + ] + + view_on_site = False + + def formatted_amount(self, obj): + return "$ %.2f" % (obj.amount / 100) diff --git a/payments/apps.py b/payments/apps.py new file mode 100644 index 00000000..58d36ac3 --- /dev/null +++ b/payments/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PaymentsConfig(AppConfig): + name = 'payments' diff --git a/payments/forms.py b/payments/forms.py new file mode 100644 index 00000000..86ffa00c --- /dev/null +++ b/payments/forms.py @@ -0,0 +1,9 @@ +from django import forms + + +class DonateForm(forms.Form): + # first_name = forms.CharField() + # last_name = forms.CharField() + name = forms.CharField() + email = forms.EmailField() + amount = forms.DecimalField() diff --git a/payments/migrations/0001_initial.py b/payments/migrations/0001_initial.py new file mode 100644 index 00000000..d97e5ea5 --- /dev/null +++ b/payments/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.1 on 2019-05-20 21:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Donation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('email', models.EmailField(max_length=254)), + ('stripe_customer_id', models.CharField(max_length=350)), + ('amount', models.IntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/payments/migrations/__init__.py b/payments/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/models.py b/payments/models.py new file mode 100644 index 00000000..d2b0ea7a --- /dev/null +++ b/payments/models.py @@ -0,0 +1,36 @@ +from django.contrib.auth import get_user_model +from django.db import models + +User = get_user_model() + + +class Donation(models.Model): + customer = models.ForeignKey( + User, + on_delete=models.SET_NULL, + related_name="customer", + blank=True, + null=True, + ) + name = models.CharField( + max_length=100, + blank=True, + null=True, + ) + email = models.EmailField() + stripe_customer_id = models.CharField( + max_length=350, + ) + amount = models.IntegerField() + created_at = models.DateTimeField( + auto_now_add=True, + ) + updated_at = models.DateTimeField( + auto_now=True, + ) + + def __str__(self): + return self.email + + def get_absolute_url(self): + return reverse("Donation_detail", kwargs={"pk": self.pk}) diff --git a/payments/templates/contact.html b/payments/templates/contact.html new file mode 100644 index 00000000..a0e8e4b7 --- /dev/null +++ b/payments/templates/contact.html @@ -0,0 +1,4 @@ +
{% csrf_token %} + {{ form.as_p }} + +
diff --git a/payments/templates/donate.html b/payments/templates/donate.html new file mode 100644 index 00000000..cb26ad9a --- /dev/null +++ b/payments/templates/donate.html @@ -0,0 +1,154 @@ +{% extends "_base.html" %} + +{% load i18n %} + +{% block title %}Donate | {{ block.super }}{% endblock %} + +{% block body_class %}page-donate{% endblock %} + +{% block contained_content %} + +
+

{% trans "Donate" %}

+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ $ + +
+
+
+
+ +
+ +
+ + + +
+
+ +
+
+
+
+
+ +
+

{% trans "You can help!" %}

+

We All Code is a registered non-profit organization supported entirely by...you! Please help us continue our + mission by donating a small amount to our cause. Thank you!

+ + {% if referral_disclaimer %} +

{{ referral_disclaimer }}

+ {% endif %} + + {{ form.render }} +
+ +{% endblock %} + +{% block extra_script %} + + + +{% endblock %} diff --git a/payments/tests.py b/payments/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/payments/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/payments/urls.py b/payments/urls.py new file mode 100644 index 00000000..b390d996 --- /dev/null +++ b/payments/urls.py @@ -0,0 +1,45 @@ +# from django.conf import settings +from django.conf.urls import include, url +# from django.conf.urls.static import static +# from django.contrib import admin +# from django.contrib.auth import views as django_views +# from django.http import HttpResponse +# from django.urls import path +# from django.views import defaults +# from django.views.generic import RedirectView +# from loginas import views as loginas_views +from django.urls import path + +from .views import DonateView + +urlpatterns = [ + +] + + +urlpatterns += [ + path('', include([ + # Donation + # /donate/ + # path('', views.donate, name='donate'), + + path('', DonateView.as_view(), name='donate'), + + # /donate/charge + # path('charge/', views.donate_charge, name='donate-charge'), + + # /donate/cancel/ + # path('cancel/', payments.donate_cancel, name='donate-cancel'), + + # /donate/return/ + # path('return/', payments.donate_return, name='donate-return'), + + # /donate/paypal/ + # path('paypal/', include('paypal.standard.ipn.urls')), + ])), +] + +# Stripe specific +urlpatterns += [ + path('stripe/', include('djstripe.urls', namespace='djstripe')), +] diff --git a/payments/views.py b/payments/views.py new file mode 100644 index 00000000..a1411adf --- /dev/null +++ b/payments/views.py @@ -0,0 +1,163 @@ +from decimal import * + +import stripe +from django.conf import settings +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +from django.views.generic.edit import FormView + +from .forms import DonateForm +from .models import Donation + +# from djstripe.models import Customer + + +class DonateView(FormView): + template_name = 'donate.html' + form_class = DonateForm + success_url = reverse_lazy('donate') + + def get_initial(self): + initial = self.initial.copy() + + if self.request.user.is_authenticated: + initial['email'] = self.request.user.email + initial['name'] = self.request.user.get_full_name() + + return initial + + def form_valid(self, form): + # This method is called when valid form data has been POSTed. + # It should return an HttpResponse. + # form.send_email() + data = form.cleaned_data + stripe_token = self.request.POST['stripeToken'] + stripe.api_key = settings.STRIPE_SECRET_KEY + user_email = self.request.POST['email'] + amount = int(float(data['amount'] * 100)) + is_registered_customer = Donation.objects.filter( + email=user_email).exists() + + if self.request.user.is_authenticated & is_registered_customer: + print("User is authenticated and registered customer") + # user = Donation.objects.get(email=user_email) + # customer = stripe.Customer.retrieve(user.stripe_customer_id) + + # try: + + # donation = Donation( + # email=stripe_customer.email, + # amount=amount, + # stripe_customer_id=stripe_customer.id + # ) + # donation.save() + + # charge = stripe.Charge.create( + # amount=amount, + # currency='usd', + # customer=stripe_customer.id, + # description='Donation to We All Code', + # receipt_email=data['email'], + # statement_descriptor='Donation to WeAllCode' + # ) + + # messages.success( + # self.request, f"Thank you for your donation of ${data['amount']}!") + + # except stripe.error.StripeError as e: + # messages.error( + # self.request, + # f"There was an error processing your payment: {e}. " + # f" Please try again, or contact us if you're having trouble." + # ) + else: + print("User anonymous and not stripe user") + + try: + + stripe_customer = stripe.Customer.create( + card=stripe_token, + email=user_email, + description=user_email + ) + + donation = Donation( + email=stripe_customer.email, + amount=amount, + stripe_customer_id=stripe_customer.id + ) + donation.save() + + charge = stripe.Charge.create( + amount=amount, + currency='usd', + customer=stripe_customer.id, + description='Donation to We All Code', + receipt_email=data['email'], + statement_descriptor='Donation to WeAllCode' + ) + + messages.success( + self.request, f"Thank you for your donation of ${data['amount']}!") + + except stripe.error.StripeError as e: + messages.error( + self.request, + f"There was an error processing your payment: {e}. " + f" Please try again, or contact us if you're having trouble." + ) + + return HttpResponseRedirect(self.get_success_url()) + + # if not self.request.user.is_authenticated: + # customer = stripe.Customer.create( + # name=data['name'], + # email=data['email'], + # description=data['name'], + # source=stripe_token + # ) + # donation = Donation( + # email=customer.email, + # stripe_customer_id=customer.id + # ) + # donation.save() + + # try: + # charge = stripe.Charge.create( + # amount=int(float(data['amount']) * 100), + # currency='usd', + # customer=customer, + # description='Donation to We All Code', + # receipt_email=data['email'], + # statement_descriptor='Donation to WeAllCode' + # ) + # messages.success( + # self.request, f"Thank you for your donation of ${data['amount']}!") + + # except stripe.error.StripeError as e: + # messages.error( + # self.request, + # f"There was an error processing your payment: {e}. " + # f" Please try again, or contact us if you're having trouble." + # ) + + # else: + # TODO: Save to model? Send Receipt? Something... + # if self.request.user.is_authenticated: + # user = self.request.user + # customer, created = Customer.get_or_create(subscriber=user) + # else: + # new_user = { + # 'name': data['name'], + # 'email': data['email'] + # } + # customer, created = Customer.get_or_create( + # subscriber=new_user + # ) + # if not self.request.user.is_authenticated: + + # messages.success( + # self.request, + # "Else command worked" + # ) diff --git a/payments/views_old.py b/payments/views_old.py new file mode 100644 index 00000000..4fe41345 --- /dev/null +++ b/payments/views_old.py @@ -0,0 +1,87 @@ +import calendar +import logging +import operator +from collections import Counter +from datetime import date, timedelta +from functools import reduce + +import arrow +import stripe +from coderdojochi.forms import (CDCModelForm, ContactForm, DonationForm, + GuardianForm, MentorForm, StudentForm) +from coderdojochi.models import (Donation, Equipment, EquipmentType, Guardian, + Meeting, MeetingOrder, Mentor, MentorOrder, + Order, PartnerPasswordAccess, Session, + Student) +from coderdojochi.util import email +from dateutil.relativedelta import relativedelta +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.core.mail import EmailMultiAlternatives, get_connection +from django.db.models import Case, Count, IntegerField, When +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils import timezone +from django.utils.html import strip_tags +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import TemplateView +from djstripe.models import Charge, Customer + +# this will assign User to our custom CDCUser +User = get_user_model() + + +def donate(request, template_name="donate.html"): + return render( + request, + template_name + ) + + +@csrf_exempt +def donate_charge(request): + + donor_name = request.POST['donor-name'] + donor_email = request.POST['donor-email'] + donor_phone = request.POST['donor-phone'] + donor_amount = request.POST['donor-amount'] + donor_statement = f"Donation to We All Code from {donor_name}" + stripe_token = request.POST['stripeToken'] + stripe.api_key = settings.STRIPE_SECRET_KEY + + try: + charge = stripe.Charge.create( + amount=int(float(donor_amount) * 100), + currency='usd', + card=stripe_token, + description=donor_statement, + metadata={ + 'name': donor_name, + 'email': donor_email, + 'receipt_email': donor_email, + 'statement_descriptor': donor_statement, + } + ) + messages.success( + request, f"Thank you for your donation of ${donor_amount}!") + + except stripe.error.StripeError as e: + messages.error( + request, + f"There was an error processing your payment: {e}. " + f" Please try again, or contact us if you're having trouble." + ) + + else: + # TODO: Save to model? Send Receipt? Something... + messages.success( + request, + "Else command worked" + ) + + return redirect('donate') From e7cf32ab3ade99d76293004992b6bc503d13b296 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Tue, 21 May 2019 12:11:28 -0500 Subject: [PATCH 04/18] Donation functionality works. Anonymous users can donate to the platform and also existing ones. --- payments/admin.py | 2 +- payments/forms.py | 2 - payments/views.py | 251 +++++++++++++++++++++++----------------------- 3 files changed, 126 insertions(+), 129 deletions(-) diff --git a/payments/admin.py b/payments/admin.py index 2c183dea..30c717b2 100644 --- a/payments/admin.py +++ b/payments/admin.py @@ -26,4 +26,4 @@ class DonationAdmin(ImportExportMixin, ImportExportActionModelAdmin): view_on_site = False def formatted_amount(self, obj): - return "$ %.2f" % (obj.amount / 100) + return "$ {:.2f}".format(obj.amount / 100) diff --git a/payments/forms.py b/payments/forms.py index 86ffa00c..68af6faf 100644 --- a/payments/forms.py +++ b/payments/forms.py @@ -2,8 +2,6 @@ class DonateForm(forms.Form): - # first_name = forms.CharField() - # last_name = forms.CharField() name = forms.CharField() email = forms.EmailField() amount = forms.DecimalField() diff --git a/payments/views.py b/payments/views.py index a1411adf..23cf50d8 100644 --- a/payments/views.py +++ b/payments/views.py @@ -12,12 +12,23 @@ # from djstripe.models import Customer +stripe.api_key = settings.STRIPE_SECRET_KEY + + +class Amount: + def __init__(self, amount): + self.pennies = int(float(amount) * 100) + + def __str__(self): + return "{:.2f}".format(self.pennies / 100) + class DonateView(FormView): template_name = 'donate.html' form_class = DonateForm success_url = reverse_lazy('donate') + # Populate the form is user is authenticated. def get_initial(self): initial = self.initial.copy() @@ -27,137 +38,125 @@ def get_initial(self): return initial + # An already registered customer is making a donation + def donation_is_registered_customer(self, email, name, amount): + + # Get the user from Donation model with email + user = Donation.objects.get(email=email) + + # Save the donation to the model. + donation = Donation( + email=user.email, + name=user.name, + amount=amount.pennies, + stripe_customer_id=user.stripe_customer_id, + ) + donation.save() + + try: + # Charge the user on Stripe. + charge = stripe.Charge.create( + amount=amount.pennies, + currency='usd', + customer=user.stripe_customer_id, + description='Donation to We All Code', + receipt_email=user.email, + statement_descriptor='Donation to WeAllCode' + ) + + # On Charge success alert the user. + messages.success( + self.request, + f"Thank you for your donation of ${ amount }!" + ) + + # On Charge error alert the user. + except stripe.error.StripeError as e: + messages.error( + self.request, + f"There was an error processing your payment: {e}. " + f" Please try again, or contact us if you're having trouble." + ) + + # A new customer is making a donation. + def donation_is_new_customer(self, token, email, name, amount): + + try: + # Register him as a Stripe Customer first. + stripe_customer = stripe.Customer.create( + card=token, + email=email, + name=name, + description="We All Code Customer" + ) + + # Save the Donation to the model + donation = Donation( + email=stripe_customer.email, + amount=amount.pennies, + name=name, + stripe_customer_id=stripe_customer.id + ) + donation.save() + + # Charge the user. + charge = stripe.Charge.create( + amount=amount.pennies, + currency='usd', + customer=stripe_customer.id, + description='Donation to We All Code', + receipt_email=email, + statement_descriptor='Donation to WeAllCode' + ) + + messages.success( + self.request, + f"Thank you for your donation of ${ amount }!" + ) + + except stripe.error.StripeError as e: + messages.error( + self.request, + f"There was an error processing your payment: {e}. " + f" Please try again, or contact us if you're having trouble." + ) + def form_valid(self, form): - # This method is called when valid form data has been POSTed. - # It should return an HttpResponse. - # form.send_email() + + # Clean up the submited data. data = form.cleaned_data + + # Stripe token is retrieved from Stripe.JS stripe_token = self.request.POST['stripeToken'] - stripe.api_key = settings.STRIPE_SECRET_KEY + + # User email retrieved from form user_email = self.request.POST['email'] - amount = int(float(data['amount'] * 100)) + + # Name retrieved from form + name = self.request.POST['name'] + + # Amount class atached to the amount var + amount = Amount(data['amount']) + + # Check if the user exists in Donation model. Returns bool is_registered_customer = Donation.objects.filter( - email=user_email).exists() - - if self.request.user.is_authenticated & is_registered_customer: - print("User is authenticated and registered customer") - # user = Donation.objects.get(email=user_email) - # customer = stripe.Customer.retrieve(user.stripe_customer_id) - - # try: - - # donation = Donation( - # email=stripe_customer.email, - # amount=amount, - # stripe_customer_id=stripe_customer.id - # ) - # donation.save() - - # charge = stripe.Charge.create( - # amount=amount, - # currency='usd', - # customer=stripe_customer.id, - # description='Donation to We All Code', - # receipt_email=data['email'], - # statement_descriptor='Donation to WeAllCode' - # ) - - # messages.success( - # self.request, f"Thank you for your donation of ${data['amount']}!") - - # except stripe.error.StripeError as e: - # messages.error( - # self.request, - # f"There was an error processing your payment: {e}. " - # f" Please try again, or contact us if you're having trouble." - # ) + email=user_email + ).exists() + + # If true call the function for a registered customer + if is_registered_customer: + self.donation_is_registered_customer( + user_email, + name, + amount, + ) else: - print("User anonymous and not stripe user") - - try: - - stripe_customer = stripe.Customer.create( - card=stripe_token, - email=user_email, - description=user_email - ) - - donation = Donation( - email=stripe_customer.email, - amount=amount, - stripe_customer_id=stripe_customer.id - ) - donation.save() - - charge = stripe.Charge.create( - amount=amount, - currency='usd', - customer=stripe_customer.id, - description='Donation to We All Code', - receipt_email=data['email'], - statement_descriptor='Donation to WeAllCode' - ) - - messages.success( - self.request, f"Thank you for your donation of ${data['amount']}!") - - except stripe.error.StripeError as e: - messages.error( - self.request, - f"There was an error processing your payment: {e}. " - f" Please try again, or contact us if you're having trouble." - ) + # Else call the function for a new customer. + self.donation_is_new_customer( + stripe_token, + user_email, + name, + amount, + ) return HttpResponseRedirect(self.get_success_url()) - - # if not self.request.user.is_authenticated: - # customer = stripe.Customer.create( - # name=data['name'], - # email=data['email'], - # description=data['name'], - # source=stripe_token - # ) - # donation = Donation( - # email=customer.email, - # stripe_customer_id=customer.id - # ) - # donation.save() - - # try: - # charge = stripe.Charge.create( - # amount=int(float(data['amount']) * 100), - # currency='usd', - # customer=customer, - # description='Donation to We All Code', - # receipt_email=data['email'], - # statement_descriptor='Donation to WeAllCode' - # ) - # messages.success( - # self.request, f"Thank you for your donation of ${data['amount']}!") - - # except stripe.error.StripeError as e: - # messages.error( - # self.request, - # f"There was an error processing your payment: {e}. " - # f" Please try again, or contact us if you're having trouble." - # ) - - # else: - # TODO: Save to model? Send Receipt? Something... - # if self.request.user.is_authenticated: - # user = self.request.user - # customer, created = Customer.get_or_create(subscriber=user) - # else: - # new_user = { - # 'name': data['name'], - # 'email': data['email'] - # } - # customer, created = Customer.get_or_create( - # subscriber=new_user - # ) - # if not self.request.user.is_authenticated: - - # messages.success( - # self.request, - # "Else command worked" - # ) From c98e886de6e50b3c9e428dd903cc230935d9e582 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Tue, 21 May 2019 14:06:03 -0500 Subject: [PATCH 05/18] Removed the unused DJStripe package. --- .env.sample | 7 +- Pipfile | 2 +- Pipfile.lock | 131 +++++++++++++++----------------------- app.json | 3 - coderdojochi/old_views.py | 2 - coderdojochi/settings.py | 12 +--- coderdojochi/urls.py | 5 -- payments/urls.py | 6 +- payments/views.py | 5 +- payments/views_old.py | 87 ------------------------- 10 files changed, 65 insertions(+), 195 deletions(-) delete mode 100644 payments/views_old.py diff --git a/.env.sample b/.env.sample index c3a7b52f..37ec0e2e 100644 --- a/.env.sample +++ b/.env.sample @@ -24,12 +24,9 @@ AWS_SECRET_ACCESS_KEY= AWS_STORAGE_BUCKET_NAME= # Stripe -STRIPE_LIVE_PUBLIC_KEY= -STRIPE_LIVE_SECRET_KEY= -STRIPE_TEST_PUBLIC_KEY= -STRIPE_TEST_SECRET_KEY= +STRIPE_PUBLIC_KEY= +STRIPE_SECRET_KEY= STRIPE_LIVE_MODE=False -DJSTRIPE_WEBHOOK_SECRET= # Sentry SENTRY_DSN= diff --git a/Pipfile b/Pipfile index 8750ca84..8fc08bf1 100644 --- a/Pipfile +++ b/Pipfile @@ -32,7 +32,7 @@ icalendar = "*" json-logging-py = "*" django-anymail = "*" raven = "*" -dj-stripe = "*" +stripe = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 28b7db16..5bb874f1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "143432609575656a89c803e765f121e355065db3c34a1c0eb38266d3e96612bf" + "sha256": "2b9affdeaa9d74cccaa110e2d147a5942082abe0c405449d1fc73522f80bef90" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "arrow": { "hashes": [ - "sha256:3397e5448952e18e1295bf047014659effa5ae8da6a5371d37ff0ddc46fa6872", - "sha256:6f54d9f016c0b7811fac9fb8c2c7fa7421d80c54dbdd75ffb12913c55db60b8a" + "sha256:002f2315cf4c8404de737c42860441732d339bbc57fee584e2027520e055ecc1", + "sha256:82dd5e13b733787d4eb0fef42d1ee1a99136dc1d65178f70373b3678b3181bfc" ], "index": "pypi", - "version": "==0.13.1" + "version": "==0.13.2" }, "backports.csv": { "hashes": [ @@ -33,18 +33,18 @@ }, "boto3": { "hashes": [ - "sha256:484650b86ea843587f484a8f9cc9629465ad805aff0ffaabf95345960168f569", - "sha256:635e1864cd35d78d33fd7ce325f9baa15c93a932403953b2b4801567a791b869" + "sha256:ad311139cc3ee4464e5f3b558805e684988e71f591dbed6dd19a28dd6854455f", + "sha256:bae8f2ab3732a7a51da34816f014c0dc98fc75de0bdb5ab19350b7964fba5763" ], "index": "pypi", - "version": "==1.9.143" + "version": "==1.9.152" }, "botocore": { "hashes": [ - "sha256:0247ad0da9fdbf4e8025b0dafb3982b945d335bcd7043518fdabe9d99f704e17", - "sha256:94846e90fc4dbe91a9e70f6a24ca823b4f3acc9a4047b497266d003fe12c80ce" + "sha256:19e54674913a7f697f11d7deb634353ceaf593e5486cab15664ea62a4a341dc9", + "sha256:79a463b96ed16f6c4b7f387cc086ef3323e88c2d153cea407e2c3981fa9849de" ], - "version": "==1.12.143" + "version": "==1.12.152" }, "certifi": { "hashes": [ @@ -80,14 +80,6 @@ ], "version": "==0.5.0" }, - "dj-stripe": { - "hashes": [ - "sha256:603b307683557e1e5291efebabf5e080844053f50c3abc41d00b0738fe097f57", - "sha256:8e39fdbef0dda4d82597e2d1b56b674d520dd233abc916db569cb58433b82067" - ], - "index": "pypi", - "version": "==2.0.1" - }, "django": { "hashes": [ "sha256:6fcc3cbd55b16f9a01f37de8bcbe286e0ea22e87096557f1511051780338eaea", @@ -105,11 +97,11 @@ }, "django-anymail": { "hashes": [ - "sha256:ab5cbfb280492c9cb2ef0497f8ebda4dcb3f99603dcf63c88ae623a66cad71f4", - "sha256:f872089943f30283d485d121924598f4156f5bfa76a0885fd66df5205102be0b" + "sha256:cb70bc28c188a4424b212bc91593cbc4247f2f21440fef2d209400c6dce7a18e", + "sha256:fdd87c6818b78f6d503e7dafeef679b6849613e0fe19129a30425914822630a3" ], "index": "pypi", - "version": "==6.0" + "version": "==6.0.1" }, "django-appconf": { "hashes": [ @@ -230,24 +222,24 @@ "et-xmlfile": { "hashes": [ "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b", - "sha256:87d0c1be722fb61688e7f167aece7cae527faa158e2fe269a4bd75484ddda8e2" + "sha256:8951dd932bd7348bce46fbebad40f1841dfa3aaec42b31c4c9e8a0df0a55cc4c" ], "version": "==1.0.1" }, "factory-boy": { "hashes": [ - "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca", - "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f" + "sha256:728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee", + "sha256:faf48d608a1735f0d0a3c9cbf536d64f9132b547dae7ba452c4d99a79e84a370" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.12.0" }, "faker": { "hashes": [ - "sha256:167cef2454482dc2fbd8b0ff6a5ba3dbac8d3a3ebdee6ba819d008100d9d9428", - "sha256:3f2f4570df28df2eb8f39b00520eb610081d6552975e926c6a2cbc64fd89c4c1" + "sha256:1c0a5e7bb54d2c54569986a27124715c83899e592d8d61d4e372dbff6c699573", + "sha256:60477f757a80f665bbe1fb3d1cfe5d205ec7b99d5240114de7b27b4c25d236ca" ], - "version": "==1.0.5" + "version": "==1.0.7" }, "gunicorn": { "hashes": [ @@ -302,20 +294,13 @@ "index": "pypi", "version": "==0.2" }, - "jsonfield": { - "hashes": [ - "sha256:a0a7fdee736ff049059409752b045281a225610fecbda9b9bd588ba976493c12", - "sha256:beb1cd4850d6d6351c32daefcb826c01757744e9c863228a642f87a1a4acb834" - ], - "version": "==2.0.2" - }, "mock": { "hashes": [ - "sha256:21a2c07af3bbc4a77f9d14ac18fcc1782e8e7ea363df718740cdeaf61995b5e7", - "sha256:7868db2825a1563578869d4a011a036503a2f1d60f9ff9dd1e3205cd6e25fcec" + "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3", + "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8" ], "index": "pypi", - "version": "==3.0.4" + "version": "==3.0.5" }, "nose": { "hashes": [ @@ -488,11 +473,11 @@ }, "requests": { "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "markers": "python_version >= '3.0'", - "version": "==2.21.0" + "version": "==2.22.0" }, "requests-oauthlib": { "hashes": [ @@ -524,10 +509,11 @@ }, "stripe": { "hashes": [ - "sha256:43cf1addbd5685d166c483f29f2b13e514304b091086561c2e22a3c7664043a2", - "sha256:814e6a87fdb679cf2ddca9a12099a93aaf437dac0b09edac4851c6ba9ebd7e5f" + "sha256:1b495c81412752b6aedd3bd94dd21a4be817a4073c04d9e427ceaf62942eab19", + "sha256:242a27691e319e1c57b3dd63297feeeda0d56ce5856f25a21cdc573281cd2736" ], - "version": "==2.27.0" + "index": "pypi", + "version": "==2.28.1" }, "tablib": { "hashes": [ @@ -590,44 +576,33 @@ }, "isort": { "hashes": [ - "sha256:1349c6f7c2a0f7539f5f2ace51a9a8e4a37086ce4de6f78f5f53fb041d0a3cd5", - "sha256:f09911f6eb114e5592abe635aded8bf3d2c3144ebcfcaf81ee32e7af7b7d1870" + "sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a", + "sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d" ], - "version": "==4.3.18" + "version": "==4.3.20" }, "lazy-object-proxy": { "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" + "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", + "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", + "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", + "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", + "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", + "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", + "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", + "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", + "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", + "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", + "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", + "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", + "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", + "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", + "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", + "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", + "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", + "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" + ], + "version": "==1.4.1" }, "mccabe": { "hashes": [ diff --git a/app.json b/app.json index e6d11440..5d06e0e2 100644 --- a/app.json +++ b/app.json @@ -31,9 +31,6 @@ "STRIPE_LIVE_MODE": { "required": true }, - "DJSTRIPE_WEBHOOK_SECRET": { - "required": true - }, "SECRET_KEY": { "required": true }, diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index a573d8a2..e96d3f71 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -30,8 +30,6 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView -# Stripe specific imports -from djstripe.models import Charge from icalendar import Calendar, Event, vText from paypal.standard.forms import PayPalPaymentsForm diff --git a/coderdojochi/settings.py b/coderdojochi/settings.py index d771a372..05012f83 100644 --- a/coderdojochi/settings.py +++ b/coderdojochi/settings.py @@ -93,7 +93,7 @@ 'stdimage', 'import_export', 'django_nose', - 'djstripe', + 'stripe', # apps 'accounts', @@ -303,15 +303,9 @@ def MediaRootS3BotoStorage(): return S3Boto3Storage(location='media', file_overw # Stripe -STRIPE_LIVE_PUBLIC_KEY = os.environ.get('STRIPE_LIVE_PUBLIC_KEY', '') -STRIPE_LIVE_SECRET_KEY = os.environ.get('STRIPE_LIVE_SECRET_KEY', '') -STRIPE_TEST_PUBLIC_KEY = os.environ.get('STRIPE_TEST_PUBLIC_KEY') -STRIPE_TEST_SECRET_KEY = os.environ.get('STRIPE_TEST_SECRET_KEY') +STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_PUBLIC_KEY', '') +STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY', '') STRIPE_LIVE_MODE = os.environ.get('STRIPE_LIVE_MODE') -DJSTRIPE_WEBHOOK_SECRET = os.environ.get('DJSTRIPE_WEBHOOK_SECRET') - -STRIPE_PUBLIC_KEY = STRIPE_TEST_PUBLIC_KEY if STRIPE_TEST_PUBLIC_KEY != '' else STRIPE_LIVE_PUBLIC_KEY -STRIPE_SECRET_KEY = STRIPE_TEST_SECRET_KEY if STRIPE_TEST_SECRET_KEY != '' else STRIPE_LIVE_SECRET_KEY # django allauth LOGIN_REDIRECT_URL = '/account' diff --git a/coderdojochi/urls.py b/coderdojochi/urls.py index 9386b633..c9dede15 100644 --- a/coderdojochi/urls.py +++ b/coderdojochi/urls.py @@ -311,11 +311,6 @@ path('anymail/', include('anymail.urls')), ] -# Stripe -# urlpatterns += [ -# path('stripe/', include('djstripe.urls', namespace='djstripe')), -# ] - # Media urlpatterns += static( settings.MEDIA_URL, diff --git a/payments/urls.py b/payments/urls.py index b390d996..8759cc15 100644 --- a/payments/urls.py +++ b/payments/urls.py @@ -40,6 +40,6 @@ ] # Stripe specific -urlpatterns += [ - path('stripe/', include('djstripe.urls', namespace='djstripe')), -] +# urlpatterns += [ +# path('stripe/', include('djstripe.urls', namespace='djstripe')), +# ] diff --git a/payments/views.py b/payments/views.py index 23cf50d8..bf4691c2 100644 --- a/payments/views.py +++ b/payments/views.py @@ -10,16 +10,17 @@ from .forms import DonateForm from .models import Donation -# from djstripe.models import Customer - stripe.api_key = settings.STRIPE_SECRET_KEY class Amount: + # A class to format the amount. + # Params (amount) def __init__(self, amount): self.pennies = int(float(amount) * 100) def __str__(self): + # Format to dollars return "{:.2f}".format(self.pennies / 100) diff --git a/payments/views_old.py b/payments/views_old.py deleted file mode 100644 index 4fe41345..00000000 --- a/payments/views_old.py +++ /dev/null @@ -1,87 +0,0 @@ -import calendar -import logging -import operator -from collections import Counter -from datetime import date, timedelta -from functools import reduce - -import arrow -import stripe -from coderdojochi.forms import (CDCModelForm, ContactForm, DonationForm, - GuardianForm, MentorForm, StudentForm) -from coderdojochi.models import (Donation, Equipment, EquipmentType, Guardian, - Meeting, MeetingOrder, Mentor, MentorOrder, - Order, PartnerPasswordAccess, Session, - Student) -from coderdojochi.util import email -from dateutil.relativedelta import relativedelta -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.core.mail import EmailMultiAlternatives, get_connection -from django.db.models import Case, Count, IntegerField, When -from django.http import HttpResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse -from django.utils import timezone -from django.utils.html import strip_tags -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import TemplateView -from djstripe.models import Charge, Customer - -# this will assign User to our custom CDCUser -User = get_user_model() - - -def donate(request, template_name="donate.html"): - return render( - request, - template_name - ) - - -@csrf_exempt -def donate_charge(request): - - donor_name = request.POST['donor-name'] - donor_email = request.POST['donor-email'] - donor_phone = request.POST['donor-phone'] - donor_amount = request.POST['donor-amount'] - donor_statement = f"Donation to We All Code from {donor_name}" - stripe_token = request.POST['stripeToken'] - stripe.api_key = settings.STRIPE_SECRET_KEY - - try: - charge = stripe.Charge.create( - amount=int(float(donor_amount) * 100), - currency='usd', - card=stripe_token, - description=donor_statement, - metadata={ - 'name': donor_name, - 'email': donor_email, - 'receipt_email': donor_email, - 'statement_descriptor': donor_statement, - } - ) - messages.success( - request, f"Thank you for your donation of ${donor_amount}!") - - except stripe.error.StripeError as e: - messages.error( - request, - f"There was an error processing your payment: {e}. " - f" Please try again, or contact us if you're having trouble." - ) - - else: - # TODO: Save to model? Send Receipt? Something... - messages.success( - request, - "Else command worked" - ) - - return redirect('donate') From 9ac02ed88f2ea4f02bcfb55bde6b40588e7ec2e4 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Tue, 21 May 2019 15:22:11 -0500 Subject: [PATCH 06/18] If user is not auth but exists in our database --- payments/views.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/payments/views.py b/payments/views.py index bf4691c2..125b7b3e 100644 --- a/payments/views.py +++ b/payments/views.py @@ -1,6 +1,7 @@ from decimal import * import stripe +from coderdojochi.models import CDCUser from django.conf import settings from django.contrib import messages from django.http import HttpResponseRedirect @@ -43,10 +44,18 @@ def get_initial(self): def donation_is_registered_customer(self, email, name, amount): # Get the user from Donation model with email - user = Donation.objects.get(email=email) + user = Donation.objects.filter(email=email).first() + + # Check if user exists in our Database + user_email_exists = CDCUser.objects.filter(email=email).exists() + + # If the user exists attach it to customer + if user_email_exists: + customer = CDCUser.objects.get(email=email) # Save the donation to the model. donation = Donation( + customer=customer, email=user.email, name=user.name, amount=amount.pennies, @@ -91,8 +100,19 @@ def donation_is_new_customer(self, token, email, name, amount): description="We All Code Customer" ) + # Check if user is authenticated + if self.request.user.is_authenticated: + customer = self.request.user + + # Check if user exists in our Database + user_email_exists = CDCUser.objects.filter(email=email).exists() + + if user_email_exists: + customer = CDCUser.objects.get(email=email) + # Save the Donation to the model donation = Donation( + customer=customer, email=stripe_customer.email, amount=amount.pennies, name=name, From 76b963e4e45e62d89181d77a61804fd576a3f172 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Tue, 21 May 2019 15:41:55 -0500 Subject: [PATCH 07/18] Small bug fix with rounding payments --- payments/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/payments/views.py b/payments/views.py index 125b7b3e..23f54c12 100644 --- a/payments/views.py +++ b/payments/views.py @@ -18,7 +18,9 @@ class Amount: # A class to format the amount. # Params (amount) def __init__(self, amount): - self.pennies = int(float(amount) * 100) + self.pennies = float(amount) + self.pennies = round(self.pennies) * 100 + self.pennies = int(self.pennies) def __str__(self): # Format to dollars @@ -90,7 +92,7 @@ def donation_is_registered_customer(self, email, name, amount): # A new customer is making a donation. def donation_is_new_customer(self, token, email, name, amount): - + customer = None try: # Register him as a Stripe Customer first. stripe_customer = stripe.Customer.create( From da3d8398e14e468f922eee800a5f7e92f787e9a0 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Wed, 22 May 2019 13:16:27 -0500 Subject: [PATCH 08/18] Fixes to the donation page --- accounts/templates/account/_base.html | 19 +++-- .../templates/account/account-payments.html | 39 +++++++++ accounts/templates/account/home_guardian.html | 2 +- accounts/urls.py | 9 +- accounts/views.py | 23 ++++- coderdojochi/models.py | 2 +- coderdojochi/old_views.py | 18 ++-- coderdojochi/templates/_topnav.html | 2 +- coderdojochi/templates/student-detail.html | 3 +- .../templatetags/coderdojochi_extras.py | 3 +- coderdojochi/views/general.py | 6 +- coderdojochi/views/profile.py | 2 +- coderdojochi/views/sessions.py | 2 +- fixtures/06-weallcode-payments.json | 84 +++++++++++++++++++ payments/admin.py | 11 +++ payments/migrations/0001_initial.py | 5 +- payments/models.py | 8 +- payments/views.py | 44 +++++----- weallcode/static/weallcode/css/app.css | 4 + weallcode/templates/weallcode/_header.html | 4 +- 20 files changed, 226 insertions(+), 64 deletions(-) create mode 100644 accounts/templates/account/account-payments.html create mode 100644 fixtures/06-weallcode-payments.json diff --git a/accounts/templates/account/_base.html b/accounts/templates/account/_base.html index f099c557..af39504b 100644 --- a/accounts/templates/account/_base.html +++ b/accounts/templates/account/_base.html @@ -1,17 +1,20 @@ {% extends "_base.html" %} +{% load coderdojochi_extras %} {% block subheader %} {% if user.is_authenticated %} - + {% endif %} {% endblock subheader %} {% block contained_content %} - {% block content_placeholder %}{% endblock %} +{% block content_placeholder %}{% endblock %} {% endblock %} diff --git a/accounts/templates/account/account-payments.html b/accounts/templates/account/account-payments.html new file mode 100644 index 00000000..ae514dbf --- /dev/null +++ b/accounts/templates/account/account-payments.html @@ -0,0 +1,39 @@ +{% extends "account/_base.html" %} +{% load humanize coderdojochi_extras %} + +{% block title %}Donations | {{ block.super }}{% endblock %} +{% block meta_facebook_title %}My Account | {{ block.super }}{% endblock %} +{% block meta_twitter_title %}My Account | {{ block.super }}{% endblock %} + +{% block body_class %}page-dojo{% endblock %} + + +{% block contained_content %} + +
+

Donations

+ {% if object_list %} + + + + + + + + + + {% for payment in object_list %} + + + + + + + {% endfor %} + +
Payment IDDateAmount
{{ payment.id }}{{ payment.created_at | date}}${{ payment.get_formatted_amount | floatformat:2 | intcomma }}
+ {% else %} +

You have no donations yet. Donate now!

+ {% endif %} +
+{% endblock %} diff --git a/accounts/templates/account/home_guardian.html b/accounts/templates/account/home_guardian.html index 0e73ebb7..31c6ea6f 100644 --- a/accounts/templates/account/home_guardian.html +++ b/accounts/templates/account/home_guardian.html @@ -62,7 +62,7 @@

Students

{% endfor %} -

Add another student

+

Add another student

{% else %}

You have no students registered yet, register a student now.

{% endif %} diff --git a/accounts/urls.py b/accounts/urls.py index ba0fdea1..499e689e 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,14 +1,11 @@ from django.conf.urls import include from django.urls import path -from . import views +from .views import AccountHomeView, PaymentsView urlpatterns = [ - path( - '', - views.AccountHomeView.as_view(), - name='account_home', - ), + path('', AccountHomeView.as_view(), name='account-home',), + path('payments/', PaymentsView.as_view(), name='account-payments',), path('', include('allauth.urls')), ] diff --git a/accounts/views.py b/accounts/views.py index 4911e052..6d52feb9 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,12 +1,17 @@ +from coderdojochi.forms import CDCModelForm, GuardianForm, MentorForm +from coderdojochi.models import (Guardian, MeetingOrder, Mentor, MentorOrder, + Order, Student) from django.contrib import messages +from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic import TemplateView +from django.views.generic.list import ListView +from payments.models import Donation -from coderdojochi.forms import CDCModelForm, GuardianForm, MentorForm -from coderdojochi.models import Guardian, MeetingOrder, Mentor, MentorOrder, Order, Student +User = get_user_model() @method_decorator(login_required, name='dispatch') @@ -183,7 +188,7 @@ def post_for_mentor(self, **kwargs): 'Profile information saved.' ) - return redirect('account_home') + return redirect('account-home') else: messages.error( @@ -219,7 +224,7 @@ def post_for_guardian(self, **kwargs): 'Profile information saved.' ) - return redirect('account_home') + return redirect('account-home') else: messages.error( @@ -231,3 +236,13 @@ def post_for_guardian(self, **kwargs): context['user_form'] = user_form return render(self.request, 'account/home_guardian.html', context) + + +@method_decorator(login_required, name='dispatch') +class PaymentsView(ListView): + model = Donation + template_name = "account/account-payments.html" + # queryset = Donation.objects.filter(customer='ACME Publishing') + + def get_queryset(self): + return Donation.objects.filter(customer=self.request.user).order_by('-created_at') diff --git a/coderdojochi/models.py b/coderdojochi/models.py index 4ec683a4..959a0975 100644 --- a/coderdojochi/models.py +++ b/coderdojochi/models.py @@ -36,7 +36,7 @@ def save(self, *args, **kwargs): super(CDCUser, self).save(*args, **kwargs) def get_absolute_url(self): - return reverse('account_home') + return reverse('account-home') def generate_filename(instance, filename): diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index e96d3f71..26f55f56 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -181,7 +181,7 @@ def faqs(request, template_name="faqs.html"): # 'Profile information saved.' # ) -# return redirect('account_home') +# return redirect('account-home') # else: # messages.error( @@ -260,7 +260,7 @@ def faqs(request, template_name="faqs.html"): # 'Profile information saved.' # ) -# return redirect('account_home') +# return redirect('account-home') # else: # messages.error( @@ -410,12 +410,12 @@ def student_detail( try: student = Student.objects.get(id=student_id, is_active=True) except ObjectDoesNotExist: - return redirect('account_home') + return redirect('account-home') try: guardian = Guardian.objects.get(user=request.user, is_active=True) except ObjectDoesNotExist: - return redirect('account_home') + return redirect('account-home') if not student.guardian == guardian: access = False @@ -425,7 +425,7 @@ def student_detail( access = False if not access: - return redirect('account_home') + return redirect('account-home') messages.error( request, 'You do not have permissions to edit this student.' @@ -439,13 +439,13 @@ def student_detail( request, f"Student \"{student.first_name} {student.last_name}\" Deleted." ) - return redirect('account_home') + return redirect('account-home') form = StudentForm(request.POST, instance=student) if form.is_valid(): form.save() messages.success(request, 'Student Updated.') - return redirect('account_home') + return redirect('account-home') return render( request, @@ -1035,7 +1035,7 @@ def session_donations(request, pk, template_name="session-donations.html"): request, 'You do not have permission to access this page.' ) - return redirect('account_home') + return redirect('account-home') session = get_object_or_404(Session, pk=pk) @@ -1086,7 +1086,7 @@ def meeting_check_in( request, 'You do not have permission to access this page.' ) - return redirect('account_home') + return redirect('account-home') if request.method == 'POST': if 'order_id' in request.POST: diff --git a/coderdojochi/templates/_topnav.html b/coderdojochi/templates/_topnav.html index d98c6b53..228d100b 100644 --- a/coderdojochi/templates/_topnav.html +++ b/coderdojochi/templates/_topnav.html @@ -18,7 +18,7 @@ {% if user.is_authenticated %} - +
  • Logout
  • {% else %} diff --git a/coderdojochi/templates/student-detail.html b/coderdojochi/templates/student-detail.html index 8d51a249..f508652d 100644 --- a/coderdojochi/templates/student-detail.html +++ b/coderdojochi/templates/student-detail.html @@ -13,7 +13,7 @@

    Edit Student Info

    {% csrf_token %} {% bootstrap_form form %}   - Cancel + Cancel
    @@ -23,4 +23,3 @@

    Edit Student Info

    {% endblock %} - diff --git a/coderdojochi/templatetags/coderdojochi_extras.py b/coderdojochi/templatetags/coderdojochi_extras.py index 7d3d17f6..0d772e0b 100644 --- a/coderdojochi/templatetags/coderdojochi_extras.py +++ b/coderdojochi/templatetags/coderdojochi_extras.py @@ -32,7 +32,8 @@ def student_register_link(context, student, session): is_active=True ) - url = reverse('session-sign-up', kwargs={'pk': session.id, 'student_id': student.id, }) + url = reverse('session-sign-up', + kwargs={'pk': session.id, 'student_id': student.id, }) button_tag = 'a' button_modifier = '' diff --git a/coderdojochi/views/general.py b/coderdojochi/views/general.py index 80779150..f252f5ec 100644 --- a/coderdojochi/views/general.py +++ b/coderdojochi/views/general.py @@ -107,7 +107,7 @@ def dispatch(self, request, *args, **kwargs): ): mentor = get_object_or_404(Mentor, user=request.user) if mentor.user.first_name: - return redirect(next_url if next_url else 'account_home') + return redirect(next_url if next_url else 'account-home') kwargs['mentor'] = mentor return super(WelcomeView, self).dispatch(request, *args, **kwargs) @@ -172,7 +172,7 @@ def update_account(self, request, account, next_url): if 'enroll' in request.GET: next_url = f"{next_url}?enroll=True" else: - next_url = 'account_home' if isinstance(account, Mentor) else 'welcome' + next_url = 'account-home' if isinstance(account, Mentor) else 'welcome' return redirect(next_url) return render(request, self.template_name, { @@ -240,7 +240,7 @@ def create_new_user(self, request, user, next_url): f"{settings.SITE_URL}{next_meeting.get_calendar_url()}" ) if not next_url: - next_url = reverse('account_home') + next_url = reverse('account-home') else: # check for next upcoming class next_class = Session.objects.filter( diff --git a/coderdojochi/views/profile.py b/coderdojochi/views/profile.py index ce296d79..e2dfd7e5 100644 --- a/coderdojochi/views/profile.py +++ b/coderdojochi/views/profile.py @@ -124,7 +124,7 @@ def post(self, request, *args, **kwargs): 'Profile information saved.' ) - return redirect('account_home') + return redirect('account-home') else: messages.error( diff --git a/coderdojochi/views/sessions.py b/coderdojochi/views/sessions.py index 0b09dff2..a1173333 100644 --- a/coderdojochi/views/sessions.py +++ b/coderdojochi/views/sessions.py @@ -329,7 +329,7 @@ def dispatch(self, request, *args, **kwargs): access_dict = self.check_access(request, *args, **kwargs) if access_dict.get('message'): - if access_dict.get('redirect') == 'account_home': + if access_dict.get('redirect') == 'account-home': messages.warning( request, access_dict['message'] diff --git a/fixtures/06-weallcode-payments.json b/fixtures/06-weallcode-payments.json new file mode 100644 index 00000000..5dac0fa6 --- /dev/null +++ b/fixtures/06-weallcode-payments.json @@ -0,0 +1,84 @@ +[{ + "model": "payments.donation", + "pk": 1, + "fields": { + "customer": 2, + "name": "John Doe", + "email": "guardian@sink.sendgrid.net", + "stripe_customer_id": "cus_F7B6UUdHaC3SY2", + "stripe_payment_id": "ch_1Ecy3sDC33cb7CGNz15Ye325", + "amount": 1000, + "created_at": "2019-05-01T13:07:25.346-06:00", + "updated_at": "2019-05-01T13:07:25.346-06:00" + } + }, + { + "model": "payments.donation", + "pk": 2, + "fields": { + "customer": 2, + "name": "John Doe", + "email": "guardian@sink.sendgrid.net", + "stripe_customer_id": "cus_F7B6UUdHaC3SY2", + "stripe_payment_id": "ch_1Ecy3sDC33cb7CGNz15Ye323", + "amount": 100, + "created_at": "2019-05-02T13:07:25.346-06:00", + "updated_at": "2019-05-02T13:07:25.346-06:00" + } + }, + { + "model": "payments.donation", + "pk": 3, + "fields": { + "customer": 2, + "name": "John Doe", + "email": "guardian@sink.sendgrid.net", + "stripe_customer_id": "cus_F7B6UUdHaC3SY2", + "stripe_payment_id": "ch_1Ecy3sDC33cb7CGNz15Ye444", + "amount": 5000, + "created_at": "2019-05-03T13:07:25.346-06:00", + "updated_at": "2019-05-03T13:07:25.346-06:00" + } + }, + { + "model": "payments.donation", + "pk": 4, + "fields": { + "customer": 2, + "name": "John Doe", + "email": "guardian@sink.sendgrid.net", + "stripe_customer_id": "cus_F7B6UUdHaC3SY2", + "stripe_payment_id": "ch_1Ecy3sDC33cb7CGNz15Ye333", + "amount": 12000, + "created_at": "2019-05-04T13:07:25.346-06:00", + "updated_at": "2019-05-04T13:07:25.346-06:00" + } + }, + { + "model": "payments.donation", + "pk": 5, + "fields": { + "customer": 2, + "name": "John Doe", + "email": "guardian@sink.sendgrid.net", + "stripe_customer_id": "cus_F7B6UUdHaC3SY2", + "stripe_payment_id": "ch_1Ecy3sDC33cb7CGNz15Ye123", + "amount": 350000, + "created_at": "2019-05-05T13:07:25.346-06:00", + "updated_at": "2019-05-05T13:07:25.346-06:00" + } + }, + { + "model": "payments.donation", + "pk": 6, + "fields": { + "name": "John Doe", + "email": "test@test.com", + "stripe_customer_id": "cus_F7B6UUdHaC3S32", + "stripe_payment_id": "ch_1Ecy3sDC33cb7CGNz15Ye321", + "amount": 3500, + "created_at": "2019-05-05T13:07:25.346-06:00", + "updated_at": "2019-05-05T13:07:25.346-06:00" + } + } +] diff --git a/payments/admin.py b/payments/admin.py index 30c717b2..53592260 100644 --- a/payments/admin.py +++ b/payments/admin.py @@ -11,6 +11,7 @@ class DonationAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_display = [ 'id', + 'stripe_payment_id', 'email', 'formatted_amount', ] @@ -23,6 +24,16 @@ class DonationAdmin(ImportExportMixin, ImportExportActionModelAdmin): 'email' ] + readonly_fields = ( + 'name', + 'email', + 'customer', + 'stripe_customer_id', + 'stripe_payment_id', + 'amount', + 'created_at', + ) + view_on_site = False def formatted_amount(self, obj): diff --git a/payments/migrations/0001_initial.py b/payments/migrations/0001_initial.py index d97e5ea5..fca3503b 100644 --- a/payments/migrations/0001_initial.py +++ b/payments/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.1 on 2019-05-20 21:18 +# Generated by Django 2.2.1 on 2019-05-22 16:59 from django.conf import settings from django.db import migrations, models @@ -21,7 +21,8 @@ class Migration(migrations.Migration): ('name', models.CharField(blank=True, max_length=100, null=True)), ('email', models.EmailField(max_length=254)), ('stripe_customer_id', models.CharField(max_length=350)), - ('amount', models.IntegerField()), + ('stripe_payment_id', models.CharField(max_length=350)), + ('amount', models.PositiveIntegerField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer', to=settings.AUTH_USER_MODEL)), diff --git a/payments/models.py b/payments/models.py index d2b0ea7a..0cf5a331 100644 --- a/payments/models.py +++ b/payments/models.py @@ -21,7 +21,10 @@ class Donation(models.Model): stripe_customer_id = models.CharField( max_length=350, ) - amount = models.IntegerField() + stripe_payment_id = models.CharField( + max_length=350, + ) + amount = models.PositiveIntegerField() created_at = models.DateTimeField( auto_now_add=True, ) @@ -34,3 +37,6 @@ def __str__(self): def get_absolute_url(self): return reverse("Donation_detail", kwargs={"pk": self.pk}) + + def get_formatted_amount(self): + return "{:.2f}".format(self.amount / 100) diff --git a/payments/views.py b/payments/views.py index 23f54c12..de2c8cfb 100644 --- a/payments/views.py +++ b/payments/views.py @@ -44,7 +44,7 @@ def get_initial(self): # An already registered customer is making a donation def donation_is_registered_customer(self, email, name, amount): - + customer = None # Get the user from Donation model with email user = Donation.objects.filter(email=email).first() @@ -55,16 +55,6 @@ def donation_is_registered_customer(self, email, name, amount): if user_email_exists: customer = CDCUser.objects.get(email=email) - # Save the donation to the model. - donation = Donation( - customer=customer, - email=user.email, - name=user.name, - amount=amount.pennies, - stripe_customer_id=user.stripe_customer_id, - ) - donation.save() - try: # Charge the user on Stripe. charge = stripe.Charge.create( @@ -76,6 +66,17 @@ def donation_is_registered_customer(self, email, name, amount): statement_descriptor='Donation to WeAllCode' ) + # Save the donation to the model. + donation = Donation( + customer=customer, + email=user.email, + name=user.name, + amount=amount.pennies, + stripe_customer_id=user.stripe_customer_id, + stripe_payment_id=charge.id + ) + donation.save() + # On Charge success alert the user. messages.success( self.request, @@ -112,16 +113,6 @@ def donation_is_new_customer(self, token, email, name, amount): if user_email_exists: customer = CDCUser.objects.get(email=email) - # Save the Donation to the model - donation = Donation( - customer=customer, - email=stripe_customer.email, - amount=amount.pennies, - name=name, - stripe_customer_id=stripe_customer.id - ) - donation.save() - # Charge the user. charge = stripe.Charge.create( amount=amount.pennies, @@ -132,6 +123,17 @@ def donation_is_new_customer(self, token, email, name, amount): statement_descriptor='Donation to WeAllCode' ) + # Save the Donation to the model + donation = Donation( + customer=customer, + email=stripe_customer.email, + amount=amount.pennies, + name=name, + stripe_customer_id=stripe_customer.id, + stripe_payment_id=charge.id + ) + donation.save() + messages.success( self.request, f"Thank you for your donation of ${ amount }!" diff --git a/weallcode/static/weallcode/css/app.css b/weallcode/static/weallcode/css/app.css index a1727e80..87c51fd2 100644 --- a/weallcode/static/weallcode/css/app.css +++ b/weallcode/static/weallcode/css/app.css @@ -641,6 +641,10 @@ html[data-whatintent='mouse'] .main-menu a:not(.button):hover, padding-bottom: calc(1.2rem - 5px); border-bottom: 5px solid var(--black); } + + .sub-menu .current { + border-bottom: 5px solid var(--black); + } } diff --git a/weallcode/templates/weallcode/_header.html b/weallcode/templates/weallcode/_header.html index 1deec3c8..1b2edf80 100644 --- a/weallcode/templates/weallcode/_header.html +++ b/weallcode/templates/weallcode/_header.html @@ -19,7 +19,7 @@
  • Get Involved
  • {% if user.is_authenticated %} -
  • Account
  • +
  • Account
  • {% else %}
  • Log In
  • {% endif %} @@ -39,7 +39,7 @@
    {% if user.is_authenticated %} - Account + Account {% else %} Log In {% endif %} From 35b8bad5d7033eb3e4df02912813bc2123a5497b Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Wed, 22 May 2019 13:19:00 -0500 Subject: [PATCH 09/18] Deleted launch.sjon --- .vscode/launch.json | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index a761c1e7..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Django", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}\\manage.py", - "args": [ - "runserver", - "--noreload", - "--nothreading" - ], - "django": true - } - ] -} \ No newline at end of file From 7b68bfaafd2ce1b79ea2298a1cab56fdb4a5d350 Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Wed, 22 May 2019 13:20:56 -0500 Subject: [PATCH 10/18] Changed base.html --- accounts/templates/account/_base.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/accounts/templates/account/_base.html b/accounts/templates/account/_base.html index af39504b..df9f3e25 100644 --- a/accounts/templates/account/_base.html +++ b/accounts/templates/account/_base.html @@ -6,9 +6,12 @@ From 5863c10a5eb8c9175a15bd1a36b1abb29f1fa86f Mon Sep 17 00:00:00 2001 From: Lokman Musliu Date: Wed, 22 May 2019 14:53:06 -0500 Subject: [PATCH 11/18] Updated the Donate button --- .../templates/weallcode/get_involved.html | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/weallcode/templates/weallcode/get_involved.html b/weallcode/templates/weallcode/get_involved.html index d1593adc..1c2ec19e 100644 --- a/weallcode/templates/weallcode/get_involved.html +++ b/weallcode/templates/weallcode/get_involved.html @@ -9,7 +9,7 @@ {% block subheader %} {% if messages %} - {% include 'weallcode/_messages.html' %} +{% include 'weallcode/_messages.html' %} {% endif %}