change subscription mechanism to use new gocardless api

This commit is contained in:
Oly 2017-08-14 14:00:59 +01:00
parent b3d580d49c
commit 54e1bce190
26 changed files with 414 additions and 128 deletions

3
compose/data/cache/README.org vendored Normal file
View File

@ -0,0 +1,3 @@
* cache folder
Store things like pip cache here so wee dont have to download every time wee build.

View File

@ -0,0 +1,3 @@
* Logs folder
Store logs generated by containers you want to keep in this folder, usefull if your container fails on startup

View File

@ -10,15 +10,52 @@ https://docs.djangoproject.com/en/dev/ref/settings/
""" """
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import os
import time import time
import environ import environ
# from spirit.settings import *
ROOT_DIR = environ.Path(__file__) - 3 # (mhackspace/config/settings/common.py - 3 = mhackspace/) ROOT_DIR = environ.Path(__file__) - 3 # (mhackspace/config/settings/common.py - 3 = mhackspace/)
APPS_DIR = ROOT_DIR.path('mhackspace') APPS_DIR = ROOT_DIR.path('mhackspace')
env = environ.Env() env = environ.Env()
env.read_env('%s/.env' % ROOT_DIR) env.read_env('%s/.env' % ROOT_DIR)
ST_TOPIC_PRIVATE_CATEGORY_PK = 1
ST_RATELIMIT_ENABLE = True
ST_RATELIMIT_CACHE_PREFIX = 'srl'
ST_RATELIMIT_CACHE = 'default'
ST_RATELIMIT_SKIP_TIMEOUT_CHECK = False
ST_NOTIFICATIONS_PER_PAGE = 20
ST_COMMENT_MAX_LEN = 3000
ST_MENTIONS_PER_COMMENT = 30
ST_DOUBLE_POST_THRESHOLD_MINUTES = 30
ST_YT_PAGINATOR_PAGE_RANGE = 3
ST_SEARCH_QUERY_MIN_LEN = 3
ST_USER_LAST_SEEN_THRESHOLD_MINUTES = 1
ST_PRIVATE_FORUM = False
ST_ALLOWED_UPLOAD_IMAGE_FORMAT = ('jpeg', 'png', 'gif')
ST_ALLOWED_URL_PROTOCOLS = {
'http', 'https', 'mailto', 'ftp', 'ftps',
'git', 'svn', 'magnet', 'irc', 'ircs'}
ST_UNICODE_SLUGS = True
ST_UNIQUE_EMAILS = True
ST_CASE_INSENSITIVE_EMAILS = True
# Tests helpers
ST_TESTS_RATELIMIT_NEVER_EXPIRE = False
ST_BASE_DIR = os.path.dirname(__file__)
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'search', 'whoosh_index'),
},
}
# APP CONFIGURATION # APP CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DJANGO_APPS = ( DJANGO_APPS = (
@ -40,11 +77,41 @@ THIRD_PARTY_APPS = (
'allauth.socialaccount', # registration 'allauth.socialaccount', # registration
'allauth.socialaccount.providers.google', # registration 'allauth.socialaccount.providers.google', # registration
'allauth.socialaccount.providers.github', # registration 'allauth.socialaccount.providers.github', # registration
'allauth.socialaccount.providers.facebook', # registration # 'allauth.socialaccount.providers.facebook', # registration
'whitenoise.runserver_nostatic', 'whitenoise.runserver_nostatic',
'stdimage', 'stdimage',
'rest_framework', 'rest_framework',
'draceditor', 'draceditor',
'haystack',
'djconfig',
'spirit.core',
'spirit.admin',
'spirit.search',
'spirit.user',
'spirit.user.admin',
'spirit.user.auth',
'spirit.category',
'spirit.category.admin',
'spirit.topic',
'spirit.topic.admin',
'spirit.topic.favorite',
'spirit.topic.moderate',
'spirit.topic.notification',
'spirit.topic.poll', # todo: remove in Spirit v0.6
'spirit.topic.private',
'spirit.topic.unread',
'spirit.comment',
'spirit.comment.bookmark',
'spirit.comment.flag',
'spirit.comment.flag.admin',
'spirit.comment.history',
'spirit.comment.like',
'spirit.comment.poll',
) )
# Apps specific for this project go here. # Apps specific for this project go here.
@ -74,6 +141,16 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
#fix for ip logging behind a proxy
'x_forwarded_for.middleware.XForwardedForMiddleware',
'djconfig.middleware.DjConfigMiddleware',
'spirit.user.middleware.TimezoneMiddleware',
'spirit.user.middleware.LastIPMiddleware',
'spirit.user.middleware.LastSeenMiddleware',
'spirit.user.middleware.ActiveUserMiddleware',
'spirit.core.middleware.PrivateForumMiddleware',
) )
# MIGRATIONS CONFIGURATION # MIGRATIONS CONFIGURATION
@ -171,6 +248,7 @@ TEMPLATES = [
'django.template.context_processors.tz', 'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
# Your stuff: custom template context processors go here # Your stuff: custom template context processors go here
'djconfig.context_processors.config',
], ],
}, },
}, },

