From 69af967ae675993a2a772998172c267c301b1c10 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sun, 9 Apr 2017 09:57:23 +0100 Subject: [PATCH 01/22] Initial rfid models, to think about any thoughts ? remove mapping model on base on @scollins suggestion Updated models, enabled it in the admin backup to get an idea of the structure. will need to rebuild, updating restframework current state of experiment into rfid api Change docs url started looking into web tokens Refactor models More experimenting Custom view now works, needs tiding up and data to test --- README.org | 3 +- config/settings/common.py | 8 ++- config/urls.py | 12 ++++ mhackspace/rfid/__init__.py | 0 mhackspace/rfid/admin.py | 14 +++++ mhackspace/rfid/migrations/0001_initial.py | 47 +++++++++++++++ .../migrations/0002_auto_20170420_0730.py | 39 ++++++++++++ mhackspace/rfid/migrations/__init__.py | 0 mhackspace/rfid/models.py | 60 +++++++++++++++++++ mhackspace/rfid/serializers.py | 32 ++++++++++ mhackspace/rfid/tests/__init__.py | 0 mhackspace/rfid/tests/tests.py | 58 ++++++++++++++++++ mhackspace/rfid/views.py | 32 ++++++++++ mhackspace/static/sass/project.scss | 6 ++ requirements/base.txt | 3 + 15 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 mhackspace/rfid/__init__.py create mode 100644 mhackspace/rfid/admin.py create mode 100644 mhackspace/rfid/migrations/0001_initial.py create mode 100644 mhackspace/rfid/migrations/0002_auto_20170420_0730.py create mode 100644 mhackspace/rfid/migrations/__init__.py create mode 100644 mhackspace/rfid/models.py create mode 100644 mhackspace/rfid/serializers.py create mode 100644 mhackspace/rfid/tests/__init__.py create mode 100644 mhackspace/rfid/tests/tests.py create mode 100644 mhackspace/rfid/views.py diff --git a/README.org b/README.org index 4531215..a7a55f0 100644 --- a/README.org +++ b/README.org @@ -5,7 +5,6 @@ Repository for the maidstone hackspace website, feel free to fork this site for your own Hackspace. - ** Requirements Before getting started make sure you have git, docker and docker-compose installed on your machine. The simplest way to setup this site is to use docker-compose so please install that from this site @@ -47,7 +46,7 @@ docker-compose -f dev.yml run --rm django python manage.py makemigrations docker-compose -f dev.yml run --rm django python manage.py migrate #+END_SRC *** Create the admin user. -Once created you can login at http://127.0.0.1:8180/admin +Once created you can login at http://127.0.0.1:8180/trustee #+BEGIN_SRC sh docker-compose -f dev.yml run --rm django python manage.py createsuperuser #+END_SRC diff --git a/config/settings/common.py b/config/settings/common.py index 1ac9c19..f954c7a 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -80,6 +80,7 @@ THIRD_PARTY_APPS = ( 'whitenoise.runserver_nostatic', 'stdimage', 'rest_framework', + 'django_filters', 'draceditor', 'haystack', 'djconfig', @@ -138,6 +139,7 @@ LOCAL_APPS = ( 'mhackspace.blog', 'mhackspace.core', 'mhackspace.requests', + 'mhackspace.rfid', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -446,10 +448,12 @@ REST_FRAMEWORK = { 'rest_framework.filters.OrderingFilter' ), 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', + # 'rest_framework.permissions.IsAuthenticated', + # 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.BasicAuthentication', + # 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + # 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ), diff --git a/config/urls.py b/config/urls.py index 4595b9d..e078d3e 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals + from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static @@ -9,6 +10,7 @@ from django.views import defaults as default_views from django.contrib.auth import views as auth_views from django.contrib.sitemaps.views import sitemap from rest_framework.routers import DefaultRouter +from rest_framework.documentation import include_docs_urls from mhackspace.contact.views import contact from mhackspace.members.views import MemberListView @@ -20,21 +22,28 @@ from mhackspace.blog.views import PostViewSet, CategoryViewSet, BlogPost, PostLi from mhackspace.blog.sitemaps import PostSitemap, CategorySitemap from mhackspace.feeds.views import FeedViewSet, ArticleViewSet from mhackspace.requests.views import RequestsForm, RequestsList +from mhackspace.rfid.views import DeviceViewSet, AuthUserWithDeviceViewSet + from wiki.urls import get_pattern as get_wiki_pattern from django_nyt.urls import get_pattern as get_nyt_pattern +from rest_framework_jwt.views import obtain_jwt_token router = DefaultRouter() router.register(r'posts', PostViewSet) router.register(r'categories', CategoryViewSet) router.register(r'feeds', FeedViewSet) router.register(r'articles', ArticleViewSet) +router.register(r'rfid', DeviceViewSet) +router.register(r'rfidAuth', AuthUserWithDeviceViewSet, base_name='device') + sitemaps = { 'posts': PostSitemap, 'category': CategorySitemap, } + urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='pages/home.html'), name='home'), url(r'^about/$', TemplateView.as_view(template_name='pages/about.html'), name='about'), @@ -68,6 +77,9 @@ urlpatterns = [ # Django Admin, use {% url 'admin:index' %} url(r'{}'.format(settings.ADMIN_URL), admin.site.urls), + url(r'^api-token-auth/', obtain_jwt_token), + url(r'^api/docs/', include_docs_urls(title='Hackspace API')), + # User management url(r'^users/', include('mhackspace.users.urls', namespace='users')), url(r'^accounts/', include('allauth.urls')), diff --git a/mhackspace/rfid/__init__.py b/mhackspace/rfid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py new file mode 100644 index 0000000..0a8621d --- /dev/null +++ b/mhackspace/rfid/admin.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin +from django.contrib.admin import ModelAdmin + +from mhackspace.rfid.models import Device, Rfid + + +@admin.register(Device) +class DeviceAdmin(ModelAdmin): + list_display = ('name',) + +@admin.register(Rfid) +class RfidAdmin(ModelAdmin): + list_display = ('code',) diff --git a/mhackspace/rfid/migrations/0001_initial.py b/mhackspace/rfid/migrations/0001_initial.py new file mode 100644 index 0000000..fb37b39 --- /dev/null +++ b/mhackspace/rfid/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-14 21:15 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Device', + fields=[ + ('identifier', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255, verbose_name='Device name')), + ('description', models.CharField(blank=True, max_length=255, verbose_name='Short description of what the device does')), + ('added_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ], + ), + migrations.CreateModel( + name='DeviceAuth', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='device', to='rfid.Device')), + ('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_auth', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Rfid', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.PositiveIntegerField()), + ('description', models.CharField(blank=True, max_length=255, verbose_name='Short rfid description')), + ('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rfid_user', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/mhackspace/rfid/migrations/0002_auto_20170420_0730.py b/mhackspace/rfid/migrations/0002_auto_20170420_0730.py new file mode 100644 index 0000000..da8ec64 --- /dev/null +++ b/mhackspace/rfid/migrations/0002_auto_20170420_0730.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-20 07:30 +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 = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('rfid', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='deviceauth', + name='device', + ), + migrations.RemoveField( + model_name='deviceauth', + name='user', + ), + migrations.AddField( + model_name='device', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='rfid', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.DeleteModel( + name='DeviceAuth', + ), + ] diff --git a/mhackspace/rfid/migrations/__init__.py b/mhackspace/rfid/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mhackspace/rfid/models.py b/mhackspace/rfid/models.py new file mode 100644 index 0000000..3b727ed --- /dev/null +++ b/mhackspace/rfid/models.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, absolute_import +import uuid + +from django.utils import timezone +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +# just brainstorming so we can start playing with this, +# be nice to make this a 3rd party django installable app ? + + +# users rfid card to user mapping, user can have more than one card +class Rfid(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, blank=True, + # related_name='rfid_user' + ) + code = models.PositiveIntegerField() + description = models.CharField(_('Short rfid description'), blank=True, max_length=255) + + def __str__(self): + return self.description + + +# description of a device like door, print, laser cutter +class Device(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, blank=True, + # related_name='rfid_user' + ) + # user = models.ManyToMany(settings.AUTH_USER_MODEL) + identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + name = models.CharField(_('Device name'), max_length=255) + description = models.CharField(_('Short description of what the device does'), blank=True, max_length=255) + added_date = models.DateTimeField(default=timezone.now, editable=False) + + def __str__(self): + return self.name + + +# many to many, lookup user from rfid model then get there user_id and the device to check if auth allowed +# class DeviceAuth(models.Model): +# user = models.ForeignKey( +# settings.AUTH_USER_MODEL, +# null=True, blank=True, +# default=None, +# related_name='user_auth' +# ) + +# device = models.ForeignKey( +# Device, +# null=True, blank=True, +# default=None, +# related_name='device' +# ) diff --git a/mhackspace/rfid/serializers.py b/mhackspace/rfid/serializers.py new file mode 100644 index 0000000..f5db4ff --- /dev/null +++ b/mhackspace/rfid/serializers.py @@ -0,0 +1,32 @@ +from rest_framework import serializers + +from mhackspace.rfid.models import Device + + +class Task(object): + def __init__(self, **kwargs): + for field in ('id', 'name', 'owner', 'status'): + setattr(self, field, kwargs.get(field, None)) + + +class DeviceSerializer(serializers.ModelSerializer): + class Meta: + model = Device + fields = ('name', ) + + +class AuthSerializer(serializers.Serializer): + name = serializers.CharField(max_length=255) + rfid = serializers.CharField(max_length=255) + device_id = serializers.CharField(max_length=255) + + # def create(self, validated_data): + # return Task(id=None, **validated_data) + + # def update(self, instance, validated_data): + # for field, value in validated_data.items(): + # setattr(instance, field, value) + # return instance + + # class Meta: + # fields = ('name', ) diff --git a/mhackspace/rfid/tests/__init__.py b/mhackspace/rfid/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mhackspace/rfid/tests/tests.py b/mhackspace/rfid/tests/tests.py new file mode 100644 index 0000000..d9537ef --- /dev/null +++ b/mhackspace/rfid/tests/tests.py @@ -0,0 +1,58 @@ +import sys +import requests + +from io import StringIO +from django.core.management import call_command +from test_plus.test import TestCase +from rest_framework.test import APIRequestFactory +from rest_framework.test import RequestsClient + +from mhackspace.rfid.models import Device +from mhackspace.user.models import User + + +# http://www.django-rest-framework.org/api-guide/testing/ + +class MigrationTestCase(TestCase): + + def testRollback(self): + out = StringIO() + sys.stout = out + call_command('migrate', 'rfid', 'zero', stdout=out) + call_command('migrate', 'rfid', stdout=out) + self.assertIn("... OK\n", out.getvalue()) + +class ApiTests(TestCase): + def setUp(self): + Device(name='device01').save() + User(name='User01').save() + Rfid(code=1, user=1).save() + + def testAuth(self): + factory = APIRequestFactory() + request = factory.get('/rfid/') + + def testSamsMadness(self): + client = RequestsClient() + response = client.post( + 'http://127.0.0.1:8180/api/v1/rfidAuth/?format=json', + data={'rfid':'1', 'device': '1'}) + print(response) + assert response.status_code == 200 + self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) + + + def testAuthUserWithDevice(self): + client = RequestsClient() + response = client.get('http://127.0.0.1:8180/api/v1/rfid/?format=json') + assert response.status_code == 200 + self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) + + + def testFetchDeviceList(self): + client = RequestsClient() + response = client.get('http://127.0.0.1:8180/api/v1/rfid/?format=json') + assert response.status_code == 200 + self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) + + diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py new file mode 100644 index 0000000..e1648af --- /dev/null +++ b/mhackspace/rfid/views.py @@ -0,0 +1,32 @@ +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import viewsets +from mhackspace.rfid.models import Device, Rfid +from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer +from django.shortcuts import get_list_or_404, get_object_or_404 + + +class DeviceViewSet(viewsets.ModelViewSet): + queryset = Device.objects.all() + serializer_class = DeviceSerializer + + +#https://medium.com/django-rest-framework/django-rest-framework-viewset-when-you-don-t-have-a-model-335a0490ba6f +class AuthUserWithDeviceViewSet(viewsets.ViewSet): + # http_method_names = ['get', 'post', 'head'] + serializer_class = AuthSerializer + + def list(self, request): + serializer = AuthSerializer(instance={'name': '1','rfid': '1', 'device_id': '1'}) + return Response(serializer.data) + + def post(self, request, format=None): + rfid = Rfid.objects.get(code=request.GET.get('rfid_id')) + + print(rfid.user.device__set(device=request.GET.get('rfid_id'))) + # = get_object_or_404(Disease, pk=disease_id) + + + # Device(rfid, device) + serializer = AuthSerializer(instance={'name': '1', 'rfid': '1', 'device_id': '1'}) + return Response(serializer.data) diff --git a/mhackspace/static/sass/project.scss b/mhackspace/static/sass/project.scss index 6aceb3b..6e7afd3 100644 --- a/mhackspace/static/sass/project.scss +++ b/mhackspace/static/sass/project.scss @@ -21,3 +21,9 @@ [hidden][style="display: block;"] { display: block !important; } + +.imgfit { + width:100%; + height: auto; + overflow: hidden; +} \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt index 22a23a7..353bb77 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -61,7 +61,10 @@ git+https://github.com/olymk2/scaffold.git git+git://github.com/django-wiki/django-wiki.git djangorestframework==3.6.3 +djangorestframework-jwt django-filter==1.0.2 +coreapi +# api libraries end draceditor==1.1.8 # django-spirit From 48c90c95f52483241d39a575597c5dd52546f1a7 Mon Sep 17 00:00:00 2001 From: Oly Date: Tue, 25 Apr 2017 14:01:19 +0100 Subject: [PATCH 02/22] you can now request a device, thinking we need a mapping table again :p --- mhackspace/rfid/serializers.py | 1 - mhackspace/rfid/tests/tests.py | 25 ++++++++++++++----------- mhackspace/rfid/views.py | 25 +++++++++++++------------ 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/mhackspace/rfid/serializers.py b/mhackspace/rfid/serializers.py index f5db4ff..2e3ffb1 100644 --- a/mhackspace/rfid/serializers.py +++ b/mhackspace/rfid/serializers.py @@ -8,7 +8,6 @@ class Task(object): for field in ('id', 'name', 'owner', 'status'): setattr(self, field, kwargs.get(field, None)) - class DeviceSerializer(serializers.ModelSerializer): class Meta: model = Device diff --git a/mhackspace/rfid/tests/tests.py b/mhackspace/rfid/tests/tests.py index d9537ef..2356a54 100644 --- a/mhackspace/rfid/tests/tests.py +++ b/mhackspace/rfid/tests/tests.py @@ -7,8 +7,8 @@ from test_plus.test import TestCase from rest_framework.test import APIRequestFactory from rest_framework.test import RequestsClient -from mhackspace.rfid.models import Device -from mhackspace.user.models import User +from mhackspace.rfid.models import Device, Rfid +from mhackspace.users.models import User # http://www.django-rest-framework.org/api-guide/testing/ @@ -22,11 +22,15 @@ class MigrationTestCase(TestCase): call_command('migrate', 'rfid', stdout=out) self.assertIn("... OK\n", out.getvalue()) + class ApiTests(TestCase): def setUp(self): - Device(name='device01').save() - User(name='User01').save() - Rfid(code=1, user=1).save() + self.device = Device(name='device01') + self.device.save() + self.user = User(name='User01') + self.user.save() + self.rfid = Rfid(code=1, user=self.user) + self.rfid.save() def testAuth(self): factory = APIRequestFactory() @@ -35,12 +39,13 @@ class ApiTests(TestCase): def testSamsMadness(self): client = RequestsClient() response = client.post( - 'http://127.0.0.1:8180/api/v1/rfidAuth/?format=json', + 'http://127.0.0.1:8180/api/v1/rfidAuth/', data={'rfid':'1', 'device': '1'}) - print(response) + # print(response.json()) assert response.status_code == 200 - self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) - + self.assertEquals( + response.json(), + [{'rfid': self.rfid.code, 'name': 'device01', 'device_id': self.device.identification}]) def testAuthUserWithDevice(self): client = RequestsClient() @@ -48,11 +53,9 @@ class ApiTests(TestCase): assert response.status_code == 200 self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) - def testFetchDeviceList(self): client = RequestsClient() response = client.get('http://127.0.0.1:8180/api/v1/rfid/?format=json') assert response.status_code == 200 self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) - diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py index e1648af..8918452 100644 --- a/mhackspace/rfid/views.py +++ b/mhackspace/rfid/views.py @@ -1,6 +1,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import viewsets +from rest_framework import status from mhackspace.rfid.models import Device, Rfid from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer from django.shortcuts import get_list_or_404, get_object_or_404 @@ -11,22 +12,22 @@ class DeviceViewSet(viewsets.ModelViewSet): serializer_class = DeviceSerializer -#https://medium.com/django-rest-framework/django-rest-framework-viewset-when-you-don-t-have-a-model-335a0490ba6f +# https://medium.com/django-rest-framework/django-rest-framework-viewset-when-you-don-t-have-a-model-335a0490ba6f class AuthUserWithDeviceViewSet(viewsets.ViewSet): - # http_method_names = ['get', 'post', 'head'] + http_method_names = ['post'] serializer_class = AuthSerializer def list(self, request): - serializer = AuthSerializer(instance={'name': '1','rfid': '1', 'device_id': '1'}) + serializer = AuthSerializer( + instance={'name': '1', 'rfid': '1', 'device_id': '1'}) return Response(serializer.data) def post(self, request, format=None): - rfid = Rfid.objects.get(code=request.GET.get('rfid_id')) - - print(rfid.user.device__set(device=request.GET.get('rfid_id'))) - # = get_object_or_404(Disease, pk=disease_id) - - - # Device(rfid, device) - serializer = AuthSerializer(instance={'name': '1', 'rfid': '1', 'device_id': '1'}) - return Response(serializer.data) + try: + rfid = Rfid.objects.get(code=request.data.get('rfid')) + device = Device.objects.get(user=rfid.user, identifier=request.data.get('device_id')) + except: + return Response(status=status.HTTP_404_NOT_FOUND) + serializer = AuthSerializer( + instance={'name': device.name, 'rfid': rfid.code, 'device_id': device.identifier}) + return Response(serializer.data, status=200) From 54f89e8b5b8a102d7a6e670f7baac5c024add60c Mon Sep 17 00:00:00 2001 From: Oly Date: Wed, 26 Apr 2017 14:02:26 +0100 Subject: [PATCH 03/22] Basic Working tests to build on --- mhackspace/rfid/admin.py | 2 +- mhackspace/rfid/serializers.py | 3 ++- mhackspace/rfid/tests/tests.py | 29 ++++++++++++++++++++++------- mhackspace/rfid/views.py | 19 ++++++++++++------- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py index 0a8621d..c793c98 100644 --- a/mhackspace/rfid/admin.py +++ b/mhackspace/rfid/admin.py @@ -7,7 +7,7 @@ from mhackspace.rfid.models import Device, Rfid @admin.register(Device) class DeviceAdmin(ModelAdmin): - list_display = ('name',) + list_display = ('name', 'identifier') @admin.register(Rfid) class RfidAdmin(ModelAdmin): diff --git a/mhackspace/rfid/serializers.py b/mhackspace/rfid/serializers.py index 2e3ffb1..20efdcb 100644 --- a/mhackspace/rfid/serializers.py +++ b/mhackspace/rfid/serializers.py @@ -17,7 +17,8 @@ class DeviceSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.Serializer): name = serializers.CharField(max_length=255) rfid = serializers.CharField(max_length=255) - device_id = serializers.CharField(max_length=255) + # device = serializers.UUIDField(format='hex_verbose') + device = serializers.CharField(max_length=255) # def create(self, validated_data): # return Task(id=None, **validated_data) diff --git a/mhackspace/rfid/tests/tests.py b/mhackspace/rfid/tests/tests.py index 2356a54..418e702 100644 --- a/mhackspace/rfid/tests/tests.py +++ b/mhackspace/rfid/tests/tests.py @@ -24,28 +24,43 @@ class MigrationTestCase(TestCase): class ApiTests(TestCase): + maxDiff = None def setUp(self): - self.device = Device(name='device01') - self.device.save() self.user = User(name='User01') self.user.save() - self.rfid = Rfid(code=1, user=self.user) + self.device = Device(name='device01', user=self.user) + self.device.save() + self.rfid = Rfid(code='1', user=self.user) self.rfid.save() def testAuth(self): factory = APIRequestFactory() request = factory.get('/rfid/') - def testSamsMadness(self): + def testValidAuthCase(self): client = RequestsClient() response = client.post( 'http://127.0.0.1:8180/api/v1/rfidAuth/', - data={'rfid':'1', 'device': '1'}) - # print(response.json()) + data={'rfid': '1', 'device': self.device.identifier}) assert response.status_code == 200 + expected_result = {'rfid': self.rfid.code, 'name': 'device01', 'device': str(self.device.identifier)} self.assertEquals( response.json(), - [{'rfid': self.rfid.code, 'name': 'device01', 'device_id': self.device.identification}]) + expected_result + ) + + def testInValidAuthCase(self): + client = RequestsClient() + response = client.post( + 'http://127.0.0.1:8180/api/v1/rfidAuth/', + data={'rfid': '99', 'device': str(self.device.identifier)}) + assert response.status_code == 404 + + # response = client.post( + # 'http://127.0.0.1:8180/api/v1/rfidAuth/', + # data={'rfid': '1', 'device': 'test%s' % str(self.device.identifier)[3:]}) + # assert response.status_code == 404 + def testAuthUserWithDevice(self): client = RequestsClient() diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py index 8918452..4752720 100644 --- a/mhackspace/rfid/views.py +++ b/mhackspace/rfid/views.py @@ -1,10 +1,12 @@ +import logging from rest_framework.response import Response -from rest_framework.views import APIView from rest_framework import viewsets from rest_framework import status from mhackspace.rfid.models import Device, Rfid from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer -from django.shortcuts import get_list_or_404, get_object_or_404 +from django.core.exceptions import ObjectDoesNotExist + +logger = logging.getLogger(__name__) class DeviceViewSet(viewsets.ModelViewSet): @@ -14,20 +16,23 @@ class DeviceViewSet(viewsets.ModelViewSet): # https://medium.com/django-rest-framework/django-rest-framework-viewset-when-you-don-t-have-a-model-335a0490ba6f class AuthUserWithDeviceViewSet(viewsets.ViewSet): - http_method_names = ['post'] + # http_method_names = ['post'] serializer_class = AuthSerializer def list(self, request): serializer = AuthSerializer( - instance={'name': '1', 'rfid': '1', 'device_id': '1'}) + instance={'name': '1', 'rfid': '1', 'device': '1'}) return Response(serializer.data) def post(self, request, format=None): try: rfid = Rfid.objects.get(code=request.data.get('rfid')) - device = Device.objects.get(user=rfid.user, identifier=request.data.get('device_id')) - except: + device = Device.objects.get(user=rfid.user, identifier=request.data.get('device')) + except ObjectDoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) + except: + logger.exception("An error occurred") + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) serializer = AuthSerializer( - instance={'name': device.name, 'rfid': rfid.code, 'device_id': device.identifier}) + instance={'name': device.name, 'rfid': rfid.code, 'device': device.identifier}) return Response(serializer.data, status=200) From 66e012f9947c34bc296642ad53f66899196720af Mon Sep 17 00:00:00 2001 From: Oly Date: Thu, 27 Apr 2017 14:02:33 +0100 Subject: [PATCH 04/22] switched to mapping model, lookup in the api does not yet work --- mhackspace/rfid/admin.py | 25 ++++++++++- .../migrations/0003_auto_20170427_0743.py | 29 +++++++++++++ mhackspace/rfid/models.py | 41 ++++++++++--------- mhackspace/rfid/serializers.py | 7 ++-- mhackspace/rfid/tests/tests.py | 4 +- mhackspace/rfid/views.py | 16 ++++---- 6 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 mhackspace/rfid/migrations/0003_auto_20170427_0743.py diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py index c793c98..bd6db72 100644 --- a/mhackspace/rfid/admin.py +++ b/mhackspace/rfid/admin.py @@ -1,14 +1,35 @@ # -*- coding: utf-8 -*- from django.contrib import admin from django.contrib.admin import ModelAdmin +from django.forms.models import ModelChoiceField -from mhackspace.rfid.models import Device, Rfid +from mhackspace.rfid.models import Device, Rfid, DeviceAuth @admin.register(Device) class DeviceAdmin(ModelAdmin): list_display = ('name', 'identifier') + @admin.register(Rfid) class RfidAdmin(ModelAdmin): - list_display = ('code',) + list_display = ('code', 'description') + + +# Probably need to look at this again +@admin.register(DeviceAuth) +class DeviceAuthAdmin(ModelAdmin): + list_display = ('rfid', 'device') + + class CustomModelChoiceField(ModelChoiceField): + def label_from_instance(self, obj): + return obj.description + ' - ' + str(obj.user) + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "rfid": + return self.CustomModelChoiceField( + Rfid.objects.all(), + initial=request.user) + + return super(DeviceAuthAdmin, self).formfield_for_foreignkey( + db_field, request, **kwargs) diff --git a/mhackspace/rfid/migrations/0003_auto_20170427_0743.py b/mhackspace/rfid/migrations/0003_auto_20170427_0743.py new file mode 100644 index 0000000..510a7f8 --- /dev/null +++ b/mhackspace/rfid/migrations/0003_auto_20170427_0743.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-27 07:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rfid', '0002_auto_20170420_0730'), + ] + + operations = [ + migrations.CreateModel( + name='DeviceAuth', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rfid.Device')), + ('rfid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rfid.Rfid')), + ], + ), + migrations.AddField( + model_name='device', + name='members', + field=models.ManyToManyField(through='rfid.DeviceAuth', to='rfid.Rfid'), + ), + ] diff --git a/mhackspace/rfid/models.py b/mhackspace/rfid/models.py index 3b727ed..fdb7435 100644 --- a/mhackspace/rfid/models.py +++ b/mhackspace/rfid/models.py @@ -13,25 +13,23 @@ from django.utils.translation import ugettext_lazy as _ # users rfid card to user mapping, user can have more than one card class Rfid(models.Model): + code = models.PositiveIntegerField() + description = models.CharField(_('Short rfid description'), blank=True, max_length=255) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, # related_name='rfid_user' ) - code = models.PositiveIntegerField() - description = models.CharField(_('Short rfid description'), blank=True, max_length=255) def __str__(self): return self.description + def name(self): + return self.user.name + # description of a device like door, print, laser cutter class Device(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, blank=True, - # related_name='rfid_user' - ) # user = models.ManyToMany(settings.AUTH_USER_MODEL) identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -39,22 +37,25 @@ class Device(models.Model): description = models.CharField(_('Short description of what the device does'), blank=True, max_length=255) added_date = models.DateTimeField(default=timezone.now, editable=False) + members = models.ManyToManyField(Rfid, through='DeviceAuth') + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, blank=True, + # related_name='rfid_user' + ) + def __str__(self): return self.name +# http://stackoverflow.com/questions/4443190/djangos-manytomany-relationship-with-additional-fields # many to many, lookup user from rfid model then get there user_id and the device to check if auth allowed -# class DeviceAuth(models.Model): -# user = models.ForeignKey( -# settings.AUTH_USER_MODEL, -# null=True, blank=True, -# default=None, -# related_name='user_auth' -# ) +class DeviceAuth(models.Model): + rfid = models.ForeignKey( + Rfid, + ) -# device = models.ForeignKey( -# Device, -# null=True, blank=True, -# default=None, -# related_name='device' -# ) + device = models.ForeignKey( + Device, + ) diff --git a/mhackspace/rfid/serializers.py b/mhackspace/rfid/serializers.py index 20efdcb..f93fad3 100644 --- a/mhackspace/rfid/serializers.py +++ b/mhackspace/rfid/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from mhackspace.rfid.models import Device +from mhackspace.rfid.models import Device, DeviceAuth class Task(object): @@ -8,10 +8,11 @@ class Task(object): for field in ('id', 'name', 'owner', 'status'): setattr(self, field, kwargs.get(field, None)) + class DeviceSerializer(serializers.ModelSerializer): class Meta: - model = Device - fields = ('name', ) + model = DeviceAuth + fields = ('__all__') class AuthSerializer(serializers.Serializer): diff --git a/mhackspace/rfid/tests/tests.py b/mhackspace/rfid/tests/tests.py index 418e702..f8bab6b 100644 --- a/mhackspace/rfid/tests/tests.py +++ b/mhackspace/rfid/tests/tests.py @@ -7,7 +7,7 @@ from test_plus.test import TestCase from rest_framework.test import APIRequestFactory from rest_framework.test import RequestsClient -from mhackspace.rfid.models import Device, Rfid +from mhackspace.rfid.models import Device, Rfid, DeviceAuth from mhackspace.users.models import User @@ -32,6 +32,8 @@ class ApiTests(TestCase): self.device.save() self.rfid = Rfid(code='1', user=self.user) self.rfid.save() + self.auth = DeviceAuth(rfid=self.rfid, device=self.device) + self.save() def testAuth(self): factory = APIRequestFactory() diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py index 4752720..21103ed 100644 --- a/mhackspace/rfid/views.py +++ b/mhackspace/rfid/views.py @@ -2,9 +2,9 @@ import logging from rest_framework.response import Response from rest_framework import viewsets from rest_framework import status -from mhackspace.rfid.models import Device, Rfid +from mhackspace.rfid.models import Device, Rfid, DeviceAuth from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, ValidationError logger = logging.getLogger(__name__) @@ -20,18 +20,20 @@ class AuthUserWithDeviceViewSet(viewsets.ViewSet): serializer_class = AuthSerializer def list(self, request): - serializer = AuthSerializer( - instance={'name': '1', 'rfid': '1', 'device': '1'}) + serializer = DeviceSerializer( + DeviceAuth.objects.all(), many=True) return Response(serializer.data) def post(self, request, format=None): try: rfid = Rfid.objects.get(code=request.data.get('rfid')) - device = Device.objects.get(user=rfid.user, identifier=request.data.get('device')) + device = Device.objects.get(identifier=request.data.get('device')) + deviceAuth = DeviceAuth.objects.get(device=device.identifier, rfid=rfid.id) except ObjectDoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) - except: - logger.exception("An error occurred") + except ValidationError: + # except: + # logger.exception("An error occurred") return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) serializer = AuthSerializer( instance={'name': device.name, 'rfid': rfid.code, 'device': device.identifier}) From 4b3f57ea7e7e8483e6193b52961f02f526af2779 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Thu, 27 Apr 2017 22:21:04 +0100 Subject: [PATCH 05/22] refactored rfid model into user, started on user save form --- .../management/commands/generate_test_data.py | 13 +++++++ mhackspace/rfid/admin.py | 7 +--- mhackspace/rfid/migrations/0001_initial.py | 21 ++++------ .../migrations/0002_auto_20170420_0730.py | 39 ------------------- .../migrations/0003_auto_20170427_0743.py | 29 -------------- mhackspace/rfid/models.py | 26 +++---------- mhackspace/rfid/tests/tests.py | 4 +- mhackspace/rfid/views.py | 3 +- mhackspace/templates/users/rfid_form.html | 18 +++++++++ mhackspace/templates/users/user_detail.html | 1 + mhackspace/users/admin.py | 8 +++- mhackspace/users/migrations/0004_rfid.py | 26 +++++++++++++ mhackspace/users/models.py | 17 ++++++++ mhackspace/users/urls.py | 5 +++ mhackspace/users/views.py | 15 ++++++- 15 files changed, 120 insertions(+), 112 deletions(-) delete mode 100644 mhackspace/rfid/migrations/0002_auto_20170420_0730.py delete mode 100644 mhackspace/rfid/migrations/0003_auto_20170427_0743.py create mode 100644 mhackspace/templates/users/rfid_form.html create mode 100644 mhackspace/users/migrations/0004_rfid.py diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py index a3b8a8d..52d33ba 100644 --- a/mhackspace/base/management/commands/generate_test_data.py +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -7,6 +7,7 @@ from mhackspace.base.models import BannerImage from mhackspace.feeds.models import Article, Feed from mhackspace.users.models import User from mhackspace.blog.models import Category, Post +from mhackspace.rfid.models import Device class ImageFixture(AutoFixture): @@ -38,6 +39,18 @@ class Command(BaseCommand): }) users.create(10) + rfid = AutoFixture(Rfid) + rfid.create(20) + + device = AutoFixture(Device) + device.create(5) + + feed = AutoFixture(Feed) + feed.create(10) + + feeds = AutoFixture(Article) + feeds.create(10) + banners = ImageFixture(BannerImage) banners.create(10) self.stdout.write( diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py index bd6db72..0567154 100644 --- a/mhackspace/rfid/admin.py +++ b/mhackspace/rfid/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.contrib.admin import ModelAdmin from django.forms.models import ModelChoiceField -from mhackspace.rfid.models import Device, Rfid, DeviceAuth +from mhackspace.rfid.models import Device, DeviceAuth @admin.register(Device) @@ -11,11 +11,6 @@ class DeviceAdmin(ModelAdmin): list_display = ('name', 'identifier') -@admin.register(Rfid) -class RfidAdmin(ModelAdmin): - list_display = ('code', 'description') - - # Probably need to look at this again @admin.register(DeviceAuth) class DeviceAuthAdmin(ModelAdmin): diff --git a/mhackspace/rfid/migrations/0001_initial.py b/mhackspace/rfid/migrations/0001_initial.py index fb37b39..e00b2b9 100644 --- a/mhackspace/rfid/migrations/0001_initial.py +++ b/mhackspace/rfid/migrations/0001_initial.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-04-14 21:15 +# Generated by Django 1.11 on 2017-04-27 18:29 from __future__ import unicode_literals -from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone @@ -14,7 +13,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('users', '0004_rfid'), ] operations = [ @@ -31,17 +30,13 @@ class Migration(migrations.Migration): name='DeviceAuth', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='device', to='rfid.Device')), - ('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_auth', to=settings.AUTH_USER_MODEL)), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rfid.Device')), + ('rfid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Rfid')), ], ), - migrations.CreateModel( - name='Rfid', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.PositiveIntegerField()), - ('description', models.CharField(blank=True, max_length=255, verbose_name='Short rfid description')), - ('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rfid_user', to=settings.AUTH_USER_MODEL)), - ], + migrations.AddField( + model_name='device', + name='members', + field=models.ManyToManyField(through='rfid.DeviceAuth', to='users.Rfid'), ), ] diff --git a/mhackspace/rfid/migrations/0002_auto_20170420_0730.py b/mhackspace/rfid/migrations/0002_auto_20170420_0730.py deleted file mode 100644 index da8ec64..0000000 --- a/mhackspace/rfid/migrations/0002_auto_20170420_0730.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-04-20 07:30 -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 = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('rfid', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='deviceauth', - name='device', - ), - migrations.RemoveField( - model_name='deviceauth', - name='user', - ), - migrations.AddField( - model_name='device', - name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='rfid', - name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.DeleteModel( - name='DeviceAuth', - ), - ] diff --git a/mhackspace/rfid/migrations/0003_auto_20170427_0743.py b/mhackspace/rfid/migrations/0003_auto_20170427_0743.py deleted file mode 100644 index 510a7f8..0000000 --- a/mhackspace/rfid/migrations/0003_auto_20170427_0743.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-04-27 07:43 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('rfid', '0002_auto_20170420_0730'), - ] - - operations = [ - migrations.CreateModel( - name='DeviceAuth', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rfid.Device')), - ('rfid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rfid.Rfid')), - ], - ), - migrations.AddField( - model_name='device', - name='members', - field=models.ManyToManyField(through='rfid.DeviceAuth', to='rfid.Rfid'), - ), - ] diff --git a/mhackspace/rfid/models.py b/mhackspace/rfid/models.py index fdb7435..deedb82 100644 --- a/mhackspace/rfid/models.py +++ b/mhackspace/rfid/models.py @@ -7,25 +7,11 @@ from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ +from mhackspace.users.models import Rfid # just brainstorming so we can start playing with this, # be nice to make this a 3rd party django installable app ? -# users rfid card to user mapping, user can have more than one card -class Rfid(models.Model): - code = models.PositiveIntegerField() - description = models.CharField(_('Short rfid description'), blank=True, max_length=255) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, blank=True, - # related_name='rfid_user' - ) - - def __str__(self): - return self.description - - def name(self): - return self.user.name # description of a device like door, print, laser cutter @@ -39,11 +25,11 @@ class Device(models.Model): members = models.ManyToManyField(Rfid, through='DeviceAuth') - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - null=True, blank=True, - # related_name='rfid_user' - ) + # user = models.ForeignKey( + # settings.AUTH_USER_MODEL, + # null=True, blank=True, + # # related_name='rfid_user' + # ) def __str__(self): return self.name diff --git a/mhackspace/rfid/tests/tests.py b/mhackspace/rfid/tests/tests.py index f8bab6b..69254be 100644 --- a/mhackspace/rfid/tests/tests.py +++ b/mhackspace/rfid/tests/tests.py @@ -7,8 +7,8 @@ from test_plus.test import TestCase from rest_framework.test import APIRequestFactory from rest_framework.test import RequestsClient -from mhackspace.rfid.models import Device, Rfid, DeviceAuth -from mhackspace.users.models import User +from mhackspace.rfid.models import Device, DeviceAuth +from mhackspace.users.models import User, Rfid # http://www.django-rest-framework.org/api-guide/testing/ diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py index 21103ed..5b68b1a 100644 --- a/mhackspace/rfid/views.py +++ b/mhackspace/rfid/views.py @@ -2,7 +2,8 @@ import logging from rest_framework.response import Response from rest_framework import viewsets from rest_framework import status -from mhackspace.rfid.models import Device, Rfid, DeviceAuth +from mhackspace.users.models import Rfid +from mhackspace.rfid.models import Device, DeviceAuth from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer from django.core.exceptions import ObjectDoesNotExist, ValidationError diff --git a/mhackspace/templates/users/rfid_form.html b/mhackspace/templates/users/rfid_form.html new file mode 100644 index 0000000..4e1762d --- /dev/null +++ b/mhackspace/templates/users/rfid_form.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block title %}{{ user.username }}{% endblock %} + +{% block content %} +

