Compare commits

..

1 Commits

Author SHA1 Message Date
ducoterra
6610c2896b add visitor model and api 2020-04-26 12:05:03 -04:00
35 changed files with 236 additions and 680 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,3 @@
venv/ venv/
__pycache__/ __pycache__/
db/ db/
staticfiles/
.vscode/settings.json

View File

@@ -1,7 +1,6 @@
variables: variables:
CI_PROJECT_DIR: "." CI_PROJECT_DIR: "."
CI_REGISTRY_IMAGE: hub.ducoterra.net/ducoterra/button CI_REGISTRY_IMAGE: hub.ducoterra.net/ducoterra/mysite
DEPLOY: button
stages: stages:
- build - build
@@ -17,25 +16,23 @@ 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 stage: test
variables:
POSTGRES_PASSWORD: postgres
only: only:
variables: variables:
- $CI_COMMIT_TAG - $CI_COMMIT_TAG
services:
- name: postgres:12
alias: postgres
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 stage: deploy
only: only:
variables: variables:
@@ -45,10 +42,35 @@ deploy:
entrypoint: [""] entrypoint: [""]
script: script:
- 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 -o /usr/bin/kubectl -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 /usr/bin/kubectl - chmod +x ./kubectl
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash - mkdir /deploy
- helm upgrade --install $DEPLOY ./helm --set image=$CI_REGISTRY_IMAGE --set tag=$CI_COMMIT_TAG - for f in $(find k8s -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
- sleep 10 - for f in $(find k8s/test -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
- POD=$(kubectl get pods --selector=app=$DEPLOY --output=jsonpath='{.items[*].metadata.name}') - ./kubectl apply -f /deploy
- kubectl exec $POD -- python manage.py migrate - ./kubectl rollout status deploy $DEPLOY
- 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

@@ -12,11 +12,7 @@
"args": [ "args": [
"test", "test",
], ],
"env": { "django": true
"DB_HOST": "localhost"
},
"django": true,
"preLaunchTask": "docker-compose up"
}, },
{ {
"name": "Run Server", "name": "Run Server",
@@ -25,12 +21,9 @@
"program": "${workspaceFolder}/manage.py", "program": "${workspaceFolder}/manage.py",
"args": [ "args": [
"runserver", "runserver",
"--noreload"
], ],
"env": { "django": true
"DB_HOST": "localhost"
},
"django": true,
"preLaunchTask": "docker-compose up"
} }
] ]
} }

10
.vscode/tasks.json vendored
View File

@@ -1,10 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "docker-compose up",
"command": "docker-compose up -d",
"type": "shell"
}
]
}

View File

@@ -2,4 +2,23 @@
My CI testing pipeline for a django project. 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) [![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

@@ -24,7 +24,6 @@ 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 if os.getenv("DEBUG") == "True" else False DEBUG = True if os.getenv("DEBUG") == "True" else False
LOGGING_CONFIG = None
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",")
@@ -79,12 +78,8 @@ WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.getenv('DB_NAME', 'postgres'), 'NAME': os.path.join(BASE_DIR, 'db/db.sqlite3'),
'USER': os.getenv('POSTGRES_USER', 'postgres'),
'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'postgres'),
'HOST': os.getenv('DB_HOST', 'postgres'),
'PORT': os.getenv('DB_PORT', '5432'),
} }
} }

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('api/', 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),
] ]

View File