View File

@ -47,14 +47,14 @@ CACHES = {
'IGNORE_EXCEPTIONS': True, # mimics memcache behavior. 'IGNORE_EXCEPTIONS': True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
} }
},
'st_rate_limit': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'spirit_rl_cache',
'TIMEOUT': None
} }
} }
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
# 'LOCATION': ''
# }
# }
# django-debug-toolbar # django-debug-toolbar
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -92,3 +92,5 @@ CAPTCHA = {
WHITENOISE_AUTOREFRESH = True WHITENOISE_AUTOREFRESH = True
WHITENOISE_USE_FINDERS = True WHITENOISE_USE_FINDERS = True
PAYMENT_PROVIDERS['gocardless']['redirect_url'] = 'http://127.0.0.1:8180'

View File

@ -151,6 +151,11 @@ CACHES = {
'IGNORE_EXCEPTIONS': True, # mimics memcache behavior. 'IGNORE_EXCEPTIONS': True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
} }
},
'st_rate_limit': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'spirit_rl_cache',
'TIMEOUT': None
} }
} }

View File

@ -152,6 +152,11 @@ CACHES = {
'IGNORE_EXCEPTIONS': True, # mimics memcache behavior. 'IGNORE_EXCEPTIONS': True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
} }
},
'st_rate_limit': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'spirit_rl_cache',
'TIMEOUT': None
} }
} }

View File