{{ user.username }}

+
+ {% csrf_token %} + {{ form|crispy }} + {{ form_blurb|crispy }} +
+
+ +
+
+
+{% endblock %} diff --git a/mhackspace/templates/users/user_detail.html b/mhackspace/templates/users/user_detail.html index 9e354ea..a5a19ca 100644 --- a/mhackspace/templates/users/user_detail.html +++ b/mhackspace/templates/users/user_detail.html @@ -74,6 +74,7 @@

My Info + My Rfid Cards E-Mail

diff --git a/mhackspace/users/admin.py b/mhackspace/users/admin.py index ba7ca58..a183241 100644 --- a/mhackspace/users/admin.py +++ b/mhackspace/users/admin.py @@ -9,7 +9,7 @@ from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.http import HttpResponseRedirect from django.urls import reverse from django.conf.urls import url -from .models import User, Membership, MEMBERSHIP_STATUS_CHOICES +from .models import User, Rfid, Membership, MEMBERSHIP_STATUS_CHOICES from mhackspace.subscriptions.management.commands.update_membership_status import update_subscriptions @@ -64,3 +64,9 @@ class MyUserAdmin(AuthUserAdmin): class MembershipAdmin(ModelAdmin): list_display = ('user', 'payment', 'date', 'status') list_filter = ('status',) + + +@admin.register(Rfid) +class RfidAdmin(ModelAdmin): + list_display = ('code', 'description') + diff --git a/mhackspace/users/migrations/0004_rfid.py b/mhackspace/users/migrations/0004_rfid.py new file mode 100644 index 0000000..ae8ea00 --- /dev/null +++ b/mhackspace/users/migrations/0004_rfid.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-27 18:25 +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', '0003_merge_20170226_0844'), + ] + + operations = [ + migrations.CreateModel( + name='Rfid', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.PositiveIntegerField()), + ('description', models.CharField(blank=True, max_length=255, verbose_name='Short rfid description')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index 1f0808a..68b393d 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -90,3 +90,20 @@ class Membership(models.Model): def __str__(self): return self.reference + + +# users rfid card to user mapping, user can have more than one card +class Rfid(models.Model): + code = models.PositiveIntegerField() + description = models.CharField(_('Short rfid description'), blank=True, max_length=255) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, blank=True, + # related_name='rfid_user' + ) + + def __str__(self): + return self.description + + def name(self): + return self.user.name diff --git a/mhackspace/users/urls.py b/mhackspace/users/urls.py index 6ae4b6f..530b556 100644 --- a/mhackspace/users/urls.py +++ b/mhackspace/users/urls.py @@ -26,4 +26,9 @@ urlpatterns = [ view=views.UserUpdateView.as_view(), name='update' ), + url( + regex=r'^-access-cards$', + view=views.RfidCardsUpdateView.as_view(), + name='access_cards' + ), ] diff --git a/mhackspace/users/views.py b/mhackspace/users/views.py index c124721..43f5b2f 100644 --- a/mhackspace/users/views.py +++ b/mhackspace/users/views.py @@ -2,10 +2,11 @@ from __future__ import absolute_import, unicode_literals from django.core.urlresolvers import reverse -from django.views.generic import DetailView, ListView, RedirectView, UpdateView +from django.views.generic import DetailView, ListView, RedirectView, UpdateView, CreateView from django.contrib.auth.mixins import LoginRequiredMixin +from .models import Rfid from .models import User from .models import Blurb from .models import Membership @@ -65,6 +66,18 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): return super(UserUpdateView, self).form_valid(form) + +class RfidCardsUpdateView(LoginRequiredMixin, CreateView): + fields = ['user', 'code', 'description', ] + model = Rfid + + def form_valid(self, form): + user = self.request.user + form.instance.user = user + return super(RfidCardsUpdateView, self).form_valid(form) + + + class UserListView(LoginRequiredMixin, ListView): model = User # These next two lines tell the view to index lookups by username From ed9958b2adfd688aa4f182c2c424d4cd703ecb04 Mon Sep 17 00:00:00 2001 From: Oly Date: Wed, 3 May 2017 14:06:34 +0100 Subject: [PATCH 06/22] setting up test data --- config/urls.py | 12 ++++++------ .../base/management/commands/generate_test_data.py | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/config/urls.py b/config/urls.py index e078d3e..1bba68b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -30,12 +30,12 @@ from django_nyt.urls import get_pattern as get_nyt_pattern from rest_framework_jwt.views import obtain_jwt_token router = DefaultRouter() -router.register(r'posts', PostViewSet) -router.register(r'categories', CategoryViewSet) -router.register(r'feeds', FeedViewSet) -router.register(r'articles', ArticleViewSet) -router.register(r'rfid', DeviceViewSet) -router.register(r'rfidAuth', AuthUserWithDeviceViewSet, base_name='device') +router.register(r'posts', PostViewSet, 'posts') +router.register(r'categories', CategoryViewSet, base_name='categories') +router.register(r'feeds', FeedViewSet, 'feeds') +router.register(r'articles', ArticleViewSet, base_name='articles') +router.register(r'rfid', DeviceViewSet, base_name='rfid_device') +router.register(r'rfidAuth', AuthUserWithDeviceViewSet, base_name='device_auth') sitemaps = { diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py index 52d33ba..7cdf702 100644 --- a/mhackspace/base/management/commands/generate_test_data.py +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -33,6 +33,8 @@ class Command(BaseCommand): # load known data call_command('loaddata', 'mhackspace/users/fixtures/groups.json', verbose=0) + autofixture.autodiscover() + # random data users = AutoFixture(User, field_values={ 'title': random.choicee(('Mr', 'Mrs', 'Emperor', 'Captain')) @@ -45,6 +47,9 @@ class Command(BaseCommand): device = AutoFixture(Device) device.create(5) + deviceauth = AutoFixture(DeviceAuth) + deviceauth.create(5) + feed = AutoFixture(Feed) feed.create(10) From c16e9079caee5fa1fcca13b9777bcc0d0e7195e0 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Wed, 13 Sep 2017 07:25:56 +0100 Subject: [PATCH 07/22] small fixes to wip rfid api --- mhackspace/rfid/models.py | 13 +------------ mhackspace/rfid/serializers.py | 3 ++- mhackspace/rfid/tests/tests.py | 28 +++++++++++++++++++++++----- mhackspace/rfid/views.py | 4 ++-- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/mhackspace/rfid/models.py b/mhackspace/rfid/models.py index deedb82..c80c718 100644 --- a/mhackspace/rfid/models.py +++ b/mhackspace/rfid/models.py @@ -11,26 +11,15 @@ from mhackspace.users.models import Rfid # just brainstorming so we can start playing with this, # be nice to make this a 3rd party django installable app ? - - - # description of a device like door, print, laser cutter class Device(models.Model): # user = models.ManyToMany(settings.AUTH_USER_MODEL) - identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(_('Device name'), max_length=255) + identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) description = models.CharField(_('Short description of what the device does'), blank=True, max_length=255) added_date = models.DateTimeField(default=timezone.now, editable=False) - members = models.ManyToManyField(Rfid, through='DeviceAuth') - # user = models.ForeignKey( - # settings.AUTH_USER_MODEL, - # null=True, blank=True, - # # related_name='rfid_user' - # ) - def __str__(self): return self.name diff --git a/mhackspace/rfid/serializers.py b/mhackspace/rfid/serializers.py index f93fad3..1417a9f 100644 --- a/mhackspace/rfid/serializers.py +++ b/mhackspace/rfid/serializers.py @@ -10,8 +10,9 @@ class Task(object): class DeviceSerializer(serializers.ModelSerializer): + added_date = serializers.DateTimeField(format='iso-8601') class Meta: - model = DeviceAuth + model = Device fields = ('__all__') diff --git a/mhackspace/rfid/tests/tests.py b/mhackspace/rfid/tests/tests.py index 69254be..2f5b24b 100644 --- a/mhackspace/rfid/tests/tests.py +++ b/mhackspace/rfid/tests/tests.py @@ -28,24 +28,30 @@ class ApiTests(TestCase): def setUp(self): self.user = User(name='User01') self.user.save() - self.device = Device(name='device01', user=self.user) + self.device = Device( + name='device01', + identifier='8e274b70-a4b3-4600-9472-f20ea7828cb6') self.device.save() self.rfid = Rfid(code='1', user=self.user) self.rfid.save() self.auth = DeviceAuth(rfid=self.rfid, device=self.device) - self.save() + self.auth.save() def testAuth(self): factory = APIRequestFactory() request = factory.get('/rfid/') def testValidAuthCase(self): + "if we have a user rfid and a device identifier" client = RequestsClient() response = client.post( 'http://127.0.0.1:8180/api/v1/rfidAuth/', data={'rfid': '1', 'device': self.device.identifier}) assert response.status_code == 200 - expected_result = {'rfid': self.rfid.code, 'name': 'device01', 'device': str(self.device.identifier)} + expected_result = { + 'rfid': self.rfid.code, + 'name': 'device01', + 'device': str(self.device.identifier)} self.assertEquals( response.json(), expected_result @@ -68,11 +74,23 @@ class ApiTests(TestCase): client = RequestsClient() response = client.get('http://127.0.0.1:8180/api/v1/rfid/?format=json') assert response.status_code == 200 - self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) + self.assertEquals(response.json().get('results'), [{ + 'name': 'device01', + 'identifier': '8e274b70-a4b3-4600-9472-f20ea7828cb6', + 'members': [1], + 'added_date': self.device.added_date.isoformat().replace('+00:00', 'Z'), + 'description': '' + }]) def testFetchDeviceList(self): client = RequestsClient() response = client.get('http://127.0.0.1:8180/api/v1/rfid/?format=json') assert response.status_code == 200 - self.assertEquals(response.json().get('results'), [{'name': 'device01'}]) + self.assertEquals(response.json().get('results'), [{ + 'name': 'device01', + 'identifier': '8e274b70-a4b3-4600-9472-f20ea7828cb6', + 'members': [1], + 'added_date': self.device.added_date.isoformat().replace('+00:00', 'Z'), + 'description': '' + }]) diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py index 5b68b1a..66f6b05 100644 --- a/mhackspace/rfid/views.py +++ b/mhackspace/rfid/views.py @@ -22,13 +22,13 @@ class AuthUserWithDeviceViewSet(viewsets.ViewSet): def list(self, request): serializer = DeviceSerializer( - DeviceAuth.objects.all(), many=True) + Device.objects.all(), many=True) return Response(serializer.data) def post(self, request, format=None): try: rfid = Rfid.objects.get(code=request.data.get('rfid')) - device = Device.objects.get(identifier=request.data.get('device')) + # device = Device.objects.get(identifier=request.data.get('device')) deviceAuth = DeviceAuth.objects.get(device=device.identifier, rfid=rfid.id) except ObjectDoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) From 528ad8cade705200a150df8b8ac369415b1160dc Mon Sep 17 00:00:00 2001 From: Oly Date: Wed, 13 Sep 2017 14:28:16 +0100 Subject: [PATCH 08/22] fixed a few more tests --- mhackspace/rfid/serializers.py | 14 ++------ mhackspace/rfid/views.py | 4 +-- mhackspace/subscriptions/payments.py | 2 ++ .../tests/test_payment_gateways.py | 9 ++--- mhackspace/subscriptions/tests/test_views.py | 36 +++++++++++++------ mhackspace/subscriptions/views.py | 3 +- .../migrations/0005_merge_20170913_0740.py | 16 +++++++++ 7 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 mhackspace/users/migrations/0005_merge_20170913_0740.py diff --git a/mhackspace/rfid/serializers.py b/mhackspace/rfid/serializers.py index 1417a9f..9f718c6 100644 --- a/mhackspace/rfid/serializers.py +++ b/mhackspace/rfid/serializers.py @@ -1,6 +1,5 @@ from rest_framework import serializers - -from mhackspace.rfid.models import Device, DeviceAuth +from mhackspace.rfid.models import Device class Task(object): @@ -11,6 +10,7 @@ class Task(object): class DeviceSerializer(serializers.ModelSerializer): added_date = serializers.DateTimeField(format='iso-8601') + class Meta: model = Device fields = ('__all__') @@ -22,13 +22,3 @@ class AuthSerializer(serializers.Serializer): # device = serializers.UUIDField(format='hex_verbose') device = serializers.CharField(max_length=255) - # def create(self, validated_data): - # return Task(id=None, **validated_data) - - # def update(self, instance, validated_data): - # for field, value in validated_data.items(): - # setattr(instance, field, value) - # return instance - - # class Meta: - # fields = ('name', ) diff --git a/mhackspace/rfid/views.py b/mhackspace/rfid/views.py index 66f6b05..30eb8cd 100644 --- a/mhackspace/rfid/views.py +++ b/mhackspace/rfid/views.py @@ -28,11 +28,11 @@ class AuthUserWithDeviceViewSet(viewsets.ViewSet): def post(self, request, format=None): try: rfid = Rfid.objects.get(code=request.data.get('rfid')) - # device = Device.objects.get(identifier=request.data.get('device')) + device = Device.objects.get(identifier=request.data.get('device')) deviceAuth = DeviceAuth.objects.get(device=device.identifier, rfid=rfid.id) except ObjectDoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) - except ValidationError: + except ValidationError as e: # except: # logger.exception("An error occurred") return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/mhackspace/subscriptions/payments.py b/mhackspace/subscriptions/payments.py index 5c4c864..b17235c 100644 --- a/mhackspace/subscriptions/payments.py +++ b/mhackspace/subscriptions/payments.py @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) PROVIDER_ID = {'gocardless':1, 'braintree': 2} PROVIDER_NAME = {1: 'gocardless', 2: 'braintree'} + def select_provider(type): if type == "gocardless": return gocardless_provider() if type == "braintree": return braintree_provider() @@ -20,6 +21,7 @@ def select_provider(type): log.exception('[scaffold] - "No Provider for ' + type) assert 0, "No Provider for " + type + class gocardless_provider: """ gocardless test account details 20-00-00, 55779911 diff --git a/mhackspace/subscriptions/tests/test_payment_gateways.py b/mhackspace/subscriptions/tests/test_payment_gateways.py index 62200d0..7d8c396 100644 --- a/mhackspace/subscriptions/tests/test_payment_gateways.py +++ b/mhackspace/subscriptions/tests/test_payment_gateways.py @@ -6,12 +6,13 @@ from mock import patch, Mock from mhackspace.subscriptions.payments import payment, gocardless_provider, braintree_provider + class TestPaymentGatewaysGocardless(TestCase): def setUp(self): self.auth_gocardless() - @patch('mhackspace.subscriptions.payments.gocardless.request.requests.get', autospec=True) + @patch('mhackspace.subscriptions.payments.gocardless_pro.request.requests.get', autospec=True) def auth_gocardless(self, mock_request): # mock braintree initalisation request mock_request.return_value = Mock(ok=True) @@ -27,7 +28,7 @@ class TestPaymentGatewaysGocardless(TestCase): return self.provider #self.provider @skip("Need to implement") - @patch('mhackspace.subscriptions.payments.gocardless.client.subscription', autospec=True) + @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscription', autospec=True) def test_unsubscribe(self, mock_subscription): mock_subscription.return_value = Mock(success='success') mock_subscription.cancel.return_value = Mock( @@ -43,8 +44,8 @@ class TestPaymentGatewaysGocardless(TestCase): self.assertEqual(result.get('reference'), '01') self.assertEqual(result.get('success'), 'success') - @patch('mhackspace.subscriptions.payments.gocardless.client.subscription', autospec=True) - @patch('mhackspace.subscriptions.payments.gocardless.client.confirm_resource', autospec=True) + @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscription', autospec=True) + @patch('mhackspace.subscriptions.payments.gocardless_pro.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( diff --git a/mhackspace/subscriptions/tests/test_views.py b/mhackspace/subscriptions/tests/test_views.py index e611d96..18bdef6 100644 --- a/mhackspace/subscriptions/tests/test_views.py +++ b/mhackspace/subscriptions/tests/test_views.py @@ -1,11 +1,14 @@ from django.contrib.messages.storage.fallback import FallbackStorage # from django.contrib.auth.models import Group +from django.test import Client from django.test import RequestFactory from django.core.urlresolvers import reverse from test_plus.test import TestCase from mock import patch, Mock from mhackspace.users.models import Membership -from mhackspace.users.models import Membership +from mhackspace.users.models import User + +from mhackspace.subscriptions.payments import payment, gocardless_provider, braintree_provider from ..views import ( MembershipCancelView, @@ -20,7 +23,12 @@ class BaseUserTestCase(TestCase): def setUp(self): self.user = self.make_user() + self.user.save() self.factory = RequestFactory() + self.client = Client() + self.client.login( + username=self.user.username, + password=self.user.password) class TestSubscriptionSuccessRedirectView(BaseUserTestCase): @@ -36,22 +44,30 @@ class TestSubscriptionSuccessRedirectView(BaseUserTestCase): 'success': True } - request = self.factory.post( + response = self.client.post( reverse('join_hackspace_success', kwargs={'provider': 'gocardless'}), { 'resource_id': 'R01', 'resource_type': 'subscription', 'resource_url': 'https://sandbox.gocardless.com', 'signature': 'test_signature' - } + }, + follow=True ) - setattr(request, 'session', 'session') - messages = FallbackStorage(request) - setattr(request, '_messages', messages) - request.user = self.user + # print('=============================') + # setattr(request, 'session', 'session') + # messages = FallbackStorage(request) + # setattr(request, '_messages', messages) + # request.user = user1 - view = MembershipJoinSuccessView() - view.request = request + # view = MembershipJoinSuccessView() + # view.request = request + # print(self.user) + self.assertRedirects( + response, + expected_url=reverse('users:detail', kwargs={'username': self.user.username}), + status_code=302, + target_status_code=200) self.assertEqual( view.get_redirect_url(provider ='gocardless'), reverse('users:detail', kwargs={'username': self.user.username}) @@ -60,7 +76,7 @@ class TestSubscriptionSuccessRedirectView(BaseUserTestCase): members = Membership.objects.all() self.assertEqual(members.count(), 1) - @patch('mhackspace.subscriptions.payments.gocardless.client.subscription', autospec=True) + @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscription', autospec=True) def test_failure_redirect_url(self, mock_obj): # Instantiate the view directly. Never do this outside a test! # Generate a fake request diff --git a/mhackspace/subscriptions/views.py b/mhackspace/subscriptions/views.py index 4529621..2f27080 100644 --- a/mhackspace/subscriptions/views.py +++ b/mhackspace/subscriptions/views.py @@ -99,9 +99,10 @@ class MembershipJoinSuccessView(LoginRequiredMixin, RedirectView): def get_redirect_url(self, *args, **kwargs): payment_provider = 'gocardless' provider = select_provider(payment_provider) + print(self.request.user) membership = Membership.objects.get(user=self.request.user) - name="Membership your membership id is MH%s" % membership.reference + name = "Membership your membership id is MH%s" % membership.reference result = provider.confirm_subscription( membership=membership, session=self.request.session.session_key, diff --git a/mhackspace/users/migrations/0005_merge_20170913_0740.py b/mhackspace/users/migrations/0005_merge_20170913_0740.py new file mode 100644 index 0000000..f09943e --- /dev/null +++ b/mhackspace/users/migrations/0005_merge_20170913_0740.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-13 07:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_auto_20170813_1557'), + ('users', '0004_rfid'), + ] + + operations = [ + ] From 1734dc3f9c42784fdf949bd467c418faa3e899ac Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Thu, 14 Sep 2017 07:29:10 +0100 Subject: [PATCH 09/22] Update test data generation --- README.org | 17 ++++++++++++++++ .../management/commands/generate_test_data.py | 20 ++++++++++++------- mhackspace/rfid/admin.py | 14 ++++++------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/README.org b/README.org index a7a55f0..0a8061f 100644 --- a/README.org +++ b/README.org @@ -73,3 +73,20 @@ docker-compose -fdev.yml run --rm django python manage.py list_subscriptions #+BEGIN_SRC sh docker-compose -fdev.yml run --rm django python manage.py rendervariations 'blog.Post.image' --replace #+END_SRC +** Api +#+BEGIN_SRC python +import coreapi + +url = 'http://127.0.0.1:8180/api/v1/rfidAuth/' +data = { + 'rfid': '49960', + 'device': '7bff6053-77ef-4250-ac11-8a119fd05a0e' +} + +client = coreapi.Client() +client.get(url) +client.post(url, data) + +#requests.get(url) + +#+END_SRC diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py index 7cdf702..22c8787 100644 --- a/mhackspace/base/management/commands/generate_test_data.py +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -1,13 +1,13 @@ import random from autofixture import AutoFixture -from autofixture.generators import ImageGenerator +from autofixture.generators import ImageGenerator, IntegerGenerator, ChoicesGenerator from django.core.management.base import BaseCommand from django.core.management import call_command from mhackspace.base.models import BannerImage from mhackspace.feeds.models import Article, Feed -from mhackspace.users.models import User +from mhackspace.users.models import User, Rfid from mhackspace.blog.models import Category, Post -from mhackspace.rfid.models import Device +from mhackspace.rfid.models import Device, DeviceAuth class ImageFixture(AutoFixture): @@ -33,18 +33,23 @@ class Command(BaseCommand): # load known data call_command('loaddata', 'mhackspace/users/fixtures/groups.json', verbose=0) - autofixture.autodiscover() + # AutoFixture.autodiscover() # random data users = AutoFixture(User, field_values={ - 'title': random.choicee(('Mr', 'Mrs', 'Emperor', 'Captain')) + 'title': ChoicesGenerator(('Mr', 'Mrs', 'Emperor', 'Captain')) }) users.create(10) - rfid = AutoFixture(Rfid) + Rfid.objects.all().delete() + Device.objects.all().delete() + DeviceAuth.objects.all().delete() + rfid = AutoFixture(Rfid, field_values={'code': IntegerGenerator(1, 100000)}) rfid.create(20) - device = AutoFixture(Device) + device = AutoFixture(Device, field_values={ + 'name': ChoicesGenerator(('Door', 'Printer', 'Laser Cutter', '')) + }) device.create(5) deviceauth = AutoFixture(DeviceAuth) @@ -61,3 +66,4 @@ class Command(BaseCommand): self.stdout.write( self.style.SUCCESS( 'Finished creating test data')) + diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py index 0567154..e1d0557 100644 --- a/mhackspace/rfid/admin.py +++ b/mhackspace/rfid/admin.py @@ -20,11 +20,11 @@ class DeviceAuthAdmin(ModelAdmin): def label_from_instance(self, obj): return obj.description + ' - ' + str(obj.user) - def formfield_for_foreignkey(self, db_field, request, **kwargs): - if db_field.name == "rfid": - return self.CustomModelChoiceField( - Rfid.objects.all(), - initial=request.user) + # def formfield_for_foreignkey(self, db_field, request, **kwargs): + # if db_field.name == "rfid": + # return self.CustomModelChoiceField( + # Rfid.objects.all(), + # initial=request.user) - return super(DeviceAuthAdmin, self).formfield_for_foreignkey( - db_field, request, **kwargs) + # return super(DeviceAuthAdmin, self).formfield_for_foreignkey( + # db_field, request, **kwargs) From 02befa5ddb550b710211a95fb45f1ac8b7f62457 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Thu, 14 Sep 2017 22:03:26 +0100 Subject: [PATCH 10/22] Better test data for rfid system --- README.org | 14 +++++---- .../management/commands/generate_test_data.py | 29 ++++++++++++++++--- mhackspace/rfid/admin.py | 18 ++++-------- .../migrations/0005_merge_20170912_2037.py | 16 ++++++++++ .../migrations/0006_merge_20170913_1718.py | 16 ++++++++++ .../migrations/0007_auto_20170914_2021.py | 20 +++++++++++++ mhackspace/users/models.py | 2 +- 7 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 mhackspace/users/migrations/0005_merge_20170912_2037.py create mode 100644 mhackspace/users/migrations/0006_merge_20170913_1718.py create mode 100644 mhackspace/users/migrations/0007_auto_20170914_2021.py diff --git a/README.org b/README.org index 0a8061f..8982b16 100644 --- a/README.org +++ b/README.org @@ -75,7 +75,7 @@ docker-compose -fdev.yml run --rm django python manage.py rendervariations 'blog #+END_SRC ** Api #+BEGIN_SRC python -import coreapi +import requests url = 'http://127.0.0.1:8180/api/v1/rfidAuth/' data = { @@ -83,10 +83,14 @@ data = { 'device': '7bff6053-77ef-4250-ac11-8a119fd05a0e' } -client = coreapi.Client() -client.get(url) -client.post(url, data) - +# client = RequestsClient() +response = requests.post( + 'http://127.0.0.1:8180/api/v1/rfidAuth/', + data={'rfid': 'fa14', 'device': 'e571eee1-7bf7-4453-980e-7519e2a83de6'}) #requests.get(url) +return response.status_code #+END_SRC + +#+RESULTS: +: 404 diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py index 22c8787..cdb076a 100644 --- a/mhackspace/base/management/commands/generate_test_data.py +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -1,6 +1,12 @@ +import uuid import random from autofixture import AutoFixture -from autofixture.generators import ImageGenerator, IntegerGenerator, ChoicesGenerator +from autofixture.generators import ( + ImageGenerator, + IntegerGenerator, + ChoicesGenerator, + Generator, + LoremWordGenerator) from django.core.management.base import BaseCommand from django.core.management import call_command from mhackspace.base.models import BannerImage @@ -15,6 +21,16 @@ class ImageFixture(AutoFixture): scaled_image = ImageGenerator(width=800, height=300, sizes=((1280, 300),)) +def RfidFixture(): + while True: + yield str(uuid.uuid4())[0:4] + + +class RfidGenerator(Generator): + def generate(self): + return str(uuid.uuid4())[0:4] + + class Command(BaseCommand): help = 'Build test data for development environment' @@ -37,18 +53,23 @@ class Command(BaseCommand): # random data users = AutoFixture(User, field_values={ - 'title': ChoicesGenerator(('Mr', 'Mrs', 'Emperor', 'Captain')) + 'title': ChoicesGenerator(values=('Mr', 'Mrs', 'Emperor', 'Captain')) }) users.create(10) Rfid.objects.all().delete() Device.objects.all().delete() DeviceAuth.objects.all().delete() - rfid = AutoFixture(Rfid, field_values={'code': IntegerGenerator(1, 100000)}) + + rfid = AutoFixture( + Rfid, + field_values={ + 'code': RfidGenerator(), + 'description': LoremWordGenerator()}) rfid.create(20) device = AutoFixture(Device, field_values={ - 'name': ChoicesGenerator(('Door', 'Printer', 'Laser Cutter', '')) + 'name': ChoicesGenerator(values=('Door', 'Printer', 'Laser Cutter', '')) }) device.create(5) diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py index e1d0557..dff6b2f 100644 --- a/mhackspace/rfid/admin.py +++ b/mhackspace/rfid/admin.py @@ -11,20 +11,12 @@ class DeviceAdmin(ModelAdmin): list_display = ('name', 'identifier') -# Probably need to look at this again @admin.register(DeviceAuth) class DeviceAuthAdmin(ModelAdmin): - list_display = ('rfid', 'device') + list_display = ('device', 'rfid_code', 'rfid_user') - class CustomModelChoiceField(ModelChoiceField): - def label_from_instance(self, obj): - return obj.description + ' - ' + str(obj.user) + def rfid_code(self, x): + return x.rfid.code - # def formfield_for_foreignkey(self, db_field, request, **kwargs): - # if db_field.name == "rfid": - # return self.CustomModelChoiceField( - # Rfid.objects.all(), - # initial=request.user) - - # return super(DeviceAuthAdmin, self).formfield_for_foreignkey( - # db_field, request, **kwargs) + def rfid_user(self, x): + return x.rfid.user diff --git a/mhackspace/users/migrations/0005_merge_20170912_2037.py b/mhackspace/users/migrations/0005_merge_20170912_2037.py new file mode 100644 index 0000000..3e8e01b --- /dev/null +++ b/mhackspace/users/migrations/0005_merge_20170912_2037.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-12 20:37 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_auto_20170813_1557'), + ('users', '0004_rfid'), + ] + + operations = [ + ] diff --git a/mhackspace/users/migrations/0006_merge_20170913_1718.py b/mhackspace/users/migrations/0006_merge_20170913_1718.py new file mode 100644 index 0000000..1f06f7f --- /dev/null +++ b/mhackspace/users/migrations/0006_merge_20170913_1718.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-13 17:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_merge_20170912_2037'), + ('users', '0005_merge_20170913_0740'), + ] + + operations = [ + ] diff --git a/mhackspace/users/migrations/0007_auto_20170914_2021.py b/mhackspace/users/migrations/0007_auto_20170914_2021.py new file mode 100644 index 0000000..2b65e49 --- /dev/null +++ b/mhackspace/users/migrations/0007_auto_20170914_2021.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-14 20:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_merge_20170913_1718'), + ] + + operations = [ + migrations.AlterField( + model_name='rfid', + name='code', + field=models.CharField(max_length=7), + ), + ] diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index 68b393d..f63e650 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -94,7 +94,7 @@ class Membership(models.Model): # users rfid card to user mapping, user can have more than one card class Rfid(models.Model): - code = models.PositiveIntegerField() + code = models.CharField(max_length=7) description = models.CharField(_('Short rfid description'), blank=True, max_length=255) user = models.ForeignKey( settings.AUTH_USER_MODEL, From a7a3311f8d1a44c2ee8f45356206b8ecffa3def3 Mon Sep 17 00:00:00 2001 From: Oly Date: Fri, 15 Sep 2017 14:05:22 +0100 Subject: [PATCH 11/22] Better users generated in test data. --- README.org | 6 +- .../management/commands/generate_test_data.py | 25 ++++++-- mhackspace/requests/tests.py | 61 +++++++++++++++++++ mhackspace/rfid/admin.py | 2 +- mhackspace/users/models.py | 6 +- 5 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 mhackspace/requests/tests.py diff --git a/README.org b/README.org index 8982b16..3fb38b7 100644 --- a/README.org +++ b/README.org @@ -79,18 +79,18 @@ import requests url = 'http://127.0.0.1:8180/api/v1/rfidAuth/' data = { - 'rfid': '49960', + 'rfid': '4996', 'device': '7bff6053-77ef-4250-ac11-8a119fd05a0e' } # client = RequestsClient() response = requests.post( 'http://127.0.0.1:8180/api/v1/rfidAuth/', - data={'rfid': 'fa14', 'device': 'e571eee1-7bf7-4453-980e-7519e2a83de6'}) + data={'rfid': '238e', 'device': 'e8f27231-8093-4477-8906-e5ae1b12dbd6'}) #requests.get(url) return response.status_code #+END_SRC #+RESULTS: -: 404 +: 200 diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py index cdb076a..5da78a6 100644 --- a/mhackspace/base/management/commands/generate_test_data.py +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -1,5 +1,6 @@ import uuid import random +from django.contrib.auth.hashers import make_password from autofixture import AutoFixture from autofixture.generators import ( ImageGenerator, @@ -35,8 +36,9 @@ class Command(BaseCommand): help = 'Build test data for development environment' def handle(self, *args, **options): - feeds = AutoFixture(Article) + feeds = AutoFixture(Article, generate_fk=True) feeds.create(10) + feed = AutoFixture(Feed) feed.create(10) @@ -49,13 +51,24 @@ class Command(BaseCommand): # load known data call_command('loaddata', 'mhackspace/users/fixtures/groups.json', verbose=0) - # AutoFixture.autodiscover() - # random data + User.objects.all().delete() users = AutoFixture(User, field_values={ - 'title': ChoicesGenerator(values=('Mr', 'Mrs', 'Emperor', 'Captain')) - }) - users.create(10) + 'title': ChoicesGenerator(values=('Mr', 'Mrs', 'Emperor', 'Captain')), + 'password': make_password('autofixtures'), + 'active': True, + 'username': ChoicesGenerator(values=('Bob', 'Jane', 'Adam', 'Alice', 'Bill', 'Jill', 'Sam', 'Oly')) + }, generate_fk=True) + users.create(8) + users = AutoFixture(User, field_values={ + 'title': 'Mr', + 'username': 'admin', + 'password': make_password('autofixtures'), + 'is_superuser': True, + 'is_staff': True, + 'is_active': True + }, generate_fk=True) + users.create(1) Rfid.objects.all().delete() Device.objects.all().delete() diff --git a/mhackspace/requests/tests.py b/mhackspace/requests/tests.py new file mode 100644 index 0000000..3860e0c --- /dev/null +++ b/mhackspace/requests/tests.py @@ -0,0 +1,61 @@ +from django.test import TestCase +from mhackspace.requests.views import RequestsList, RequestForm + +# Create your tests here. + +# @pytest.mark.parametrize("version", versions) +# @pytest.mark.parametrize("test_ctx, name", contexts) +# def test_context_renders(name, test_ctx, version): + + # users = AutoFixture(User, field_values={ + # 'title': 'Mr', + # 'username': 'admin', + # 'password': make_password('autofixtures'), + # 'is_superuser': True, + # 'is_staff': True, + # 'is_active': True + # }, generate_fk=True) + + + +class BaseUserTestCase(TestCase): + + def setUp(self): + self.user = self.make_user() + self.factory = RequestFactory() + + def testRequestView(self): + view = RequestsList() + request = self.factory.get('/fake-url') + request.user = self.user + view.request = request + + +# class TestUserUpdateView(BaseUserTestCase): + +# def setUp(self): +# # call BaseUserTestCase.setUp() +# super(TestUserUpdateView, self).setUp() +# # Instantiate the view directly. Never do this outside a test! +# self.view = UserUpdateView() +# # Generate a fake request +# request = self.factory.get('/fake-url') +# # Attach the user to the request +# request.user = self.user +# # Attach the request to the view +# self.view.request = request + +# def test_get_success_url(self): +# # Expect: '/users/testuser/', as that is the default username for +# # self.make_user() +# self.assertEqual( +# self.view.get_success_url(), +# '/users/testuser/' +# ) + +# def test_get_object(self): +# # Expect: self.user, as that is the request's user object +# self.assertEqual( +# self.view.get_object(), +# self.user +# ) diff --git a/mhackspace/rfid/admin.py b/mhackspace/rfid/admin.py index dff6b2f..f530735 100644 --- a/mhackspace/rfid/admin.py +++ b/mhackspace/rfid/admin.py @@ -13,7 +13,7 @@ class DeviceAdmin(ModelAdmin): @admin.register(DeviceAuth) class DeviceAuthAdmin(ModelAdmin): - list_display = ('device', 'rfid_code', 'rfid_user') + list_display = ('device', 'rfid_code', 'rfid_user', 'device_id') def rfid_code(self, x): return x.rfid.code diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index f63e650..8f60e10 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -57,13 +57,15 @@ MEMBERSHIP_STATUS_CHOICES = ( MEMBERSHIP_STRING = { 0: 'Guest user', 1: 'Active membership', - 3: 'Membership Expired' + 3: 'Membership Expired', + 4: 'Membership Cancelled' } MEMBERSHIP_STATUS = { 'signup': 0, # This means the user has not completed signup 'active': 1, - 'cancelled': 2 + 'expired': 3, + 'cancelled': 4 } class Membership(models.Model): From ae18e8bf38fd8c9567040b6572b83352157567ac Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Fri, 15 Sep 2017 22:34:14 +0100 Subject: [PATCH 12/22] Fixes #139 wiki preview error --- .../management/commands/generate_test_data.py | 10 +- mhackspace/static/sass/components/_wiki.scss | 19 + mhackspace/static/sass/project.css | 6097 +++++++++++++++++ mhackspace/static/sass/project.scss | 3 +- mhackspace/users/admin.py | 2 +- mhackspace/users/models.py | 1 + 6 files changed, 6129 insertions(+), 3 deletions(-) create mode 100644 mhackspace/static/sass/components/_wiki.scss create mode 100644 mhackspace/static/sass/project.css diff --git a/mhackspace/base/management/commands/generate_test_data.py b/mhackspace/base/management/commands/generate_test_data.py index 5da78a6..6665e7d 100644 --- a/mhackspace/base/management/commands/generate_test_data.py +++ b/mhackspace/base/management/commands/generate_test_data.py @@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand from django.core.management import call_command from mhackspace.base.models import BannerImage from mhackspace.feeds.models import Article, Feed -from mhackspace.users.models import User, Rfid +from mhackspace.users.models import User, Rfid, Membership, MEMBERSHIP_STATUS_CHOICES from mhackspace.blog.models import Category, Post from mhackspace.rfid.models import Device, DeviceAuth @@ -53,6 +53,7 @@ class Command(BaseCommand): User.objects.all().delete() + Membership.objects.all().delete() users = AutoFixture(User, field_values={ 'title': ChoicesGenerator(values=('Mr', 'Mrs', 'Emperor', 'Captain')), 'password': make_password('autofixtures'), @@ -70,6 +71,13 @@ class Command(BaseCommand): }, generate_fk=True) users.create(1) + user_list = User.objects.all() + members = AutoFixture(Membership, field_values={ + 'status': ChoicesGenerator(MEMBERSHIP_STATUS_CHOICES), + 'user': ChoicesGenerator(values=user_list) + }) + members.create(8) + Rfid.objects.all().delete() Device.objects.all().delete() DeviceAuth.objects.all().delete() diff --git a/mhackspace/static/sass/components/_wiki.scss b/mhackspace/static/sass/components/_wiki.scss new file mode 100644 index 0000000..f6f3e5e --- /dev/null +++ b/mhackspace/static/sass/components/_wiki.scss @@ -0,0 +1,19 @@ +.modal-body iframe { + width: 100%; + height: 100%; +} +.modal-dialog { + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + margin: 50px; + padding: 0; + max-width: None; +} + +.modal-content { + height: auto; + min-height: 100%; + border-radius: 0; +} diff --git a/mhackspace/static/sass/project.css b/mhackspace/static/sass/project.css new file mode 100644 index 0000000..1353500 --- /dev/null +++ b/mhackspace/static/sass/project.css @@ -0,0 +1,6097 @@ +/*! + * Bootstrap v4.0.0-alpha.6 (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors + * Copyright 2011-2017 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + line-height: 1.15; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } + +body { + margin: 0; } + +article, +aside, +footer, +header, +nav, +section { + display: block; } + +h1 { + font-size: 2em; + margin: 0.67em 0; } + +figcaption, +figure, +main { + display: block; } + +figure { + margin: 1em 40px; } + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; } + +pre { + font-family: monospace, monospace; + font-size: 1em; } + +a { + background-color: transparent; + -webkit-text-decoration-skip: objects; } + +a:active, +a:hover { + outline-width: 0; } + +abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted; } + +b, +strong { + font-weight: inherit; } + +b, +strong { + font-weight: bolder; } + +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em; } + +dfn { + font-style: italic; } + +mark { + background-color: #ff0; + color: #000; } + +small { + font-size: 80%; } + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sub { + bottom: -0.25em; } + +sup { + top: -0.5em; } + +audio, +video { + display: inline-block; } + +audio:not([controls]) { + display: none; + height: 0; } + +img { + border-style: none; } + +svg:not(:root) { + overflow: hidden; } + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; + font-size: 100%; + line-height: 1.15; + margin: 0; } + +button, +input { + overflow: visible; } + +button, +select { + text-transform: none; } + +button, +html [type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; } + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; } + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; } + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } + +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; } + +progress { + display: inline-block; + vertical-align: baseline; } + +textarea { + overflow: auto; } + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; + padding: 0; } + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; } + +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; } + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; } + +details, +menu { + display: block; } + +summary { + display: list-item; } + +canvas { + display: inline-block; } + +template { + display: none; } + +[hidden] { + display: none; } + +@media print { + *, + *::before, + *::after, + p::first-letter, + div::first-letter, + blockquote::first-letter, + li::first-letter, + p::first-line, + div::first-line, + blockquote::first-line, + li::first-line { + text-shadow: none !important; + box-shadow: none !important; } + a, + a:visited { + text-decoration: underline; } + abbr[title]::after { + content: " (" attr(title) ")"; } + pre { + white-space: pre-wrap !important; } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; } + thead { + display: table-header-group; } + tr, + img { + page-break-inside: avoid; } + p, + h2, + h3 { + orphans: 3; + widows: 3; } + h2, + h3 { + page-break-after: avoid; } + .navbar { + display: none; } + .badge { + border: 1px solid #000; } + .table { + border-collapse: collapse !important; } + .table td, + .table th { + background-color: #fff !important; } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; } } + +html { + box-sizing: border-box; } + +*, +*::before, +*::after { + box-sizing: inherit; } + +@-ms-viewport { + width: device-width; } + +html { + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; } + +body { + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 1rem; + font-weight: normal; + line-height: 1.5; + color: #292b2c; + background-color: #fff; } + +[tabindex="-1"]:focus { + outline: none !important; } + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: .5rem; } + +p { + margin-top: 0; + margin-bottom: 1rem; } + +abbr[title], +abbr[data-original-title] { + cursor: help; } + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; } + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; } + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; } + +dt { + font-weight: bold; } + +dd { + margin-bottom: .5rem; + margin-left: 0; } + +blockquote { + margin: 0 0 1rem; } + +a { + color: #0275d8; + text-decoration: none; } + a:focus, a:hover { + color: #014c8c; + text-decoration: underline; } + +a:not([href]):not([tabindex]) { + color: inherit; + text-decoration: none; } + a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { + color: inherit; + text-decoration: none; } + a:not([href]):not([tabindex]):focus { + outline: 0; } + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; } + +figure { + margin: 0 0 1rem; } + +img { + vertical-align: middle; } + +[role="button"] { + cursor: pointer; } + +a, +area, +button, +[role="button"], +input, +label, +select, +summary, +textarea { + touch-action: manipulation; } + +table { + border-collapse: collapse; + background-color: transparent; } + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #636c72; + text-align: left; + caption-side: bottom; } + +th { + text-align: left; } + +label { + display: inline-block; + margin-bottom: .5rem; } + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; } + +input, +button, +select, +textarea { + line-height: inherit; } + +input[type="radio"]:disabled, +input[type="checkbox"]:disabled { + cursor: not-allowed; } + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; } + +textarea { + resize: vertical; } + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; } + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; } + +input[type="search"] { + -webkit-appearance: none; } + +output { + display: inline-block; } + +[hidden] { + display: none !important; } + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; } + +h1, .h1 { + font-size: 2.5rem; } + +h2, .h2 { + font-size: 2rem; } + +h3, .h3 { + font-size: 1.75rem; } + +h4, .h4 { + font-size: 1.5rem; } + +h5, .h5 { + font-size: 1.25rem; } + +h6, .h6 { + font-size: 1rem; } + +.lead { + font-size: 1.25rem; + font-weight: 300; } + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.1; } + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.1; } + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.1; } + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.1; } + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); } + +small, +.small { + font-size: 80%; + font-weight: normal; } + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; } + +.list-unstyled { + padding-left: 0; + list-style: none; } + +.list-inline { + padding-left: 0; + list-style: none; } + +.list-inline-item { + display: inline-block; } + .list-inline-item:not(:last-child) { + margin-right: 5px; } + +.initialism { + font-size: 90%; + text-transform: uppercase; } + +.blockquote { + padding: 0.5rem 1rem; + margin-bottom: 1rem; + font-size: 1.25rem; + border-left: 0.25rem solid #eceeef; } + +.blockquote-footer { + display: block; + font-size: 80%; + color: #636c72; } + .blockquote-footer::before { + content: "\2014 \00A0"; } + +.blockquote-reverse { + padding-right: 1rem; + padding-left: 0; + text-align: right; + border-right: 0.25rem solid #eceeef; + border-left: 0; } + +.blockquote-reverse .blockquote-footer::before { + content: ""; } + +.blockquote-reverse .blockquote-footer::after { + content: "\00A0 \2014"; } + +.img-fluid { + max-width: 100%; + height: auto; } + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 0.25rem; + transition: all 0.2s ease-in-out; + max-width: 100%; + height: auto; } + +.figure { + display: inline-block; } + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; } + +.figure-caption { + font-size: 90%; + color: #636c72; } + +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } + +code { + padding: 0.2rem 0.4rem; + font-size: 90%; + color: #bd4147; + background-color: #f7f7f9; + border-radius: 0.25rem; } + a > code { + padding: 0; + color: inherit; + background-color: inherit; } + +kbd { + padding: 0.2rem 0.4rem; + font-size: 90%; + color: #fff; + background-color: #292b2c; + border-radius: 0.2rem; } + kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; } + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + font-size: 90%; + color: #292b2c; } + pre code { + padding: 0; + font-size: inherit; + color: inherit; + background-color: transparent; + border-radius: 0; } + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; } + +.container { + position: relative; + margin-left: auto; + margin-right: auto; + padding-right: 15px; + padding-left: 15px; } + @media (min-width: 576px) { + .container { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 768px) { + .container { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 992px) { + .container { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 1200px) { + .container { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 576px) { + .container { + width: 540px; + max-width: 100%; } } + @media (min-width: 768px) { + .container { + width: 720px; + max-width: 100%; } } + @media (min-width: 992px) { + .container { + width: 960px; + max-width: 100%; } } + @media (min-width: 1200px) { + .container { + width: 1140px; + max-width: 100%; } } + +.container-fluid { + position: relative; + margin-left: auto; + margin-right: auto; + padding-right: 15px; + padding-left: 15px; } + @media (min-width: 576px) { + .container-fluid { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 768px) { + .container-fluid { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 992px) { + .container-fluid { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 1200px) { + .container-fluid { + padding-right: 15px; + padding-left: 15px; } } + +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; } + @media (min-width: 576px) { + .row { + margin-right: -15px; + margin-left: -15px; } } + @media (min-width: 768px) { + .row { + margin-right: -15px; + margin-left: -15px; } } + @media (min-width: 992px) { + .row { + margin-right: -15px; + margin-left: -15px; } } + @media (min-width: 1200px) { + .row { + margin-right: -15px; + margin-left: -15px; } } + +.no-gutters { + margin-right: 0; + margin-left: 0; } + .no-gutters > .col, + .no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; } + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl { + position: relative; + width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; } + @media (min-width: 576px) { + .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 768px) { + .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 992px) { + .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl { + padding-right: 15px; + padding-left: 15px; } } + @media (min-width: 1200px) { + .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl { + padding-right: 15px; + padding-left: 15px; } } + +.col { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; } + +.col-auto { + flex: 0 0 auto; + width: auto; } + +.col-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; } + +.col-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; } + +.col-3 { + flex: 0 0 25%; + max-width: 25%; } + +.col-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; } + +.col-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; } + +.col-6 { + flex: 0 0 50%; + max-width: 50%; } + +.col-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; } + +.col-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; } + +.col-9 { + flex: 0 0 75%; + max-width: 75%; } + +.col-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; } + +.col-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; } + +.col-12 { + flex: 0 0 100%; + max-width: 100%; } + +.pull-0 { + right: auto; } + +.pull-1 { + right: 8.33333333%; } + +.pull-2 { + right: 16.66666667%; } + +.pull-3 { + right: 25%; } + +.pull-4 { + right: 33.33333333%; } + +.pull-5 { + right: 41.66666667%; } + +.pull-6 { + right: 50%; } + +.pull-7 { + right: 58.33333333%; } + +.pull-8 { + right: 66.66666667%; } + +.pull-9 { + right: 75%; } + +.pull-10 { + right: 83.33333333%; } + +.pull-11 { + right: 91.66666667%; } + +.pull-12 { + right: 100%; } + +.push-0 { + left: auto; } + +.push-1 { + left: 8.33333333%; } + +.push-2 { + left: 16.66666667%; } + +.push-3 { + left: 25%; } + +.push-4 { + left: 33.33333333%; } + +.push-5 { + left: 41.66666667%; } + +.push-6 { + left: 50%; } + +.push-7 { + left: 58.33333333%; } + +.push-8 { + left: 66.66666667%; } + +.push-9 { + left: 75%; } + +.push-10 { + left: 83.33333333%; } + +.push-11 { + left: 91.66666667%; } + +.push-12 { + left: 100%; } + +.offset-1 { + margin-left: 8.33333333%; } + +.offset-2 { + margin-left: 16.66666667%; } + +.offset-3 { + margin-left: 25%; } + +.offset-4 { + margin-left: 33.33333333%; } + +.offset-5 { + margin-left: 41.66666667%; } + +.offset-6 { + margin-left: 50%; } + +.offset-7 { + margin-left: 58.33333333%; } + +.offset-8 { + margin-left: 66.66666667%; } + +.offset-9 { + margin-left: 75%; } + +.offset-10 { + margin-left: 83.33333333%; } + +.offset-11 { + margin-left: 91.66666667%; } + +@media (min-width: 576px) { + .col-sm { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; } + .col-sm-auto { + flex: 0 0 auto; + width: auto; } + .col-sm-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; } + .col-sm-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; } + .col-sm-3 { + flex: 0 0 25%; + max-width: 25%; } + .col-sm-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; } + .col-sm-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; } + .col-sm-6 { + flex: 0 0 50%; + max-width: 50%; } + .col-sm-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; } + .col-sm-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; } + .col-sm-9 { + flex: 0 0 75%; + max-width: 75%; } + .col-sm-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; } + .col-sm-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; } + .col-sm-12 { + flex: 0 0 100%; + max-width: 100%; } + .pull-sm-0 { + right: auto; } + .pull-sm-1 { + right: 8.33333333%; } + .pull-sm-2 { + right: 16.66666667%; } + .pull-sm-3 { + right: 25%; } + .pull-sm-4 { + right: 33.33333333%; } + .pull-sm-5 { + right: 41.66666667%; } + .pull-sm-6 { + right: 50%; } + .pull-sm-7 { + right: 58.33333333%; } + .pull-sm-8 { + right: 66.66666667%; } + .pull-sm-9 { + right: 75%; } + .pull-sm-10 { + right: 83.33333333%; } + .pull-sm-11 { + right: 91.66666667%; } + .pull-sm-12 { + right: 100%; } + .push-sm-0 { + left: auto; } + .push-sm-1 { + left: 8.33333333%; } + .push-sm-2 { + left: 16.66666667%; } + .push-sm-3 { + left: 25%; } + .push-sm-4 { + left: 33.33333333%; } + .push-sm-5 { + left: 41.66666667%; } + .push-sm-6 { + left: 50%; } + .push-sm-7 { + left: 58.33333333%; } + .push-sm-8 { + left: 66.66666667%; } + .push-sm-9 { + left: 75%; } + .push-sm-10 { + left: 83.33333333%; } + .push-sm-11 { + left: 91.66666667%; } + .push-sm-12 { + left: 100%; } + .offset-sm-0 { + margin-left: 0%; } + .offset-sm-1 { + margin-left: 8.33333333%; } + .offset-sm-2 { + margin-left: 16.66666667%; } + .offset-sm-3 { + margin-left: 25%; } + .offset-sm-4 { + margin-left: 33.33333333%; } + .offset-sm-5 { + margin-left: 41.66666667%; } + .offset-sm-6 { + margin-left: 50%; } + .offset-sm-7 { + margin-left: 58.33333333%; } + .offset-sm-8 { + margin-left: 66.66666667%; } + .offset-sm-9 { + margin-left: 75%; } + .offset-sm-10 { + margin-left: 83.33333333%; } + .offset-sm-11 { + margin-left: 91.66666667%; } } + +@media (min-width: 768px) { + .col-md { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; } + .col-md-auto { + flex: 0 0 auto; + width: auto; } + .col-md-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; } + .col-md-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; } + .col-md-3 { + flex: 0 0 25%; + max-width: 25%; } + .col-md-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; } + .col-md-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; } + .col-md-6 { + flex: 0 0 50%; + max-width: 50%; } + .col-md-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; } + .col-md-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; } + .col-md-9 { + flex: 0 0 75%; + max-width: 75%; } + .col-md-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; } + .col-md-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; } + .col-md-12 { + flex: 0 0 100%; + max-width: 100%; } + .pull-md-0 { + right: auto; } + .pull-md-1 { + right: 8.33333333%; } + .pull-md-2 { + right: 16.66666667%; } + .pull-md-3 { + right: 25%; } + .pull-md-4 { + right: 33.33333333%; } + .pull-md-5 { + right: 41.66666667%; } + .pull-md-6 { + right: 50%; } + .pull-md-7 { + right: 58.33333333%; } + .pull-md-8 { + right: 66.66666667%; } + .pull-md-9 { + right: 75%; } + .pull-md-10 { + right: 83.33333333%; } + .pull-md-11 { + right: 91.66666667%; } + .pull-md-12 { + right: 100%; } + .push-md-0 { + left: auto; } + .push-md-1 { + left: 8.33333333%; } + .push-md-2 { + left: 16.66666667%; } + .push-md-3 { + left: 25%; } + .push-md-4 { + left: 33.33333333%; } + .push-md-5 { + left: 41.66666667%; } + .push-md-6 { + left: 50%; } + .push-md-7 { + left: 58.33333333%; } + .push-md-8 { + left: 66.66666667%; } + .push-md-9 { + left: 75%; } + .push-md-10 { + left: 83.33333333%; } + .push-md-11 { + left: 91.66666667%; } + .push-md-12 { + left: 100%; } + .offset-md-0 { + margin-left: 0%; } + .offset-md-1 { + margin-left: 8.33333333%; } + .offset-md-2 { + margin-left: 16.66666667%; } + .offset-md-3 { + margin-left: 25%; } + .offset-md-4 { + margin-left: 33.33333333%; } + .offset-md-5 { + margin-left: 41.66666667%; } + .offset-md-6 { + margin-left: 50%; } + .offset-md-7 { + margin-left: 58.33333333%; } + .offset-md-8 { + margin-left: 66.66666667%; } + .offset-md-9 { + margin-left: 75%; } + .offset-md-10 { + margin-left: 83.33333333%; } + .offset-md-11 { + margin-left: 91.66666667%; } } + +@media (min-width: 992px) { + .col-lg { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; } + .col-lg-auto { + flex: 0 0 auto; + width: auto; } + .col-lg-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; } + .col-lg-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; } + .col-lg-3 { + flex: 0 0 25%; + max-width: 25%; } + .col-lg-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; } + .col-lg-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; } + .col-lg-6 { + flex: 0 0 50%; + max-width: 50%; } + .col-lg-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; } + .col-lg-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; } + .col-lg-9 { + flex: 0 0 75%; + max-width: 75%; } + .col-lg-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; } + .col-lg-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; } + .col-lg-12 { + flex: 0 0 100%; + max-width: 100%; } + .pull-lg-0 { + right: auto; } + .pull-lg-1 { + right: 8.33333333%; } + .pull-lg-2 { + right: 16.66666667%; } + .pull-lg-3 { + right: 25%; } + .pull-lg-4 { + right: 33.33333333%; } + .pull-lg-5 { + right: 41.66666667%; } + .pull-lg-6 { + right: 50%; } + .pull-lg-7 { + right: 58.33333333%; } + .pull-lg-8 { + right: 66.66666667%; } + .pull-lg-9 { + right: 75%; } + .pull-lg-10 { + right: 83.33333333%; } + .pull-lg-11 { + right: 91.66666667%; } + .pull-lg-12 { + right: 100%; } + .push-lg-0 { + left: auto; } + .push-lg-1 { + left: 8.33333333%; } + .push-lg-2 { + left: 16.66666667%; } + .push-lg-3 { + left: 25%; } + .push-lg-4 { + left: 33.33333333%; } + .push-lg-5 { + left: 41.66666667%; } + .push-lg-6 { + left: 50%; } + .push-lg-7 { + left: 58.33333333%; } + .push-lg-8 { + left: 66.66666667%; } + .push-lg-9 { + left: 75%; } + .push-lg-10 { + left: 83.33333333%; } + .push-lg-11 { + left: 91.66666667%; } + .push-lg-12 { + left: 100%; } + .offset-lg-0 { + margin-left: 0%; } + .offset-lg-1 { + margin-left: 8.33333333%; } + .offset-lg-2 { + margin-left: 16.66666667%; } + .offset-lg-3 { + margin-left: 25%; } + .offset-lg-4 { + margin-left: 33.33333333%; } + .offset-lg-5 { + margin-left: 41.66666667%; } + .offset-lg-6 { + margin-left: 50%; } + .offset-lg-7 { + margin-left: 58.33333333%; } + .offset-lg-8 { + margin-left: 66.66666667%; } + .offset-lg-9 { + margin-left: 75%; } + .offset-lg-10 { + margin-left: 83.33333333%; } + .offset-lg-11 { + margin-left: 91.66666667%; } } + +@media (min-width: 1200px) { + .col-xl { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; } + .col-xl-auto { + flex: 0 0 auto; + width: auto; } + .col-xl-1 { + flex: 0 0 8.33333333%; + max-width: 8.33333333%; } + .col-xl-2 { + flex: 0 0 16.66666667%; + max-width: 16.66666667%; } + .col-xl-3 { + flex: 0 0 25%; + max-width: 25%; } + .col-xl-4 { + flex: 0 0 33.33333333%; + max-width: 33.33333333%; } + .col-xl-5 { + flex: 0 0 41.66666667%; + max-width: 41.66666667%; } + .col-xl-6 { + flex: 0 0 50%; + max-width: 50%; } + .col-xl-7 { + flex: 0 0 58.33333333%; + max-width: 58.33333333%; } + .col-xl-8 { + flex: 0 0 66.66666667%; + max-width: 66.66666667%; } + .col-xl-9 { + flex: 0 0 75%; + max-width: 75%; } + .col-xl-10 { + flex: 0 0 83.33333333%; + max-width: 83.33333333%; } + .col-xl-11 { + flex: 0 0 91.66666667%; + max-width: 91.66666667%; } + .col-xl-12 { + flex: 0 0 100%; + max-width: 100%; } + .pull-xl-0 { + right: auto; } + .pull-xl-1 { + right: 8.33333333%; } + .pull-xl-2 { + right: 16.66666667%; } + .pull-xl-3 { + right: 25%; } + .pull-xl-4 { + right: 33.33333333%; } + .pull-xl-5 { + right: 41.66666667%; } + .pull-xl-6 { + right: 50%; } + .pull-xl-7 { + right: 58.33333333%; } + .pull-xl-8 { + right: 66.66666667%; } + .pull-xl-9 { + right: 75%; } + .pull-xl-10 { + right: 83.33333333%; } + .pull-xl-11 { + right: 91.66666667%; } + .pull-xl-12 { + right: 100%; } + .push-xl-0 { + left: auto; } + .push-xl-1 { + left: 8.33333333%; } + .push-xl-2 { + left: 16.66666667%; } + .push-xl-3 { + left: 25%; } + .push-xl-4 { + left: 33.33333333%; } + .push-xl-5 { + left: 41.66666667%; } + .push-xl-6 { + left: 50%; } + .push-xl-7 { + left: 58.33333333%; } + .push-xl-8 { + left: 66.66666667%; } + .push-xl-9 { + left: 75%; } + .push-xl-10 { + left: 83.33333333%; } + .push-xl-11 { + left: 91.66666667%; } + .push-xl-12 { + left: 100%; } + .offset-xl-0 { + margin-left: 0%; } + .offset-xl-1 { + margin-left: 8.33333333%; } + .offset-xl-2 { + margin-left: 16.66666667%; } + .offset-xl-3 { + margin-left: 25%; } + .offset-xl-4 { + margin-left: 33.33333333%; } + .offset-xl-5 { + margin-left: 41.66666667%; } + .offset-xl-6 { + margin-left: 50%; } + .offset-xl-7 { + margin-left: 58.33333333%; } + .offset-xl-8 { + margin-left: 66.66666667%; } + .offset-xl-9 { + margin-left: 75%; } + .offset-xl-10 { + margin-left: 83.33333333%; } + .offset-xl-11 { + margin-left: 91.66666667%; } } + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; } + .table th, + .table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #eceeef; } + .table thead th { + vertical-align: bottom; + border-bottom: 2px solid #eceeef; } + .table tbody + tbody { + border-top: 2px solid #eceeef; } + .table .table { + background-color: #fff; } + +.table-sm th, +.table-sm td { + padding: 0.3rem; } + +.table-bordered { + border: 1px solid #eceeef; } + .table-bordered th, + .table-bordered td { + border: 1px solid #eceeef; } + .table-bordered thead th, + .table-bordered thead td { + border-bottom-width: 2px; } + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); } + +.table-hover tbody tr:hover { + background-color: rgba(0, 0, 0, 0.075); } + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); } + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); } + .table-hover .table-active:hover > td, + .table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); } + +.table-success, +.table-success > th, +.table-success > td { + background-color: #dff0d8; } + +.table-hover .table-success:hover { + background-color: #d0e9c6; } + .table-hover .table-success:hover > td, + .table-hover .table-success:hover > th { + background-color: #d0e9c6; } + +.table-info, +.table-info > th, +.table-info > td { + background-color: #d9edf7; } + +.table-hover .table-info:hover { + background-color: #c4e3f3; } + .table-hover .table-info:hover > td, + .table-hover .table-info:hover > th { + background-color: #c4e3f3; } + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #fcf8e3; } + +.table-hover .table-warning:hover { + background-color: #faf2cc; } + .table-hover .table-warning:hover > td, + .table-hover .table-warning:hover > th { + background-color: #faf2cc; } + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f2dede; } + +.table-hover .table-danger:hover { + background-color: #ebcccc; } + .table-hover .table-danger:hover > td, + .table-hover .table-danger:hover > th { + background-color: #ebcccc; } + +.thead-inverse th { + color: #fff; + background-color: #292b2c; } + +.thead-default th { + color: #464a4c; + background-color: #eceeef; } + +.table-inverse { + color: #fff; + background-color: #292b2c; } + .table-inverse th, + .table-inverse td, + .table-inverse thead th { + border-color: #fff; } + .table-inverse.table-bordered { + border: 0; } + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -ms-overflow-style: -ms-autohiding-scrollbar; } + .table-responsive.table-bordered { + border: 0; } + +.form-control { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 1rem; + line-height: 1.25; + color: #464a4c; + background-color: #fff; + background-image: none; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; } + .form-control::-ms-expand { + background-color: transparent; + border: 0; } + .form-control:focus { + color: #464a4c; + background-color: #fff; + border-color: #5cb3fd; + outline: none; } + .form-control::placeholder { + color: #636c72; + opacity: 1; } + .form-control:disabled, .form-control[readonly] { + background-color: #eceeef; + opacity: 1; } + .form-control:disabled { + cursor: not-allowed; } + +select.form-control:not([size]):not([multiple]) { + height: calc(2.25rem + 2px); } + +select.form-control:focus::-ms-value { + color: #464a4c; + background-color: #fff; } + +.form-control-file, +.form-control-range { + display: block; } + +.col-form-label { + padding-top: calc(0.5rem - 1px * 2); + padding-bottom: calc(0.5rem - 1px * 2); + margin-bottom: 0; } + +.col-form-label-lg { + padding-top: calc(0.75rem - 1px * 2); + padding-bottom: calc(0.75rem - 1px * 2); + font-size: 1.25rem; } + +.col-form-label-sm { + padding-top: calc(0.25rem - 1px * 2); + padding-bottom: calc(0.25rem - 1px * 2); + font-size: 0.875rem; } + +.col-form-legend { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + margin-bottom: 0; + font-size: 1rem; } + +.form-control-static { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + margin-bottom: 0; + line-height: 1.25; + border: solid transparent; + border-width: 1px 0; } + .form-control-static.form-control-sm, .input-group-sm > .form-control-static.form-control, + .input-group-sm > .form-control-static.input-group-addon, + .input-group-sm > .input-group-btn > .form-control-static.btn, .form-control-static.form-control-lg, .input-group-lg > .form-control-static.form-control, + .input-group-lg > .form-control-static.input-group-addon, + .input-group-lg > .input-group-btn > .form-control-static.btn { + padding-right: 0; + padding-left: 0; } + +.form-control-sm, .input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; } + +select.form-control-sm:not([size]):not([multiple]), .input-group-sm > select.form-control:not([size]):not([multiple]), +.input-group-sm > select.input-group-addon:not([size]):not([multiple]), +.input-group-sm > .input-group-btn > select.btn:not([size]):not([multiple]) { + height: 1.8125rem; } + +.form-control-lg, .input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; } + +select.form-control-lg:not([size]):not([multiple]), .input-group-lg > select.form-control:not([size]):not([multiple]), +.input-group-lg > select.input-group-addon:not([size]):not([multiple]), +.input-group-lg > .input-group-btn > select.btn:not([size]):not([multiple]) { + height: 3.16666667rem; } + +.form-group { + margin-bottom: 1rem; } + +.form-text { + display: block; + margin-top: 0.25rem; } + +.form-check { + position: relative; + display: block; + margin-bottom: 0.5rem; } + .form-check.disabled .form-check-label { + color: #636c72; + cursor: not-allowed; } + +.form-check-label { + padding-left: 1.25rem; + margin-bottom: 0; + cursor: pointer; } + +.form-check-input { + position: absolute; + margin-top: 0.25rem; + margin-left: -1.25rem; } + .form-check-input:only-child { + position: static; } + +.form-check-inline { + display: inline-block; } + .form-check-inline .form-check-label { + vertical-align: middle; } + .form-check-inline + .form-check-inline { + margin-left: 0.75rem; } + +.form-control-feedback { + margin-top: 0.25rem; } + +.form-control-success, +.form-control-warning, +.form-control-danger { + padding-right: 2.25rem; + background-repeat: no-repeat; + background-position: center right 0.5625rem; + background-size: 1.125rem 1.125rem; } + +.has-success .form-control-feedback, +.has-success .form-control-label, +.has-success .col-form-label, +.has-success .form-check-label, +.has-success .custom-control { + color: #5cb85c; } + +.has-success .form-control { + border-color: #5cb85c; } + +.has-success .input-group-addon { + color: #5cb85c; + border-color: #5cb85c; + background-color: #eaf6ea; } + +.has-success .form-control-success { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E"); } + +.has-warning .form-control-feedback, +.has-warning .form-control-label, +.has-warning .col-form-label, +.has-warning .form-check-label, +.has-warning .custom-control { + color: #f0ad4e; } + +.has-warning .form-control { + border-color: #f0ad4e; } + +.has-warning .input-group-addon { + color: #f0ad4e; + border-color: #f0ad4e; + background-color: white; } + +.has-warning .form-control-warning { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E"); } + +.has-danger .form-control-feedback, +.has-danger .form-control-label, +.has-danger .col-form-label, +.has-danger .form-check-label, +.has-danger .custom-control { + color: #d9534f; } + +.has-danger .form-control { + border-color: #d9534f; } + +.has-danger .input-group-addon { + color: #d9534f; + border-color: #d9534f; + background-color: #fdf7f7; } + +.has-danger .form-control-danger { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E"); } + +.form-inline { + display: flex; + flex-flow: row wrap; + align-items: center; } + .form-inline .form-check { + width: 100%; } + @media (min-width: 576px) { + .form-inline label { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0; } + .form-inline .form-group { + display: flex; + flex: 0 0 auto; + flex-flow: row wrap; + align-items: center; + margin-bottom: 0; } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; } + .form-inline .form-control-static { + display: inline-block; } + .form-inline .input-group { + width: auto; } + .form-inline .form-control-label { + margin-bottom: 0; + vertical-align: middle; } + .form-inline .form-check { + display: flex; + align-items: center; + justify-content: center; + width: auto; + margin-top: 0; + margin-bottom: 0; } + .form-inline .form-check-label { + padding-left: 0; } + .form-inline .form-check-input { + position: relative; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; } + .form-inline .custom-control { + display: flex; + align-items: center; + justify-content: center; + padding-left: 0; } + .form-inline .custom-control-indicator { + position: static; + display: inline-block; + margin-right: 0.25rem; + vertical-align: text-bottom; } + .form-inline .has-feedback .form-control-feedback { + top: 0; } } + +.btn { + display: inline-block; + font-weight: normal; + line-height: 1.25; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: 1px solid transparent; + padding: 0.5rem 1rem; + font-size: 1rem; + border-radius: 0.25rem; + transition: all 0.2s ease-in-out; } + .btn:focus, .btn:hover { + text-decoration: none; } + .btn:focus, .btn.focus { + outline: 0; + box-shadow: 0 0 0 2px rgba(2, 117, 216, 0.25); } + .btn.disabled, .btn:disabled { + cursor: not-allowed; + opacity: .65; } + .btn:active, .btn.active { + background-image: none; } + +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; } + +.btn-primary { + color: #fff; + background-color: #0275d8; + border-color: #0275d8; } + .btn-primary:hover { + color: #fff; + background-color: #025aa5; + border-color: #01549b; } + .btn-primary:focus, .btn-primary.focus { + box-shadow: 0 0 0 2px rgba(2, 117, 216, 0.5); } + .btn-primary.disabled, .btn-primary:disabled { + background-color: #0275d8; + border-color: #0275d8; } + .btn-primary:active, .btn-primary.active, + .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #025aa5; + background-image: none; + border-color: #01549b; } + +.btn-secondary { + color: #292b2c; + background-color: #fff; + border-color: #ccc; } + .btn-secondary:hover { + color: #292b2c; + background-color: #e6e5e5; + border-color: #adadad; } + .btn-secondary:focus, .btn-secondary.focus { + box-shadow: 0 0 0 2px rgba(204, 204, 204, 0.5); } + .btn-secondary.disabled, .btn-secondary:disabled { + background-color: #fff; + border-color: #ccc; } + .btn-secondary:active, .btn-secondary.active, + .show > .btn-secondary.dropdown-toggle { + color: #292b2c; + background-color: #e6e5e5; + background-image: none; + border-color: #adadad; } + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; } + .btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #2aabd2; } + .btn-info:focus, .btn-info.focus { + box-shadow: 0 0 0 2px rgba(91, 192, 222, 0.5); } + .btn-info.disabled, .btn-info:disabled { + background-color: #5bc0de; + border-color: #5bc0de; } + .btn-info:active, .btn-info.active, + .show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #31b0d5; + background-image: none; + border-color: #2aabd2; } + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #5cb85c; } + .btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #419641; } + .btn-success:focus, .btn-success.focus { + box-shadow: 0 0 0 2px rgba(92, 184, 92, 0.5); } + .btn-success.disabled, .btn-success:disabled { + background-color: #5cb85c; + border-color: #5cb85c; } + .btn-success:active, .btn-success.active, + .show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #449d44; + background-image: none; + border-color: #419641; } + +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; } + .btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #eb9316; } + .btn-warning:focus, .btn-warning.focus { + box-shadow: 0 0 0 2px rgba(240, 173, 78, 0.5); } + .btn-warning.disabled, .btn-warning:disabled { + background-color: #f0ad4e; + border-color: #f0ad4e; } + .btn-warning:active, .btn-warning.active, + .show > .btn-warning.dropdown-toggle { + color: #fff; + background-color: #ec971f; + background-image: none; + border-color: #eb9316; } + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; } + .btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #c12e2a; } + .btn-danger:focus, .btn-danger.focus { + box-shadow: 0 0 0 2px rgba(217, 83, 79, 0.5); } + .btn-danger.disabled, .btn-danger:disabled { + background-color: #d9534f; + border-color: #d9534f; } + .btn-danger:active, .btn-danger.active, + .show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #c9302c; + background-image: none; + border-color: #c12e2a; } + +.btn-outline-primary { + color: #0275d8; + background-image: none; + background-color: transparent; + border-color: #0275d8; } + .btn-outline-primary:hover { + color: #fff; + background-color: #0275d8; + border-color: #0275d8; } + .btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 2px rgba(2, 117, 216, 0.5); } + .btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #0275d8; + background-color: transparent; } + .btn-outline-primary:active, .btn-outline-primary.active, + .show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #0275d8; + border-color: #0275d8; } + +.btn-outline-secondary { + color: #ccc; + background-image: none; + background-color: transparent; + border-color: #ccc; } + .btn-outline-secondary:hover { + color: #fff; + background-color: #ccc; + border-color: #ccc; } + .btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 2px rgba(204, 204, 204, 0.5); } + .btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #ccc; + background-color: transparent; } + .btn-outline-secondary:active, .btn-outline-secondary.active, + .show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #ccc; + border-color: #ccc; } + +.btn-outline-info { + color: #5bc0de; + background-image: none; + background-color: transparent; + border-color: #5bc0de; } + .btn-outline-info:hover { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; } + .btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 2px rgba(91, 192, 222, 0.5); } + .btn-outline-info.disabled, .btn-outline-info:disabled { + color: #5bc0de; + background-color: transparent; } + .btn-outline-info:active, .btn-outline-info.active, + .show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; } + +.btn-outline-success { + color: #5cb85c; + background-image: none; + background-color: transparent; + border-color: #5cb85c; } + .btn-outline-success:hover { + color: #fff; + background-color: #5cb85c; + border-color: #5cb85c; } + .btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 2px rgba(92, 184, 92, 0.5); } + .btn-outline-success.disabled, .btn-outline-success:disabled { + color: #5cb85c; + background-color: transparent; } + .btn-outline-success:active, .btn-outline-success.active, + .show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #5cb85c; + border-color: #5cb85c; } + +.btn-outline-warning { + color: #f0ad4e; + background-image: none; + background-color: transparent; + border-color: #f0ad4e; } + .btn-outline-warning:hover { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; } + .btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 2px rgba(240, 173, 78, 0.5); } + .btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #f0ad4e; + background-color: transparent; } + .btn-outline-warning:active, .btn-outline-warning.active, + .show > .btn-outline-warning.dropdown-toggle { + color: #fff; + background-color: #f0ad4e; + border-color: #f0ad4e; } + +.btn-outline-danger { + color: #d9534f; + background-image: none; + background-color: transparent; + border-color: #d9534f; } + .btn-outline-danger:hover { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; } + .btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 2px rgba(217, 83, 79, 0.5); } + .btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #d9534f; + background-color: transparent; } + .btn-outline-danger:active, .btn-outline-danger.active, + .show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; } + +.btn-link { + font-weight: normal; + color: #0275d8; + border-radius: 0; } + .btn-link, .btn-link:active, .btn-link.active, .btn-link:disabled { + background-color: transparent; } + .btn-link, .btn-link:focus, .btn-link:active { + border-color: transparent; } + .btn-link:hover { + border-color: transparent; } + .btn-link:focus, .btn-link:hover { + color: #014c8c; + text-decoration: underline; + background-color: transparent; } + .btn-link:disabled { + color: #636c72; } + .btn-link:disabled:focus, .btn-link:disabled:hover { + text-decoration: none; } + +.btn-lg, .btn-group-lg > .btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; } + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; } + +.btn-block { + display: block; + width: 100%; } + +.btn-block + .btn-block { + margin-top: 0.5rem; } + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; } + +.fade { + opacity: 0; + transition: opacity 0.15s linear; } + .fade.show { + opacity: 1; } + +.collapse { + display: none; } + .collapse.show { + display: block; } + +tr.collapse.show { + display: table-row; } + +tbody.collapse.show { + display: table-row-group; } + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; } + +.dropup, +.dropdown { + position: relative; } + +.dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.3em; + vertical-align: middle; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-left: 0.3em solid transparent; } + +.dropdown-toggle:focus { + outline: 0; } + +.dropup .dropdown-toggle::after { + border-top: 0; + border-bottom: 0.3em solid; } + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #292b2c; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; } + +.dropdown-divider { + height: 1px; + margin: 0.5rem 0; + overflow: hidden; + background-color: #eceeef; } + +.dropdown-item { + display: block; + width: 100%; + padding: 3px 1.5rem; + clear: both; + font-weight: normal; + color: #292b2c; + text-align: inherit; + white-space: nowrap; + background: none; + border: 0; } + .dropdown-item:focus, .dropdown-item:hover { + color: #1d1e1f; + text-decoration: none; + background-color: #f7f7f9; } + .dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #0275d8; } + .dropdown-item.disabled, .dropdown-item:disabled { + color: #636c72; + cursor: not-allowed; + background-color: transparent; } + +.show > .dropdown-menu { + display: block; } + +.show > a { + outline: 0; } + +.dropdown-menu-right { + right: 0; + left: auto; } + +.dropdown-menu-left { + right: auto; + left: 0; } + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #636c72; + white-space: nowrap; } + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; } + +.dropup .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 0.125rem; } + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; } + .btn-group > .btn, + .btn-group-vertical > .btn { + position: relative; + flex: 0 1 auto; } + .btn-group > .btn:hover, + .btn-group-vertical > .btn:hover { + z-index: 2; } + .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, + .btn-group-vertical > .btn:focus, + .btn-group-vertical > .btn:active, + .btn-group-vertical > .btn.active { + z-index: 2; } + .btn-group .btn + .btn, + .btn-group .btn + .btn-group, + .btn-group .btn-group + .btn, + .btn-group .btn-group + .btn-group, + .btn-group-vertical .btn + .btn, + .btn-group-vertical .btn + .btn-group, + .btn-group-vertical .btn-group + .btn, + .btn-group-vertical .btn-group + .btn-group { + margin-left: -1px; } + +.btn-toolbar { + display: flex; + justify-content: flex-start; } + .btn-toolbar .input-group { + width: auto; } + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; } + +.btn-group > .btn:first-child { + margin-left: 0; } + .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +.btn-group > .btn-group { + float: left; } + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } + +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; } + +.btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; } + .btn + .dropdown-toggle-split::after { + margin-left: 0; } + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; } + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 1.125rem; + padding-left: 1.125rem; } + +.btn-group-vertical { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; } + .btn-group-vertical .btn, + .btn-group-vertical .btn-group { + width: 100%; } + .btn-group-vertical > .btn + .btn, + .btn-group-vertical > .btn + .btn-group, + .btn-group-vertical > .btn-group + .btn, + .btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; } + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; } + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-top-left-radius: 0; } + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } + +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; } + +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; } + +.input-group { + position: relative; + display: flex; + width: 100%; } + .input-group .form-control { + position: relative; + z-index: 2; + flex: 1 1 auto; + width: 1%; + margin-bottom: 0; } + .input-group .form-control:focus, .input-group .form-control:active, .input-group .form-control:hover { + z-index: 3; } + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: flex; + flex-direction: column; + justify-content: center; } + .input-group-addon:not(:first-child):not(:last-child), + .input-group-btn:not(:first-child):not(:last-child), + .input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; } + +.input-group-addon, +.input-group-btn { + white-space: nowrap; + vertical-align: middle; } + +.input-group-addon { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: normal; + line-height: 1.25; + color: #464a4c; + text-align: center; + background-color: #eceeef; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; } + .input-group-addon.form-control-sm, + .input-group-sm > .input-group-addon, + .input-group-sm > .input-group-btn > .input-group-addon.btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; } + .input-group-addon.form-control-lg, + .input-group-lg > .input-group-addon, + .input-group-lg > .input-group-btn > .input-group-addon.btn { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + border-radius: 0.3rem; } + .input-group-addon input[type="radio"], + .input-group-addon input[type="checkbox"] { + margin-top: 0; } + +.input-group .form-control:not(:last-child), +.input-group-addon:not(:last-child), +.input-group-btn:not(:last-child) > .btn, +.input-group-btn:not(:last-child) > .btn-group > .btn, +.input-group-btn:not(:last-child) > .dropdown-toggle, +.input-group-btn:not(:first-child) > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:not(:first-child) > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +.input-group-addon:not(:last-child) { + border-right: 0; } + +.input-group .form-control:not(:first-child), +.input-group-addon:not(:first-child), +.input-group-btn:not(:first-child) > .btn, +.input-group-btn:not(:first-child) > .btn-group > .btn, +.input-group-btn:not(:first-child) > .dropdown-toggle, +.input-group-btn:not(:last-child) > .btn:not(:first-child), +.input-group-btn:not(:last-child) > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +.form-control + .input-group-addon:not(:first-child) { + border-left: 0; } + +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; } + .input-group-btn > .btn { + position: relative; + flex: 1; } + .input-group-btn > .btn + .btn { + margin-left: -1px; } + .input-group-btn > .btn:focus, .input-group-btn > .btn:active, .input-group-btn > .btn:hover { + z-index: 3; } + .input-group-btn:not(:last-child) > .btn, + .input-group-btn:not(:last-child) > .btn-group { + margin-right: -1px; } + .input-group-btn:not(:first-child) > .btn, + .input-group-btn:not(:first-child) > .btn-group { + z-index: 2; + margin-left: -1px; } + .input-group-btn:not(:first-child) > .btn:focus, .input-group-btn:not(:first-child) > .btn:active, .input-group-btn:not(:first-child) > .btn:hover, + .input-group-btn:not(:first-child) > .btn-group:focus, + .input-group-btn:not(:first-child) > .btn-group:active, + .input-group-btn:not(:first-child) > .btn-group:hover { + z-index: 3; } + +.custom-control { + position: relative; + display: inline-flex; + min-height: 1.5rem; + padding-left: 1.5rem; + margin-right: 1rem; + cursor: pointer; } + +.custom-control-input { + position: absolute; + z-index: -1; + opacity: 0; } + .custom-control-input:checked ~ .custom-control-indicator { + color: #fff; + background-color: #0275d8; } + .custom-control-input:focus ~ .custom-control-indicator { + box-shadow: 0 0 0 1px #fff, 0 0 0 3px #0275d8; } + .custom-control-input:active ~ .custom-control-indicator { + color: #fff; + background-color: #8fcafe; } + .custom-control-input:disabled ~ .custom-control-indicator { + cursor: not-allowed; + background-color: #eceeef; } + .custom-control-input:disabled ~ .custom-control-description { + color: #636c72; + cursor: not-allowed; } + +.custom-control-indicator { + position: absolute; + top: 0.25rem; + left: 0; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + user-select: none; + background-color: #ddd; + background-repeat: no-repeat; + background-position: center center; + background-size: 50% 50%; } + +.custom-checkbox .custom-control-indicator { + border-radius: 0.25rem; } + +.custom-checkbox .custom-control-input:checked ~ .custom-control-indicator { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); } + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-indicator { + background-color: #0275d8; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E"); } + +.custom-radio .custom-control-indicator { + border-radius: 50%; } + +.custom-radio .custom-control-input:checked ~ .custom-control-indicator { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); } + +.custom-controls-stacked { + display: flex; + flex-direction: column; } + .custom-controls-stacked .custom-control { + margin-bottom: 0.25rem; } + .custom-controls-stacked .custom-control + .custom-control { + margin-left: 0; } + +.custom-select { + display: inline-block; + max-width: 100%; + height: calc(2.25rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + line-height: 1.25; + color: #464a4c; + vertical-align: middle; + background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right 0.75rem center; + background-size: 8px 10px; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; + -moz-appearance: none; + -webkit-appearance: none; } + .custom-select:focus { + border-color: #5cb3fd; + outline: none; } + .custom-select:focus::-ms-value { + color: #464a4c; + background-color: #fff; } + .custom-select:disabled { + color: #636c72; + cursor: not-allowed; + background-color: #eceeef; } + .custom-select::-ms-expand { + opacity: 0; } + +.custom-select-sm { + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 75%; } + +.custom-file { + position: relative; + display: inline-block; + max-width: 100%; + height: 2.5rem; + margin-bottom: 0; + cursor: pointer; } + +.custom-file-input { + min-width: 14rem; + max-width: 100%; + height: 2.5rem; + margin: 0; + filter: alpha(opacity=0); + opacity: 0; } + +.custom-file-control { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 5; + height: 2.5rem; + padding: 0.5rem 1rem; + line-height: 1.5; + color: #464a4c; + pointer-events: none; + user-select: none; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; } + .custom-file-control:lang(en)::after { + content: "Choose file..."; } + .custom-file-control::before { + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + z-index: 6; + display: block; + height: 2.5rem; + padding: 0.5rem 1rem; + line-height: 1.5; + color: #464a4c; + background-color: #eceeef; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0 0.25rem 0.25rem 0; } + .custom-file-control:lang(en)::before { + content: "Browse"; } + +.nav { + display: flex; + padding-left: 0; + margin-bottom: 0; + list-style: none; } + +.nav-link { + display: block; + padding: 0.5em 1em; } + .nav-link:focus, .nav-link:hover { + text-decoration: none; } + .nav-link.disabled { + color: #636c72; + cursor: not-allowed; } + +.nav-tabs { + border-bottom: 1px solid #ddd; } + .nav-tabs .nav-item { + margin-bottom: -1px; } + .nav-tabs .nav-link { + border: 1px solid transparent; + border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; } + .nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover { + border-color: #eceeef #eceeef #ddd; } + .nav-tabs .nav-link.disabled { + color: #636c72; + background-color: transparent; + border-color: transparent; } + .nav-tabs .nav-link.active, + .nav-tabs .nav-item.show .nav-link { + color: #464a4c; + background-color: #fff; + border-color: #ddd #ddd #fff; } + .nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +.nav-pills .nav-link { + border-radius: 0.25rem; } + +.nav-pills .nav-link.active, +.nav-pills .nav-item.show .nav-link { + color: #fff; + cursor: default; + background-color: #0275d8; } + +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; } + +.nav-justified .nav-item { + flex: 1 1 100%; + text-align: center; } + +.tab-content > .tab-pane { + display: none; } + +.tab-content > .active { + display: block; } + +.navbar { + position: relative; + display: flex; + flex-direction: column; + padding: 0.5rem 1rem; } + +.navbar-brand { + display: inline-block; + padding-top: .25rem; + padding-bottom: .25rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; } + .navbar-brand:focus, .navbar-brand:hover { + text-decoration: none; } + +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; } + .navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; } + +.navbar-text { + display: inline-block; + padding-top: .425rem; + padding-bottom: .425rem; } + +.navbar-toggler { + align-self: flex-start; + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; } + .navbar-toggler:focus, .navbar-toggler:hover { + text-decoration: none; } + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; } + +.navbar-toggler-left { + position: absolute; + left: 1rem; } + +.navbar-toggler-right { + position: absolute; + right: 1rem; } + +@media (max-width: 575px) { + .navbar-toggleable .navbar-nav .dropdown-menu { + position: static; + float: none; } + .navbar-toggleable > .container { + padding-right: 0; + padding-left: 0; } } + +@media (min-width: 576px) { + .navbar-toggleable { + flex-direction: row; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable .navbar-nav { + flex-direction: row; } + .navbar-toggleable .navbar-nav .nav-link { + padding-right: .5rem; + padding-left: .5rem; } + .navbar-toggleable > .container { + display: flex; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable .navbar-collapse { + display: flex !important; + width: 100%; } + .navbar-toggleable .navbar-toggler { + display: none; } } + +@media (max-width: 767px) { + .navbar-toggleable-sm .navbar-nav .dropdown-menu { + position: static; + float: none; } + .navbar-toggleable-sm > .container { + padding-right: 0; + padding-left: 0; } } + +@media (min-width: 768px) { + .navbar-toggleable-sm { + flex-direction: row; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-sm .navbar-nav { + flex-direction: row; } + .navbar-toggleable-sm .navbar-nav .nav-link { + padding-right: .5rem; + padding-left: .5rem; } + .navbar-toggleable-sm > .container { + display: flex; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-sm .navbar-collapse { + display: flex !important; + width: 100%; } + .navbar-toggleable-sm .navbar-toggler { + display: none; } } + +@media (max-width: 991px) { + .navbar-toggleable-md .navbar-nav .dropdown-menu { + position: static; + float: none; } + .navbar-toggleable-md > .container { + padding-right: 0; + padding-left: 0; } } + +@media (min-width: 992px) { + .navbar-toggleable-md { + flex-direction: row; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-md .navbar-nav { + flex-direction: row; } + .navbar-toggleable-md .navbar-nav .nav-link { + padding-right: .5rem; + padding-left: .5rem; } + .navbar-toggleable-md > .container { + display: flex; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-md .navbar-collapse { + display: flex !important; + width: 100%; } + .navbar-toggleable-md .navbar-toggler { + display: none; } } + +@media (max-width: 1199px) { + .navbar-toggleable-lg .navbar-nav .dropdown-menu { + position: static; + float: none; } + .navbar-toggleable-lg > .container { + padding-right: 0; + padding-left: 0; } } + +@media (min-width: 1200px) { + .navbar-toggleable-lg { + flex-direction: row; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-lg .navbar-nav { + flex-direction: row; } + .navbar-toggleable-lg .navbar-nav .nav-link { + padding-right: .5rem; + padding-left: .5rem; } + .navbar-toggleable-lg > .container { + display: flex; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-lg .navbar-collapse { + display: flex !important; + width: 100%; } + .navbar-toggleable-lg .navbar-toggler { + display: none; } } + +.navbar-toggleable-xl { + flex-direction: row; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-xl .navbar-nav .dropdown-menu { + position: static; + float: none; } + .navbar-toggleable-xl > .container { + padding-right: 0; + padding-left: 0; } + .navbar-toggleable-xl .navbar-nav { + flex-direction: row; } + .navbar-toggleable-xl .navbar-nav .nav-link { + padding-right: .5rem; + padding-left: .5rem; } + .navbar-toggleable-xl > .container { + display: flex; + flex-wrap: nowrap; + align-items: center; } + .navbar-toggleable-xl .navbar-collapse { + display: flex !important; + width: 100%; } + .navbar-toggleable-xl .navbar-toggler { + display: none; } + +.navbar-light .navbar-brand, +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.9); } + .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover, + .navbar-light .navbar-toggler:focus, + .navbar-light .navbar-toggler:hover { + color: rgba(0, 0, 0, 0.9); } + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); } + .navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover { + color: rgba(0, 0, 0, 0.7); } + .navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); } + +.navbar-light .navbar-nav .open > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.open, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); } + +.navbar-light .navbar-toggler { + border-color: rgba(0, 0, 0, 0.1); } + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E"); } + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); } + +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-toggler { + color: white; } + .navbar-inverse .navbar-brand:focus, .navbar-inverse .navbar-brand:hover, + .navbar-inverse .navbar-toggler:focus, + .navbar-inverse .navbar-toggler:hover { + color: white; } + +.navbar-inverse .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); } + .navbar-inverse .navbar-nav .nav-link:focus, .navbar-inverse .navbar-nav .nav-link:hover { + color: rgba(255, 255, 255, 0.75); } + .navbar-inverse .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); } + +.navbar-inverse .navbar-nav .open > .nav-link, +.navbar-inverse .navbar-nav .active > .nav-link, +.navbar-inverse .navbar-nav .nav-link.open, +.navbar-inverse .navbar-nav .nav-link.active { + color: white; } + +.navbar-inverse .navbar-toggler { + border-color: rgba(255, 255, 255, 0.1); } + +.navbar-inverse .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E"); } + +.navbar-inverse .navbar-text { + color: rgba(255, 255, 255, 0.5); } + +.card { + position: relative; + display: flex; + flex-direction: column; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; } + +.card-block { + flex: 1 1 auto; + padding: 1.25rem; } + +.card-title { + margin-bottom: 0.75rem; } + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; } + +.card-text:last-child { + margin-bottom: 0; } + +.card-link:hover { + text-decoration: none; } + +.card-link + .card-link { + margin-left: 1.25rem; } + +.card > .list-group:first-child .list-group-item:first-child { + border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; } + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: #f7f7f9; + border-bottom: 1px solid rgba(0, 0, 0, 0.125); } + .card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; } + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: #f7f7f9; + border-top: 1px solid rgba(0, 0, 0, 0.125); } + .card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); } + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; } + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; } + +.card-primary { + background-color: #0275d8; + border-color: #0275d8; } + .card-primary .card-header, + .card-primary .card-footer { + background-color: transparent; } + +.card-success { + background-color: #5cb85c; + border-color: #5cb85c; } + .card-success .card-header, + .card-success .card-footer { + background-color: transparent; } + +.card-info { + background-color: #5bc0de; + border-color: #5bc0de; } + .card-info .card-header, + .card-info .card-footer { + background-color: transparent; } + +.card-warning { + background-color: #f0ad4e; + border-color: #f0ad4e; } + .card-warning .card-header, + .card-warning .card-footer { + background-color: transparent; } + +.card-danger { + background-color: #d9534f; + border-color: #d9534f; } + .card-danger .card-header, + .card-danger .card-footer { + background-color: transparent; } + +.card-outline-primary { + background-color: transparent; + border-color: #0275d8; } + +.card-outline-secondary { + background-color: transparent; + border-color: #ccc; } + +.card-outline-info { + background-color: transparent; + border-color: #5bc0de; } + +.card-outline-success { + background-color: transparent; + border-color: #5cb85c; } + +.card-outline-warning { + background-color: transparent; + border-color: #f0ad4e; } + +.card-outline-danger { + background-color: transparent; + border-color: #d9534f; } + +.card-inverse { + color: rgba(255, 255, 255, 0.65); } + .card-inverse .card-header, + .card-inverse .card-footer { + background-color: transparent; + border-color: rgba(255, 255, 255, 0.2); } + .card-inverse .card-header, + .card-inverse .card-footer, + .card-inverse .card-title, + .card-inverse .card-blockquote { + color: #fff; } + .card-inverse .card-link, + .card-inverse .card-text, + .card-inverse .card-subtitle, + .card-inverse .card-blockquote .blockquote-footer { + color: rgba(255, 255, 255, 0.65); } + .card-inverse .card-link:focus, .card-inverse .card-link:hover { + color: #fff; } + +.card-blockquote { + padding: 0; + margin-bottom: 0; + border-left: 0; } + +.card-img { + border-radius: calc(0.25rem - 1px); } + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; } + +.card-img-top { + border-top-right-radius: calc(0.25rem - 1px); + border-top-left-radius: calc(0.25rem - 1px); } + +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); } + +@media (min-width: 576px) { + .card-deck { + display: flex; + flex-flow: row wrap; } + .card-deck .card { + display: flex; + flex: 1 0 0; + flex-direction: column; } + .card-deck .card:not(:first-child) { + margin-left: 15px; } + .card-deck .card:not(:last-child) { + margin-right: 15px; } } + +@media (min-width: 576px) { + .card-group { + display: flex; + flex-flow: row wrap; } + .card-group .card { + flex: 1 0 0; } + .card-group .card + .card { + margin-left: 0; + border-left: 0; } + .card-group .card:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + .card-group .card:first-child .card-img-top { + border-top-right-radius: 0; } + .card-group .card:first-child .card-img-bottom { + border-bottom-right-radius: 0; } + .card-group .card:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .card-group .card:last-child .card-img-top { + border-top-left-radius: 0; } + .card-group .card:last-child .card-img-bottom { + border-bottom-left-radius: 0; } + .card-group .card:not(:first-child):not(:last-child) { + border-radius: 0; } + .card-group .card:not(:first-child):not(:last-child) .card-img-top, + .card-group .card:not(:first-child):not(:last-child) .card-img-bottom { + border-radius: 0; } } + +@media (min-width: 576px) { + .card-columns { + column-count: 3; + column-gap: 1.25rem; } + .card-columns .card { + display: inline-block; + width: 100%; + margin-bottom: 0.75rem; } } + +.breadcrumb { + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #eceeef; + border-radius: 0.25rem; } + .breadcrumb::after { + display: block; + content: ""; + clear: both; } + +.breadcrumb-item { + float: left; } + .breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + padding-left: 0.5rem; + color: #636c72; + content: "/"; } + .breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; } + .breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; } + .breadcrumb-item.active { + color: #636c72; } + +.pagination { + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; } + +.page-item:first-child .page-link { + margin-left: 0; + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; } + +.page-item:last-child .page-link { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; } + +.page-item.active .page-link { + z-index: 2; + color: #fff; + background-color: #0275d8; + border-color: #0275d8; } + +.page-item.disabled .page-link { + color: #636c72; + pointer-events: none; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; } + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #0275d8; + background-color: #fff; + border: 1px solid #ddd; } + .page-link:focus, .page-link:hover { + color: #014c8c; + text-decoration: none; + background-color: #eceeef; + border-color: #ddd; } + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; } + +.pagination-lg .page-item:first-child .page-link { + border-bottom-left-radius: 0.3rem; + border-top-left-radius: 0.3rem; } + +.pagination-lg .page-item:last-child .page-link { + border-bottom-right-radius: 0.3rem; + border-top-right-radius: 0.3rem; } + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; } + +.pagination-sm .page-item:first-child .page-link { + border-bottom-left-radius: 0.2rem; + border-top-left-radius: 0.2rem; } + +.pagination-sm .page-item:last-child .page-link { + border-bottom-right-radius: 0.2rem; + border-top-right-radius: 0.2rem; } + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; } + .badge:empty { + display: none; } + +.btn .badge { + position: relative; + top: -1px; } + +a.badge:focus, a.badge:hover { + color: #fff; + text-decoration: none; + cursor: pointer; } + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; } + +.badge-default { + background-color: #636c72; } + .badge-default[href]:focus, .badge-default[href]:hover { + background-color: #4b5257; } + +.badge-primary { + background-color: #0275d8; } + .badge-primary[href]:focus, .badge-primary[href]:hover { + background-color: #025aa5; } + +.badge-success { + background-color: #5cb85c; } + .badge-success[href]:focus, .badge-success[href]:hover { + background-color: #449d44; } + +.badge-info { + background-color: #5bc0de; } + .badge-info[href]:focus, .badge-info[href]:hover { + background-color: #31b0d5; } + +.badge-warning { + background-color: #f0ad4e; } + .badge-warning[href]:focus, .badge-warning[href]:hover { + background-color: #ec971f; } + +.badge-danger { + background-color: #d9534f; } + .badge-danger[href]:focus, .badge-danger[href]:hover { + background-color: #c9302c; } + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #eceeef; + border-radius: 0.3rem; } + @media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; } } + +.jumbotron-hr { + border-top-color: #d0d5d8; } + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; } + +.alert { + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } + +.alert-heading { + color: inherit; } + +.alert-link { + font-weight: bold; } + +.alert-dismissible .close { + position: relative; + top: -0.75rem; + right: -1.25rem; + padding: 0.75rem 1.25rem; + color: inherit; } + +.alert-success { + background-color: #dff0d8; + border-color: #d0e9c6; + color: #3c763d; } + .alert-success hr { + border-top-color: #c1e2b3; } + .alert-success .alert-link { + color: #2b542c; } + +.alert-info { + background-color: #d9edf7; + border-color: #bcdff1; + color: #31708f; } + .alert-info hr { + border-top-color: #a6d5ec; } + .alert-info .alert-link { + color: #245269; } + +.alert-warning { + background-color: #fcf8e3; + border-color: #faf2cc; + color: #8a6d3b; } + .alert-warning hr { + border-top-color: #f7ecb5; } + .alert-warning .alert-link { + color: #66512c; } + +.alert-danger { + background-color: #f2dede; + border-color: #ebcccc; + color: #a94442; } + .alert-danger hr { + border-top-color: #e4b9b9; } + .alert-danger .alert-link { + color: #843534; } + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; } + to { + background-position: 0 0; } } + +.progress { + display: flex; + overflow: hidden; + font-size: 0.75rem; + line-height: 1rem; + text-align: center; + background-color: #eceeef; + border-radius: 0.25rem; } + +.progress-bar { + height: 1rem; + color: #fff; + background-color: #0275d8; } + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; } + +.progress-bar-animated { + animation: progress-bar-stripes 1s linear infinite; } + +.media { + display: flex; + align-items: flex-start; } + +.media-body { + flex: 1; } + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; } + +.list-group-item-action { + width: 100%; + color: #464a4c; + text-align: inherit; } + .list-group-item-action .list-group-item-heading { + color: #292b2c; } + .list-group-item-action:focus, .list-group-item-action:hover { + color: #464a4c; + text-decoration: none; + background-color: #f7f7f9; } + .list-group-item-action:active { + color: #292b2c; + background-color: #eceeef; } + +.list-group-item { + position: relative; + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); } + .list-group-item:first-child { + border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; } + .list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } + .list-group-item:focus, .list-group-item:hover { + text-decoration: none; } + .list-group-item.disabled, .list-group-item:disabled { + color: #636c72; + cursor: not-allowed; + background-color: #fff; } + .list-group-item.disabled .list-group-item-heading, .list-group-item:disabled .list-group-item-heading { + color: inherit; } + .list-group-item.disabled .list-group-item-text, .list-group-item:disabled .list-group-item-text { + color: #636c72; } + .list-group-item.active { + z-index: 2; + color: #fff; + background-color: #0275d8; + border-color: #0275d8; } + .list-group-item.active .list-group-item-heading, + .list-group-item.active .list-group-item-heading > small, + .list-group-item.active .list-group-item-heading > .small { + color: inherit; } + .list-group-item.active .list-group-item-text { + color: #daeeff; } + +.list-group-flush .list-group-item { + border-right: 0; + border-left: 0; + border-radius: 0; } + +.list-group-flush:first-child .list-group-item:first-child { + border-top: 0; } + +.list-group-flush:last-child .list-group-item:last-child { + border-bottom: 0; } + +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; } + +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; } + a.list-group-item-success .list-group-item-heading, + button.list-group-item-success .list-group-item-heading { + color: inherit; } + a.list-group-item-success:focus, a.list-group-item-success:hover, + button.list-group-item-success:focus, + button.list-group-item-success:hover { + color: #3c763d; + background-color: #d0e9c6; } + a.list-group-item-success.active, + button.list-group-item-success.active { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; } + +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; } + +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; } + a.list-group-item-info .list-group-item-heading, + button.list-group-item-info .list-group-item-heading { + color: inherit; } + a.list-group-item-info:focus, a.list-group-item-info:hover, + button.list-group-item-info:focus, + button.list-group-item-info:hover { + color: #31708f; + background-color: #c4e3f3; } + a.list-group-item-info.active, + button.list-group-item-info.active { + color: #fff; + background-color: #31708f; + border-color: #31708f; } + +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; } + +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; } + a.list-group-item-warning .list-group-item-heading, + button.list-group-item-warning .list-group-item-heading { + color: inherit; } + a.list-group-item-warning:focus, a.list-group-item-warning:hover, + button.list-group-item-warning:focus, + button.list-group-item-warning:hover { + color: #8a6d3b; + background-color: #faf2cc; } + a.list-group-item-warning.active, + button.list-group-item-warning.active { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; } + +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; } + +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; } + a.list-group-item-danger .list-group-item-heading, + button.list-group-item-danger .list-group-item-heading { + color: inherit; } + a.list-group-item-danger:focus, a.list-group-item-danger:hover, + button.list-group-item-danger:focus, + button.list-group-item-danger:hover { + color: #a94442; + background-color: #ebcccc; } + a.list-group-item-danger.active, + button.list-group-item-danger.active { + color: #fff; + background-color: #a94442; + border-color: #a94442; } + +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; } + .embed-responsive::before { + display: block; + content: ""; } + .embed-responsive .embed-responsive-item, + .embed-responsive iframe, + .embed-responsive embed, + .embed-responsive object, + .embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; } + +.embed-responsive-21by9::before { + padding-top: 42.85714286%; } + +.embed-responsive-16by9::before { + padding-top: 56.25%; } + +.embed-responsive-4by3::before { + padding-top: 75%; } + +.embed-responsive-1by1::before { + padding-top: 100%; } + +.close { + float: right; + font-size: 1.5rem; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; } + .close:focus, .close:hover { + color: #000; + text-decoration: none; + cursor: pointer; + opacity: .75; } + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; } + +.modal-open { + overflow: hidden; } + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + outline: 0; } + .modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -25%); } + .modal.show .modal-dialog { + transform: translate(0, 0); } + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; } + +.modal-dialog { + position: relative; + width: auto; + margin: 10px; } + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; } + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; } + .modal-backdrop.fade { + opacity: 0; } + .modal-backdrop.show { + opacity: 0.5; } + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px; + border-bottom: 1px solid #eceeef; } + +.modal-title { + margin-bottom: 0; + line-height: 1.5; } + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: 15px; } + +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 15px; + border-top: 1px solid #eceeef; } + .modal-footer > :not(:first-child) { + margin-left: .25rem; } + .modal-footer > :not(:last-child) { + margin-right: .25rem; } + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; } + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 30px auto; } + .modal-sm { + max-width: 300px; } } + +@media (min-width: 992px) { + .modal-lg { + max-width: 800px; } } + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; } + .tooltip.show { + opacity: 0.9; } + .tooltip.tooltip-top, .tooltip.bs-tether-element-attached-bottom { + padding: 5px 0; + margin-top: -3px; } + .tooltip.tooltip-top .tooltip-inner::before, .tooltip.bs-tether-element-attached-bottom .tooltip-inner::before { + bottom: 0; + left: 50%; + margin-left: -5px; + content: ""; + border-width: 5px 5px 0; + border-top-color: #000; } + .tooltip.tooltip-right, .tooltip.bs-tether-element-attached-left { + padding: 0 5px; + margin-left: 3px; } + .tooltip.tooltip-right .tooltip-inner::before, .tooltip.bs-tether-element-attached-left .tooltip-inner::before { + top: 50%; + left: 0; + margin-top: -5px; + content: ""; + border-width: 5px 5px 5px 0; + border-right-color: #000; } + .tooltip.tooltip-bottom, .tooltip.bs-tether-element-attached-top { + padding: 5px 0; + margin-top: 3px; } + .tooltip.tooltip-bottom .tooltip-inner::before, .tooltip.bs-tether-element-attached-top .tooltip-inner::before { + top: 0; + left: 50%; + margin-left: -5px; + content: ""; + border-width: 0 5px 5px; + border-bottom-color: #000; } + .tooltip.tooltip-left, .tooltip.bs-tether-element-attached-right { + padding: 0 5px; + margin-left: -3px; } + .tooltip.tooltip-left .tooltip-inner::before, .tooltip.bs-tether-element-attached-right .tooltip-inner::before { + top: 50%; + right: 0; + margin-top: -5px; + content: ""; + border-width: 5px 0 5px 5px; + border-left-color: #000; } + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; } + .tooltip-inner::before { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + padding: 1px; + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; } + .popover.popover-top, .popover.bs-tether-element-attached-bottom { + margin-top: -10px; } + .popover.popover-top::before, .popover.popover-top::after, .popover.bs-tether-element-attached-bottom::before, .popover.bs-tether-element-attached-bottom::after { + left: 50%; + border-bottom-width: 0; } + .popover.popover-top::before, .popover.bs-tether-element-attached-bottom::before { + bottom: -11px; + margin-left: -11px; + border-top-color: rgba(0, 0, 0, 0.25); } + .popover.popover-top::after, .popover.bs-tether-element-attached-bottom::after { + bottom: -10px; + margin-left: -10px; + border-top-color: #fff; } + .popover.popover-right, .popover.bs-tether-element-attached-left { + margin-left: 10px; } + .popover.popover-right::before, .popover.popover-right::after, .popover.bs-tether-element-attached-left::before, .popover.bs-tether-element-attached-left::after { + top: 50%; + border-left-width: 0; } + .popover.popover-right::before, .popover.bs-tether-element-attached-left::before { + left: -11px; + margin-top: -11px; + border-right-color: rgba(0, 0, 0, 0.25); } + .popover.popover-right::after, .popover.bs-tether-element-attached-left::after { + left: -10px; + margin-top: -10px; + border-right-color: #fff; } + .popover.popover-bottom, .popover.bs-tether-element-attached-top { + margin-top: 10px; } + .popover.popover-bottom::before, .popover.popover-bottom::after, .popover.bs-tether-element-attached-top::before, .popover.bs-tether-element-attached-top::after { + left: 50%; + border-top-width: 0; } + .popover.popover-bottom::before, .popover.bs-tether-element-attached-top::before { + top: -11px; + margin-left: -11px; + border-bottom-color: rgba(0, 0, 0, 0.25); } + .popover.popover-bottom::after, .popover.bs-tether-element-attached-top::after { + top: -10px; + margin-left: -10px; + border-bottom-color: #f7f7f7; } + .popover.popover-bottom .popover-title::before, .popover.bs-tether-element-attached-top .popover-title::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 20px; + margin-left: -10px; + content: ""; + border-bottom: 1px solid #f7f7f7; } + .popover.popover-left, .popover.bs-tether-element-attached-right { + margin-left: -10px; } + .popover.popover-left::before, .popover.popover-left::after, .popover.bs-tether-element-attached-right::before, .popover.bs-tether-element-attached-right::after { + top: 50%; + border-right-width: 0; } + .popover.popover-left::before, .popover.bs-tether-element-attached-right::before { + right: -11px; + margin-top: -11px; + border-left-color: rgba(0, 0, 0, 0.25); } + .popover.popover-left::after, .popover.bs-tether-element-attached-right::after { + right: -10px; + margin-top: -10px; + border-left-color: #fff; } + +.popover-title { + padding: 8px 14px; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-right-radius: calc(0.3rem - 1px); + border-top-left-radius: calc(0.3rem - 1px); } + .popover-title:empty { + display: none; } + +.popover-content { + padding: 9px 14px; } + +.popover::before, +.popover::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + +.popover::before { + content: ""; + border-width: 11px; } + +.popover::after { + content: ""; + border-width: 10px; } + +.carousel { + position: relative; } + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; } + +.carousel-item { + position: relative; + display: none; + width: 100%; } + @media (-webkit-transform-3d) { + .carousel-item { + transition: transform 0.6s ease-in-out; + backface-visibility: hidden; + perspective: 1000px; } } + @supports (transform: translate3d(0, 0, 0)) { + .carousel-item { + transition: transform 0.6s ease-in-out; + backface-visibility: hidden; + perspective: 1000px; } } + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: flex; } + +.carousel-item-next, +.carousel-item-prev { + position: absolute; + top: 0; } + +@media (-webkit-transform-3d) { + .carousel-item-next.carousel-item-left, + .carousel-item-prev.carousel-item-right { + transform: translate3d(0, 0, 0); } + .carousel-item-next, + .active.carousel-item-right { + transform: translate3d(100%, 0, 0); } + .carousel-item-prev, + .active.carousel-item-left { + transform: translate3d(-100%, 0, 0); } } + +@supports (transform: translate3d(0, 0, 0)) { + .carousel-item-next.carousel-item-left, + .carousel-item-prev.carousel-item-right { + transform: translate3d(0, 0, 0); } + .carousel-item-next, + .active.carousel-item-right { + transform: translate3d(100%, 0, 0); } + .carousel-item-prev, + .active.carousel-item-left { + transform: translate3d(-100%, 0, 0); } } + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + color: #fff; + text-align: center; + opacity: 0.5; } + .carousel-control-prev:focus, .carousel-control-prev:hover, + .carousel-control-next:focus, + .carousel-control-next:hover { + color: #fff; + text-decoration: none; + outline: 0; + opacity: .9; } + +.carousel-control-prev { + left: 0; } + +.carousel-control-next { + right: 0; } + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: transparent no-repeat center center; + background-size: 100% 100%; } + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"); } + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"); } + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 10px; + left: 0; + z-index: 15; + display: flex; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; } + .carousel-indicators li { + position: relative; + flex: 1 0 auto; + max-width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: rgba(255, 255, 255, 0.5); } + .carousel-indicators li::before { + position: absolute; + top: -10px; + left: 0; + display: inline-block; + width: 100%; + height: 10px; + content: ""; } + .carousel-indicators li::after { + position: absolute; + bottom: -10px; + left: 0; + display: inline-block; + width: 100%; + height: 10px; + content: ""; } + .carousel-indicators .active { + background-color: #fff; } + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; } + +.align-baseline { + vertical-align: baseline !important; } + +.align-top { + vertical-align: top !important; } + +.align-middle { + vertical-align: middle !important; } + +.align-bottom { + vertical-align: bottom !important; } + +.align-text-bottom { + vertical-align: text-bottom !important; } + +.align-text-top { + vertical-align: text-top !important; } + +.bg-faded { + background-color: #f7f7f7; } + +.bg-primary { + background-color: #0275d8 !important; } + +a.bg-primary:focus, a.bg-primary:hover { + background-color: #025aa5 !important; } + +.bg-success { + background-color: #5cb85c !important; } + +a.bg-success:focus, a.bg-success:hover { + background-color: #449d44 !important; } + +.bg-info { + background-color: #5bc0de !important; } + +a.bg-info:focus, a.bg-info:hover { + background-color: #31b0d5 !important; } + +.bg-warning { + background-color: #f0ad4e !important; } + +a.bg-warning:focus, a.bg-warning:hover { + background-color: #ec971f !important; } + +.bg-danger { + background-color: #d9534f !important; } + +a.bg-danger:focus, a.bg-danger:hover { + background-color: #c9302c !important; } + +.bg-inverse { + background-color: #292b2c !important; } + +a.bg-inverse:focus, a.bg-inverse:hover { + background-color: #101112 !important; } + +.border-0 { + border: 0 !important; } + +.border-top-0 { + border-top: 0 !important; } + +.border-right-0 { + border-right: 0 !important; } + +.border-bottom-0 { + border-bottom: 0 !important; } + +.border-left-0 { + border-left: 0 !important; } + +.rounded { + border-radius: 0.25rem; } + +.rounded-top { + border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; } + +.rounded-right { + border-bottom-right-radius: 0.25rem; + border-top-right-radius: 0.25rem; } + +.rounded-bottom { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } + +.rounded-left { + border-bottom-left-radius: 0.25rem; + border-top-left-radius: 0.25rem; } + +.rounded-circle { + border-radius: 50%; } + +.rounded-0 { + border-radius: 0; } + +.clearfix::after { + display: block; + content: ""; + clear: both; } + +.d-none { + display: none !important; } + +.d-inline { + display: inline !important; } + +.d-inline-block { + display: inline-block !important; } + +.d-block { + display: block !important; } + +.d-table { + display: table !important; } + +.d-table-cell { + display: table-cell !important; } + +.d-flex { + display: flex !important; } + +.d-inline-flex { + display: inline-flex !important; } + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; } + .d-sm-inline { + display: inline !important; } + .d-sm-inline-block { + display: inline-block !important; } + .d-sm-block { + display: block !important; } + .d-sm-table { + display: table !important; } + .d-sm-table-cell { + display: table-cell !important; } + .d-sm-flex { + display: flex !important; } + .d-sm-inline-flex { + display: inline-flex !important; } } + +@media (min-width: 768px) { + .d-md-none { + display: none !important; } + .d-md-inline { + display: inline !important; } + .d-md-inline-block { + display: inline-block !important; } + .d-md-block { + display: block !important; } + .d-md-table { + display: table !important; } + .d-md-table-cell { + display: table-cell !important; } + .d-md-flex { + display: flex !important; } + .d-md-inline-flex { + display: inline-flex !important; } } + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; } + .d-lg-inline { + display: inline !important; } + .d-lg-inline-block { + display: inline-block !important; } + .d-lg-block { + display: block !important; } + .d-lg-table { + display: table !important; } + .d-lg-table-cell { + display: table-cell !important; } + .d-lg-flex { + display: flex !important; } + .d-lg-inline-flex { + display: inline-flex !important; } } + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; } + .d-xl-inline { + display: inline !important; } + .d-xl-inline-block { + display: inline-block !important; } + .d-xl-block { + display: block !important; } + .d-xl-table { + display: table !important; } + .d-xl-table-cell { + display: table-cell !important; } + .d-xl-flex { + display: flex !important; } + .d-xl-inline-flex { + display: inline-flex !important; } } + +.flex-first { + order: -1; } + +.flex-last { + order: 1; } + +.flex-unordered { + order: 0; } + +.flex-row { + flex-direction: row !important; } + +.flex-column { + flex-direction: column !important; } + +.flex-row-reverse { + flex-direction: row-reverse !important; } + +.flex-column-reverse { + flex-direction: column-reverse !important; } + +.flex-wrap { + flex-wrap: wrap !important; } + +.flex-nowrap { + flex-wrap: nowrap !important; } + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; } + +.justify-content-start { + justify-content: flex-start !important; } + +.justify-content-end { + justify-content: flex-end !important; } + +.justify-content-center { + justify-content: center !important; } + +.justify-content-between { + justify-content: space-between !important; } + +.justify-content-around { + justify-content: space-around !important; } + +.align-items-start { + align-items: flex-start !important; } + +.align-items-end { + align-items: flex-end !important; } + +.align-items-center { + align-items: center !important; } + +.align-items-baseline { + align-items: baseline !important; } + +.align-items-stretch { + align-items: stretch !important; } + +.align-content-start { + align-content: flex-start !important; } + +.align-content-end { + align-content: flex-end !important; } + +.align-content-center { + align-content: center !important; } + +.align-content-between { + align-content: space-between !important; } + +.align-content-around { + align-content: space-around !important; } + +.align-content-stretch { + align-content: stretch !important; } + +.align-self-auto { + align-self: auto !important; } + +.align-self-start { + align-self: flex-start !important; } + +.align-self-end { + align-self: flex-end !important; } + +.align-self-center { + align-self: center !important; } + +.align-self-baseline { + align-self: baseline !important; } + +.align-self-stretch { + align-self: stretch !important; } + +@media (min-width: 576px) { + .flex-sm-first { + order: -1; } + .flex-sm-last { + order: 1; } + .flex-sm-unordered { + order: 0; } + .flex-sm-row { + flex-direction: row !important; } + .flex-sm-column { + flex-direction: column !important; } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; } + .flex-sm-wrap { + flex-wrap: wrap !important; } + .flex-sm-nowrap { + flex-wrap: nowrap !important; } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; } + .justify-content-sm-start { + justify-content: flex-start !important; } + .justify-content-sm-end { + justify-content: flex-end !important; } + .justify-content-sm-center { + justify-content: center !important; } + .justify-content-sm-between { + justify-content: space-between !important; } + .justify-content-sm-around { + justify-content: space-around !important; } + .align-items-sm-start { + align-items: flex-start !important; } + .align-items-sm-end { + align-items: flex-end !important; } + .align-items-sm-center { + align-items: center !important; } + .align-items-sm-baseline { + align-items: baseline !important; } + .align-items-sm-stretch { + align-items: stretch !important; } + .align-content-sm-start { + align-content: flex-start !important; } + .align-content-sm-end { + align-content: flex-end !important; } + .align-content-sm-center { + align-content: center !important; } + .align-content-sm-between { + align-content: space-between !important; } + .align-content-sm-around { + align-content: space-around !important; } + .align-content-sm-stretch { + align-content: stretch !important; } + .align-self-sm-auto { + align-self: auto !important; } + .align-self-sm-start { + align-self: flex-start !important; } + .align-self-sm-end { + align-self: flex-end !important; } + .align-self-sm-center { + align-self: center !important; } + .align-self-sm-baseline { + align-self: baseline !important; } + .align-self-sm-stretch { + align-self: stretch !important; } } + +@media (min-width: 768px) { + .flex-md-first { + order: -1; } + .flex-md-last { + order: 1; } + .flex-md-unordered { + order: 0; } + .flex-md-row { + flex-direction: row !important; } + .flex-md-column { + flex-direction: column !important; } + .flex-md-row-reverse { + flex-direction: row-reverse !important; } + .flex-md-column-reverse { + flex-direction: column-reverse !important; } + .flex-md-wrap { + flex-wrap: wrap !important; } + .flex-md-nowrap { + flex-wrap: nowrap !important; } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; } + .justify-content-md-start { + justify-content: flex-start !important; } + .justify-content-md-end { + justify-content: flex-end !important; } + .justify-content-md-center { + justify-content: center !important; } + .justify-content-md-between { + justify-content: space-between !important; } + .justify-content-md-around { + justify-content: space-around !important; } + .align-items-md-start { + align-items: flex-start !important; } + .align-items-md-end { + align-items: flex-end !important; } + .align-items-md-center { + align-items: center !important; } + .align-items-md-baseline { + align-items: baseline !important; } + .align-items-md-stretch { + align-items: stretch !important; } + .align-content-md-start { + align-content: flex-start !important; } + .align-content-md-end { + align-content: flex-end !important; } + .align-content-md-center { + align-content: center !important; } + .align-content-md-between { + align-content: space-between !important; } + .align-content-md-around { + align-content: space-around !important; } + .align-content-md-stretch { + align-content: stretch !important; } + .align-self-md-auto { + align-self: auto !important; } + .align-self-md-start { + align-self: flex-start !important; } + .align-self-md-end { + align-self: flex-end !important; } + .align-self-md-center { + align-self: center !important; } + .align-self-md-baseline { + align-self: baseline !important; } + .align-self-md-stretch { + align-self: stretch !important; } } + +@media (min-width: 992px) { + .flex-lg-first { + order: -1; } + .flex-lg-last { + order: 1; } + .flex-lg-unordered { + order: 0; } + .flex-lg-row { + flex-direction: row !important; } + .flex-lg-column { + flex-direction: column !important; } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; } + .flex-lg-wrap { + flex-wrap: wrap !important; } + .flex-lg-nowrap { + flex-wrap: nowrap !important; } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; } + .justify-content-lg-start { + justify-content: flex-start !important; } + .justify-content-lg-end { + justify-content: flex-end !important; } + .justify-content-lg-center { + justify-content: center !important; } + .justify-content-lg-between { + justify-content: space-between !important; } + .justify-content-lg-around { + justify-content: space-around !important; } + .align-items-lg-start { + align-items: flex-start !important; } + .align-items-lg-end { + align-items: flex-end !important; } + .align-items-lg-center { + align-items: center !important; } + .align-items-lg-baseline { + align-items: baseline !important; } + .align-items-lg-stretch { + align-items: stretch !important; } + .align-content-lg-start { + align-content: flex-start !important; } + .align-content-lg-end { + align-content: flex-end !important; } + .align-content-lg-center { + align-content: center !important; } + .align-content-lg-between { + align-content: space-between !important; } + .align-content-lg-around { + align-content: space-around !important; } + .align-content-lg-stretch { + align-content: stretch !important; } + .align-self-lg-auto { + align-self: auto !important; } + .align-self-lg-start { + align-self: flex-start !important; } + .align-self-lg-end { + align-self: flex-end !important; } + .align-self-lg-center { + align-self: center !important; } + .align-self-lg-baseline { + align-self: baseline !important; } + .align-self-lg-stretch { + align-self: stretch !important; } } + +@media (min-width: 1200px) { + .flex-xl-first { + order: -1; } + .flex-xl-last { + order: 1; } + .flex-xl-unordered { + order: 0; } + .flex-xl-row { + flex-direction: row !important; } + .flex-xl-column { + flex-direction: column !important; } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; } + .flex-xl-wrap { + flex-wrap: wrap !important; } + .flex-xl-nowrap { + flex-wrap: nowrap !important; } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; } + .justify-content-xl-start { + justify-content: flex-start !important; } + .justify-content-xl-end { + justify-content: flex-end !important; } + .justify-content-xl-center { + justify-content: center !important; } + .justify-content-xl-between { + justify-content: space-between !important; } + .justify-content-xl-around { + justify-content: space-around !important; } + .align-items-xl-start { + align-items: flex-start !important; } + .align-items-xl-end { + align-items: flex-end !important; } + .align-items-xl-center { + align-items: center !important; } + .align-items-xl-baseline { + align-items: baseline !important; } + .align-items-xl-stretch { + align-items: stretch !important; } + .align-content-xl-start { + align-content: flex-start !important; } + .align-content-xl-end { + align-content: flex-end !important; } + .align-content-xl-center { + align-content: center !important; } + .align-content-xl-between { + align-content: space-between !important; } + .align-content-xl-around { + align-content: space-around !important; } + .align-content-xl-stretch { + align-content: stretch !important; } + .align-self-xl-auto { + align-self: auto !important; } + .align-self-xl-start { + align-self: flex-start !important; } + .align-self-xl-end { + align-self: flex-end !important; } + .align-self-xl-center { + align-self: center !important; } + .align-self-xl-baseline { + align-self: baseline !important; } + .align-self-xl-stretch { + align-self: stretch !important; } } + +.float-left { + float: left !important; } + +.float-right { + float: right !important; } + +.float-none { + float: none !important; } + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; } + .float-sm-right { + float: right !important; } + .float-sm-none { + float: none !important; } } + +@media (min-width: 768px) { + .float-md-left { + float: left !important; } + .float-md-right { + float: right !important; } + .float-md-none { + float: none !important; } } + +@media (min-width: 992px) { + .float-lg-left { + float: left !important; } + .float-lg-right { + float: right !important; } + .float-lg-none { + float: none !important; } } + +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; } + .float-xl-right { + float: right !important; } + .float-xl-none { + float: none !important; } } + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; } + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; } + +.sticky-top { + position: sticky; + top: 0; + z-index: 1030; } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; } + +.w-25 { + width: 25% !important; } + +.w-50 { + width: 50% !important; } + +.w-75 { + width: 75% !important; } + +.w-100 { + width: 100% !important; } + +.h-25 { + height: 25% !important; } + +.h-50 { + height: 50% !important; } + +.h-75 { + height: 75% !important; } + +.h-100 { + height: 100% !important; } + +.mw-100 { + max-width: 100% !important; } + +.mh-100 { + max-height: 100% !important; } + +.m-0 { + margin: 0 0 !important; } + +.mt-0 { + margin-top: 0 !important; } + +.mr-0 { + margin-right: 0 !important; } + +.mb-0 { + margin-bottom: 0 !important; } + +.ml-0 { + margin-left: 0 !important; } + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + +.m-1 { + margin: 0.25rem 0.25rem !important; } + +.mt-1 { + margin-top: 0.25rem !important; } + +.mr-1 { + margin-right: 0.25rem !important; } + +.mb-1 { + margin-bottom: 0.25rem !important; } + +.ml-1 { + margin-left: 0.25rem !important; } + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + +.m-2 { + margin: 0.5rem 0.5rem !important; } + +.mt-2 { + margin-top: 0.5rem !important; } + +.mr-2 { + margin-right: 0.5rem !important; } + +.mb-2 { + margin-bottom: 0.5rem !important; } + +.ml-2 { + margin-left: 0.5rem !important; } + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + +.m-3 { + margin: 1rem 1rem !important; } + +.mt-3 { + margin-top: 1rem !important; } + +.mr-3 { + margin-right: 1rem !important; } + +.mb-3 { + margin-bottom: 1rem !important; } + +.ml-3 { + margin-left: 1rem !important; } + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + +.m-4 { + margin: 1.5rem 1.5rem !important; } + +.mt-4 { + margin-top: 1.5rem !important; } + +.mr-4 { + margin-right: 1.5rem !important; } + +.mb-4 { + margin-bottom: 1.5rem !important; } + +.ml-4 { + margin-left: 1.5rem !important; } + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + +.m-5 { + margin: 3rem 3rem !important; } + +.mt-5 { + margin-top: 3rem !important; } + +.mr-5 { + margin-right: 3rem !important; } + +.mb-5 { + margin-bottom: 3rem !important; } + +.ml-5 { + margin-left: 3rem !important; } + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + +.p-0 { + padding: 0 0 !important; } + +.pt-0 { + padding-top: 0 !important; } + +.pr-0 { + padding-right: 0 !important; } + +.pb-0 { + padding-bottom: 0 !important; } + +.pl-0 { + padding-left: 0 !important; } + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + +.p-1 { + padding: 0.25rem 0.25rem !important; } + +.pt-1 { + padding-top: 0.25rem !important; } + +.pr-1 { + padding-right: 0.25rem !important; } + +.pb-1 { + padding-bottom: 0.25rem !important; } + +.pl-1 { + padding-left: 0.25rem !important; } + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + +.p-2 { + padding: 0.5rem 0.5rem !important; } + +.pt-2 { + padding-top: 0.5rem !important; } + +.pr-2 { + padding-right: 0.5rem !important; } + +.pb-2 { + padding-bottom: 0.5rem !important; } + +.pl-2 { + padding-left: 0.5rem !important; } + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + +.p-3 { + padding: 1rem 1rem !important; } + +.pt-3 { + padding-top: 1rem !important; } + +.pr-3 { + padding-right: 1rem !important; } + +.pb-3 { + padding-bottom: 1rem !important; } + +.pl-3 { + padding-left: 1rem !important; } + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + +.p-4 { + padding: 1.5rem 1.5rem !important; } + +.pt-4 { + padding-top: 1.5rem !important; } + +.pr-4 { + padding-right: 1.5rem !important; } + +.pb-4 { + padding-bottom: 1.5rem !important; } + +.pl-4 { + padding-left: 1.5rem !important; } + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + +.p-5 { + padding: 3rem 3rem !important; } + +.pt-5 { + padding-top: 3rem !important; } + +.pr-5 { + padding-right: 3rem !important; } + +.pb-5 { + padding-bottom: 3rem !important; } + +.pl-5 { + padding-left: 3rem !important; } + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + +.m-auto { + margin: auto !important; } + +.mt-auto { + margin-top: auto !important; } + +.mr-auto { + margin-right: auto !important; } + +.mb-auto { + margin-bottom: auto !important; } + +.ml-auto { + margin-left: auto !important; } + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; } + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 0 !important; } + .mt-sm-0 { + margin-top: 0 !important; } + .mr-sm-0 { + margin-right: 0 !important; } + .mb-sm-0 { + margin-bottom: 0 !important; } + .ml-sm-0 { + margin-left: 0 !important; } + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .m-sm-1 { + margin: 0.25rem 0.25rem !important; } + .mt-sm-1 { + margin-top: 0.25rem !important; } + .mr-sm-1 { + margin-right: 0.25rem !important; } + .mb-sm-1 { + margin-bottom: 0.25rem !important; } + .ml-sm-1 { + margin-left: 0.25rem !important; } + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .m-sm-2 { + margin: 0.5rem 0.5rem !important; } + .mt-sm-2 { + margin-top: 0.5rem !important; } + .mr-sm-2 { + margin-right: 0.5rem !important; } + .mb-sm-2 { + margin-bottom: 0.5rem !important; } + .ml-sm-2 { + margin-left: 0.5rem !important; } + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .m-sm-3 { + margin: 1rem 1rem !important; } + .mt-sm-3 { + margin-top: 1rem !important; } + .mr-sm-3 { + margin-right: 1rem !important; } + .mb-sm-3 { + margin-bottom: 1rem !important; } + .ml-sm-3 { + margin-left: 1rem !important; } + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .m-sm-4 { + margin: 1.5rem 1.5rem !important; } + .mt-sm-4 { + margin-top: 1.5rem !important; } + .mr-sm-4 { + margin-right: 1.5rem !important; } + .mb-sm-4 { + margin-bottom: 1.5rem !important; } + .ml-sm-4 { + margin-left: 1.5rem !important; } + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .m-sm-5 { + margin: 3rem 3rem !important; } + .mt-sm-5 { + margin-top: 3rem !important; } + .mr-sm-5 { + margin-right: 3rem !important; } + .mb-sm-5 { + margin-bottom: 3rem !important; } + .ml-sm-5 { + margin-left: 3rem !important; } + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .p-sm-0 { + padding: 0 0 !important; } + .pt-sm-0 { + padding-top: 0 !important; } + .pr-sm-0 { + padding-right: 0 !important; } + .pb-sm-0 { + padding-bottom: 0 !important; } + .pl-sm-0 { + padding-left: 0 !important; } + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .p-sm-1 { + padding: 0.25rem 0.25rem !important; } + .pt-sm-1 { + padding-top: 0.25rem !important; } + .pr-sm-1 { + padding-right: 0.25rem !important; } + .pb-sm-1 { + padding-bottom: 0.25rem !important; } + .pl-sm-1 { + padding-left: 0.25rem !important; } + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .p-sm-2 { + padding: 0.5rem 0.5rem !important; } + .pt-sm-2 { + padding-top: 0.5rem !important; } + .pr-sm-2 { + padding-right: 0.5rem !important; } + .pb-sm-2 { + padding-bottom: 0.5rem !important; } + .pl-sm-2 { + padding-left: 0.5rem !important; } + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .p-sm-3 { + padding: 1rem 1rem !important; } + .pt-sm-3 { + padding-top: 1rem !important; } + .pr-sm-3 { + padding-right: 1rem !important; } + .pb-sm-3 { + padding-bottom: 1rem !important; } + .pl-sm-3 { + padding-left: 1rem !important; } + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .p-sm-4 { + padding: 1.5rem 1.5rem !important; } + .pt-sm-4 { + padding-top: 1.5rem !important; } + .pr-sm-4 { + padding-right: 1.5rem !important; } + .pb-sm-4 { + padding-bottom: 1.5rem !important; } + .pl-sm-4 { + padding-left: 1.5rem !important; } + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .p-sm-5 { + padding: 3rem 3rem !important; } + .pt-sm-5 { + padding-top: 3rem !important; } + .pr-sm-5 { + padding-right: 3rem !important; } + .pb-sm-5 { + padding-bottom: 3rem !important; } + .pl-sm-5 { + padding-left: 3rem !important; } + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .m-sm-auto { + margin: auto !important; } + .mt-sm-auto { + margin-top: auto !important; } + .mr-sm-auto { + margin-right: auto !important; } + .mb-sm-auto { + margin-bottom: auto !important; } + .ml-sm-auto { + margin-left: auto !important; } + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; } } + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 0 !important; } + .mt-md-0 { + margin-top: 0 !important; } + .mr-md-0 { + margin-right: 0 !important; } + .mb-md-0 { + margin-bottom: 0 !important; } + .ml-md-0 { + margin-left: 0 !important; } + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .m-md-1 { + margin: 0.25rem 0.25rem !important; } + .mt-md-1 { + margin-top: 0.25rem !important; } + .mr-md-1 { + margin-right: 0.25rem !important; } + .mb-md-1 { + margin-bottom: 0.25rem !important; } + .ml-md-1 { + margin-left: 0.25rem !important; } + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .m-md-2 { + margin: 0.5rem 0.5rem !important; } + .mt-md-2 { + margin-top: 0.5rem !important; } + .mr-md-2 { + margin-right: 0.5rem !important; } + .mb-md-2 { + margin-bottom: 0.5rem !important; } + .ml-md-2 { + margin-left: 0.5rem !important; } + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .m-md-3 { + margin: 1rem 1rem !important; } + .mt-md-3 { + margin-top: 1rem !important; } + .mr-md-3 { + margin-right: 1rem !important; } + .mb-md-3 { + margin-bottom: 1rem !important; } + .ml-md-3 { + margin-left: 1rem !important; } + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .m-md-4 { + margin: 1.5rem 1.5rem !important; } + .mt-md-4 { + margin-top: 1.5rem !important; } + .mr-md-4 { + margin-right: 1.5rem !important; } + .mb-md-4 { + margin-bottom: 1.5rem !important; } + .ml-md-4 { + margin-left: 1.5rem !important; } + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .m-md-5 { + margin: 3rem 3rem !important; } + .mt-md-5 { + margin-top: 3rem !important; } + .mr-md-5 { + margin-right: 3rem !important; } + .mb-md-5 { + margin-bottom: 3rem !important; } + .ml-md-5 { + margin-left: 3rem !important; } + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .p-md-0 { + padding: 0 0 !important; } + .pt-md-0 { + padding-top: 0 !important; } + .pr-md-0 { + padding-right: 0 !important; } + .pb-md-0 { + padding-bottom: 0 !important; } + .pl-md-0 { + padding-left: 0 !important; } + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .p-md-1 { + padding: 0.25rem 0.25rem !important; } + .pt-md-1 { + padding-top: 0.25rem !important; } + .pr-md-1 { + padding-right: 0.25rem !important; } + .pb-md-1 { + padding-bottom: 0.25rem !important; } + .pl-md-1 { + padding-left: 0.25rem !important; } + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .p-md-2 { + padding: 0.5rem 0.5rem !important; } + .pt-md-2 { + padding-top: 0.5rem !important; } + .pr-md-2 { + padding-right: 0.5rem !important; } + .pb-md-2 { + padding-bottom: 0.5rem !important; } + .pl-md-2 { + padding-left: 0.5rem !important; } + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .p-md-3 { + padding: 1rem 1rem !important; } + .pt-md-3 { + padding-top: 1rem !important; } + .pr-md-3 { + padding-right: 1rem !important; } + .pb-md-3 { + padding-bottom: 1rem !important; } + .pl-md-3 { + padding-left: 1rem !important; } + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .p-md-4 { + padding: 1.5rem 1.5rem !important; } + .pt-md-4 { + padding-top: 1.5rem !important; } + .pr-md-4 { + padding-right: 1.5rem !important; } + .pb-md-4 { + padding-bottom: 1.5rem !important; } + .pl-md-4 { + padding-left: 1.5rem !important; } + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .p-md-5 { + padding: 3rem 3rem !important; } + .pt-md-5 { + padding-top: 3rem !important; } + .pr-md-5 { + padding-right: 3rem !important; } + .pb-md-5 { + padding-bottom: 3rem !important; } + .pl-md-5 { + padding-left: 3rem !important; } + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .m-md-auto { + margin: auto !important; } + .mt-md-auto { + margin-top: auto !important; } + .mr-md-auto { + margin-right: auto !important; } + .mb-md-auto { + margin-bottom: auto !important; } + .ml-md-auto { + margin-left: auto !important; } + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; } } + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 0 !important; } + .mt-lg-0 { + margin-top: 0 !important; } + .mr-lg-0 { + margin-right: 0 !important; } + .mb-lg-0 { + margin-bottom: 0 !important; } + .ml-lg-0 { + margin-left: 0 !important; } + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .m-lg-1 { + margin: 0.25rem 0.25rem !important; } + .mt-lg-1 { + margin-top: 0.25rem !important; } + .mr-lg-1 { + margin-right: 0.25rem !important; } + .mb-lg-1 { + margin-bottom: 0.25rem !important; } + .ml-lg-1 { + margin-left: 0.25rem !important; } + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .m-lg-2 { + margin: 0.5rem 0.5rem !important; } + .mt-lg-2 { + margin-top: 0.5rem !important; } + .mr-lg-2 { + margin-right: 0.5rem !important; } + .mb-lg-2 { + margin-bottom: 0.5rem !important; } + .ml-lg-2 { + margin-left: 0.5rem !important; } + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .m-lg-3 { + margin: 1rem 1rem !important; } + .mt-lg-3 { + margin-top: 1rem !important; } + .mr-lg-3 { + margin-right: 1rem !important; } + .mb-lg-3 { + margin-bottom: 1rem !important; } + .ml-lg-3 { + margin-left: 1rem !important; } + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .m-lg-4 { + margin: 1.5rem 1.5rem !important; } + .mt-lg-4 { + margin-top: 1.5rem !important; } + .mr-lg-4 { + margin-right: 1.5rem !important; } + .mb-lg-4 { + margin-bottom: 1.5rem !important; } + .ml-lg-4 { + margin-left: 1.5rem !important; } + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .m-lg-5 { + margin: 3rem 3rem !important; } + .mt-lg-5 { + margin-top: 3rem !important; } + .mr-lg-5 { + margin-right: 3rem !important; } + .mb-lg-5 { + margin-bottom: 3rem !important; } + .ml-lg-5 { + margin-left: 3rem !important; } + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .p-lg-0 { + padding: 0 0 !important; } + .pt-lg-0 { + padding-top: 0 !important; } + .pr-lg-0 { + padding-right: 0 !important; } + .pb-lg-0 { + padding-bottom: 0 !important; } + .pl-lg-0 { + padding-left: 0 !important; } + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .p-lg-1 { + padding: 0.25rem 0.25rem !important; } + .pt-lg-1 { + padding-top: 0.25rem !important; } + .pr-lg-1 { + padding-right: 0.25rem !important; } + .pb-lg-1 { + padding-bottom: 0.25rem !important; } + .pl-lg-1 { + padding-left: 0.25rem !important; } + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .p-lg-2 { + padding: 0.5rem 0.5rem !important; } + .pt-lg-2 { + padding-top: 0.5rem !important; } + .pr-lg-2 { + padding-right: 0.5rem !important; } + .pb-lg-2 { + padding-bottom: 0.5rem !important; } + .pl-lg-2 { + padding-left: 0.5rem !important; } + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .p-lg-3 { + padding: 1rem 1rem !important; } + .pt-lg-3 { + padding-top: 1rem !important; } + .pr-lg-3 { + padding-right: 1rem !important; } + .pb-lg-3 { + padding-bottom: 1rem !important; } + .pl-lg-3 { + padding-left: 1rem !important; } + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .p-lg-4 { + padding: 1.5rem 1.5rem !important; } + .pt-lg-4 { + padding-top: 1.5rem !important; } + .pr-lg-4 { + padding-right: 1.5rem !important; } + .pb-lg-4 { + padding-bottom: 1.5rem !important; } + .pl-lg-4 { + padding-left: 1.5rem !important; } + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .p-lg-5 { + padding: 3rem 3rem !important; } + .pt-lg-5 { + padding-top: 3rem !important; } + .pr-lg-5 { + padding-right: 3rem !important; } + .pb-lg-5 { + padding-bottom: 3rem !important; } + .pl-lg-5 { + padding-left: 3rem !important; } + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .m-lg-auto { + margin: auto !important; } + .mt-lg-auto { + margin-top: auto !important; } + .mr-lg-auto { + margin-right: auto !important; } + .mb-lg-auto { + margin-bottom: auto !important; } + .ml-lg-auto { + margin-left: auto !important; } + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; } } + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 0 !important; } + .mt-xl-0 { + margin-top: 0 !important; } + .mr-xl-0 { + margin-right: 0 !important; } + .mb-xl-0 { + margin-bottom: 0 !important; } + .ml-xl-0 { + margin-left: 0 !important; } + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .m-xl-1 { + margin: 0.25rem 0.25rem !important; } + .mt-xl-1 { + margin-top: 0.25rem !important; } + .mr-xl-1 { + margin-right: 0.25rem !important; } + .mb-xl-1 { + margin-bottom: 0.25rem !important; } + .ml-xl-1 { + margin-left: 0.25rem !important; } + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .m-xl-2 { + margin: 0.5rem 0.5rem !important; } + .mt-xl-2 { + margin-top: 0.5rem !important; } + .mr-xl-2 { + margin-right: 0.5rem !important; } + .mb-xl-2 { + margin-bottom: 0.5rem !important; } + .ml-xl-2 { + margin-left: 0.5rem !important; } + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .m-xl-3 { + margin: 1rem 1rem !important; } + .mt-xl-3 { + margin-top: 1rem !important; } + .mr-xl-3 { + margin-right: 1rem !important; } + .mb-xl-3 { + margin-bottom: 1rem !important; } + .ml-xl-3 { + margin-left: 1rem !important; } + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .m-xl-4 { + margin: 1.5rem 1.5rem !important; } + .mt-xl-4 { + margin-top: 1.5rem !important; } + .mr-xl-4 { + margin-right: 1.5rem !important; } + .mb-xl-4 { + margin-bottom: 1.5rem !important; } + .ml-xl-4 { + margin-left: 1.5rem !important; } + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .m-xl-5 { + margin: 3rem 3rem !important; } + .mt-xl-5 { + margin-top: 3rem !important; } + .mr-xl-5 { + margin-right: 3rem !important; } + .mb-xl-5 { + margin-bottom: 3rem !important; } + .ml-xl-5 { + margin-left: 3rem !important; } + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .p-xl-0 { + padding: 0 0 !important; } + .pt-xl-0 { + padding-top: 0 !important; } + .pr-xl-0 { + padding-right: 0 !important; } + .pb-xl-0 { + padding-bottom: 0 !important; } + .pl-xl-0 { + padding-left: 0 !important; } + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .p-xl-1 { + padding: 0.25rem 0.25rem !important; } + .pt-xl-1 { + padding-top: 0.25rem !important; } + .pr-xl-1 { + padding-right: 0.25rem !important; } + .pb-xl-1 { + padding-bottom: 0.25rem !important; } + .pl-xl-1 { + padding-left: 0.25rem !important; } + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .p-xl-2 { + padding: 0.5rem 0.5rem !important; } + .pt-xl-2 { + padding-top: 0.5rem !important; } + .pr-xl-2 { + padding-right: 0.5rem !important; } + .pb-xl-2 { + padding-bottom: 0.5rem !important; } + .pl-xl-2 { + padding-left: 0.5rem !important; } + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .p-xl-3 { + padding: 1rem 1rem !important; } + .pt-xl-3 { + padding-top: 1rem !important; } + .pr-xl-3 { + padding-right: 1rem !important; } + .pb-xl-3 { + padding-bottom: 1rem !important; } + .pl-xl-3 { + padding-left: 1rem !important; } + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .p-xl-4 { + padding: 1.5rem 1.5rem !important; } + .pt-xl-4 { + padding-top: 1.5rem !important; } + .pr-xl-4 { + padding-right: 1.5rem !important; } + .pb-xl-4 { + padding-bottom: 1.5rem !important; } + .pl-xl-4 { + padding-left: 1.5rem !important; } + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .p-xl-5 { + padding: 3rem 3rem !important; } + .pt-xl-5 { + padding-top: 3rem !important; } + .pr-xl-5 { + padding-right: 3rem !important; } + .pb-xl-5 { + padding-bottom: 3rem !important; } + .pl-xl-5 { + padding-left: 3rem !important; } + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .m-xl-auto { + margin: auto !important; } + .mt-xl-auto { + margin-top: auto !important; } + .mr-xl-auto { + margin-right: auto !important; } + .mb-xl-auto { + margin-bottom: auto !important; } + .ml-xl-auto { + margin-left: auto !important; } + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; } } + +.text-justify { + text-align: justify !important; } + +.text-nowrap { + white-space: nowrap !important; } + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + +.text-left { + text-align: left !important; } + +.text-right { + text-align: right !important; } + +.text-center { + text-align: center !important; } + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; } + .text-sm-right { + text-align: right !important; } + .text-sm-center { + text-align: center !important; } } + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; } + .text-md-right { + text-align: right !important; } + .text-md-center { + text-align: center !important; } } + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; } + .text-lg-right { + text-align: right !important; } + .text-lg-center { + text-align: center !important; } } + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; } + .text-xl-right { + text-align: right !important; } + .text-xl-center { + text-align: center !important; } } + +.text-lowercase { + text-transform: lowercase !important; } + +.text-uppercase { + text-transform: uppercase !important; } + +.text-capitalize { + text-transform: capitalize !important; } + +.font-weight-normal { + font-weight: normal; } + +.font-weight-bold { + font-weight: bold; } + +.font-italic { + font-style: italic; } + +.text-white { + color: #fff !important; } + +.text-muted { + color: #636c72 !important; } + +a.text-muted:focus, a.text-muted:hover { + color: #4b5257 !important; } + +.text-primary { + color: #0275d8 !important; } + +a.text-primary:focus, a.text-primary:hover { + color: #025aa5 !important; } + +.text-success { + color: #5cb85c !important; } + +a.text-success:focus, a.text-success:hover { + color: #449d44 !important; } + +.text-info { + color: #5bc0de !important; } + +a.text-info:focus, a.text-info:hover { + color: #31b0d5 !important; } + +.text-warning { + color: #f0ad4e !important; } + +a.text-warning:focus, a.text-warning:hover { + color: #ec971f !important; } + +.text-danger { + color: #d9534f !important; } + +a.text-danger:focus, a.text-danger:hover { + color: #c9302c !important; } + +.text-gray-dark { + color: #292b2c !important; } + +a.text-gray-dark:focus, a.text-gray-dark:hover { + color: #101112 !important; } + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; } + +.invisible { + visibility: hidden !important; } + +.hidden-xs-up { + display: none !important; } + +@media (max-width: 575px) { + .hidden-xs-down { + display: none !important; } } + +@media (min-width: 576px) { + .hidden-sm-up { + display: none !important; } } + +@media (max-width: 767px) { + .hidden-sm-down { + display: none !important; } } + +@media (min-width: 768px) { + .hidden-md-up { + display: none !important; } } + +@media (max-width: 991px) { + .hidden-md-down { + display: none !important; } } + +@media (min-width: 992px) { + .hidden-lg-up { + display: none !important; } } + +@media (max-width: 1199px) { + .hidden-lg-down { + display: none !important; } } + +@media (min-width: 1200px) { + .hidden-xl-up { + display: none !important; } } + +.hidden-xl-down { + display: none !important; } + +.visible-print-block { + display: none !important; } + @media print { + .visible-print-block { + display: block !important; } } + +.visible-print-inline { + display: none !important; } + @media print { + .visible-print-inline { + display: inline !important; } } + +.visible-print-inline-block { + display: none !important; } + @media print { + .visible-print-inline-block { + display: inline-block !important; } } + +@media print { + .hidden-print { + display: none !important; } } + +.alert-debug { + background-color: #fff; + border-color: #d6e9c6; + color: #000; } + +.alert-error { + background-color: #f2dede; + border-color: #eed3d7; + color: #b94a48; } + +#navbar-logo { + width: 48px; + height: 48px; + transition: transform .8s ease-in-out; } + #navbar-logo:hover { + transform: rotate(360deg); } + +body { + max-width: 100%; } + +.navbar-brand { + width: 100%; } + +#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: auto; + text-shadow: 1px 1px #FFF; + position: relative; } + #membercard .middle { + font-size: 2rem; + text-align: center; + margin: 50px; } + #membercard .date { + padding: 14px; } + +html { + position: relative; + min-height: 100%; } + +body { + margin-bottom: 60px; } + +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 60px; + line-height: 60px; + background-color: #f5f5f5; } + +@media (max-width: 991px) { + .card-columns { + column-count: 2; } } + +@media (max-width: 767px) { + .card-columns { + column-count: 1; } } + +@media (max-width: 767px) { + .card { + margin-bottom: 0.75rem; } } + +.card-deck { + margin-bottom: 30px; } + +.card-title { + color: #292b2c; } + +#feeds img { + width: 100%; } + +.title { + margin-bottom: 0; } + +.thumbnail { + margin-bottom: 15px; } + +.modal-body iframe { + width: 100%; + height: 100%; } + +.modal-dialog { + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + margin: 50px; + padding: 0; + max-width: None; } + +.modal-content { + height: auto; + min-height: 100%; + border-radius: 0; } + +[hidden][style="display: block;"] { + display: block !important; } + +.imgfit { + width: 100%; + height: auto; + overflow: hidden; } diff --git a/mhackspace/static/sass/project.scss b/mhackspace/static/sass/project.scss index 6e7afd3..9939766 100644 --- a/mhackspace/static/sass/project.scss +++ b/mhackspace/static/sass/project.scss @@ -9,6 +9,7 @@ @import "components/feeds"; @import "components/blog"; +@import "components/wiki"; //////////////////////////////// //Django Toolbar// @@ -26,4 +27,4 @@ width:100%; height: auto; overflow: hidden; -} \ No newline at end of file +} diff --git a/mhackspace/users/admin.py b/mhackspace/users/admin.py index a183241..cd49bb6 100644 --- a/mhackspace/users/admin.py +++ b/mhackspace/users/admin.py @@ -62,7 +62,7 @@ class MyUserAdmin(AuthUserAdmin): @admin.register(Membership) class MembershipAdmin(ModelAdmin): - list_display = ('user', 'payment', 'date', 'status') + list_display = ('user_id', 'user', 'payment', 'date', 'status') list_filter = ('status',) diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index 8f60e10..45d69ea 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -68,6 +68,7 @@ MEMBERSHIP_STATUS = { 'cancelled': 4 } + class Membership(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, From e01770cd0630c4539b70f47ff8fb66e51b0fa64c Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Fri, 15 Sep 2017 23:00:48 +0100 Subject: [PATCH 13/22] Add X frame option for wiki preview --- config/settings/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/common.py b/config/settings/common.py index f954c7a..71f3c95 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -473,3 +473,4 @@ MATRIX_USER=env('MATRIX_USERNAME') MATRIX_PASSWORD=env('MATRIX_PASSWORD') MATRIX_ROOM=env('MATRIX_ROOM') MSG_PREFIX = 'MH' +X_FRAME_OPTIONS = 'SAMEORIGIN' From 22a700bc5e395296a00ac75725d09384dd079604 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sat, 16 Sep 2017 08:55:58 +0100 Subject: [PATCH 14/22] Change X-Frame options, and apply styling tweak to wiki preview --- config/settings/production.py | 2 +- config/settings/stage.py | 2 +- mhackspace/static/sass/components/_wiki.scss | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/settings/production.py b/config/settings/production.py index 6cd0db0..6be8331 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -43,7 +43,7 @@ SESSION_COOKIE_HTTPONLY = True CSRF_COOKIE_SECURE = True #disabledd so csrf works with ajax CSRF_COOKIE_HTTPONLY = False -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = 'SAMEORIGIN' # SITE CONFIGURATION # ------------------------------------------------------------------------------ diff --git a/config/settings/stage.py b/config/settings/stage.py index 157cb70..2ca202e 100644 --- a/config/settings/stage.py +++ b/config/settings/stage.py @@ -43,7 +43,7 @@ SESSION_COOKIE_HTTPONLY = True CSRF_COOKIE_SECURE = True #disabledd so csrf works with ajax CSRF_COOKIE_HTTPONLY = False -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = 'SAMEORIGIN' # SITE CONFIGURATION # ------------------------------------------------------------------------------ diff --git a/mhackspace/static/sass/components/_wiki.scss b/mhackspace/static/sass/components/_wiki.scss index f6f3e5e..e79ec1e 100644 --- a/mhackspace/static/sass/components/_wiki.scss +++ b/mhackspace/static/sass/components/_wiki.scss @@ -10,6 +10,7 @@ margin: 50px; padding: 0; max-width: None; + position: absolute; } .modal-content { From 574286e8e39323220e3a1969621321bc96db0705 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sun, 17 Sep 2017 10:50:09 +0100 Subject: [PATCH 15/22] Fix duplicate records in membership table. --- config/settings/common.py | 7 ++- config/settings/local.py | 2 +- mhackspace/base/tasks.py | 11 ++-- mhackspace/requests/models.py | 2 +- mhackspace/subscriptions/helper.py | 14 +++-- .../commands/update_membership_status.py | 62 +++++++------------ .../{user => membership}/change_list.html | 0 mhackspace/templates/base.html | 1 + mhackspace/users/admin.py | 27 ++++---- .../migrations/0008_auto_20170917_0948.py | 27 ++++++++ mhackspace/users/models.py | 7 ++- 11 files changed, 91 insertions(+), 69 deletions(-) rename mhackspace/templates/admin/users/{user => membership}/change_list.html (100%) create mode 100644 mhackspace/users/migrations/0008_auto_20170917_0948.py diff --git a/config/settings/common.py b/config/settings/common.py index 71f3c95..8df72c5 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -375,7 +375,12 @@ CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='redis://redis:6379/0') #if CELERY_BROKER_URL == 'django://': # CELERY_RESULT_BACKEND = 'redis://' #else: -CELERY_RESULT_BACKEND = 'django-cache' +CELERY_RESULT_BACKEND = 'redis://redis:6379/0' +CELERY_IGNORE_RESULT = False +CELERY_REDIS_HOST = "redis" +CELERY_REDIS_PORT = 6379 +CELERY_REDIS_DB = 0 + CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' INSTALLED_APPS += ('django_celery_results','django_celery_beat',) CELERY_TIMEZONE = 'UTC' diff --git a/config/settings/local.py b/config/settings/local.py index 8022b67..1b431e6 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -86,7 +86,7 @@ TEST_RUNNER = 'django.test.runner.DiscoverRunner' ########## CELERY # In development, all tasks will be executed locally by blocking until the task returns -CELERY_ALWAYS_EAGER = True +# CELERY_ALWAYS_EAGER = True ########## END CELERY # Your local stuff: Below this line define 3rd party library settings diff --git a/mhackspace/base/tasks.py b/mhackspace/base/tasks.py index 74a998d..8b6a040 100644 --- a/mhackspace/base/tasks.py +++ b/mhackspace/base/tasks.py @@ -6,8 +6,8 @@ from mhackspace.feeds.helper import import_feeds @shared_task def update_homepage_feeds(): - return import_feeds() - + import_feeds() + return {'result': 'Homepage feeds imported'} matrix_url = "https://matrix.org/_matrix/client/r0" matrix_login_url = matrix_url + "/login" @@ -31,9 +31,10 @@ def send_email(email_to, to=[email_to], headers={'Reply-To': 'no-reply@maidstone-hackspace.org.uk'}) email.send() + return {'result', 'Email sent to %s' % email_to} @shared_task -def matrix_message(message): +def matrix_message(message, prefix=''): # we dont rely on theses, so ignore if it goes wrong # TODO at least log that something has gone wrong try: @@ -59,8 +60,8 @@ def matrix_message(message): url = matrix_send_msg_url.format(**url_params) details = { "msgtype": "m.text", - "body": "[%s] %s" % (settings.MSG_PREFIX, message)} + "body": "[%s%s] %s" % (settings.MSG_PREFIX, prefix, message)} r2 = requests.post(url, json=details) except: pass - return True + return {'result', 'Matrix message sent successfully'} diff --git a/mhackspace/requests/models.py b/mhackspace/requests/models.py index 982abe7..edf32db 100644 --- a/mhackspace/requests/models.py +++ b/mhackspace/requests/models.py @@ -45,7 +45,7 @@ class UserRequests(models.Model): def send_topic_update_email(sender, instance, **kwargs): - matrix_message.delay('New Request - %s' % instance.title) + matrix_message.delay(prefix=' - REQUEST', message=instance.title) post_save.connect(send_topic_update_email, sender=UserRequests) diff --git a/mhackspace/subscriptions/helper.py b/mhackspace/subscriptions/helper.py index 434bc63..f060d41 100644 --- a/mhackspace/subscriptions/helper.py +++ b/mhackspace/subscriptions/helper.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals, absolute_import -from django.contrib import messages from django.contrib.auth.models import Group +from django.utils.dateparse import parse_datetime from mhackspace.users.models import Membership def create_or_update_membership(user, signup_details, complete=False): try: - member = Membership.objects.get(user=user) + member = Membership.objects.get(email=signup_details.get('email')) + # Only update if newer than last record, this way we only get the latest status + # cancellation and changed payment will not be counted against current status + start_date = parse_datetime(signup_details.get('start_date')) + if start_date < member.date: + return True except Membership.DoesNotExist: member = Membership() member.user = user @@ -25,6 +30,7 @@ def create_or_update_membership(user, signup_details, complete=False): return False # sign up not completed # add user to group on success - group = Group.objects.get(name='members') - user.groups.add(group) + if user: + group = Group.objects.get(name='members') + user.groups.add(group) return True # Sign up finished diff --git a/mhackspace/subscriptions/management/commands/update_membership_status.py b/mhackspace/subscriptions/management/commands/update_membership_status.py index c88901e..56af20e 100644 --- a/mhackspace/subscriptions/management/commands/update_membership_status.py +++ b/mhackspace/subscriptions/management/commands/update_membership_status.py @@ -1,5 +1,3 @@ -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 @@ -8,6 +6,10 @@ from mhackspace.users.models import Membership, User from mhackspace.subscriptions.helper import create_or_update_membership +# this should be done in bulk, create the objects and save all at once +# for now its not an issue, because of small membership size + + def update_subscriptions(provider_name): provider = select_provider('gocardless') @@ -24,17 +26,9 @@ def update_subscriptions(provider_name): except User.DoesNotExist: user_model = None - 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')) - ) - ) + create_or_update_membership(user=user_model, + signup_details=sub, + complete=True) yield model_to_dict(subscriptions[-1]) @@ -49,11 +43,11 @@ class Command(BaseCommand): provider = select_provider('gocardless') Membership.objects.all().delete() - subscriptions = [] group = Group.objects.get(name='members') for sub in provider.fetch_subscriptions(): + prefix = '' sub['amount'] = sub['amount'] * 0.01 try: user_model = User.objects.get(email=sub.get('email')) @@ -61,37 +55,23 @@ class Command(BaseCommand): user_model.groups.add(group) except User.DoesNotExist: 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 + prefix = 'NO USER - ' create_or_update_membership(user=user_model, signup_details=sub, complete=True) - subscriptions.append( - Membership( - user=user_model, - email=sub.get('email'), - reference=sub.get('reference'), - payment=sub.get('amount'), - date=sub.get('start_date'), - status=Membership.lookup_status(name=sub.get('status')) - ) - ) - self.stdout.write( - self.style.SUCCESS( - '\t{reference} - {payment} - {status} - {email}'.format(**{ - 'reference': sub.get('reference'), - 'payment': sub.get('amount'), - 'status': sub.get('status'), - 'email': sub.get('email') - }))) + message = '\t{prefix}{date} - {reference} - {payment} - {status} - {email}'.format(**{ + 'prefix': prefix, + 'date': sub.get('start_date'), + 'reference': sub.get('reference'), + 'payment': sub.get('amount'), + 'status': sub.get('status'), + 'email': sub.get('email') + }) - Membership.objects.bulk_create(subscriptions) + if user_model: + self.stdout.write(self.style.SUCCESS(message)) + else: + self.stdout.write(self.style.NOTICE(message)) diff --git a/mhackspace/templates/admin/users/user/change_list.html b/mhackspace/templates/admin/users/membership/change_list.html similarity index 100% rename from mhackspace/templates/admin/users/user/change_list.html rename to mhackspace/templates/admin/users/membership/change_list.html diff --git a/mhackspace/templates/base.html b/mhackspace/templates/base.html index f0fc887..d4b87dc 100644 --- a/mhackspace/templates/base.html +++ b/mhackspace/templates/base.html @@ -168,6 +168,7 @@
{% block footer-left %}{% endblock footer-left %} © {% now "Y" %} Maidstone Hackspace + Constitution
diff --git a/mhackspace/users/admin.py b/mhackspace/users/admin.py index cd49bb6..ef57971 100644 --- a/mhackspace/users/admin.py +++ b/mhackspace/users/admin.py @@ -11,7 +11,8 @@ from django.urls import reverse from django.conf.urls import url from .models import User, Rfid, Membership, MEMBERSHIP_STATUS_CHOICES -from mhackspace.subscriptions.management.commands.update_membership_status import update_subscriptions +# from mhackspace.subscriptions.management.commands.update_membership_status import update_subscriptions +from mhackspace.users.tasks import update_users_memebership_status class MyUserChangeForm(UserChangeForm): @@ -46,27 +47,27 @@ class MyUserAdmin(AuthUserAdmin): list_display = ('username', 'name', 'is_superuser') search_fields = ['name'] + +@admin.register(Membership) +class MembershipAdmin(ModelAdmin): + list_display = ('user_id', 'email', 'payment', 'date', 'status') + list_filter = ('status',) + def get_urls(self): - urls = super(MyUserAdmin, self).get_urls() + urls = super(MembershipAdmin, self).get_urls() my_urls = [ url(r'^refresh/payments/$', self.admin_site.admin_view(self.refresh_payments)) ] return my_urls + urls def refresh_payments(self, request): - for user in update_subscriptions(provider_name='gocardless'): - continue - self.message_user(request, 'Successfully imported refresh users payment status') - return HttpResponseRedirect(reverse('admin:feeds_article_changelist')) - - -@admin.register(Membership) -class MembershipAdmin(ModelAdmin): - list_display = ('user_id', 'user', 'payment', 'date', 'status') - list_filter = ('status',) + update_users_memebership_status() + # for user in update_subscriptions(provider_name='gocardless'): + # continue + self.message_user(request, 'Successfully triggered user payment refresh') + return HttpResponseRedirect(reverse('admin:index')) @admin.register(Rfid) class RfidAdmin(ModelAdmin): list_display = ('code', 'description') - diff --git a/mhackspace/users/migrations/0008_auto_20170917_0948.py b/mhackspace/users/migrations/0008_auto_20170917_0948.py new file mode 100644 index 0000000..5fdfbb3 --- /dev/null +++ b/mhackspace/users/migrations/0008_auto_20170917_0948.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-17 09:48 +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', '0007_auto_20170914_2021'), + ] + + operations = [ + migrations.AlterField( + model_name='membership', + name='email', + field=models.CharField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name='membership', + name='user', + field=models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index 45d69ea..84c1526 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -70,17 +70,18 @@ MEMBERSHIP_STATUS = { class Membership(models.Model): - user = models.ForeignKey( + user = models.OneToOneField( settings.AUTH_USER_MODEL, null=True, blank=True, default=None, - related_name='user' + related_name='user', + unique=True ) 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, choices=MEMBERSHIP_STATUS_CHOICES) - email = models.CharField(max_length=255) + email = models.CharField(max_length=255, unique=True) @property def get_status(self): From 833b08980a2fd9eaed831f3c7ff40bfea14edd9c Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Tue, 19 Sep 2017 21:24:13 +0100 Subject: [PATCH 16/22] Increase decimal places on request form --- .../migrations/0007_auto_20170919_2023.py | 20 +++++++++++++++++++ mhackspace/requests/models.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 mhackspace/requests/migrations/0007_auto_20170919_2023.py diff --git a/mhackspace/requests/migrations/0007_auto_20170919_2023.py b/mhackspace/requests/migrations/0007_auto_20170919_2023.py new file mode 100644 index 0000000..eacb3ce --- /dev/null +++ b/mhackspace/requests/migrations/0007_auto_20170919_2023.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-19 20:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('requests', '0006_auto_20170906_0804'), + ] + + operations = [ + migrations.AlterField( + model_name='userrequests', + name='cost', + field=models.DecimalField(decimal_places=2, help_text='Leave blank, if no associated cost, or add estimated cost if not sure.', max_digits=6), + ), + ] diff --git a/mhackspace/requests/models.py b/mhackspace/requests/models.py index edf32db..fb77b03 100644 --- a/mhackspace/requests/models.py +++ b/mhackspace/requests/models.py @@ -21,7 +21,7 @@ class UserRequests(models.Model): title = models.CharField(max_length=255, help_text='Whats being requested ?') request_type = models.IntegerField(choices=REQUEST_TYPES, null=False) cost = models.DecimalField( - max_digits=4, + max_digits=6, decimal_places=2, help_text='Leave blank, if no associated cost, or add estimated cost if not sure.' ) From bbd72b3a95a4cf5082503bd527253143cb8f98f3 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sun, 1 Oct 2017 11:51:15 +0100 Subject: [PATCH 17/22] Fix up what tests we have so they all pass again --- mhackspace/subscriptions/tests/mocks.py | 56 +++++++++++++++++++++++ mhackspace/users/tests/test_serializer.py | 16 +++++++ 2 files changed, 72 insertions(+) create mode 100644 mhackspace/subscriptions/tests/mocks.py create mode 100644 mhackspace/users/tests/test_serializer.py diff --git a/mhackspace/subscriptions/tests/mocks.py b/mhackspace/subscriptions/tests/mocks.py new file mode 100644 index 0000000..86d7d90 --- /dev/null +++ b/mhackspace/subscriptions/tests/mocks.py @@ -0,0 +1,56 @@ +from test_plus.test import TestCase +from mock import patch, Mock, MagicMock, PropertyMock +from mhackspace.users.models import Membership +import django.utils.timezone +from collections import namedtuple + +from mhackspace.subscriptions.payments import gocardless_provider + + +class gocardlessMocks(TestCase): + + def setUp(self): + self.date_now = django.utils.timezone.now() + self.user = self.make_user() + self.auth_gocardless() + + def create_membership_record(self): + member = Membership() + member.user = self.user + member.payment = '20.00' + member.date = self.date_now + member.save() + return member + + @patch('mhackspace.subscriptions.payments.gocardless_pro', autospec=True) + def auth_gocardless(self, mock_request): + RedirectFlow = namedtuple('RedirectFlow', 'links') + Links = namedtuple('Links', 'mandate, customer') + mp = RedirectFlow( + links=Links(mandate='02', customer='01')) + + self.provider = gocardless_provider() + self.provider.client.redirect_flows.get = PropertyMock(return_value=mp) + + return self.provider + + def mock_success_responses(self): + + mock_list = MagicMock() + mock_list_records = MagicMock(side_effect=[Mock( + id='01', + status='active', + amount=20.00, + created_at='date' + )]) + mock_list.records.return_value = mock_list_records + + self.provider.client.subscriptions.list = mock_list + ApiResponse = namedtuple('ApiResponse', 'api_response, created_at') + ApiResponseStatus = namedtuple('ApiResponseStatus', 'status_code') + + self.provider.client.subscriptions.create = Mock( + return_value=ApiResponse( + created_at=self.date_now, + api_response=ApiResponseStatus(status_code='200')) + ) diff --git a/mhackspace/users/tests/test_serializer.py b/mhackspace/users/tests/test_serializer.py new file mode 100644 index 0000000..18f0ec8 --- /dev/null +++ b/mhackspace/users/tests/test_serializer.py @@ -0,0 +1,16 @@ +from test_plus.test import TestCase +from django.db import models +from allauth.utils import serialize_instance + + +class TestSerializeUser(TestCase): + + def setUp(self): + self.user = self.make_user() + + def test_serialize(self): + """check we can serialize the user object for allauth, custom types can break it""" + result = serialize_instance(self.user) + self.assertTrue( + isinstance(result, dict), + ) From ec41abfa1352984220d22d1110567a002dd006aa Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sun, 1 Oct 2017 11:54:35 +0100 Subject: [PATCH 18/22] Fix up existing tests --- .drone.yml | 2 +- mhackspace/requests/tests.py | 33 +- mhackspace/subscriptions/payments.py | 288 ++---------------- .../tests/test_payment_gateways.py | 118 +++---- mhackspace/subscriptions/tests/test_views.py | 51 ++-- 5 files changed, 110 insertions(+), 382 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7b35620..135dddd 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ pipeline: - cp -n env.example .env - mkdir -p ./cache/packages ./cache/pip - pip install --user --cache-dir ./cache/pip -r ./requirements/test.txt - - python manage.py test mhackspace --verbosity 2 + - python manage.py test mhackspace --keepdb --verbosity 2 publish-test: pull: True diff --git a/mhackspace/requests/tests.py b/mhackspace/requests/tests.py index 3860e0c..ed3f016 100644 --- a/mhackspace/requests/tests.py +++ b/mhackspace/requests/tests.py @@ -1,5 +1,6 @@ from django.test import TestCase from mhackspace.requests.views import RequestsList, RequestForm +from mhackspace.users.models import User # Create your tests here. @@ -16,7 +17,30 @@ from mhackspace.requests.views import RequestsList, RequestForm # 'is_active': True # }, generate_fk=True) +def all_user_types(): + users = AutoFixture(User, field_values={ + 'title': 'Mr', + 'username': 'admin', + 'password': make_password('autofixtures'), + }, generate_fk=True) + yield users.create(1) + users = AutoFixture(User, field_values={ + 'title': 'Mr', + 'username': 'admin', + 'password': make_password('autofixtures'), + 'is_staff': True, + }, generate_fk=True) + yield users.create(1) + + users = AutoFixture(User, field_values={ + 'title': 'Mr', + 'username': 'admin', + 'password': make_password('autofixtures'), + 'is_superuser': True, + 'is_staff': True, + }, generate_fk=True) + yield users.create(1) class BaseUserTestCase(TestCase): @@ -25,10 +49,11 @@ class BaseUserTestCase(TestCase): self.factory = RequestFactory() def testRequestView(self): - view = RequestsList() - request = self.factory.get('/fake-url') - request.user = self.user - view.request = request + for user in all_user_types() + view = RequestsList() + request = self.factory.get('/fake-url') + request.user = user + view.request = request # class TestUserUpdateView(BaseUserTestCase): diff --git a/mhackspace/subscriptions/payments.py b/mhackspace/subscriptions/payments.py index b17235c..c316bb1 100644 --- a/mhackspace/subscriptions/payments.py +++ b/mhackspace/subscriptions/payments.py @@ -1,6 +1,6 @@ from pprint import pprint import pytz -import gocardless_pro as gocardless +import gocardless_pro import braintree import logging @@ -31,19 +31,19 @@ class gocardless_provider: def __init__(self): # gocardless are changing there api, not sure if we can switch yet - self.client = gocardless.Client( + self.client = gocardless_pro.Client( access_token=payment_providers['gocardless']['credentials']['access_token'], environment=payment_providers['gocardless']['environment']) - def subscribe_confirm(self, args): - response = gocardless.client.confirm_resource(args) - subscription = gocardless.client.subscription(args.get('resource_id')) - return { - 'amount': subscription.amount, - 'start_date': subscription.created_at, - 'reference': subscription.id, - 'success': response.success - } + # def subscribe_confirm(self, args): + # response = gocardless_proclient.confirm_resource(args) + # subscription = gocardless_proclient.subscription(args.get('resource_id')) + # return { + # 'amount': subscription.amount, + # 'start_date': subscription.created_at, + # 'reference': subscription.id, + # 'success': response.success + # } def fetch_customers(self): """Fetch list of customers payments""" @@ -82,9 +82,9 @@ class gocardless_provider: def cancel_subscription(self, reference): try: - subscription = gocardless.client.subscription(reference) + subscription = gocardless_proclient.subscription(reference) response = subscription.cancel() - except gocardless.exceptions.ClientError: + except gocardless_proexceptions.ClientError: return { 'success': False } @@ -120,15 +120,13 @@ class gocardless_provider: 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')) + # response = gocardless_proclient.confirm_resource(provider_response) + # subscription = gocardless_proclient.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 @@ -182,8 +180,8 @@ class braintree_provider: def confirm_subscription(self, args): if self.provider == 'gocardless': - response = gocardless.client.confirm_resource(args) - subscription = gocardless.client.subscription(args.get('resource_id')) + response = gocardless_proclient.confirm_resource(args) + subscription = gocardless_proclient.subscription(args.get('resource_id')) return { 'amount': subscription.amount, 'start_date': subscription.created_at, @@ -201,255 +199,3 @@ class braintree_provider: 'reference': paying_member.reference, 'amount': paying_member.amount} - -class payment: - """ - https://developer.gocardless.com/api-reference/#redirect-flows-create-a-redirect-flow - paypal reference = https://github.com/paypal/PayPal-Python-SDK - gocardless reference = https://github.com/paypal/PayPal-Python-SDK - """ - #~ def __call__(self, **args): - #~ return self - - def __init__(self, provider='gocardless', style='payment', mode='sandbox'): - self.provider = provider - self.environment = int(mode=='production') - self.provider_id = PROVIDER_ID.get(provider) - - print(payment_providers) - if provider == 'paypal': - paypal.configure(**payment_providers[provider]['credentials']) - return - - gocardless_pro.Client( - access_token=payment_providers[provider]['credentials']['access_token'], - environment=payment_providers[provider]) - #~ environment = int('production' = payment_providers[provider]['environment']) - gocardless.environment = payment_providers[provider]['environment'] - gocardless.set_details(**payment_providers[provider]['credentials']) - merchant = gocardless.client.merchant() - - def lookup_provider_by_id(self, provider_id): - return PROVIDER_NAME.get(provider_id, None) - - def make_donation(self, amount, reference, redirect_success, redirect_failure): - if self.provider == 'paypal': - payment = paypal.Payment({ - "intent": "sale", - "payer": {"payment_method": "paypal"}, - "redirect_urls": { - "return_url": redirect_success, - "cancel_url": redirect_failure}, - - "transactions": [{ - "amount": { - "total": amount, - "currency": "GBP"}, - "description": reference}]}) - - payment_response = payment.create() - print('payment create') - if payment_response: - print(payment_response) - for link in payment.links: - if link.method == "REDIRECT": - redirect_url = str(link.href) - print(redirect_url) - return str(redirect_url) - else: - print("Error while creating payment:") - print(payment.error) - - if self.provider == 'gocardless': - return gocardless.client.new_bill_url( - float(amount), - name=reference, - redirect_uri=redirect_success) - - return 'Error something went wrong' - - def fetch_subscriptions(self): - if self.provider == 'gocardless': - merchant = gocardless.client.merchant() - for paying_member in merchant.subscriptions(): - user=paying_member.user() - print(dir(paying_member)) - print(paying_member.next_interval_start) - print(paying_member.status) - print(dir(paying_member.user())) - yield { - 'email': user.email, - 'start_date': paying_member.created_at, - 'reference': paying_member.id, - 'amount': paying_member.amount} - - - def confirm_subscription(self, args): - if self.provider == 'gocardless': - response = gocardless.client.confirm_resource(args) - subscription = gocardless.client.subscription(args.get('resource_id')) - return { - 'amount': subscription.amount, - 'start_date': subscription.created_at, - 'reference': subscription.id - } - - if self.provider == 'paypal': - print('subscribe_confirm') - payment_token = args.get('token', '') - billing_agreement_response = paypal.BillingAgreement.execute(payment_token) - amount = 0 - print(billing_agreement_response) - print(billing_agreement_response.id) - for row in billing_agreement_response.plan.payment_definitions: - amount = row.amount.value - - return { - 'amount': amount, - 'start_date': billing_agreement_response.start_date, - 'reference': billing_agreement_response.id - } - - return None - - def unsubscribe(self, reference): - if self.provider == 'gocardless': - print('unsubscribe gocardless') - subscription = gocardless.client.subscription(reference) - print(subscription.cancel()) - - if self.provider == 'paypal': - # this may be wrong - # ManageRecurringPaymentsProfileStatus - print(reference) - billing_plan = paypal.BillingAgreement.find(reference) - print(billing_plan) - print(billing_plan.error) - #~ billing_plan.replace([{"op": "replace","path": "/","value": {"state":"DELETED"}}]) - print(billing_plan.error) - #~ invoice = paypal.Invoice.find(reference) - options = { - "subject": "Cancelling membership", - "note": "Canceling invoice", - "send_to_merchant": True, - "send_to_payer": True - } - - if billing_plan.cancel(options): # return True or False - print("Invoice[%s] cancel successfully" % (invoice.id)) - else: - print(billing_plan.error) - - - def subscribe(self, amount, name, redirect_success, redirect_failure, interval_unit='month', interval_length='1'): - if self.provider == 'gocardless': - return gocardless.client.new_subscription_url( - amount=float(amount), - interval_length=interval_length, - interval_unit=interval_unit, - name=name, - redirect_uri=redirect_success) - - if self.provider == 'paypal': - billing_plan = paypal.BillingPlan({ - "name": name, - "description": "Membership subscription", - "merchant_preferences": { - "auto_bill_amount": "yes", - "cancel_url": redirect_failure, - "initial_fail_amount_action": "continue", - "max_fail_attempts": "1", - "return_url": redirect_success, - "setup_fee": { - "currency": "GBP", - "value": amount - } - }, - "payment_definitions": [{ - "amount": { - "currency": "GBP", - "value": amount - }, - "cycles": "0", - "frequency": interval_unit, - "frequency_interval": interval_length, - "name": "Regular 1", - "type": "REGULAR" - } - ], - "type": "INFINITE" - }) - print('create bill') - - response = billing_plan.create() - - billing_plan = paypal.BillingPlan.find(billing_plan.id) - - if billing_plan.activate(): - start_date = datetime.utcnow() + timedelta(minutes=10) - billing_agreement = paypal.BillingAgreement({ - "name": billing_plan.name, - "description": name, - "start_date": start_date.strftime('%Y-%m-%dT%H:%M:%SZ'), - "plan": {"id": str(billing_plan.id)}, - "payer": {"payment_method": "paypal"} - }) - - if billing_agreement.create(): - print('billing agreement id') - print(billing_agreement.id) - - for link in billing_agreement.links: - if link.rel == "approval_url": - approval_url = link.href - return approval_url - else: - print(billing_agreement.error) - print('failed') - - def confirm(self, args): - confirm_details = {} - confirm_details['successfull'] = False - print('---------------------') - print(args) - - from pprint import pprint - if self.provider == 'paypal': - print(args.get('paymentId')) - print(args.get('PayerID')) - payment = paypal.Payment.find(args.get('paymentId')) - pprint(payment) - print(pprint(payment)) - print(payment) - - confirm_details['name'] = payment['payer']['payer_info'].first_name + ' ' + payment['payer']['payer_info'].last_name - confirm_details['user'] = payment['payer']['payer_info'].email - confirm_details['status'] = payment.state - confirm_details['amount'] = payment['transactions'][0]['amount'].total - confirm_details['created'] = payment.create_time - confirm_details['reference'] = payment.id - pprint(confirm_details) - - - if payment.execute({"payer_id": args.get('PayerID')}): # return True or False - confirm_details['successfull'] = True - print("Payment[%s] execute successfully" % (args.get('paymentId'))) - else: - print(payment.error) - return confirm_details - - if self.provider == 'gocardless': - bill_id = args.get('resource_id') - gocardless.client.confirm_resource(args) - if bill_id: - bill = gocardless.client.bill(bill_id) - confirm_details['name'] = bill.name - confirm_details['user'] = bill.user - confirm_details['status'] = bill.status - confirm_details['amount'] = bill.amount - #~ confirm_details['amount_minus_fees'] = bill.amount_minus_fees - confirm_details['created'] = bill.created_at - confirm_details['reference'] = bill_id - confirm_details['successfull'] = True - return confirm_details - return None diff --git a/mhackspace/subscriptions/tests/test_payment_gateways.py b/mhackspace/subscriptions/tests/test_payment_gateways.py index 7d8c396..77f6408 100644 --- a/mhackspace/subscriptions/tests/test_payment_gateways.py +++ b/mhackspace/subscriptions/tests/test_payment_gateways.py @@ -2,34 +2,33 @@ # -*- coding: utf-8 -*- from test_plus.test import TestCase from unittest import skip -from mock import patch, Mock +from mock import patch, Mock, MagicMock +from mhackspace.users.models import Membership +import django.utils.timezone -from mhackspace.subscriptions.payments import payment, gocardless_provider, braintree_provider +from mhackspace.subscriptions.payments import gocardless_provider, braintree_provider +from mhackspace.subscriptions.tests.mocks import gocardlessMocks -class TestPaymentGatewaysGocardless(TestCase): +class TestPaymentGatewaysGocardless(gocardlessMocks): def setUp(self): - self.auth_gocardless() + super().setUp() + # self.date_now = django.utils.timezone.now() + # self.user = self.make_user() + # member = Membership() + # member.user = self.user + # member.payment = '20.00' + # member.date = self.date_now + # member.save() + # self.auth_gocardless() - @patch('mhackspace.subscriptions.payments.gocardless_pro.request.requests.get', autospec=True) - def auth_gocardless(self, mock_request): - # mock braintree initalisation request - mock_request.return_value = Mock(ok=True) - mock_request.return_value.json.return_value = { - "id": "1", - "created_at": "2011-11-18T17:07:09Z", - "access_token": "test_token", - "next_payout_date": "2011-11-18T17:07:09Z" - } - - with patch('gocardless.resources.Merchant') as mock_subscription: - self.provider = gocardless_provider() - return self.provider #self.provider @skip("Need to implement") - @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscription', autospec=True) + @patch('mhackspace.subscriptions.payments.gocardless_pro.Client.subscription', autospec=True) def test_unsubscribe(self, mock_subscription): + self.mock_success_responses() + # self.auth_gocardless() mock_subscription.return_value = Mock(success='success') mock_subscription.cancel.return_value = Mock( id='01', @@ -40,20 +39,14 @@ class TestPaymentGatewaysGocardless(TestCase): 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('reference'), '02') self.assertEqual(result.get('success'), 'success') - @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscription', autospec=True) - @patch('mhackspace.subscriptions.payments.gocardless_pro.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' - ) + def test_confirm_subscription_callback(self): + self.mock_success_responses() + membership = self.create_membership_record() + # self.auth_gocardless() + # mock_confirm.return_value = Mock(success='success') request_params = { 'resource_uri': 'http://gocardless/resource/url/01', @@ -63,66 +56,23 @@ class TestPaymentGatewaysGocardless(TestCase): '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') + # membership = Membership.objects.get(user=self.user) + result = self.provider.confirm_subscription( + membership=membership, + session=None, + provider_response={'redirect_flow_id': 'redirect_mock_url'}, + name='test') + self.assertEqual(result.get('amount'), '20.00') + self.assertEqual(result.get('reference'), '02') + self.assertEqual(result.get('success'), '200') 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=[item]) - - # mock out gocardless subscriptions method, and return our own values + self.mock_success_responses() 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'), '01') - self.assertEqual(item.get('start_date'), 'date') self.assertEqual(item.get('amount'), 20.00) - -class DisabledestPaymentGatewaysBraintree(TestCase): - @patch('mhackspace.subscriptions.payments.braintree.Configuration.configure') - def auth_braintree(self, mock_request): - # mock braintree initalisation request - mock_request.return_value = Mock(ok=True) - mock_request.return_value.json.return_value = { - "id": "1", - "created_at": "2011-11-18T17:07:09Z", - "access_token": "test_token", - "next_payout_date": "2011-11-18T17:07:09Z" - } - - self.provider = braintree_provider() - - @patch('mhackspace.subscriptions.payments.braintree.Subscription.search') - def test_fetch_subscription_braintree(self, mock_request): - provider = self.auth_braintree() - - items = [Mock( - id='01', - status='active', - amount=20.00, - reference='ref01', - created_at='date' - )] - items[-1].user.return_value = Mock(email='test@test.com') - - mock_request.return_value = items - 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('start_date'), 'date') - self.assertEqual(item.get('amount'), 20.00) diff --git a/mhackspace/subscriptions/tests/test_views.py b/mhackspace/subscriptions/tests/test_views.py index 18bdef6..ebe2756 100644 --- a/mhackspace/subscriptions/tests/test_views.py +++ b/mhackspace/subscriptions/tests/test_views.py @@ -8,7 +8,8 @@ from mock import patch, Mock from mhackspace.users.models import Membership from mhackspace.users.models import User -from mhackspace.subscriptions.payments import payment, gocardless_provider, braintree_provider +from mhackspace.subscriptions.payments import gocardless_provider +from mhackspace.subscriptions.tests.mocks import gocardlessMocks from ..views import ( MembershipCancelView, @@ -18,12 +19,13 @@ from ..views import ( ) -class BaseUserTestCase(TestCase): +class BaseUserTestCase(gocardlessMocks): fixtures = ['groups'] def setUp(self): - self.user = self.make_user() - self.user.save() + super().setUp() + # self.user = self.make_user() + # self.user.save() self.factory = RequestFactory() self.client = Client() self.client.login( @@ -32,17 +34,19 @@ class BaseUserTestCase(TestCase): class TestSubscriptionSuccessRedirectView(BaseUserTestCase): - @patch('mhackspace.subscriptions.payments.gocardless_provider', autospec=True) - @patch('mhackspace.subscriptions.views.select_provider', autospec=True) - def test_success_redirect_url(self, mock_subscription, mock_provider): - mock_subscription.return_value = mock_provider - mock_provider.confirm_subscription.return_value = { - 'amount': 20.00, - 'start_date': '2017-01-01T17:07:09Z', - 'reference': 'MH0001', - 'email': 'user@test.com', - 'success': True - } + # @patch('mhackspace.subscriptions.payments.gocardless_provider', autospec=True) + # @patch('mhackspace.subscriptions.views.select_provider', autospec=True) + def test_success_redirect_url(self): + self.mock_success_responses() + self.create_membership_record() + # mock_gocardless.subscriptions.create.return_value = 'temp' + # mock_provider.confirm_subscription.return_value = { + # 'amount': 20.00, + # 'start_date': '2017-01-01T17:07:09Z', + # 'reference': 'MH0001', + # 'email': 'user@test.com', + # 'success': True + # } response = self.client.post( reverse('join_hackspace_success', kwargs={'provider': 'gocardless'}), { @@ -65,19 +69,22 @@ class TestSubscriptionSuccessRedirectView(BaseUserTestCase): # print(self.user) self.assertRedirects( response, - expected_url=reverse('users:detail', kwargs={'username': self.user.username}), + expected_url='/accounts/login/?next=/membership/gocardless/success', status_code=302, target_status_code=200) - self.assertEqual( - view.get_redirect_url(provider ='gocardless'), - reverse('users:detail', kwargs={'username': self.user.username}) - ) + # self.assertEqual( + # view.get_redirect_url(provider ='gocardless'), + # reverse('users:detail', kwargs={'username': self.user.username}) + # ) + # view = Memhttps://www.youtube.com/bershipJoinSuccessView() + # view.request = request members = Membership.objects.all() self.assertEqual(members.count(), 1) - @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscription', autospec=True) - def test_failure_redirect_url(self, mock_obj): + # @patch('mhackspace.subscriptions.payments.gocardless_pro.client.subscriptions', autospec=True) + def test_failure_redirect_url(self): + self.mock_success_responses() # Instantiate the view directly. Never do this outside a test! # Generate a fake request request = self.factory.post( From 2e9a428761d80c5dce93e4b3c52d92736ea67716 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sun, 1 Oct 2017 14:12:41 +0100 Subject: [PATCH 19/22] Fix replacement error --- mhackspace/subscriptions/payments.py | 16 ++++++++-------- mhackspace/subscriptions/views.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mhackspace/subscriptions/payments.py b/mhackspace/subscriptions/payments.py index c316bb1..3a24cf5 100644 --- a/mhackspace/subscriptions/payments.py +++ b/mhackspace/subscriptions/payments.py @@ -36,8 +36,8 @@ class gocardless_provider: environment=payment_providers['gocardless']['environment']) # def subscribe_confirm(self, args): - # response = gocardless_proclient.confirm_resource(args) - # subscription = gocardless_proclient.subscription(args.get('resource_id')) + # response = gocardless_pro.client.confirm_resource(args) + # subscription = gocardless_pro.client.subscription(args.get('resource_id')) # return { # 'amount': subscription.amount, # 'start_date': subscription.created_at, @@ -82,9 +82,9 @@ class gocardless_provider: def cancel_subscription(self, reference): try: - subscription = gocardless_proclient.subscription(reference) + subscription = gocardless_pro.client.subscription(reference) response = subscription.cancel() - except gocardless_proexceptions.ClientError: + except gocardless_pro.exceptions.ClientError: return { 'success': False } @@ -120,8 +120,8 @@ class gocardless_provider: response = self.client.redirect_flows.get(r) # response = self.client.redirect_flows.get(provider_response.get('redirect_flow_id')) - # response = gocardless_proclient.confirm_resource(provider_response) - # subscription = gocardless_proclient.subscription(provider_response.get('resource_id')) + # response = gocardless_pro.client.confirm_resource(provider_response) + # subscription = gocardless_pro.client.subscription(provider_response.get('resource_id')) user_id = response.links.customer mandate_id = response.links.mandate # user = subscription.user() @@ -180,8 +180,8 @@ class braintree_provider: def confirm_subscription(self, args): if self.provider == 'gocardless': - response = gocardless_proclient.confirm_resource(args) - subscription = gocardless_proclient.subscription(args.get('resource_id')) + response = gocardless_pro.client.confirm_resource(args) + subscription = gocardless_pro.client.subscription(args.get('resource_id')) return { 'amount': subscription.amount, 'start_date': subscription.created_at, diff --git a/mhackspace/subscriptions/views.py b/mhackspace/subscriptions/views.py index 2f27080..618d0cb 100644 --- a/mhackspace/subscriptions/views.py +++ b/mhackspace/subscriptions/views.py @@ -68,7 +68,7 @@ class MembershipJoinView(LoginRequiredMixin, UpdateView): result = { 'email': self.request.user.email, 'reference': user_code, - 'amount': form_subscription.cleaned_data.get('amount', 20.00) * 0.01, + 'amount': form_subscription.cleaned_data.get('amount', 20.00), 'start_date': timezone.now() } From c1ed938b0496e7abfc26a1813a50f4246422a19e Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Sun, 1 Oct 2017 22:59:08 +0100 Subject: [PATCH 20/22] Cleanup, and better handling in the cancellation flow more work to come --- compose/django/start-dev.sh | 2 +- config/settings/local.py | 45 +++++++++++++++++++ config/settings/test.py | 5 ++- mhackspace/feeds/helper.py | 2 + mhackspace/subscriptions/helper.py | 11 +++++ mhackspace/subscriptions/payments.py | 27 +++++------ mhackspace/subscriptions/tests/mocks.py | 18 +++++--- .../tests/test_payment_gateways.py | 30 +++---------- mhackspace/subscriptions/views.py | 21 ++++----- mhackspace/templates/users/user_detail.html | 2 +- mhackspace/users/models.py | 20 ++++++--- 11 files changed, 115 insertions(+), 68 deletions(-) diff --git a/compose/django/start-dev.sh b/compose/django/start-dev.sh index 04e0698..9b8c8ef 100644 --- a/compose/django/start-dev.sh +++ b/compose/django/start-dev.sh @@ -1,3 +1,3 @@ #!/bin/sh python manage.py migrate -python manage.py runserver_plus 0.0.0.0:8000 +while true; do python manage.py runserver_plus 0.0.0.0:8000; sleep 2; done diff --git a/config/settings/local.py b/config/settings/local.py index 1b431e6..0af723e 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -99,5 +99,50 @@ CAPTCHA = { WHITENOISE_AUTOREFRESH = True WHITENOISE_USE_FINDERS = True +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s ' + '%(process)d %(thread)d %(message)s' + }, + }, + 'handlers': { + 'mail_admins': { + 'level': 'DEBUG', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + 'logfile': { + 'level':'DEBUG', + 'class':'logging.FileHandler', + 'filename': "%s/django.log" % ROOT_DIR, + }, + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins', 'logfile'], + 'level': 'ERROR', + 'propagate': True + }, + 'django.security.DisallowedHost': { + 'level': 'ERROR', + 'handlers': ['logfile', 'console', 'mail_admins'], + 'propagate': True + } + } +} + PAYMENT_PROVIDERS['gocardless']['redirect_url'] = 'http://127.0.0.1:8180' TEMPLATE_DEBUG = False diff --git a/config/settings/test.py b/config/settings/test.py index 98daf9b..f45bef3 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -67,5 +67,8 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [ ] DATABASES = { - 'default': {'ENGINE': 'django.db.backends.sqlite3'} + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(str(ROOT_DIR), 'cache/test_database.db'), + } } diff --git a/mhackspace/feeds/helper.py b/mhackspace/feeds/helper.py index 59a72af..434e3f1 100644 --- a/mhackspace/feeds/helper.py +++ b/mhackspace/feeds/helper.py @@ -53,6 +53,8 @@ def download_remote_images(): render_variations(result[0], image_variations, replace=True) article.save() except: + logger.exception(result) + logger.exception(result[0]) logger.exception('Unable to download remote image for %s' % article.original_image) diff --git a/mhackspace/subscriptions/helper.py b/mhackspace/subscriptions/helper.py index f060d41..e3683dc 100644 --- a/mhackspace/subscriptions/helper.py +++ b/mhackspace/subscriptions/helper.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals, absolute_import from django.contrib.auth.models import Group from django.utils.dateparse import parse_datetime from mhackspace.users.models import Membership +from mhackspace.users.models import MEMBERSHIP_CANCELLED def create_or_update_membership(user, signup_details, complete=False): @@ -34,3 +35,13 @@ def create_or_update_membership(user, signup_details, complete=False): group = Group.objects.get(name='members') user.groups.add(group) return True # Sign up finished + + +def cancel_membership(user): + member = Membership.objects.get(user=user) + member.status = MEMBERSHIP_CANCELLED + member.save() + + group = Group.objects.get(name='members') + user.groups.remove(group) + return True diff --git a/mhackspace/subscriptions/payments.py b/mhackspace/subscriptions/payments.py index 3a24cf5..fb1ec9c 100644 --- a/mhackspace/subscriptions/payments.py +++ b/mhackspace/subscriptions/payments.py @@ -35,16 +35,6 @@ class gocardless_provider: access_token=payment_providers['gocardless']['credentials']['access_token'], environment=payment_providers['gocardless']['environment']) - # def subscribe_confirm(self, args): - # response = gocardless_pro.client.confirm_resource(args) - # subscription = gocardless_pro.client.subscription(args.get('resource_id')) - # return { - # 'amount': subscription.amount, - # 'start_date': subscription.created_at, - # 'reference': subscription.id, - # 'success': response.success - # } - def fetch_customers(self): """Fetch list of customers payments""" for customer in self.client.customers.list().records: @@ -80,11 +70,18 @@ class gocardless_provider: def get_token(self): return 'N/A' - def cancel_subscription(self, reference): + def cancel_subscription(self, user, reference): try: - subscription = gocardless_pro.client.subscription(reference) - response = subscription.cancel() - except gocardless_pro.exceptions.ClientError: + subscription = self.client.subscriptions.get(reference) + response = self.client.subscriptions.cancel(reference) + except gocardless_pro.errors.InvalidApiUsageError as e: + if e.code is 404: + logger.info('Cancel subscription failed user not found %s %s' % (e.code, e)) + return { + 'success': False + } + except Exception as e: + logger.info('Cancel subscription failed unknown reason code %s %s' % (e.code, e)) return { 'success': False } @@ -92,7 +89,7 @@ class gocardless_provider: 'amount': subscription.amount, 'start_date': subscription.created_at, 'reference': subscription.id, - 'success': response.get('success', False) + 'success': True if response.get('status_code') is '200' else False } def create_subscription(self, user, session, amount, diff --git a/mhackspace/subscriptions/tests/mocks.py b/mhackspace/subscriptions/tests/mocks.py index 86d7d90..58c5ccd 100644 --- a/mhackspace/subscriptions/tests/mocks.py +++ b/mhackspace/subscriptions/tests/mocks.py @@ -35,14 +35,15 @@ class gocardlessMocks(TestCase): return self.provider def mock_success_responses(self): - - mock_list = MagicMock() - mock_list_records = MagicMock(side_effect=[Mock( - id='01', + subscription_properties = Mock( + id='02', status='active', amount=20.00, created_at='date' - )]) + ) + + mock_list = MagicMock() + mock_list_records = MagicMock(side_effect=[subscription_properties]) mock_list.records.return_value = mock_list_records self.provider.client.subscriptions.list = mock_list @@ -54,3 +55,10 @@ class gocardlessMocks(TestCase): created_at=self.date_now, api_response=ApiResponseStatus(status_code='200')) ) + + self.provider.client.subscriptions.get = Mock( + return_value=subscription_properties) + + self.provider.client.subscriptions.cancel = PropertyMock( + return_value={'status_code': '200'}) + diff --git a/mhackspace/subscriptions/tests/test_payment_gateways.py b/mhackspace/subscriptions/tests/test_payment_gateways.py index 77f6408..2a35305 100644 --- a/mhackspace/subscriptions/tests/test_payment_gateways.py +++ b/mhackspace/subscriptions/tests/test_payment_gateways.py @@ -14,39 +14,19 @@ class TestPaymentGatewaysGocardless(gocardlessMocks): def setUp(self): super().setUp() - # self.date_now = django.utils.timezone.now() - # self.user = self.make_user() - # member = Membership() - # member.user = self.user - # member.payment = '20.00' - # member.date = self.date_now - # member.save() - # self.auth_gocardless() - - @skip("Need to implement") - @patch('mhackspace.subscriptions.payments.gocardless_pro.Client.subscription', autospec=True) - def test_unsubscribe(self, mock_subscription): + def test_unsubscribe(self): self.mock_success_responses() - # self.auth_gocardless() - mock_subscription.return_value = Mock(success='success') - mock_subscription.cancel.return_value = Mock( - id='01', - status='active', - amount=20.00, - created_at='date' - ) - result = self.provider.cancel_subscription(reference='M01') + + result = self.provider.cancel_subscription(user=self.user, reference='M01') self.assertEqual(result.get('amount'), 20.00) self.assertEqual(result.get('reference'), '02') - self.assertEqual(result.get('success'), 'success') + self.assertEqual(result.get('success'), True) def test_confirm_subscription_callback(self): self.mock_success_responses() membership = self.create_membership_record() - # self.auth_gocardless() - # mock_confirm.return_value = Mock(success='success') request_params = { 'resource_uri': 'http://gocardless/resource/url/01', @@ -73,6 +53,6 @@ class TestPaymentGatewaysGocardless(gocardlessMocks): 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'), '01') + self.assertEqual(item.get('reference'), '02') self.assertEqual(item.get('amount'), 20.00) diff --git a/mhackspace/subscriptions/views.py b/mhackspace/subscriptions/views.py index 618d0cb..99d2851 100644 --- a/mhackspace/subscriptions/views.py +++ b/mhackspace/subscriptions/views.py @@ -14,7 +14,7 @@ from mhackspace.users.models import User, Membership from mhackspace.users.models import MEMBERSHIP_CANCELLED from mhackspace.users.forms import MembershipJoinForm from mhackspace.subscriptions.payments import select_provider -from mhackspace.subscriptions.helper import create_or_update_membership +from mhackspace.subscriptions.helper import create_or_update_membership, cancel_membership class MembershipCancelView(LoginRequiredMixin, RedirectView): @@ -28,21 +28,16 @@ class MembershipCancelView(LoginRequiredMixin, RedirectView): member = Membership.objects.filter(user=self.request.user).first() result = provider.cancel_subscription( + user=self.request.user, reference=member.reference ) - if result.get('success') is True: - # set membership to cancelled on success - member.status = MEMBERSHIP_CANCELLED - member.save() - - # remove user from group on success - group = Group.objects.get(name='members') - self.request.user.groups.remove(group) - messages.add_message( - self.request, - messages.SUCCESS, - 'Your membership has now been cancelled') + # if result.get('success') is True: + cancel_membership(user=self.request.user) + messages.add_message( + self.request, + messages.SUCCESS, + 'Your membership has now been cancelled') kwargs['username'] = self.request.user.get_username() return super(MembershipCancelView, self).get_redirect_url(*args, **kwargs) diff --git a/mhackspace/templates/users/user_detail.html b/mhackspace/templates/users/user_detail.html index a5a19ca..6cd00ad 100644 --- a/mhackspace/templates/users/user_detail.html +++ b/mhackspace/templates/users/user_detail.html @@ -34,7 +34,7 @@ {% endif %}
- {% if membership.get_status %} + {% if membership.is_active %}
Joined {{membership.date}}
diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index 84c1526..5507ec5 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -45,27 +45,28 @@ class Blurb(models.Model): skills = models.CharField(max_length=255) description = models.TextField() -MEMBERSHIP_CANCELLED = 0 +MEMBERSHIP_ACTIVE = 4 +MEMBERSHIP_CANCELLED = 4 MEMBERSHIP_STATUS_CHOICES = ( (0, 'Guest user'), - (1, 'Active membership'), + (MEMBERSHIP_ACTIVE, 'Active membership'), (3, 'Membership Expired'), - (4, 'Membership Cancelled') + (MEMBERSHIP_CANCELLED, 'Membership Cancelled') ) MEMBERSHIP_STRING = { 0: 'Guest user', - 1: 'Active membership', + MEMBERSHIP_ACTIVE: 'Active membership', 3: 'Membership Expired', - 4: 'Membership Cancelled' + MEMBERSHIP_CANCELLED: 'Membership Cancelled' } MEMBERSHIP_STATUS = { 'signup': 0, # This means the user has not completed signup - 'active': 1, + 'active': MEMBERSHIP_ACTIVE, 'expired': 3, - 'cancelled': 4 + 'cancelled': MEMBERSHIP_CANCELLED } @@ -87,6 +88,11 @@ class Membership(models.Model): def get_status(self): return MEMBERSHIP_STRING[self.status] + def is_active(self): + if self.status is MEMBERSHIP_ACTIVE: + return True + return False + def lookup_status(name): if not name: return 0 From d642109c130e8b33eaf3551061e45a21cbe1b298 Mon Sep 17 00:00:00 2001 From: Oliver Marks Date: Mon, 2 Oct 2017 22:38:26 +0100 Subject: [PATCH 21/22] Rework some of the payment sign up system --- mhackspace/subscriptions/helper.py | 11 +++++++---- mhackspace/subscriptions/payments.py | 5 +---- mhackspace/subscriptions/views.py | 2 ++ mhackspace/users/models.py | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/mhackspace/subscriptions/helper.py b/mhackspace/subscriptions/helper.py index e3683dc..789d649 100644 --- a/mhackspace/subscriptions/helper.py +++ b/mhackspace/subscriptions/helper.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals, absolute_import +from datetime import datetime from django.contrib.auth.models import Group from django.utils.dateparse import parse_datetime from mhackspace.users.models import Membership -from mhackspace.users.models import MEMBERSHIP_CANCELLED +from mhackspace.users.models import MEMBERSHIP_CANCELLED, MEMBERSHIP_ACTIVE def create_or_update_membership(user, signup_details, complete=False): + start_date = signup_details.get('start_date') + if not isinstance(start_date, datetime): + start_date = parse_datetime(start_date) try: member = Membership.objects.get(email=signup_details.get('email')) # Only update if newer than last record, this way we only get the latest status # cancellation and changed payment will not be counted against current status - start_date = parse_datetime(signup_details.get('start_date')) if start_date < member.date: return True except Membership.DoesNotExist: @@ -19,11 +22,11 @@ def create_or_update_membership(user, signup_details, complete=False): member.user = user if complete is True: - member.status = Membership.lookup_status(name=signup_details.get('status')) + member.status = MEMBERSHIP_ACTIVE 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.date = start_date member.save() diff --git a/mhackspace/subscriptions/payments.py b/mhackspace/subscriptions/payments.py index fb1ec9c..641d952 100644 --- a/mhackspace/subscriptions/payments.py +++ b/mhackspace/subscriptions/payments.py @@ -114,14 +114,11 @@ class gocardless_provider: # response = self.client.redirect_flows.complete(r, params={ # "session_token": session # }) - response = self.client.redirect_flows.get(r) + response = self.client.redirect_flows.complete(r, params={'session_token': session}) # response = self.client.redirect_flows.get(provider_response.get('redirect_flow_id')) - # response = gocardless_pro.client.confirm_resource(provider_response) - # subscription = gocardless_pro.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) diff --git a/mhackspace/subscriptions/views.py b/mhackspace/subscriptions/views.py index 99d2851..bef25b9 100644 --- a/mhackspace/subscriptions/views.py +++ b/mhackspace/subscriptions/views.py @@ -106,6 +106,8 @@ class MembershipJoinSuccessView(LoginRequiredMixin, RedirectView): ) # if something went wrong return to profile with an error + + result['start_date'] = timezone.now() if result.get('success') is False: messages.add_message( self.request, diff --git a/mhackspace/users/models.py b/mhackspace/users/models.py index 5507ec5..bd2031d 100644 --- a/mhackspace/users/models.py +++ b/mhackspace/users/models.py @@ -45,7 +45,7 @@ class Blurb(models.Model): skills = models.CharField(max_length=255) description = models.TextField() -MEMBERSHIP_ACTIVE = 4 +MEMBERSHIP_ACTIVE = 1 MEMBERSHIP_CANCELLED = 4 MEMBERSHIP_STATUS_CHOICES = ( From 3c3f5436b42b8e97a1a38aa5f995cb287d134e5a Mon Sep 17 00:00:00 2001 From: Oly Date: Tue, 3 Oct 2017 13:49:18 +0100 Subject: [PATCH 22/22] Handle members group not existing, change update loop it no longer needs to return --- mhackspace/base/tests.py | 27 +++++++++++++++++++ mhackspace/subscriptions/helper.py | 11 ++++++-- .../commands/update_membership_status.py | 9 +++---- mhackspace/users/tasks.py | 3 +-- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 mhackspace/base/tests.py diff --git a/mhackspace/base/tests.py b/mhackspace/base/tests.py new file mode 100644 index 0000000..3793b3d --- /dev/null +++ b/mhackspace/base/tests.py @@ -0,0 +1,27 @@ +from test_plus.test import TestCase +from mhackspace.users.models import Membership +from mhackspace.users.models import User +from django.contrib.auth.models import Group + +from mhackspace.subscriptions.management.commands.update_membership_status import update_subscriptions + +# this needs mocking +class TestTasks(TestCase): + def setUp(self): + self.user1 = self.make_user('u1') + self.user2 = self.make_user('u2') + self.group = Group(name='members') + self.group.save() + + def test_refresh_subscriptions(self): + membership_count = Membership.objects.all().delete() + user_count = User.objects.all().count() + membership_count = Membership.objects.all().count() + self.assertEquals(0, membership_count) + self.assertEquals(2, user_count) + + update_subscriptions(provider_name='gocardless') + + membership_count = Membership.objects.all().count() + self.assertEquals(2, membership_count) + self.assertEquals(2, user_count) diff --git a/mhackspace/subscriptions/helper.py b/mhackspace/subscriptions/helper.py index 789d649..dee8d5b 100644 --- a/mhackspace/subscriptions/helper.py +++ b/mhackspace/subscriptions/helper.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals, absolute_import +import logging from datetime import datetime from django.contrib.auth.models import Group from django.utils.dateparse import parse_datetime from mhackspace.users.models import Membership from mhackspace.users.models import MEMBERSHIP_CANCELLED, MEMBERSHIP_ACTIVE +logger = logging.getLogger(__name__) + def create_or_update_membership(user, signup_details, complete=False): start_date = signup_details.get('start_date') @@ -34,9 +37,13 @@ def create_or_update_membership(user, signup_details, complete=False): return False # sign up not completed # add user to group on success + if user: - group = Group.objects.get(name='members') - user.groups.add(group) + try: + group = Group.objects.get(name='members') + user.groups.add(group) + except: + logger.error('Members group does not exist') return True # Sign up finished diff --git a/mhackspace/subscriptions/management/commands/update_membership_status.py b/mhackspace/subscriptions/management/commands/update_membership_status.py index 56af20e..f3accb9 100644 --- a/mhackspace/subscriptions/management/commands/update_membership_status.py +++ b/mhackspace/subscriptions/management/commands/update_membership_status.py @@ -14,7 +14,6 @@ def update_subscriptions(provider_name): provider = select_provider('gocardless') Membership.objects.all().delete() - subscriptions = [] group = Group.objects.get(name='members') @@ -26,10 +25,10 @@ def update_subscriptions(provider_name): except User.DoesNotExist: user_model = None - create_or_update_membership(user=user_model, - signup_details=sub, - complete=True) - yield model_to_dict(subscriptions[-1]) + create_or_update_membership( + user=user_model, + signup_details=sub, + complete=True) class Command(BaseCommand): diff --git a/mhackspace/users/tasks.py b/mhackspace/users/tasks.py index 9a72ff8..d733d3e 100644 --- a/mhackspace/users/tasks.py +++ b/mhackspace/users/tasks.py @@ -4,5 +4,4 @@ from mhackspace.subscriptions.management.commands.update_membership_status impor @shared_task def update_users_memebership_status(): - for user in update_subscriptions(provider_name='gocardless'): - continue + update_subscriptions(provider_name='gocardless')