@@ -1,56 +0,0 @@
version: '3.5'
services:
button:
build: .
labels:
- "traefik.http.routers.button.rule=Host(`button.localhost`)"
- "traefik.http.services.button-service.loadbalancer.server.port=8000"
- "traefik.http.middlewares.btn-ratelimit.ratelimit.average=50"
- "traefik.http.middlewares.btn-ratelimit.ratelimit.burst=10"
- "traefik.http.routers.button.middlewares=btn-ratelimit@docker"
volumes:
- ./manage.py:/app/manage.py
- ./config:/app/config
- ./api:/app/api
environment:
- DEBUG=True
- SECRET_KEY=secret
- ALLOWED_HOSTS=button.localhost
- DJANGO_SUPERUSER_PASSWORD=django
postgres:
image: postgres:12
volumes:
- data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
pgadmin:
image: dpage/pgadmin4:4
labels:
- "traefik.http.routers.pgadmin.rule=Host(`pgadmin.localhost`)"
- "traefik.http.services.pgadmin-service.loadbalancer.server.port=80"
volumes:
- pgadmin:/var/lib/pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: postgres
PGADMIN_DEFAULT_PASSWORD: postgres
traefik:
image: traefik:v2.2
labels:
- "traefik.http.routers.traefik.rule=Host(`traefik.localhost`)"
- "traefik.http.services.traefik-service.loadbalancer.server.port=8080"
command: --api.insecure=true --providers.docker --log.level=ERROR --accesslog=true
ports:
- 80:80
volumes:
- /var/run/docker.sock:/var/run/docker.sock
volumes:
data:
pgadmin:

View File

@@ -1,38 +0,0 @@
import getpass
import threading
import requests
import time
import sys
from requests.auth import HTTPBasicAuth
rate = int(sys.argv[1]) # /second
if rate == 0:
burst = True
print("Burst mode activated")
else:
burst = False
sleep = 1 / rate
print(f"Beginning load test at {rate} calls/second")
url = 'https://button.ducoterra.net/'
# url = 'http://button.localhost/'
def timer(func, *args, **kwargs):
then = time.time()
func(*args, **kwargs)
print(time.time() - then)
# init = requests.get('https://button.ducoterra.net/button/')
init = requests.get(url)
csrf = init.cookies.get('csrftoken')
session = init.cookies.get('sessionid')
me = lambda num: print(num) or print(requests.post(url, headers = {'Content-Type': 'application/json', 'X-CSRFToken': csrf, 'Cookie': f'csrftoken={csrf}; sessionid={session}'}).text)
threadme = lambda num: threading.Thread(target=timer, args=(me, num)).start()
if burst:
[threadme(num) for num in range(0,100)]
else:
num = 1
while True:
threadme(num)
num += 1
time.sleep(sleep)

View File

@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -1,23 +0,0 @@
apiVersion: v2
name: helm
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 1.16.0

View File

@@ -1,8 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}
labels:
app: {{ .Release.Name }}
data:
ALLOWED_HOSTS: {{ .Release.Name }}.ducoterra.net

View File

@@ -1,33 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- image: {{ required "A valid .Values.image entry required!" .Values.image }}:{{ required "A valid .Values.tag entry required!" .Values.tag }}
name: {{ .Release.Name }}
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: {{ .Release.Name }}
- secretRef:
name: {{ .Release.Name }}
- secretRef:
name: postgres
resources:
limits:
memory: "500Mi"
cpu: "250m"
requests:
memory: "1Mi"
cpu: "100m"

View File

@@ -1,18 +0,0 @@
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: {{ .Release.Name }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ .Release.Name }}
minReplicas: 1
maxReplicas: 4
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50

View File

@@ -1,78 +0,0 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ .Release.Name }}-internal-tls
annotations:
kubernetes.io/ingress.class: traefik-internal
spec:
entryPoints:
- websecure
tls:
certResolver: myresolver
domains:
- main: "*.ducoterra.net"
routes:
- match: Host(`{{ .Release.Name }}.ducoterra.net`)
kind: Rule
services:
- name: {{ .Release.Name }}
port: 8000
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ .Release.Name }}-internal-web
annotations:
kubernetes.io/ingress.class: traefik-internal
spec:
entryPoints:
- web
routes:
- match: Host(`{{ .Release.Name }}.ducoterra.net`)
kind: Rule
services:
- name: {{ .Release.Name }}
port: 8000
middlewares:
- name: httpsredirect
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ .Release.Name }}-external-tls
annotations:
kubernetes.io/ingress.class: traefik-external
spec:
entryPoints:
- websecure
tls:
certResolver: myresolver
routes:
- match: Host(`{{ .Release.Name }}.ducoterra.net`)
kind: Rule
services:
- name: {{ .Release.Name }}
port: 8000
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ .Release.Name }}-external-web
annotations:
kubernetes.io/ingress.class: traefik-external
spec:
entryPoints:
- web
routes:
- match: Host(`{{ .Release.Name }}.ducoterra.net`)
kind: Rule
services:
- name: {{ .Release.Name }}
port: 8000
middlewares:
- name: httpsredirect