@ -36,6 +36,11 @@ CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': '' 'LOCATION': ''
},
'st_rate_limit': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'spirit_rl_cache',
'TIMEOUT': None
} }
} }

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
@ -21,6 +20,7 @@ from mhackspace.blog.views import PostViewSet, CategoryViewSet, BlogPost, PostLi
from mhackspace.blog.sitemaps import PostSitemap, CategorySitemap from mhackspace.blog.sitemaps import PostSitemap, CategorySitemap
from mhackspace.feeds.views import FeedViewSet, ArticleViewSet from mhackspace.feeds.views import FeedViewSet, ArticleViewSet
# import spirit.urls
router = DefaultRouter() router = DefaultRouter()
router.register(r'posts', PostViewSet) router.register(r'posts', PostViewSet)
router.register(r'categories', CategoryViewSet) router.register(r'categories', CategoryViewSet)
@ -39,6 +39,7 @@ urlpatterns = [
url(r'^mailing-list/$', TemplateView.as_view(template_name='pages/mailing-list.html'), name='group'), url(r'^mailing-list/$', TemplateView.as_view(template_name='pages/mailing-list.html'), name='group'),
url(r'^contact/$', contact, name='contact'), url(r'^contact/$', contact, name='contact'),
url(r'^discuss/', include('spirit.urls')),
url(r'^api/v1/', include(router.urls, namespace='v1')), url(r'^api/v1/', include(router.urls, namespace='v1')),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^draceditor/', include('draceditor.urls')), url(r'^draceditor/', include('draceditor.urls')),
@ -79,6 +80,7 @@ urlpatterns = [
url(r'^reset/done/$', auth_views.password_reset_complete, name='password_reset_complete'), url(r'^reset/done/$', auth_views.password_reset_complete, name='password_reset_complete'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit # This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like. # these url in browser to see how these error pages look like.

View File

@ -20,9 +20,10 @@ services:
command: /start-dev.sh command: /start-dev.sh
depends_on: depends_on:
- postgres - postgres
environment: env_file: .env
- POSTGRES_USER=mhackspace # environment:
- USE_DOCKER=yes # - POSTGRES_USER=mhackspace
# - USE_DOCKER=yes
volumes: volumes:
- .:/app - .:/app
ports: ports:

View File

@ -28,6 +28,7 @@ services:
env_file: .env env_file: .env
volumes: volumes:
- .:/app - .:/app
- ./compose/data/logs:/var/log/gunicorn
- sockets:/data/sockets - sockets:/data/sockets
node: node:

View File

@ -1,3 +1,4 @@
import random
from autofixture import AutoFixture from autofixture import AutoFixture
from autofixture.generators import ImageGenerator from autofixture.generators import ImageGenerator
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@ -7,10 +8,12 @@ from mhackspace.feeds.models import Article, Feed
from mhackspace.users.models import User from mhackspace.users.models import User
from mhackspace.blog.models import Category, Post from mhackspace.blog.models import Category, Post
class ImageFixture(AutoFixture): class ImageFixture(AutoFixture):
class Values: class Values:
scaled_image = ImageGenerator(width=800, height=300, sizes=((1280, 300),)) scaled_image = ImageGenerator(width=800, height=300, sizes=((1280, 300),))
class Command(BaseCommand): class Command(BaseCommand):
help = 'Build test data for development environment' help = 'Build test data for development environment'
@ -30,10 +33,11 @@ class Command(BaseCommand):
call_command('loaddata', 'mhackspace/users/fixtures/groups.json', verbose=0) call_command('loaddata', 'mhackspace/users/fixtures/groups.json', verbose=0)
# random data # random data
users = AutoFixture(User) users = AutoFixture(User, field_values={
'title': random.choicee(('Mr', 'Mrs', 'Emperor', 'Captain'))
})
users.create(10) users.create(10)
banners = ImageFixture(BannerImage) banners = ImageFixture(BannerImage)
banners.create(10) banners.create(10)
self.stdout.write( self.stdout.write(

View File

@ -6,3 +6,11 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
body {
max-width: 100%;
}
.navbar-brand {
width:100%;
}

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
from django.contrib import messages
from django.contrib.auth.models import Group
from mhackspace.users.models import Membership
def create_or_update_membership(user, signup_details, complete=False):
try:
member = Membership.objects.get(user=user)
except Membership.DoesNotExist:
member = Membership()
member.user = user
if complete is True:
member.status = Membership.lookup_status(name=signup_details.get('status'))
member.email = signup_details.get('email')
member.reference = signup_details.get('reference')
member.payment = signup_details.get('amount')
member.date = signup_details.get('start_date')
member.save()
if complete is False:
return False # sign up not completed
# add user to group on success
group = Group.objects.get(name='members')
user.groups.add(group)
return True # Sign up finished

View File

@ -22,20 +22,16 @@ class Command(BaseCommand):
payment_objects = [] payment_objects = []
for customer in provider.fetch_customers(): for customer in provider.fetch_customers():
# self.stdout.write(str(dir(customer))) user = User.objects.get(email=customer.get('email'))
# self.stdout.write(str(customer))
payment_objects.append(Payments( payment_objects.append(Payments(
user=None, user=user,
user_reference=customer.get('user_id'), user_reference=customer.get('user_reference'),
user_email=customer.get('email'), user_email=customer.get('email'),
reference=customer.get('payment_id'), reference=customer.get('payment_id'),
amount=customer.get('amount'), amount=customer.get('amount'),
type=Payments.lookup_payment_type(customer.get('payment_type')), type=Payments.lookup_payment_type(customer.get('payment_type')),
date=customer.get('payment_date') date=customer.get('payment_date')
)) ))
# self.stdout.write(str(customer.email))
# self.stdout.write(str(dir(customer['email']())))
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
'\t{reference} - {amount} - {type} - {user_email}'.format(**model_to_dict(payment_objects[-1])))) '\t{reference} - {amount} - {type} - {user_email}'.format(**model_to_dict(payment_objects[-1]))))

View File

@ -5,7 +5,7 @@ from django.forms.models import model_to_dict
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from mhackspace.subscriptions.payments import select_provider from mhackspace.subscriptions.payments import select_provider
from mhackspace.users.models import Membership, User from mhackspace.users.models import Membership, User
from mhackspace.subscriptions.helper import create_or_update_membership
def update_subscriptions(provider_name): def update_subscriptions(provider_name):
@ -54,28 +54,44 @@ class Command(BaseCommand):
group = Group.objects.get(name='members') group = Group.objects.get(name='members')
for sub in provider.fetch_subscriptions(): for sub in provider.fetch_subscriptions():
sub['amount'] = sub['amount'] * 0.01
try: try:
user_model = User.objects.get(email=sub.get('email')) user_model = User.objects.get(email=sub.get('email'))
if sub.get('status') == 'active': if sub.get('status') == 'active':
user_model.groups.add(group) user_model.groups.add(group)
except User.DoesNotExist: except User.DoesNotExist:
user_model = None user_model = None
self.style.NOTICE(
'\tMissing User {reference} - {payment} - {status} - {email}'.format(**{
'reference': sub.get('reference'),
'payment': sub.get('amount'),
'status': sub.get('status'),
'email': sub.get('email')
}))
continue
create_or_update_membership(user=user_model,
signup_details=sub,
complete=True)
subscriptions.append( subscriptions.append(
Membership( Membership(
user=user_model, user=user_model,
email=sub.get('email'), email=sub.get('email'),
reference=sub.get('reference'), reference=sub.get('reference'),
payment=10.00, payment=sub.get('amount'),
date= sub.get('start_date'), date=sub.get('start_date'),
# date=timezone.now(),
status=Membership.lookup_status(name=sub.get('status')) status=Membership.lookup_status(name=sub.get('status'))
) )
) )
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
'\t{reference} - {payment} - {status} - {email}'.format(**model_to_dict(subscriptions[-1])))) '\t{reference} - {payment} - {status} - {email}'.format(**{
'reference': sub.get('reference'),
'payment': sub.get('amount'),
'status': sub.get('status'),
'email': sub.get('email')
})))
Membership.objects.bulk_create(subscriptions) Membership.objects.bulk_create(subscriptions)

