diff --git a/.drone.yml b/.drone.yml index 3075c22..fb192f6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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: {} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..216b870 --- /dev/null +++ b/.travis.yml @@ -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" diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..2d2ce88 --- /dev/null +++ b/circle.yml @@ -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 diff --git a/compose/django/Dockerfile b/compose/django/Dockerfile index a198604..b26d2fd 100644 --- a/compose/django/Dockerfile +++ b/compose/django/Dockerfile @@ -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 diff --git a/compose/django/entrypoint.sh b/compose/django/entrypoint.sh index eb70a65..158fe37 100644 --- a/compose/django/entrypoint.sh +++ b/compose/django/entrypoint.sh @@ -33,5 +33,4 @@ until postgres_ready; do sleep 1 done ->&2 echo "Postgres is up - continuing..." exec $cmd diff --git a/compose/django/gunicorn.sh b/compose/django/gunicorn.sh index 014f173..52c50ca 100644 --- a/compose/django/gunicorn.sh +++ b/compose/django/gunicorn.sh @@ -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 \ No newline at end of file +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 diff --git a/compose/nginx/Dockerfile b/compose/nginx/Dockerfile index 5984825..136fd5d 100644 --- a/compose/nginx/Dockerfile +++ b/compose/nginx/Dockerfile @@ -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 diff --git a/compose/nginx/nginx.conf b/compose/nginx/nginx.conf index 3b9d2a3..a200939 100644 --- a/compose/nginx/nginx.conf +++ b/compose/nginx/nginx.conf @@ -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 / { diff --git a/config/settings/common.py b/config/settings/common.py index 5dd17db..fc4941c 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -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':{ diff --git a/config/settings/production.py b/config/settings/production.py index e8d8d20..5ab3a21 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -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', ) diff --git a/docker-compose.yml b/docker-compose.yml index 8f0abc6..99d65fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/env.example b/env.example index 2324613..db57760 100644 --- a/env.example +++ b/env.example @@ -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 diff --git a/live.yml b/live.yml new file mode 100644 index 0000000..09566d7 --- /dev/null +++ b/live.yml @@ -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 diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py new file mode 100644 index 0000000..9bb4471 --- /dev/null +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -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')) diff --git a/mhackspace/feeds/__init__.py b/mhackspace/feeds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mhackspace/feeds/migrations/0001_initial.py b/mhackspace/feeds/migrations/0001_initial.py index 53bcf3c..4a5c9b9 100644 --- a/mhackspace/feeds/migrations/0001_initial.py +++ b/mhackspace/feeds/migrations/0001_initial.py @@ -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'), + ), ] diff --git a/mhackspace/feeds/migrations/0002_auto_20170104_2033.py b/mhackspace/feeds/migrations/0002_auto_20170104_2033.py deleted file mode 100644 index 4f8bd15..0000000 --- a/mhackspace/feeds/migrations/0002_auto_20170104_2033.py +++ /dev/null @@ -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), - ), - ] diff --git a/mhackspace/feeds/migrations/0003_auto_20170104_2035.py b/mhackspace/feeds/migrations/0003_auto_20170104_2035.py deleted file mode 100644 index 55a38dc..0000000 --- a/mhackspace/feeds/migrations/0003_auto_20170104_2035.py +++ /dev/null @@ -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), - ), - ] diff --git a/mhackspace/feeds/migrations/0004_feed_enabled.py b/mhackspace/feeds/migrations/0004_feed_enabled.py deleted file mode 100644 index 9579887..0000000 --- a/mhackspace/feeds/migrations/0004_feed_enabled.py +++ /dev/null @@ -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), - ), - ] diff --git a/mhackspace/feeds/migrations/0005_storing_articles.py b/mhackspace/feeds/migrations/0005_storing_articles.py deleted file mode 100644 index 0734fba..0000000 --- a/mhackspace/feeds/migrations/0005_storing_articles.py +++ /dev/null @@ -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'), - ), - ] diff --git a/mhackspace/members/views.py b/mhackspace/members/views.py index dbc597c..4518a32 100644 --- a/mhackspace/members/views.py +++ b/mhackspace/members/views.py @@ -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 diff --git a/mhackspace/subscriptions/__init__.py b/mhackspace/subscriptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mhackspace/subscriptions/management/commands/list_subscription_payments.py b/mhackspace/subscriptions/management/commands/list_subscription_payments.py new file mode 100644 index 0000000..fc575d6 --- /dev/null +++ b/mhackspace/subscriptions/management/commands/list_subscription_payments.py @@ -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) diff --git a/mhackspace/subscriptions/management/commands/list_subscriptions.py b/mhackspace/subscriptions/management/commands/list_subscriptions.py index a4e1d39..dc8f84c 100644 --- a/mhackspace/subscriptions/management/commands/list_subscriptions.py +++ b/mhackspace/subscriptions/management/commands/list_subscriptions.py @@ -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))) diff --git a/mhackspace/subscriptions/management/commands/refresh_subscriptions.py b/mhackspace/subscriptions/management/commands/refresh_subscriptions.py new file mode 100644 index 0000000..aeecff9 --- /dev/null +++ b/mhackspace/subscriptions/management/commands/refresh_subscriptions.py @@ -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) + diff --git a/mhackspace/subscriptions/migrations/0001_initial.py b/mhackspace/subscriptions/migrations/0001_initial.py new file mode 100644 index 0000000..4ce244f --- /dev/null +++ b/mhackspace/subscriptions/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/mhackspace/subscriptions/migrations/__init__.py b/mhackspace/subscriptions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mhackspace/subscriptions/models.py b/mhackspace/subscriptions/models.py index e69de29..a2b7770 100644 --- a/mhackspace/subscriptions/models.py +++ b/mhackspace/subscriptions/models.py @@ -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 diff --git a/mhackspace/subscriptions/payments.py b/mhackspace/subscriptions/payments.py index edc4bae..a92bc78 100644 --- a/mhackspace/subscriptions/payments.py +++ b/mhackspace/subscriptions/payments.py @@ -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')) diff --git a/mhackspace/subscriptions/tests/test_payment_gateways.py b/mhackspace/subscriptions/tests/test_payment_gateways.py index 81c686b..5b19544 100644 --- a/mhackspace/subscriptions/tests/test_payment_gateways.py +++ b/mhackspace/subscriptions/tests/test_payment_gateways.py @@ -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 diff --git a/mhackspace/templates/base.html b/mhackspace/templates/base.html index 2751ea0..aa5764b 100644 --- a/mhackspace/templates/base.html +++ b/mhackspace/templates/base.html @@ -91,8 +91,25 @@ -
{{ member.users.description }}
{{ member.users.skills }}
+{{ member.status }}
{% for group in member.groups.all %} {{ group.name }} {% endfor %}{{ member.userblurb }}
+{{ member.blurb }}
{{ user.username }}
{{ user.name }}
{{ user.email }}
@@ -18,13 +31,18 @@Member since
Description: {{ blurb.description }}
Skills: {{ blurb.description }}
+ +Membership Status: {{ membership.get_status }}
+Last Payment: {{membership.date}}
+Amount: £{{membership.payment}}
MHS{{ user.id|stringformat:"05d" }}
Change me
+MHS{{ user.id|stringformat:"05d" }}
{{user.name}}{{user.last_name}}
Cancel Membership