View File

@@ -1,10 +0,0 @@
{{ if and .Values.secret .Release.IsInstall }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}
type: generic
data:
SECRET_KEY: {{ randAlphaNum 64 | b64enc | quote }}
DJANGO_SUPERUSER_PASSWORD: {{ randAlphaNum 64 | b64enc | quote }}
{{ end }}

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: {{ .Release.Name }}
name: {{ .Release.Name }}
spec:
ports:
- port: 8000
protocol: TCP
name: {{ .Release.Name }}-web
targetPort: 8000
selector:
app: {{ .Release.Name }} # This selects the pod(s) that match the selector
type: ClusterIP

View File

@@ -1,3 +0,0 @@
image: hub.ducoterra.net/ducoterra/button
tag: 1.0.2
secret: true

View File

@@ -1,42 +1,21 @@
apiVersion: traefik.containo.us/v1alpha1 apiVersion: networking.k8s.io/v1beta1
kind: IngressRoute kind: Ingress
metadata: metadata:
name: $DEPLOY-internal-tls
annotations: annotations:
kubernetes.io/ingress.class: traefik-internal ingress.kubernetes.io/ssl-redirect: "true"
name: $DEPLOY
spec: spec:
entryPoints:
- websecure
tls: tls:
certResolver: myresolver - hosts:
domains: - $DEPLOY.ducoterra.net
- main: "*.ducoterra.net" secretName: letsencrypt
routes: rules:
- match: Host(`$DEPLOY.ducoterra.net`) - host: $DEPLOY.ducoterra.net
kind: Rule http:
services: paths:
- name: $DEPLOY - backend:
port: 8000 serviceName: $DEPLOY
servicePort: 8000
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: $DEPLOY-internal-web
annotations:
kubernetes.io/ingress.class: traefik-internal
spec:
entryPoints:
- web
routes:
- match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule
services:
- name: $DEPLOY
port: 8000
middlewares:
- name: httpsredirect
--- ---
@@ -44,13 +23,11 @@ apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: $DEPLOY-external-tls name: $DEPLOY-external-tls
annotations:
kubernetes.io/ingress.class: traefik-external
spec: spec:
entryPoints: entryPoints:
- websecure - websecure
tls: tls:
certResolver: myresolver secretName: letsencrypt
routes: routes:
- match: Host(`$DEPLOY.ducoterra.net`) - match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule kind: Rule
@@ -64,8 +41,6 @@ apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: $DEPLOY-external-web name: $DEPLOY-external-web
annotations:
kubernetes.io/ingress.class: traefik-external
spec: spec:
entryPoints: entryPoints:
- web - web

View File

@@ -1,39 +1,18 @@
apiVersion: traefik.containo.us/v1alpha1 apiVersion: networking.k8s.io/v1beta1
kind: IngressRoute kind: Ingress
metadata: metadata:
name: $DEPLOY-internal-tls
annotations: annotations:
kubernetes.io/ingress.class: traefik-internal ingress.kubernetes.io/ssl-redirect: "true"
name: $DEPLOY
spec: spec:
entryPoints:
- websecure
tls: tls:
certResolver: myresolver - hosts:
domains: - $DEPLOY.ducoterra.net
- main: "*.ducoterra.net" secretName: letsencrypt
routes: rules:
- match: Host(`$DEPLOY.ducoterra.net`) - host: $DEPLOY.ducoterra.net
kind: Rule http:
services: paths:
- name: $DEPLOY - backend:
port: 8000 serviceName: $DEPLOY
servicePort: 8000
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: $DEPLOY-internal-web
annotations:
kubernetes.io/ingress.class: traefik-internal
spec:
entryPoints:
- web
routes:
- match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule
services:
- name: $DEPLOY
port: 8000
middlewares:
- name: httpsredirect