View File

@ -1,19 +1,14 @@
from pprint import pprint from pprint import pprint
import pytz import pytz
import gocardless import gocardless_pro as gocardless
import braintree import braintree
import logging
from django.conf import settings from django.conf import settings
payment_providers = settings.PAYMENT_PROVIDERS payment_providers = settings.PAYMENT_PROVIDERS
logger = logging.getLogger(__name__)
# import gocardless_pro
# import paypalrestsdk as paypal # import paypalrestsdk as paypal
# from website.config import settings
# from website.config.import app_domain
# from website.config.logger import log
PROVIDER_ID = {'gocardless':1, 'braintree': 2} PROVIDER_ID = {'gocardless':1, 'braintree': 2}
PROVIDER_NAME = {1: 'gocardless', 2: 'braintree'} PROVIDER_NAME = {1: 'gocardless', 2: 'braintree'}
@ -26,19 +21,17 @@ def select_provider(type):
assert 0, "No Provider for " + type assert 0, "No Provider for " + type
class gocardless_provider: class gocardless_provider:
"""
gocardless test account details 20-00-00, 55779911
"""
form_remote = True form_remote = True
client = None client = None
def __init__(self): def __init__(self):
# gocardless are changing there api, not sure if we can switch yet # gocardless are changing there api, not sure if we can switch yet
# self.client = gocardless_pro.Client( self.client = gocardless.Client(
# access_token=payment_providers['gocardless']['credentials']['access_token'], access_token=payment_providers['gocardless']['credentials']['access_token'],
# environment=payment_providers['gocardless']['environment']) environment=payment_providers['gocardless']['environment'])
print(payment_providers.keys)
gocardless.environment = payment_providers['gocardless']['environment']
gocardless.set_details(**payment_providers['gocardless']['credentials'])
self.client = gocardless.client.merchant()
def subscribe_confirm(self, args): def subscribe_confirm(self, args):
response = gocardless.client.confirm_resource(args) response = gocardless.client.confirm_resource(args)
@ -50,49 +43,33 @@ class gocardless_provider:
'success': response.success 'success': response.success
} }
def fetch_customers(self): def fetch_customers(self):
merchant = gocardless.client.merchant() """Fetch list of customers payments"""
for customer in merchant.bills(): for customer in self.client.customers.list().records:
user = customer.user() for payment in self.client.payments.list(params={"customer": customer.id}).records:
# print(dir(customer)) yield {
# print(dir(customer.reference_fields)) 'user_reference': customer.id,
# print(customer.reference_fields) 'email': customer.email,
# print(customer.payout_id) 'status': payment.status,
# print(customer.reference_fields.payout_id) 'payment_id': payment.links.subscription,
result = { 'payment_type': 'subscription' if payment.links.subscription else 'payment',
'user_id': user.id, 'payment_date': payment.created_at,
'email': user.email, 'amount': payment.amount
'status': customer.status,
'payment_id': customer.id,
'payment_type': customer.source_type,
'payment_date': customer.created_at,
'amount': customer.amount
} }
yield result #customer
# for customer in self.client.users():
# result = {
# 'email': customer.email,
# 'created_date': customer.created_at,
# 'first_name': customer.first_name,
# 'last_name': customer.last_name
# }
# yield customer
def fetch_subscriptions(self): def fetch_subscriptions(self):
for paying_member in self.client.subscriptions(): # for paying_member in self.client.mandates.list().records:
user=paying_member.user() for paying_member in self.client.subscriptions.list().records:
mandate=self.client.mandates.get(paying_member.links.mandate)
user=self.client.customers.get(mandate.links.customer)
# gocardless does not have a reference so we use the id instead # gocardless does not have a reference so we use the id instead
yield { yield {
'status': paying_member.status, 'status': paying_member.status,
'email': user.email, 'email': user.email,
'start_date': paying_member.created_at, 'start_date': paying_member.created_at,
'reference': paying_member.id, 'reference': mandate.id,
'amount': paying_member.amount} 'amount': paying_member.amount}
def get_redirect_url(self): def get_redirect_url(self):
@ -116,25 +93,61 @@ class gocardless_provider:
'success': response.get('success', False) 'success': response.get('success', False)
} }
def create_subscription(self, amount, name, redirect_success, redirect_failure, interval_unit='month', interval_length='1'): def create_subscription(self, user, session, amount,
return gocardless.client.new_subscription_url( name, redirect_success, redirect_failure,
amount=float(amount), interval_unit='monthly', interval_length='1'):
interval_length=interval_length, return self.client.redirect_flows.create(params={
interval_unit=interval_unit, "description": name,
name=name, "session_token": session,
redirect_uri=redirect_success) "success_redirect_url": redirect_success,
"prefilled_customer": {
def confirm_subscription(self, provider_response): "given_name": user.first_name,
response = gocardless.client.confirm_resource(provider_response) "family_name": user.last_name,
subscription = gocardless.client.subscription(provider_response.get('resource_id')) "email": user.email
user = subscription.user()
return {
'amount': subscription.amount,
'email': user.email,
'start_date': subscription.created_at,
'reference': subscription.id,
'success': response.get('success')
} }
})
def confirm_subscription(self, membership, session, provider_response,
name, interval_unit='monthly', interval_length='1'):
r = provider_response.get('redirect_flow_id')
# response = self.client.redirect_flows.complete(r, params={
# "session_token": session
# })
response = self.client.redirect_flows.get(r)
# response = self.client.redirect_flows.get(provider_response.get('redirect_flow_id'))
# response = gocardless.client.confirm_resource(provider_response)
# subscription = gocardless.client.subscription(provider_response.get('resource_id'))
user_id = response.links.customer
mandate_id = response.links.mandate
# user = subscription.user()
user = self.client.customers.get(response.links.customer)
mandate = self.client.mandates.get(response.links.mandate)
logging.debug(user)
logging.debug(mandate)
# for some reason go cardless is in pence, so 20.00 needs to be sent as 2000
# what genious decided that was a good idea, now looks like i am charging £2000 :p
# the return is the same so you need to convert on send and receive
subscription_response = self.client.subscriptions.create(
params={
'amount': str(membership.payment).replace('.', ''),
'currency': 'GBP',
'interval_unit': interval_unit,
'name': name,
# 'metadata': {'reference': },
'links': {'mandate': mandate_id}
})
return {
'amount': membership.payment,
'email': user.email,
'start_date': subscription_response.created_at,
'reference': mandate_id,
'success': subscription_response.api_response.status_code
}
class braintree_provider: class braintree_provider:
form_remote = False form_remote = False
@ -189,6 +202,7 @@ class braintree_provider:
class payment: class payment:
""" """
https://developer.gocardless.com/api-reference/#redirect-flows-create-a-redirect-flow
paypal reference = https://github.com/paypal/PayPal-Python-SDK paypal reference = https://github.com/paypal/PayPal-Python-SDK
gocardless reference = https://github.com/paypal/PayPal-Python-SDK gocardless reference = https://github.com/paypal/PayPal-Python-SDK
""" """

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.generic import UpdateView, RedirectView from django.views.generic import UpdateView, RedirectView
@ -13,6 +14,7 @@ from mhackspace.users.models import User, Membership
from mhackspace.users.models import MEMBERSHIP_CANCELLED from mhackspace.users.models import MEMBERSHIP_CANCELLED
from mhackspace.users.forms import MembershipJoinForm from mhackspace.users.forms import MembershipJoinForm
from mhackspace.subscriptions.payments import select_provider from mhackspace.subscriptions.payments import select_provider
from mhackspace.subscriptions.helper import create_or_update_membership
class MembershipCancelView(LoginRequiredMixin, RedirectView): class MembershipCancelView(LoginRequiredMixin, RedirectView):
@ -54,7 +56,6 @@ class MembershipJoinView(LoginRequiredMixin, UpdateView):
return User.objects.get(username=self.request.user.username) return User.objects.get(username=self.request.user.username)
def form_valid(self, form): def form_valid(self, form):
app_domain = 'http://test.maidstone-hackspace.org.uk'
payment_provider = 'gocardless' payment_provider = 'gocardless'
provider = select_provider(payment_provider) provider = select_provider(payment_provider)
app_domain = provider.get_redirect_url() app_domain = provider.get_redirect_url()
@ -64,16 +65,31 @@ class MembershipJoinView(LoginRequiredMixin, UpdateView):
form_subscription = MembershipJoinForm(data=self.request.POST) form_subscription = MembershipJoinForm(data=self.request.POST)
form_subscription.is_valid() form_subscription.is_valid()
result = {
'email': self.request.user.email,
'reference': user_code,
'amount': form_subscription.cleaned_data.get('amount', 20.00) * 0.01,
'start_date': timezone.now()
}
create_or_update_membership(
user=self.request.user,
signup_details=result,
complete=False
)
success_url = '%s/membership/%s/success' % (app_domain, payment_provider) success_url = '%s/membership/%s/success' % (app_domain, payment_provider)
failure_url = '%s/membership/%s/failure' % (app_domain, payment_provider) failure_url = '%s/membership/%s/failure' % (app_domain, payment_provider)
url = provider.create_subscription( url = provider.create_subscription(
user=self.request.user,
session=self.request.session.session_key,
amount=form_subscription.cleaned_data.get('amount', 20.00), amount=form_subscription.cleaned_data.get('amount', 20.00),
name="Membership your membership id is MH%s" % user_code, name="Membership your membership id is MH%s" % user_code,
redirect_success=success_url, redirect_success=success_url,
redirect_failure=failure_url redirect_failure=failure_url
) )
return redirect(url) return redirect(url.redirect_url)
class MembershipJoinSuccessView(LoginRequiredMixin, RedirectView): class MembershipJoinSuccessView(LoginRequiredMixin, RedirectView):
@ -83,11 +99,17 @@ class MembershipJoinSuccessView(LoginRequiredMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
payment_provider = 'gocardless' payment_provider = 'gocardless'
provider = select_provider(payment_provider) provider = select_provider(payment_provider)
membership = Membership.objects.get(user=self.request.user)
name="Membership your membership id is MH%s" % membership.reference
result = provider.confirm_subscription( result = provider.confirm_subscription(
provider_response=self.request.GET membership=membership,
session=self.request.session.session_key,
provider_response=self.request.GET,
name=name
) )
#if something went wrong return to profile with an error # if something went wrong return to profile with an error
if result.get('success') is False: if result.get('success') is False:
messages.add_message( messages.add_message(
self.request, self.request,
@ -96,27 +118,15 @@ class MembershipJoinSuccessView(LoginRequiredMixin, RedirectView):
return super(MembershipJoinSuccessView, self).get_redirect_url(*args, **kwargs) return super(MembershipJoinSuccessView, self).get_redirect_url(*args, **kwargs)
del(kwargs['provider']) del(kwargs['provider'])
try:
member = Membership.objects.get(user=self.request.user)
except Membership.DoesNotExist:
member = Membership()
member.user = self.request.user if create_or_update_membership(user=self.request.user,
member.email = result.get('email') signup_details=result,
member.reference = result.get('reference') complete=True) is True:
member.payment = result.get('amount')
member.date = result.get('start_date')
member.status = Membership.lookup_status(name=result.get('status'))
member.save()
kwargs['username'] = self.request.user.get_username()
# add user to group on success
group = Group.objects.get(name='members')
self.request.user.groups.add(group)
messages.add_message( messages.add_message(
self.request, self.request,
messages.SUCCESS, messages.SUCCESS,
'Success your membership should now be active') 'Success your membership should now be active')
kwargs['username'] = self.request.user.get_username()
return super(MembershipJoinSuccessView, self).get_redirect_url(*args, **kwargs) return super(MembershipJoinSuccessView, self).get_redirect_url(*args, **kwargs)

View File

@ -27,11 +27,12 @@
<meta name="msapplication-config" content="{% static 'browserconfig.xml' %}"> <meta name="msapplication-config" content="{% static 'browserconfig.xml' %}">
<meta name="theme-color" content="#008080"> <meta name="theme-color" content="#008080">
<link href="{% sass_src 'sass/project.scss' %}" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous"> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
{% block css %} {% block css %}
<link href="{% sass_src 'sass/project.scss' %}" rel="stylesheet">
{% endblock %} {% endblock %}
{% block head-extra %}{% endblock head-extra %}
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "http://schema.org", "@context": "http://schema.org",
@ -45,7 +46,7 @@
"@type": "PostalAddress", "@type": "PostalAddress",
"streetAddress": "Maidstone Hackspace, Maidstone Community Support Centre", "streetAddress": "Maidstone Hackspace, Maidstone Community Support Centre",
"addressLocality": "Maidstone", "addressLocality": "Maidstone",
"addressRegion": "FLKent", "addressRegion": "Kent",
"postalCode": "ME14 1HH", "postalCode": "ME14 1HH",
"addressCountry": "UK" "addressCountry": "UK"
}, },
@ -66,6 +67,7 @@
</head> </head>
<body> <body>
<div> <div>
<nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse mb-4"> <nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse mb-4">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse"

View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load sass_tags %}
{% load static i18n %}
{% block title %}Members{% endblock %}
{% block css %}
<link rel="stylesheet" href="{% static 'spirit/stylesheets/styles.all.min.css' %}">
<link href="{% sass_src 'sass/project.scss' %}" rel="stylesheet">
{{ super.block }}
{% endblock css %}
{% block head-extra %}
<script src="{% static "spirit/scripts/all.min.js" %}"></script>
<script>
$( document ).ready(function() {
$.tab();
$( 'a.js-post' ).postify( {
csrfToken: "{{ csrf_token }}",
} );
$('.js-messages').messages();
{% if user.is_authenticated %}
$.notification( {
notificationUrl: "{% url "spirit:topic:notification:index-ajax" %}",
notificationListUrl: "{% url "spirit:topic:notification:index-unread" %}",
mentionTxt: "{% trans "{user} has mention you on {topic}" %}",
commentTxt: "{% trans "{user} has commented on {topic}" %}",
showAll: "{% trans "Show all" %}",
empty: "{% trans "No new notifications, yet" %}",
unread: "{% trans "unread" %}",
} );
{% endif %}
});
</script>
{% endblock head-extra %}
{% block content %}
<div class="container">
<h2>Users</h2>
<div class="list-group">
{% for user in user_list %}
<a href="{% url 'users:detail' user.username %}" class="list-group-item">
<h4 class="list-group-item-heading">{{ user.username }}</h4>
</a>
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load socialaccount %}
{% block title %}User: {{ object.username }}{% endblock %} {% block title %}User: {{ object.username }}{% endblock %}
@ -15,14 +16,22 @@
<p>{{ user.name }}</p> <p>{{ user.name }}</p>
<p>{{ user.email }}</p> <p>{{ user.email }}</p>
<p>Last login {{ user.last_login}}</p> <p>Last login {{ user.last_login}}</p>
<p>Member since</p> {% if blurb.description %}
<p>Description: {{ blurb.description }}</p> <p>Description: {{ blurb.description }}</p>
<p>Skills: {{ blurb.description }}</p> {% endif %}
{% if blurb.skills %}
<p>Skills: {{ blurb.skills }}</p>
{% endif %}
{% if membership %}
<h3>Membership status</h3> <h3>Membership status</h3>
<p>Member since {{membership.date}}</p>
<p>Membership Status: {{ membership.get_status }}</p> <p>Membership Status: {{ membership.get_status }}</p>
<p>Last Payment: {{membership.date}}</p> <p>Last Payment: {{membership.date}}</p>
<p>Amount: &pound;{{membership.payment}}</p> <p>Amount: &pound;{{membership.payment}}</p>
{% else %}
You are not currently a member consider signing up.
{% endif %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% if membership.get_status %} {% if membership.get_status %}
@ -63,11 +72,27 @@
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<p>
<a class="btn btn-primary" href="{% url 'users:update' %}" role="button">My Info</a> <a class="btn btn-primary" href="{% url 'users:update' %}" role="button">My Info</a>
<a class="btn btn-primary" href="{% url 'account_email' %}" role="button">E-Mail</a> <a class="btn btn-primary" href="{% url 'account_email' %}" role="button">E-Mail</a>
</p>
<!-- Your Stuff: Custom user template urls --> <!-- Your Stuff: Custom user template urls -->
</div> </div>
</div>
<div class="row">
<div class="col-sm-12">
<p>
<a class="btn btn-primary" href="{% provider_login_url "google" process="connect" %}">Link your Google account</a>
</p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<p>
<a class="btn btn-primary" href="{% provider_login_url "github" process="connect" %}">Link your Github account</a>
</p>
</div>
</div> </div>
<!-- End Action buttons --> <!-- End Action buttons -->
{% endif %} {% endif %}

