Merge branch 'master' into pyup-pin-mock-2.0.0

This commit is contained in:
Oliver Marks 2017-02-02 19:01:25 +00:00 committed by GitHub
commit cb312b34e5
46 changed files with 609 additions and 391 deletions

View File

@ -6,7 +6,12 @@ pipeline:
- USE_DOCKER=yes
- DJANGO_SETTINGS_MODULE=config.settings.test
commands:
- python manage.py test mhackspace.subscriptions --verbosity 2
- cp -n env.example .env
- python manage.py test mhackspace --verbosity 2
deploy:
#volumes:
# postgres_data_dev: {}

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
sudo: true
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb
- sudo apt-get install -qq libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms1-dev libwebp-dev
- sudo apt-get install -qq graphviz-dev python-setuptools python3-dev python-virtualenv python-pip
- sudo apt-get install -qq firefox automake libtool libreadline6 libreadline6-dev libreadline-dev
- sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm
language: python
python:
- "3.5"

6
circle.yml Normal file
View File

@ -0,0 +1,6 @@
machine:
python:
version: 3.5.0
environment:
DJANGO_SETTINGS_MODULE: config.settings.test
DATABASE_URL: postgres://ubuntu:@127.0.0.1:5432/circle_test

View File

@ -11,6 +11,7 @@ RUN pip install -r /requirements/production.txt \
COPY . /app
RUN chown -R django /app
RUN mkdir -p /data/sockets
COPY ./compose/django/gunicorn.sh /gunicorn.sh
COPY ./compose/django/entrypoint.sh /entrypoint.sh
@ -19,7 +20,8 @@ RUN sed -i 's/\r//' /entrypoint.sh \
&& chmod +x /entrypoint.sh \
&& chown django /entrypoint.sh \
&& chmod +x /gunicorn.sh \
&& chown django /gunicorn.sh
&& chown django /gunicorn.sh \
&& chown django /data/sockets
WORKDIR /app

View File

@ -33,5 +33,4 @@ until postgres_ready; do
sleep 1
done
>&2 echo "Postgres is up - continuing..."
exec $cmd

View File

@ -1,3 +1,6 @@
#!/bin/sh
python /app/manage.py collectstatic --noinput
/usr/local/bin/gunicorn config.wsgi -w 4 -b 0.0.0.0:5000 --chdir=/app
chmod 777 -R /data/sockets/
touch /data/sockets/gunicron.sock
ls -la /data/sockets/
/usr/local/bin/gunicorn config.wsgi -w 4 -b unix:/data/sockets/gunicorn.sock --chdir=/app

View File

@ -4,6 +4,6 @@ ADD nginx.conf /etc/nginx/nginx.conf
ADD start.sh /start.sh
ADD nginx-secure.conf /etc/nginx/nginx-secure.conf
ADD dhparams.pem /etc/ssl/private/dhparams.pem
#ADD dhparams.pem /etc/ssl/private/dhparams.pem
CMD /start.sh

View File

