Compare commits

...

38 Commits

Author SHA1 Message Date
ducoterra
6610c2896b add visitor model and api 2020-04-26 12:05:03 -04:00
ducoterra
15c5f5293a position fixed, overflow auto 2020-04-25 19:41:55 -04:00
ducoterra
f11f9a0d97 attempt fix with overflow 2020-04-25 19:35:09 -04:00
ducoterra
c80ef7441d change user-scalable 2020-04-25 19:28:56 -04:00
ducoterra
a2e7a92280 prod deploy 2020-04-25 19:17:52 -04:00
ducoterra
b4ef050e1f test vars 2020-04-25 19:11:27 -04:00
ducoterra
c09558c0ff manual approval to prod 2020-04-25 19:04:21 -04:00
ducoterra
c3666783e4 disable zooming 2020-04-25 18:25:26 -04:00
ducoterra
54c6336e22 proper slash handling in button.js 2020-04-25 17:07:45 -04:00
ducoterra
7da888aa09 proper / on button 2020-04-25 16:37:53 -04:00
ducoterra
07d98bf11d remove admin panel 2020-04-25 15:54:10 -04:00
ducoterra
4584ba0143 split out helper functions from button.js 2020-04-25 15:23:12 -04:00
ducoterra
6feac7ef2e apply to html and body, not button 2020-04-25 15:14:01 -04:00
ducoterra
5efb93ea68 don't zoom on double tap 2020-04-25 15:03:03 -04:00
ducoterra
1ee6b890ef remove snippets urls for now 2020-04-25 12:58:31 -04:00
ducoterra
18aab648c6 you have got to be kidding me 2020-04-25 12:34:03 -04:00
ducoterra
57328a7fc8 need secret mounts 2020-04-25 12:30:04 -04:00
ducoterra
948569a659 spacing... spacing 2020-04-25 12:21:31 -04:00
ducoterra
50cdfd8180 wrong configmap 2020-04-25 12:14:42 -04:00
ducoterra
8393efa3a6 fix yaml find and replace 2020-04-25 12:08:03 -04:00
ducoterra
b3db1816bb list dir 2020-04-25 12:01:07 -04:00
ducoterra
7123f4c389 rogue ' 2020-04-25 11:57:16 -04:00
ducoterra
477ddfe165 forgot quote 2020-04-25 11:52:15 -04:00
ducoterra
6bc472f7fe fix default environment vars 2020-04-25 11:20:15 -04:00
ducoterra
41f4549ffd Merge branch 'master' of gitlab.ducoterra.net:ducoterra/ci_builder 2020-04-25 11:15:57 -04:00
ducoterra
7dd45cc2e8 explain how to use environment variables 2020-04-25 11:15:42 -04:00
ducoterra
8ec174b2c3 default secret in manage.py 2020-04-25 11:15:31 -04:00
ducoterra
7868867908 new standard deploy templates 2020-04-25 11:15:23 -04:00
Reese
d2bf889e1f Update .gitlab-ci.yml 2020-04-25 02:32:29 +00:00
Reese
a70aa8840f Update Dockerfile 2020-04-25 02:32:06 +00:00
ducoterra
8404d43222 use venv for tests 2020-04-24 22:10:36 -04:00
ducoterra
f31106dd29 add venv 2020-04-24 22:06:56 -04:00
ducoterra
fbd8b0e2a7 python -m 2020-04-24 21:59:30 -04:00
Reese
b04ef5a579 Security fix: don’t run as root 2020-04-25 01:43:46 +00:00
ducoterra
e01bf28646 collectstatic 2020-04-24 21:26:21 -04:00
ducoterra
f362194c5e add README 2020-04-24 21:20:12 -04:00
ducoterra
bdc4c90705 wrong port for the service 2020-04-24 21:18:28 -04:00
ducoterra
b309e5fa4a fail safe 2020-04-24 21:14:32 -04:00
26 changed files with 287 additions and 121 deletions

View File

@@ -1,7 +1,6 @@
variables: variables:
CI_PROJECT_DIR: "." CI_PROJECT_DIR: "."
CI_REGISTRY_IMAGE: hub.ducoterra.net/ducoterra/mysite CI_REGISTRY_IMAGE: hub.ducoterra.net/ducoterra/mysite
DEPLOY: test
stages: stages:
- build - build
@@ -17,34 +16,61 @@ build:
name: gcr.io/kaniko-project/executor:debug name: gcr.io/kaniko-project/executor:debug
entrypoint: [""] entrypoint: [""]
script: script:
- echo $DEPLOY
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
test: test:
stage: test
only: only:
variables: variables:
- $CI_COMMIT_TAG - $CI_COMMIT_TAG
stage: test
image: image:
name: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG name: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
entrypoint: [""] entrypoint: [""]
script: script:
- python manage.py test - python manage.py test
deploy: deploy_to_test:
variables:
DEPLOY: test
stage: deploy
only: only:
variables: variables:
- $CI_COMMIT_TAG - $CI_COMMIT_TAG
stage: deploy
image: image:
name: debian:latest name: debian:10
entrypoint: [""] entrypoint: [""]
script: script:
- echo $CI_REGISTRY_IMAGE
- apt -qq update >> /dev/null && apt -qq install -y curl gettext >> /dev/null - apt -qq update >> /dev/null && apt -qq install -y curl gettext >> /dev/null
- curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl - curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
- chmod +x ./kubectl - chmod +x ./kubectl
- envsubst < k8s/deploy.yaml > out.yaml - mkdir /deploy
- mv out.yaml k8s/deploy.yaml - for f in $(find k8s -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
- ./kubectl apply -f k8s - for f in $(find k8s/test -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
- ./kubectl apply -f /deploy
- ./kubectl rollout status deploy $DEPLOY - ./kubectl rollout status deploy $DEPLOY
- ./kubectl exec $(./kubectl get pods --selector=app=$DEPLOY --output=jsonpath='{.items[*].metadata.name}') -- python manage.py migrate - POD=$(./kubectl get pods --selector=app=$DEPLOY --output=jsonpath='{.items[*].metadata.name}')
- ./kubectl exec $POD -- python manage.py migrate
deploy_to_prod:
variables:
DEPLOY: prod
stage: deploy
only:
variables:
- $CI_COMMIT_TAG
when: manual
image:
name: debian:10
entrypoint: [""]
script:
- apt -qq update >> /dev/null && apt -qq install -y curl gettext >> /dev/null
- curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
- chmod +x ./kubectl
- mkdir /deploy
- for f in $(find k8s -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
- for f in $(find k8s/prod -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
- ./kubectl apply -f /deploy
- ./kubectl rollout status deploy $DEPLOY
- POD=$(./kubectl get pods --selector=app=$DEPLOY --output=jsonpath='{.items[*].metadata.name}')
- ./kubectl exec $POD -- python manage.py migrate

13
.vscode/launch.json vendored
View File

@@ -5,7 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python: Django", "name": "Test",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/manage.py", "program": "${workspaceFolder}/manage.py",
@@ -13,6 +13,17 @@
"test", "test",
], ],
"django": true "django": true
},
{
"name": "Run Server",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": [
"runserver",
"--noreload"
],
"django": true
} }
] ]
} }

View File

@@ -8,4 +8,10 @@ COPY manage.py manage.py
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
RUN useradd -ms /bin/bash django
RUN chown -R django .
USER django
RUN python manage.py collectstatic
CMD ["gunicorn","-b",":8000", "-w", "4", "config.wsgi"] CMD ["gunicorn","-b",":8000", "-w", "4", "config.wsgi"]

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
# CI Builder
My CI testing pipeline for a django project.
[![pipeline status](http://gitlab.ducoterra.net/ducoterra/ci_builder/badges/master/pipeline.svg)](http://gitlab.ducoterra.net/ducoterra/ci_builder/-/commits/master)
## Django Environment Variables
### Django Secret
```bash
kubectl create secret generic django-secrets --from-literal=SECRET_KEY=$(python -c "import secrets ; print(secrets.token_urlsafe(32))")
```
### Django Allowed Hosts
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
ALLOWED_HOSTS: localhost,test.ducoterra.net
```

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.5 on 2020-04-26 15:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Visitor',
fields=[
('name', models.CharField(max_length=255, primary_key=True, serialize=False)),
('clicked', models.IntegerField(blank=True, default=0)),
('first_pressed', models.DateTimeField(auto_now_add=True)),
('last_pressed', models.DateTimeField(auto_now=True)),
],
),
migrations.DeleteModel(
name='Snippet',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.5 on 2020-04-26 15:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_auto_20200426_1517'),
]
operations = [
migrations.AlterField(
model_name='visitor',
name='first_pressed',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='visitor',
name='last_pressed',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,19 +1,11 @@
from django.db import models from django.db import models
from pygments.lexers import get_all_lexers from datetime import datetime
from pygments.styles import get_all_styles
LEXERS = [item for item in get_all_lexers() if item[1]] class Visitor(models.Model):
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS]) name = models.CharField(
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()]) primary_key=True,
max_length = 255
)
class Snippet(models.Model): clicked = models.IntegerField(default = 0, blank=True)
created = models.DateTimeField(auto_now_add=True) first_pressed = models.DateTimeField(blank=True, null=True)
title = models.CharField(max_length=100, blank=True, default='') last_pressed = models.DateTimeField(blank=True, null=True)
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta:
ordering = ['created']

View File

@@ -1,29 +1,23 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES from .models import *
class SnippetSerializer(serializers.Serializer): class VisitorSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True) name = serializers.CharField(required=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=100) clicked = serializers.IntegerField(read_only=True)
code = serializers.CharField(style={'base_template': 'textarea.html'}) first_pressed = serializers.DateTimeField(read_only=True)
linenos = serializers.BooleanField(required=False) last_pressed = serializers.DateTimeField(read_only=True)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
def create(self, validated_data): def create(self, validated_data):
""" """
Create and return a new `Snippet` instance, given the validated data. Create and return a new `Visitor` instance, given the validated data.
""" """
return Snippet.objects.create(**validated_data) return Visitor.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
""" """
Update and return an existing `Snippet` instance, given the validated data. Update and return an existing `Snippet` instance, given the validated data.
""" """
instance.title = validated_data.get('title', instance.title) instance.name = validated_data.get('name', instance.name)
instance.code = validated_data.get('code', instance.code)
instance.linenos = validated_data.get('linenos', instance.linenos)
instance.language = validated_data.get('language', instance.language)
instance.style = validated_data.get('style', instance.style)
instance.save() instance.save()
return instance return instance

View File

@@ -1,42 +1,53 @@
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser, User
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from django.db.utils import IntegrityError
from .views import SnippetList, SnippetDetail from .views import *
class SimpleTest(TestCase): class SimpleTest(TestCase):
def setUp(self): def setUp(self):
# Every test needs access to the request factory. # Every test needs access to the request factory.
self.factory = RequestFactory() self.factory = RequestFactory()
self.user = User.objects.create_user( # self.user = User.objects.create_user(
username='testuser', email='test@test.test', password='testpass') # username='testuser', email='test@test.test', password='testpass')
def test_snippets(self): def test_list_visitors(self):
# Create an instance of a GET request. # Create an instance of a GET request.
request = self.factory.get('/snippets') request = self.factory.get('/visitors')
# Recall that middleware are not supported. You can simulate a # Recall that middleware are not supported. You can simulate a
# logged-in user by setting request.user manually. # logged-in user by setting request.user manually.
request.user = self.user # request.user = self.user
# Or you can simulate an anonymous user by setting request.user to # Or you can simulate an anonymous user by setting request.user to
# an AnonymousUser instance. # an AnonymousUser instance.
# request.user = AnonymousUser() # request.user = AnonymousUser()
# Test my_view() as if it were deployed at /customer/details # Test my_view() as if it were deployed at /customer/details
response = SnippetList.as_view()(request) response = VisitorList.as_view()(request)
# Use this syntax for class-based views. # Use this syntax for class-based views.
# response = MyView.as_view()(request) # response = MyView.as_view()(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
request = self.factory.post('/snippets', data={ def test_add_visitor(self):
'title': 'test1', request = self.factory.post('/visitors', data={
'code': '() => {console.log("hello")};', 'name': 'test',
'lineos': False,
'language': 'js',
'style': 'abap'
}, },
content_type='application/json' content_type='application/json'
) )
response = SnippetList.as_view()(request) response = VisitorList.as_view()(request)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
vis = Visitor.objects.get(name='test')
self.assertEqual(vis.clicked, 0)
self.assertIsNone(vis.first_pressed)
self.assertIsNone(vis.last_pressed)
with self.assertRaises(IntegrityError):
request = self.factory.post('/visitors', data={
'name': 'test',
},
content_type='application/json'
)
response = VisitorList.as_view()(request)

View File

@@ -3,8 +3,8 @@ from rest_framework.urlpatterns import format_suffix_patterns
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('snippets/', views.SnippetList.as_view()), path('visitor/', views.VisitorList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()), path('visitor/<str:pk>/', views.VisitorDetail.as_view()),
] ]
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)

View File

@@ -1,15 +1,15 @@
from .models import Snippet from .models import *
from .serializers import SnippetSerializer from .serializers import *
from rest_framework import generics from rest_framework import generics
class SnippetList(generics.ListCreateAPIView): class VisitorList(generics.ListCreateAPIView):
# Add comments here # Add comments here
queryset = Snippet.objects.all() queryset = Visitor.objects.all()
serializer_class = SnippetSerializer serializer_class = VisitorSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): class VisitorDetail(generics.RetrieveUpdateDestroyAPIView):
# Add comments here # Add comments here
queryset = Snippet.objects.all() queryset = Visitor.objects.all()
serializer_class = SnippetSerializer serializer_class = VisitorSerializer

View File

@@ -20,13 +20,12 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'b8fi9=f-qj=@-#1iru34-f@a6pzfysgrf(1n_&d=ur%!1w$q*w' SECRET_KEY = os.getenv("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True if os.getenv("DEBUG") == "True" else False
ALLOWED_HOSTS = ["localhost", "test.ducoterra.net"]
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",")
# Application definition # Application definition
@@ -122,3 +121,4 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.0/howto/static-files/ # https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

View File

@@ -18,7 +18,7 @@ from django.urls import path, include
from django.http import JsonResponse from django.http import JsonResponse
urlpatterns = [ urlpatterns = [
path('', include('api.urls')), path('api/', include('api.urls')),
path('', include('ui.urls')), path('', include('ui.urls')),
path('admin/', admin.site.urls), # path('admin/', admin.site.urls),
] ]

6
k8s/configmap.yaml Normal file
View File

@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: $DEPLOY
data:
ALLOWED_HOSTS: localhost,$DEPLOY.ducoterra.net

View File

@@ -1,22 +1,27 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: test name: $DEPLOY
spec: spec:
selector: selector:
matchLabels: matchLabels:
app: test app: $DEPLOY
template: template:
metadata: metadata:
labels: labels:
app: test app: $DEPLOY
spec: spec:
containers: containers:
- name: test - name: $DEPLOY
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
envFrom:
- configMapRef:
name: $DEPLOY
- secretRef:
name: django-secrets
volumeMounts: volumeMounts:
- mountPath: /app/db - mountPath: /app/db
name: test name: $DEPLOY
resources: resources:
limits: limits:
memory: "256Mi" memory: "256Mi"
@@ -27,6 +32,6 @@ spec:
ports: ports:
- containerPort: 8000 - containerPort: 8000
volumes: volumes:
- name: test - name: $DEPLOY
persistentVolumeClaim: persistentVolumeClaim:
claimName: test claimName: $DEPLOY

View File

@@ -3,18 +3,18 @@ kind: Ingress
metadata: metadata:
annotations: annotations:
ingress.kubernetes.io/ssl-redirect: "true" ingress.kubernetes.io/ssl-redirect: "true"
name: test name: $DEPLOY
spec: spec:
tls: tls:
- hosts: - hosts:
- test.ducoterra.net - $DEPLOY.ducoterra.net
secretName: letsencrypt secretName: letsencrypt
rules: rules:
- host: test.ducoterra.net - host: $DEPLOY.ducoterra.net
http: http:
paths: paths:
- backend: - backend:
serviceName: test serviceName: $DEPLOY
servicePort: 8000 servicePort: 8000
--- ---
@@ -22,33 +22,33 @@ spec:
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: test-external-tls name: $DEPLOY-external-tls
spec: spec:
entryPoints: entryPoints:
- websecure - websecure
tls: tls:
secretName: letsencrypt secretName: letsencrypt
routes: routes:
- match: Host(`test.ducoterra.net`) - match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule kind: Rule
services: services:
- name: test-web - name: $DEPLOY
port: 80 port: 8000
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: test-external-web name: $DEPLOY-external-web
spec: spec:
entryPoints: entryPoints:
- web - web
routes: routes:
- match: Host(`test.ducoterra.net`) - match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule kind: Rule
services: services:
- name: test-web - name: $DEPLOY
port: 80 port: 8000
middlewares: middlewares:
- name: httpsredirect - name: httpsredirect

View File

@@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: test name: $DEPLOY
spec: spec:
storageClassName: nfs-encrypted storageClassName: nfs-encrypted
accessModes: accessModes:

View File

@@ -1,10 +1,10 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: test name: $DEPLOY
spec: spec:
selector: selector:
app: test app: $DEPLOY
ports: ports:
- port: 8000 - port: 8000
targetPort: 8000 targetPort: 8000

18
k8s/test/ingress.yaml Normal file
View File

@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/ssl-redirect: "true"
name: $DEPLOY
spec:
tls:
- hosts:
- $DEPLOY.ducoterra.net
secretName: letsencrypt
rules:
- host: $DEPLOY.ducoterra.net
http:
paths:
- backend:
serviceName: $DEPLOY
servicePort: 8000

View File

@@ -5,6 +5,8 @@ import sys
def main(): def main():
os.environ.setdefault('DEBUG', 'True')
os.environ.setdefault('SECRET_KEY', 'SeVOOxOHISQZv82RfCPds0B2l8M6jGju4G8F-GcuSrc')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line

View File

@@ -0,0 +1,18 @@
html, body {
height: 100%;
width: 100%;
position: fixed;
overflow: hidden;
}
.section, .container {
height: 100%;
}
.container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
text-align: center;
}

View File

@@ -1,26 +1,12 @@
function getCookie(name) { const csrftoken = getCookie('csrftoken');
var cookieValue = null; const button = document.getElementById("BUTTON");
if (document.cookie && document.cookie !== '') { const count = document.getElementById("COUNT");
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
var button = document.getElementById("BUTTON");
var count = document.getElementById("COUNT");
// when button is clicked submit an empty post request
button.addEventListener("click", event => { button.addEventListener("click", event => {
button.disabled = true; button.disabled = true;
fetch('/button', { button.classList.add("is-loading");
fetch('/button/', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -34,7 +20,9 @@ button.addEventListener("click", event => {
count.innerText = data.pressed; count.innerText = data.pressed;
}).finally(() => { }).finally(() => {
button.disabled = false; button.disabled = false;
button.classList.remove("is-loading");
}); });
}); });
// when the page is loaded automatically select the button
button.focus(); button.focus();

16
ui/static/ui/helper.js Normal file
View File

@@ -0,0 +1,16 @@
// get cookies when fetching with django
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}

View File

@@ -7,10 +7,10 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'ui/helper.js' %}"></script>
<script src="{% static 'ui/button.js' %}"></script> <script src="{% static 'ui/button.js' %}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<section class="section"> <section class="section">
<div class="container"> <div class="container">

View File

@@ -11,13 +11,13 @@ class SimpleTest(TestCase):
username='testuser', email='test@test.test', password='testpass') username='testuser', email='test@test.test', password='testpass')
def test_button(self): def test_button(self):
# Create an instance of a GET request. # Test button page returns 200
request = self.factory.get('/snippets') request = self.factory.get('/button')
request.user = self.user
request.session = self.client.session request.session = self.client.session
response = button(request) response = button(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Test button post returns 200 and increments button to 1
request = self.factory.post( request = self.factory.post(
'/button', '/button',
data={}, data={},
@@ -28,6 +28,7 @@ class SimpleTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(request.session.get('pressed'), 1) self.assertEqual(request.session.get('pressed'), 1)
# Test second click increments button to 2
response = button(request) response = button(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(request.session.get('pressed'), 2) self.assertEqual(request.session.get('pressed'), 2)

View File

@@ -2,5 +2,5 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('button', views.button, name = 'button'), path('button/', views.button, name = 'button'),
] ]