View File

@ -11,7 +11,7 @@ from django.urls import reverse
from django.conf.urls import url from django.conf.urls import url
from .models import User, Membership, MEMBERSHIP_STATUS_CHOICES from .models import User, Membership, MEMBERSHIP_STATUS_CHOICES
from mhackspace.subscriptions.management.commands.refresh_subscriptions import update_subscriptions from mhackspace.subscriptions.management.commands.update_membership_status import update_subscriptions
class MyUserChangeForm(UserChangeForm): class MyUserChangeForm(UserChangeForm):
@ -41,7 +41,7 @@ class MyUserAdmin(AuthUserAdmin):
form = MyUserChangeForm form = MyUserChangeForm
add_form = MyUserCreationForm add_form = MyUserCreationForm
fieldsets = ( fieldsets = (
('User Profile', {'fields': ('name', 'image')}), ('User Profile', {'fields': ('name', '_image')}),
) + AuthUserAdmin.fieldsets ) + AuthUserAdmin.fieldsets
list_display = ('username', 'name', 'is_superuser') list_display = ('username', 'name', 'is_superuser')
search_fields = ['name'] search_fields = ['name']

View File

@ -11,17 +11,28 @@ from stdimage.models import StdImageField
class User(AbstractUser): class User(AbstractUser):
name = models.CharField(_('Name of User'), blank=True, max_length=255) name = models.CharField(_('Name of User'), blank=True, max_length=255)
public = models.BooleanField(default=False, help_text='If the users email is public on post feeds') public = models.BooleanField(
image = StdImageField( default=False, help_text='If the users email is public on post feeds')
_image = StdImageField(
upload_to='avatars/', upload_to='avatars/',
blank=True, blank=True,
null=True, null=True,
db_column='image',
variations={ variations={
'profile': { 'profile': {
"width": 256, "width": 256,
"height": 256, "height": 256,
"crop": True}}) "crop": True}})
# https://github.com/pennersr/django-allauth/issues/520
@property
def image(self):
return self._image
@image.setter
def image(self, value):
self._image = value
def __str__(self): def __str__(self):
return self.username return self.username
@ -50,6 +61,7 @@ MEMBERSHIP_STRING = {
} }
MEMBERSHIP_STATUS = { MEMBERSHIP_STATUS = {
'signup': 0, # This means the user has not completed signup
'active': 1, 'active': 1,
'cancelled': 2 'cancelled': 2
} }