@ -26,7 +26,8 @@ http {
#gzip on;
upstream app {
server django:5000;
#server django:5000;
server unix:/data/sockets/gunicorn.sock;
}
server {
@ -36,12 +37,12 @@ http {
server_name ___my.example.com___ ;
location /.well-known/acme-challenge {
proxy_pass http://certbot:80;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
# location /.well-known/acme-challenge {
# proxy_pass http://certbot:80;
# proxy_set_header Host $host;
# proxy_set_header X-Forwarded-For $remote_addr;
# proxy_set_header X-Forwarded-Proto https;
# }
location / {

View File

@ -16,7 +16,7 @@ ROOT_DIR = environ.Path(__file__) - 3 # (mhackspace/config/settings/common.py -
APPS_DIR = ROOT_DIR.path('mhackspace')
env = environ.Env()
env.read_env()
env.read_env('%s/.env' % ROOT_DIR)
# APP CONFIGURATION
# ------------------------------------------------------------------------------
@ -48,6 +48,7 @@ LOCAL_APPS = (
# custom users app
# Your stuff: custom apps go here
'mhackspace.users.apps.UsersConfig',
'mhackspace.base',
'mhackspace.subscriptions',
'mhackspace.feeds',
'mhackspace.contact',
@ -189,6 +190,7 @@ STATICFILES_DIRS = (
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'sass_processor.finders.CssFinder',
)
# MEDIA CONFIGURATION
@ -252,7 +254,7 @@ LOGIN_URL = 'account_login'
AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify'
# django-compressor
# ------------------------------------------------------------------------------
INSTALLED_APPS += ("compressor", )
INSTALLED_APPS += ("compressor", 'sass_processor',)
STATICFILES_FINDERS += ("compressor.finders.CompressorFinder", )
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
@ -262,7 +264,7 @@ ADMIN_URL = '^admin/'
# ------------------------------------------------------------------------------
payment_providers = {
PAYMENT_PROVIDERS = {
'braintree': {
'mode': 'sandbox',
'credentials': {
@ -275,7 +277,7 @@ payment_providers = {
"mode": "sandbox", # sandbox or live
'credentials': {
"mode": "sandbox", # sandbox or live
"client_id": end('PAYPAL_CLIENT_ID'),
"client_id": env('PAYPAL_CLIENT_ID'),
"client_secret": env('PAYPAL_CLIENT_SECRET')}
},
'gocardless':{

View File

@ -56,6 +56,9 @@ X_FRAME_OPTIONS = 'DENY'
# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['maidstone-hackspace.org.uk'])
ALLOWED_HOSTS.append('172.*')
ALLOWED_HOSTS.append('172.18.0.5')
# END SITE CONFIGURATION
INSTALLED_APPS += ('gunicorn', )

View File

@ -1,8 +1,20 @@
version: '2'
volumes:
postgres_data: {}
postgres_backup: {}
gunicorn_socket:
driver: local
postgres_data:
driver: local
postgres_backup:
driver: local
# volumes:
# sockets:
# driver: local
# data:
# driver: local
services:
postgres:
@ -22,37 +34,41 @@ services:
- redis
command: /gunicorn.sh
env_file: .env
volumes:
- .:/app
- gunicorn_socket:/data/sockets
nginx:
build: ./compose/nginx
env_file: .env
depends_on:
- django
- certbot
ports:
- "0.0.0.0:80:80"
# - certbot
environment:
- MY_DOMAIN_NAME=maidstone-hackspace.org.uk
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
volumes:
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
- .:/app
- gunicorn_socket:/data/sockets
# - "0.0.0.0:443:443"
# volumes:
# - /etc/letsencrypt:/etc/letsencrypt
# - /var/lib/letsencrypt:/var/lib/letsencrypt
certbot:
image: quay.io/letsencrypt/letsencrypt
command: bash -c "sleep 6 && certbot certonly -n --standalone -d maidstone-hackspace.org.uk --text --agree-tos --email support@maidstone-hackspace.org.uk --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --standalone-supported-challenges http-01"
entrypoint: ""
volumes:
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
ports:
- "80"
- "443"
environment:
- TERM=xterm
# certbot:
# image: quay.io/letsencrypt/letsencrypt
# command: bash -c "sleep 6 && certbot certonly -n --standalone -d maidstone-hackspace.org.uk --text --agree-tos --email support@maidstone-hackspace.org.uk --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --standalone-supported-challenges http-01"
# entrypoint: ""
# volumes:
# - /etc/letsencrypt:/etc/letsencrypt
# - /var/lib/letsencrypt:/var/lib/letsencrypt
# ports:
# - "80"
# - "443"
# environment:
# - TERM=xterm
redis:

View File

@ -29,16 +29,16 @@ DJANGO_ACCOUNT_ALLOW_REGISTRATION=True
COMPRESS_ENABLED=
PAYMENT_ENVIRONMENT = 'sandbox'
PAYMENT_ENVIRONMENT=sandbox
BRAINTREE_MERCHANT_ID = ''
BRAINTREE_PUBLIC_KEY = ''
BRAINTREE_PRIVATE_KEY = ''
BRAINTREE_MERCHANT_ID=demo
BRAINTREE_PUBLIC_KEY=demo
BRAINTREE_PRIVATE_KEY=demo
PAYPAL_CLIENT_ID = ""
PAYPAL_CLIENT_SECRET = ""
PAYPAL_CLIENT_ID=demo
PAYPAL_CLIENT_SECRET=demo
GOCARDLESS_APP_ID = ''
GOCARDLESS_APP_SECRET = ''
GOCARDLESS_ACCESS_TOKEN = ''
GOCARDLESS_MERCHANT_ID = ''
GOCARDLESS_APP_ID=demo
GOCARDLESS_APP_SECRET=demo
GOCARDLESS_ACCESS_TOKEN=demo
GOCARDLESS_MERCHANT_ID=demo

55
live.yml Normal file
View File

@ -0,0 +1,55 @@
version: '2'
volumes:
gunicorn_socket: {}
postgres_data_dev: {}
postgres_backup_dev: {}
services:
postgres:
build: ./compose/postgres
volumes:
- postgres_data_dev:/var/lib/postgresql/data
- postgres_backup_dev:/backups
# environment:
# - POSTGRES_USER=${POSTGRES_USER}
env_file: .env
django:
build:
context: .
dockerfile: ./compose/django/Dockerfile
command: /start-dev.sh
depends_on:
- postgres
environment:
- POSTGRES_USER=${POSTGRES_USER}
- USE_DOCKER=yes
volumes:
- .:/app
- gunicorn_socket:/var/run/gunicorn/
ports:
- "8180:8000"
links:
- postgres
- mailhog
nginx:
build: ./compose/nginx
depends_on:
- django
environment:
- MY_DOMAIN_NAME=maidstone-hackspace.org.uk
ports:
- "0.0.0.0:80:80"
# - "0.0.0.0:443:443"
volumes:
- gunicorn_socket:/var/run/gunicorn/
mailhog:
image: mailhog/mailhog
ports:
- "8125:8025"
redis:
image: redis:latest

View File

@ -0,0 +1,19 @@
from autofixture import AutoFixture
from django.core.management.base import BaseCommand
from mhackspace.feeds.models import Article
from mhackspace.users.models import User
class Command(BaseCommand):
help = 'Imports the RSS feeds from active blogs'
def handle(self, *args, **options):
users = AutoFixture(User)
users.create(10)
feeds = AutoFixture(User)
feeds.create(10)
self.stdout.write(
self.style.SUCCESS(
'Finished creating test data'))

View File

View File

@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-04 14:04
# Generated by Django 1.10.5 on 2017-01-28 18:38
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import stdimage.models
import stdimage.utils
class Migration(migrations.Migration):
@ -13,14 +17,35 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField()),
('title', models.CharField(max_length=255)),
('original_image', models.URLField(blank=True, max_length=255, null=True)),
('image', stdimage.models.StdImageField(blank=True, null=True, upload_to=stdimage.utils.UploadToAutoSlugClassNameDir('title'))),
('description', models.TextField()),
('displayed', models.BooleanField(default=True)),
('date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.CreateModel(
name='Feed',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.CharField(max_length=255)),
('home_url', models.URLField(verbose_name='Site Home Page')),
('feed_url', models.URLField(verbose_name='RSS Feed URL')),
('title', models.CharField(max_length=255)),
('author', models.CharField(max_length=255)),
('tags', models.CharField(max_length=255)),
('image', models.ImageField(upload_to='')),
('tags', models.CharField(blank=True, max_length=255)),
('image', stdimage.models.StdImageField(blank=True, null=True, upload_to=stdimage.utils.UploadToAutoSlugClassNameDir('title'))),
('enabled', models.BooleanField(default=True)),
],
),
migrations.AddField(
model_name='article',
name='feed',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.Feed'),
),
]

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-04 20:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('feeds', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='feed',
name='id',
field=models.IntegerField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='feed',
name='image',
field=models.ImageField(blank=True, upload_to=''),
),
migrations.AlterField(
model_name='feed',
name='tags',
field=models.CharField(blank=True, max_length=255),
),
]

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-04 20:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('feeds', '0002_auto_20170104_2033'),
]
operations = [
migrations.AlterField(
model_name='feed',
name='id',
field=models.AutoField(primary_key=True, serialize=False),
),
]

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-04 21:39
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('feeds', '0003_auto_20170104_2035'),
]
operations = [
migrations.AddField(
model_name='feed',
name='enabled',
field=models.BooleanField(default=True),
),
]

View File

@ -1,69 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-08 03:42
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import stdimage.models
import stdimage.utils
class Migration(migrations.Migration):
dependencies = [
('feeds', '0004_feed_enabled'),
]
operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField()),
('title', models.CharField(max_length=255)),
('original_image', models.URLField(blank=True, max_length=255, null=True)),
('image', stdimage.models.StdImageField(blank=True, null=True, upload_to=stdimage.utils.UploadToAutoSlugClassNameDir('title'))),
('description', models.TextField()),
('displayed', models.BooleanField(default=True)),
('date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.RemoveField(
model_name='feed',
name='url',
),
migrations.AddField(
model_name='feed',
name='feed_url',
field=models.URLField(default='http://thearduinoguy.org/?feed=rss2', verbose_name='RSS Feed URL'),
preserve_default=False,
),
migrations.AddField(
model_name='feed',
name='home_url',
field=models.URLField(default='http://thearduinoguy.org/', verbose_name='Site Home Page'),
preserve_default=False,
),
migrations.AddField(
model_name='feed',
name='title',
field=models.CharField(default='The Arduino Guy', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='feed',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='feed',
name='image',
field=stdimage.models.StdImageField(blank=True, null=True, upload_to=stdimage.utils.UploadToAutoSlugClassNameDir('title')),
),
migrations.AddField(
model_name='article',
name='feed',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.Feed'),
),
]

View File

@ -10,11 +10,11 @@ from mhackspace.users.models import User
class MemberListView(LoginRequiredMixin, ListView):
template_name = 'pages/members.html'
queryset = User.objects.prefetch_related('users', 'groups')
queryset = User.objects.prefetch_related('user', 'groups')
paginate_by = 10
def get_context_data(self, **kwargs):
context = super(MemberListView, self).get_context_data(**kwargs)
context['members'] = self.get_queryset()
context['total'] = self.get_queryset().filter(groups__name='member').count()
context['total'] = self.get_queryset().filter(groups__name='members').count()
return context

View File

View File

@ -0,0 +1,45 @@
from datetime import datetime
from django.utils import timezone
from django.contrib.auth.models import Group
from django.forms.models import model_to_dict
from django.core.management.base import BaseCommand
from mhackspace.subscriptions.payments import select_provider
from mhackspace.users.models import Membership, User
from mhackspace.subscriptions.models import Payments
class Command(BaseCommand):
help = 'Update user subscriptions'
def handle(self, *args, **options):
provider = select_provider('gocardless')
self.stdout.write(
self.style.NOTICE(
'== Gocardless customer payments =='))
Payments.objects.all().delete()
payment_objects = []
for customer in provider.fetch_customers():
# self.stdout.write(str(dir(customer)))
# self.stdout.write(str(customer))
payment_objects.append(Payments(
user=None,
user_reference=customer.get('user_id'),
user_email=customer.get('email'),
reference=customer.get('payment_id'),
amount=customer.get('amount'),
type=Payments.lookup_payment_type(customer.get('payment_type')),
date=customer.get('payment_date')
))
# self.stdout.write(str(customer.email))
# self.stdout.write(str(dir(customer['email']())))
self.stdout.write(
self.style.SUCCESS(
'\t{reference} - {amount} - {type} - {user_email}'.format(**model_to_dict(payment_objects[-1]))))
Payments.objects.bulk_create(payment_objects)

View File

@ -24,4 +24,4 @@ class Command(BaseCommand):
for sub in provider.fetch_subscriptions():
self.stdout.write(
self.style.SUCCESS(
'\t{reference} - {amount} - {status} - {email}'.format(**sub)))
'\t{start_date} {reference} - {amount} - {status} - {email}'.format(**sub)))

View File

@ -0,0 +1,51 @@
from datetime import datetime
from django.utils import timezone
from django.contrib.auth.models import Group
from django.forms.models import model_to_dict
from django.core.management.base import BaseCommand
from mhackspace.subscriptions.payments import select_provider
from mhackspace.users.models import Membership, User
class Command(BaseCommand):
help = 'Update user subscriptions'
def handle(self, *args, **options):
provider = select_provider('gocardless')
self.stdout.write(
self.style.NOTICE(
'== Gocardless subscriptions =='))
Membership.objects.all().delete()
subscriptions = []
group = Group.objects.get(name='members')
for sub in provider.fetch_subscriptions():
try:
user_model = User.objects.get(email=sub.get('email'))
if sub.get('status') == 'active':
user_model.groups.add(group)
except User.DoesNotExist:
user_model = None
self.stdout.write(sub.get('status'))
subscriptions.append(
Membership(
user=user_model,
email=sub.get('email'),
reference=sub.get('reference'),
payment=10.00,
date= sub.get('start_date'),
# date=timezone.now(),
status=Membership.lookup_status(name=sub.get('status'))
)
)
self.stdout.write(
self.style.SUCCESS(
'\t{reference} - {payment} - {status} - {email}'.format(**model_to_dict(subscriptions[-1]))))
Membership.objects.bulk_create(subscriptions)

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-29 21:55
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Payments',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_reference', models.CharField(max_length=255)),
('user_email', models.CharField(max_length=255)),
('reference', models.CharField(max_length=255, unique=True)),
('amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=6)),
('type', models.PositiveSmallIntegerField(default=0)),
('date', models.DateTimeField()),
('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='from_user', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from stdimage.models import StdImageField
PAYMENT_TYPES = {
'unknown': 0,
'subscription': 1,
'payment': 2
}
@python_2_unicode_compatible
class Payments(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True, blank=True,
default=None,
related_name='from_user'
)
user_reference = models.CharField(max_length=255)
user_email = models.CharField(max_length=255)
reference = models.CharField(max_length=255, unique=True)
amount = models.DecimalField(max_digits=6, decimal_places=2, default=0.0)
type = models.PositiveSmallIntegerField(default=0)
date = models.DateTimeField()
def lookup_payment_type(name):
return PAYMENT_TYPES.get(name, 0)
def get_payment_type(self):
return self.type
def __str__(self):
return self.reference

View File

@ -3,7 +3,8 @@ import pytz
import gocardless
import braintree
from django.conf.settings import payment_providers
from django.conf import settings
payment_providers = settings.PAYMENT_PROVIDERS
# import gocardless_pro
# import paypalrestsdk as paypal
@ -45,18 +46,48 @@ class gocardless_provider:
return {
'amount': subscription.amount,
'start_date': subscription.created_at,
'reference': subscription.id
'reference': subscription.id,
'success': response.success
}
def fetch_customers(self):
merchant = gocardless.client.merchant()
for customer in merchant.bills():
user = customer.user()
# print(dir(customer))
# print(dir(customer.reference_fields))
# print(customer.reference_fields)
# print(customer.payout_id)
# print(customer.reference_fields.payout_id)
result = {
'user_id': user.id,
'email': user.email,
'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):
for paying_member in self.client.subscriptions():
user=paying_member.user()
# for bill in paying_member.bills():
# print('test')
# print(dir(bill))
# print(bill.created_at)
# print(dir(paying_member))
# print(paying_member.reference_fields)
#gocardless does not have a reference so we use the id instead
yield {
'status': paying_member.status,
'email': user.email,
@ -70,6 +101,16 @@ class gocardless_provider:
def get_token(self):
return 'N/A'
def cancel_subscribe(self, reference):
subscription = gocardless.client.subscription(reference)
response = subscription.cancel()
return {
'amount': subscription.amount,
'start_date': subscription.created_at,
'reference': subscription.id,
'success': response.success
}
def create_subscription(self, amount, name, redirect_success, redirect_failure, interval_unit='month', interval_length='1'):
return gocardless.client.new_subscription_url(
amount=float(amount),
@ -199,40 +240,6 @@ class payment:
'reference': paying_member.id,
'amount': paying_member.amount}
if self.provider == 'paypal':
#~ I-S39170DK26AF
#~ start_date, end_date = "2014-07-01", "2014-07-20"
billing_agreement = paypal.BillingAgreement.find('')
print(billing_agreement)
print(dir(billing_agreement))
#~ print billing_agreement.search_transactions(start_date, end_date)
#~ transactions = billing_agreement.search_transactions(start_date, end_date)
payment_history = paypal.Payment.all({"count": 2})
# List Payments
print("List Payment:")
print(payment_history)
for payment in payment_history.payments:
print(" -> Payment[%s]" % (payment.id))
#~ print paypal.BillingAgreement.all()
history = paypal.BillingPlan.all(
{"status": "CREATED", "page_size": 5, "page": 1, "total_required": "yes"})
print(history)
print("List BillingPlan:")
for plan in history.plans:
print(dir(plan))
print(plan.to_dict())
print(" -> BillingPlan[%s]" % (plan.id))
#~ merchant = gocardless.client.merchant()
#~ for paying_member in merchant.subscriptions():
#~ user=paying_member.user()
#~ yield {
#~ 'email': user.email,
#~ 'start_date': paying_member.created_at,
#~ 'reference': paying_member.id,
#~ 'amount': paying_member.amount}
def subscribe_confirm(self, args):
if self.provider == 'gocardless':
@ -362,9 +369,7 @@ class payment:
confirm_details['successfull'] = False
print('---------------------')
print(args)
from pprint import pprint
if self.provider == 'paypal':
print(args.get('paymentId'))

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from test_plus.test import TestCase
# import unittest
from unittest import skip
from mock import patch, Mock
from mhackspace.subscriptions.payments import payment, gocardless_provider, braintree_provider
@ -26,31 +26,73 @@ class TestPaymentGatewaysGocardless(TestCase):
self.provider = gocardless_provider()
return self.provider #self.provider
def test_confirm_subscription_callback(self):
with patch('gocardless.client.confirm_resources') as mock_subscription:
self.provider = gocardless_provider()
def test_fetch_subscription_gocardless(self):
items = [Mock(
@skip("Need to implement")
@patch('mhackspace.subscriptions.payments.gocardless.client.subscription', autospec=True)
def test_unsubscribe(self, mock_subscription):
mock_subscription.return_value = Mock(success='success')
mock_subscription.cancel.return_value = Mock(
id='01',
status='active',
amount=20.00,
reference='ref01',
created_at='date'
)]
items[-1].user.return_value = Mock(email='test@test.com')
)
result = self.provider.cancel_subscription(reference='M01')
self.assertEqual(result.get('amount'), 20.00)
self.assertEqual(result.get('start_date'), 'date')
self.assertEqual(result.get('reference'), '01')
self.assertEqual(result.get('success'), 'success')
# @patch('mhackspace.subscriptions.payments.gocardless.request.requests.get', autospec=True)
@patch('mhackspace.subscriptions.payments.gocardless.client.subscription', autospec=True)
@patch('mhackspace.subscriptions.payments.gocardless.client.confirm_resource', autospec=True)
def test_confirm_subscription_callback(self, mock_confirm, mock_subscription):
mock_confirm.return_value = Mock(success='success')
mock_subscription.return_value = Mock(
id='01',
status='active',
amount=20.00,
created_at='date'
)
request_params = {
'resource_uri': 'http://gocardless/resource/url/01',
'resource_id': '01',
'resource_type': 'subscription',
'signature': 'sig',
'state': 'inactive'
}
result = self.provider.subscribe_confirm(request_params)
self.assertEqual(result.get('amount'), 20.00)
self.assertEqual(result.get('start_date'), 'date')
self.assertEqual(result.get('reference'), '01')
self.assertEqual(result.get('success'), 'success')
def test_fetch_subscription_gocardless(self):
item = Mock(
id='01',
status='active',
amount=20.00,
created_at='date'
)
item.user.return_value = Mock(email='test@test.com')
self.provider.client = Mock()
self.provider.client.subscriptions = Mock(return_value=items)
self.provider.client.subscriptions = Mock(return_value=[item])
# mock out gocardless subscriptions method, and return our own values
for item in self.provider.fetch_subscriptions():
self.assertEqual(item.get('status'), 'active')
self.assertEqual(item.get('email'), 'test@test.com')
self.assertEqual(item.get('reference'), 'ref01')
self.assertEqual(item.get('reference'), '01')
self.assertEqual(item.get('start_date'), 'date')
self.assertEqual(item.get('amount'), 20.00)
class TestPaymentGatewaysBraintree(TestCase):
class DisabledestPaymentGatewaysBraintree(TestCase):
@patch('mhackspace.subscriptions.payments.braintree.Configuration.configure')
def auth_braintree(self, mock_request):
# mock braintree initalisation request

View File

@ -91,8 +91,25 @@
</nav>
</div>
<div class="container">
<ul class="nav flex-column navbar-left" style="position:absolute;top:80px;background-color:#bbbbbb;">
<li class="nav-item">
<a class="nav-link active" href="#">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/members">Members</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Mailing List</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="/equipment">Equipment</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="/accounts/signout">Sign out</a>
</li>
</ul>
<div class="container">
{% if messages %}
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">{{ message }}</div>

View File

@ -4,5 +4,7 @@
{% block content %}
<h2>Introduction</h2>
Hackspaces are a shared space where artists, designers, makers, hackers, programmers, tinkerers, professionals and hobbyists can work on their projects, share knowledge and collaborate.We are in the process of developing Maidstone Hackspace. We're previous members of (ICMP) and looking to form a new space in the future. At the moment, communication is via google groups, email, and the website. If you're at all intrested please join our mailing list and make yourself known!
Click here to chat with us https://hangouts.google.com/group/oDcAL0nDfQYfO3qq1
{% show_feeds %}
{% endblock content %}

View File

@ -4,8 +4,8 @@
Members feed
<div class="row">
<div class="">Users {{ paginator.count }}</div>
<div class="">Members {{ total }}</div>
<div class="col">Users {{ paginator.count }}</div>
<div class="col">Members {{ total }}</div>
</div>
<div class="card-deck-wrapper">
@ -17,13 +17,14 @@ Members feed
<h4 class="card-title">{{ member.name }}</h4>
<p class="card-text">{{ member.users.description }}</p>
<p class="card-text">{{ member.users.skills }}</p>
<p class="card-text">{{ member.status }}</p>
{% for group in member.groups.all %}
{{ group.name }}
{% endfor %}
</div>
<div class="card-block">
<p class="card-text">{{ member.userblurb }}</p>
<p class="card-text">{{ member.blurb }}</p>
</div>
<div class="card-block">
<a href="{{ feed.url }}" class="card-link">View Original</a>

View File

@ -5,12 +5,25 @@
{% block content %}
<div class="container">
<style>
#membercard {
color: #000;
background-color: #8C50A5;
border-radius: 14px;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.5);
background-image: url('/static/images/membership_card_background.png');
width: 430px;
height: 240px;
margin-left: 20px;
text-shadow: 1px 1px #FFF;
position: relative;
float: right;
}
</style>
<h2>Profile</h2>
<div class="row">
<div class="col-md-6">
<img src="{{ user.image.profile.url }}" alt="Profile image"/>
<p>{{ user.username }}</p>
<p>{{ user.name }}</p>
<p>{{ user.email }}</p>
@ -18,13 +31,18 @@
<p>Member since</p>
<p>Description: {{ blurb.description }}</p>
<p>Skills: {{ blurb.description }}</p>
<h3>Membership status</h3>
<p>Membership Status: {{ membership.get_status }}</p>
<p>Last Payment: {{membership.date}}</p>
<p>Amount: &pound;{{membership.payment}}</p>
</div>
<div class="col-md-6">
<div id="membercard" class="registered">
<div class="date">Joined </div>
<div class="date">Joined {{membership.date}}</div>
<div class="container">
<div class="middle">
<p>MHS{{ user.id|stringformat:"05d" }}</p><p>Change me</p>
<p>MHS{{ user.id|stringformat:"05d" }}</p><p>{{user.name}}{{user.last_name}}</p>
<a href="/profile/membership/cancel">Cancel Membership</a>
</div>
</div>

View File

@ -0,0 +1 @@
[{"model": "auth.group", "pk": 1, "fields": {"name": "members", "permissions": []}}]

View File

@ -2,9 +2,9 @@
from django.db import models
from django.forms import ModelForm
from .models import UserBlurb
from .models import Blurb
class UserBlurbForm(ModelForm):
class BlurbForm(ModelForm):
class Meta:
model = UserBlurb
model = Blurb
exclude = ['user']

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-23 04:36
# Generated by Django 1.10.5 on 2017-01-28 19:46
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import stdimage.models
class Migration(migrations.Migration):
@ -32,16 +35,38 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')),
('image', stdimage.models.StdImageField(blank=True, null=True, upload_to='avatars/')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name_plural': 'users',
'verbose_name': 'user',
'abstract': False,
'verbose_name_plural': 'users',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Blurb',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('skills', models.CharField(max_length=255)),
('description', models.TextField()),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Membership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment', models.DecimalField(decimal_places=2, default=0.0, max_digits=6)),
('date', models.DateTimeField()),
('reference', models.CharField(max_length=255)),
('status', models.PositiveSmallIntegerField(default=0)),
('email', models.CharField(max_length=255)),
('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-06 08:53
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Membership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment', models.DecimalField(decimal_places=2, max_digits=6)),
('date', models.DateTimeField()),
('reference', models.CharField(max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='UserBlurb',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('skills', models.CharField(max_length=255)),
('description', models.TextField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-06 19:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_membership_userblurb'),
]
operations = [
migrations.AddField(
model_name='user',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='/upload/avatars'),
),
]

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-06 20:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0003_user_image'),
]
operations = [
migrations.RenameModel(
old_name='Membership',
new_name='UserMembership',
),
migrations.AlterField(
model_name='user',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='avatars/'),
),
]

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-08 00:50
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import stdimage.models
class Migration(migrations.Migration):
dependencies = [
('users', '0004_auto_20170106_2030'),
]
operations = [
migrations.AlterField(
model_name='user',
name='image',
field=stdimage.models.StdImageField(blank=True, null=True, upload_to='avatars/'),
),
migrations.AlterField(
model_name='userblurb',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -29,14 +29,44 @@ class User(AbstractUser):
return reverse('users:detail', kwargs={'username': self.username})
class UserBlurb(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='users')
class Blurb(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='+')
skills = models.CharField(max_length=255)
description = models.TextField()
class UserMembership(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
payment = models.DecimalField(max_digits=6, decimal_places=2)
MEMBERSHIP_STRING = {
0: 'Guest user',
1: 'Active membership',
3: 'Membership Expired'
}
MEMBERSHIP_STATUS = {
'active': 1,
'cancelled': 2
}
class Membership(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True, blank=True,
default=None,
related_name='user'
)
payment = models.DecimalField(max_digits=6, decimal_places=2, default=0.0)
date = models.DateTimeField()
reference = models.CharField(max_length=255)
status = models.PositiveSmallIntegerField(default=0)
email = models.CharField(max_length=255)
@property
def get_status(self):
return MEMBERSHIP_STRING[self.status]
def lookup_status(name):
if not name:
return 0
return MEMBERSHIP_STATUS.get(name.lower(), 0)
def __str__(self):
return self.reference

View File

@ -7,10 +7,10 @@ from django.views.generic import DetailView, ListView, RedirectView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import User
from .models import UserBlurb
from .models import UserMembership
from .models import Blurb
from .models import Membership
from .forms import UserBlurbForm
from .forms import BlurbForm
class UserDetailView(LoginRequiredMixin, DetailView):
model = User
@ -21,8 +21,8 @@ class UserDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
# xxx will be available in the template as the related objects
context = super(UserDetailView, self).get_context_data(**kwargs)
context['blurb'] = UserBlurb.objects.filter(user=self.get_object()).first()
context['membership'] = UserMembership.objects.filter(user=self.get_object()).first()
context['blurb'] = Blurb.objects.filter(user=self.get_object()).first()
context['membership'] = Membership.objects.filter(user=self.get_object()).first()
return context
class UserRedirectView(LoginRequiredMixin, RedirectView):
@ -45,8 +45,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
def get_context_data(self, **kwargs):
context = super(UserUpdateView, self).get_context_data(**kwargs)
profile = UserBlurb.objects.filter(user=self.get_object()).first()
context['form_blurb'] = UserBlurbForm(instance=profile)
profile = Blurb.objects.filter(user=self.get_object()).first()
context['form_blurb'] = BlurbForm(instance=profile)
return context
def get_object(self):
@ -54,8 +54,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
return User.objects.get(username=self.request.user.username)
def form_valid(self, form):
profile = UserBlurb.objects.filter(user=self.get_object()).first()
form_blurb = UserBlurbForm(self.request.POST, instance=profile)
profile = Blurb.objects.filter(user=self.get_object()).first()
form_blurb = BlurbForm(self.request.POST, instance=profile)
if form_blurb.is_valid():
blurb_model = form_blurb.save(commit=False)
blurb_model.user = self.request.user

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
# This file is here because many Platforms as a Service look for
# requirements.txt in the root directory of a project.
-r requirements/production.txt

View File

@ -42,16 +42,19 @@ django-redis==4.7.0
redis>=2.10.5
rcssmin==1.0.6
rcssmin==1.0.6
django-compressor==2.1
django-sass-processor==0.5.3
lxml==3.7.2
# Your custom requirements go here
mock==2.0.0
gocardless
braintree
braintree==3.34.0
# Your custom requirements go here
-e git+https://github.com/olymk2/scaffold.git#egg=scaffold
django-autofixture==0.12.1
git+https://github.com/olymk2/scaffold.git

0
staticfiles/staticfiles Normal file
View File