View File

@@ -2,5 +2,4 @@ django
djangorestframework djangorestframework
pygments pygments
gunicorn gunicorn
whitenoise whitenoise
psycopg2-binary

View File

@@ -1,41 +0,0 @@
.achievement {
position: absolute;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: -1;
}
.achievement-animate {
animation-name: moveup;
animation-duration: 2s;
animation-fill-mode: forwards;
}
.achievment-column {
height: 100%;
overflow: auto;
text-align: center;
}
.achievement-text {
font-size: 4rem;
}
@media only screen and (max-width: 768px) {
.achievment-column {
height: 50%;
}
}
@keyframes moveup {
from {bottom: 0px;}
to {bottom: 200px; color: white;}
}
@keyframes fadeout {
from {}
to {color: transparent;}
}

View File

@@ -1,32 +1,18 @@
html, body { html, body {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: fixed;
overflow: hidden;
} }
.section, .container { .section, .container {
height: 100%; height: 100%;
} }
.columns { .container {
height: 100%;
}
.button-column {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center; text-align: center;
height: 100%;
}
@media only screen and (max-width: 768px) {
.button-column {
height: 50%;
}
}
.button-container {
position: absolute;
} }

View File

@@ -1,31 +1,12 @@
const csrftoken = getCookie('csrftoken'); const csrftoken = getCookie('csrftoken');
const button = document.getElementById("BUTTON"); const button = document.getElementById("BUTTON");
const count = document.getElementById("COUNT"); const count = document.getElementById("COUNT");
const button_container = document.getElementById("button-container");
const achievement = document.getElementById("achievement");
const achievement_list = document.getElementById("achievement-list");
const achievement_column = document.getElementById("achievement-column");
function add_achievement(text) {
if (text != undefined) {
achievement.querySelector(".achievement-text").innerText = text;
achievement.classList.remove("achievement-animate");
void achievement.offsetWidth;
achievement.classList.add("achievement-animate");
var elem = document.createElement("div");
elem.innerText = text;
achievement_list.appendChild(elem);
achievement_column.scrollTo(0, achievement_list.scrollHeight);
}
}
// when button is clicked submit an empty post request // when button is clicked submit an empty post request
button.addEventListener("click", event => { button.addEventListener("click", event => {
button.disabled = true; button.disabled = true;
button.classList.add("is-loading"); button.classList.add("is-loading");
fetch(button.dataset.action, { fetch('/button/', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -37,7 +18,6 @@ button.addEventListener("click", event => {
}) })
.then((data) => { .then((data) => {
count.innerText = data.pressed; count.innerText = data.pressed;
add_achievement(data.achievement);
}).finally(() => { }).finally(() => {
button.disabled = false; button.disabled = false;
button.classList.remove("is-loading"); button.classList.remove("is-loading");

View File

@@ -4,7 +4,6 @@
{% block css %} {% block css %}
<link rel="stylesheet" href="{% static 'ui/button.css' %}"> <link rel="stylesheet" href="{% static 'ui/button.css' %}">
<link rel="stylesheet" href="{% static 'ui/achievement.css' %}">
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@@ -14,33 +13,16 @@
{% block body %} {% block body %}
<section class="section"> <section class="section">
<div class="columns"> <div class="container">
<div class="column"> <div>
<h1 class="title">
The Button
</h1>
<button class="button is-primary" id="BUTTON">Press</button>
</div> </div>
<div class="button-column column"> <div><br></div>
<div> <div>
<h1 class="title"> <h1 class="title" id="COUNT">{{ pressed }}</h1>
The Button
</h1>
<button class="button is-danger" id="BUTTON" data-action = "{% url 'button' %}">Press</button>
</div>
<div><br></div>
<div>
<h1 class="title" id="COUNT">{{ pressed }}</h1>
</div>
<div id="achievement" class="achievement">
<div>
<div class="achievement-text"></div>
</div>
</div>
</div>
<div class="column achievment-column" id = "achievement-column">
<div class="achievements-list" id="achievement-list">
<h1 class="title">Achievements</h1>
{% for key,value in achievement.items %}
<div>{{ value }}</div>
{% endfor %}
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,5 +1,5 @@
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser, User
from django.test import RequestFactory, TestCase, Client from django.test import RequestFactory, TestCase
from .views import button from .views import button
@@ -11,29 +11,24 @@ 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):
# Test initial load # Test button page returns 200
c = Client() request = self.factory.get('/button')
response = c.get('/') request.session = self.client.session
response = button(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.context.get("achievement"), {})
# Test button post returns 200 and increments button to 1
request = self.factory.post(
'/button',
data={},
content_type='application/json'
)
request.session = self.client.session
response = button(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(request.session.get('pressed'), 1)
# Test first achievement # Test second click increments button to 2
response = c.post('/', {}) response = button(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json().get("pressed"), 1) self.assertEqual(request.session.get('pressed'), 2)
self.assertEqual(response.json().get("achievement"), "Clicked!")
self.assertEqual(c.session.get('pressed'), 1)
# Test second achievement
response = c.post('/', {})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json().get("pressed"), 2)
self.assertEqual(response.json().get("achievement"), "Clicked Twice!")
self.assertEqual(c.session.get('pressed'), 2)
# Test no achievement
response = c.post('/', {})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json().get("pressed"), 3)
self.assertEqual(response.json().get("achievement"), None)
self.assertEqual(c.session.get('pressed'), 3)

View File

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

View File

@@ -1,70 +1,14 @@
from django.shortcuts import render from django.shortcuts import render
from django.http import JsonResponse from django.http import JsonResponse
achievements = {
1: "Clicked!",
2: "Clicked Twice!",
4: "2^2",
8: "2^3",
16: "2^4",
24: "I'm that old",
32: "2^5",
64: "2^6",
100: "one hundred",
128: "2^7",
200: "two hundred",
250: "quarter thousand",
256: "2^8",
300: "three hundred",
400: "four hundred",
500: "half thousand",
512: "2^9",
600: "six hundred",
700: "seven hundred",
800: "eight hundred",
900: "nine hundred",
1000: "full thousand",
1024: "2^10",
1776: "America",
1914: "Some War here",
1938: "Some more war here",
1950: "Lots of war in here",
2000: "Computers die",
2008: "Houses die",
2019: "People die",
2048: "2048!",
2500: "Keep going!",
3000: "three thousand",
4000: "four thousand",
4096: "2^11",
5000: "halfway to ten thousand",
10001: "ten thousand one",
100000: "one hundred thousand",
1000000: "one million?",
10000000: "ten millions???",
100000000: "one hundo billion",
1000000000: "JK this is actually a billion though",
10000000000: "I'm not going to create another achievement",
100000000000: "one hundred billion",
1000000000000: "It's physically impossible to click this high"
}
def button(request): def button(request):
PRESSED = 'pressed' PRESSED = 'pressed'
ACHIEVE = 'achievement' try:
pressed = request.session.get(PRESSED, 0) request.session[PRESSED]
except KeyError:
request.session[PRESSED] = 0
if request.method == "POST": if request.method == "POST":
pressed = pressed + 1 request.session[PRESSED] += 1
request.session[PRESSED] = pressed return JsonResponse({PRESSED: request.session[PRESSED]})
response = { return render(request, "ui/button.html", {PRESSED: request.session[PRESSED]})
PRESSED: pressed,
ACHIEVE: achievements.get(pressed)
}
return JsonResponse(response)
response = {PRESSED: pressed}
achieved = {k:v for k,v in achievements.items() if k <= pressed}
response.update({ACHIEVE: achieved})
return render(request, "ui/button.html", response)