View File

@ -26,6 +26,7 @@ class UserDetailView(LoginRequiredMixin, DetailView):
context['membership_form'] = MembershipJoinForm(initial={'amount': 20.00}) context['membership_form'] = MembershipJoinForm(initial={'amount': 20.00})
return context return context
class UserRedirectView(LoginRequiredMixin, RedirectView): class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False permanent = False
@ -35,7 +36,7 @@ class UserRedirectView(LoginRequiredMixin, RedirectView):
class UserUpdateView(LoginRequiredMixin, UpdateView): class UserUpdateView(LoginRequiredMixin, UpdateView):
fields = ['name', 'image', ] fields = ['name', '_image', ]
model = User model = User
# send the user back to their own page after a successful update # send the user back to their own page after a successful update

View File

@ -52,7 +52,7 @@ lxml==3.7.3
# Your custom requirements go here # Your custom requirements go here
mock==2.0.0 mock==2.0.0
gocardless gocardless_pro
braintree==3.37.2 braintree==3.37.2
django-autofixture==0.12.1 django-autofixture==0.12.1
@ -62,5 +62,10 @@ git+https://github.com/olymk2/scaffold.git
djangorestframework==3.6.3 djangorestframework==3.6.3
django-filter==1.0.2 django-filter==1.0.2
#git+https://github.com/olymk2/dracos-markdown-editor.git
draceditor==1.1.8 draceditor==1.1.8
# django-spirit
django-djconfig
django-haystack
git+https://github.com/nitely/Spirit.git
# git+https://github.com/olymk2/django-xforwardedfor-middleware.git
django-xforwardedfor-middleware==2.0

View File

@ -11,3 +11,10 @@ factory-boy==2.8.1
# pytest # pytest
pytest-django==3.1.2 pytest-django==3.1.2
pytest-sugar==0.8.0 pytest-sugar==0.8.0
django-test-plus==1.0.18
# improved REPL
ipdb==0.10.3
pytest-django==3.1.2
pytest-sugar==0.8.0

View File

@ -28,6 +28,7 @@ services:
env_file: .env env_file: .env
volumes: volumes:
- .:/app - .:/app
- ./compose/data/logs:/var/log/gunicorn
- sockets:/data/sockets - sockets:/data/sockets
node: node: