Compare commits
1 Commits
1.0.7
...
visitor_tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6610c2896b |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,3 @@
|
|||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
db/
|
db/
|
||||||
staticfiles/
|
|
||||||
.vscode/settings.json
|
|
||||||
@@ -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
13
.vscode/launch.json
vendored
@@ -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
10
.vscode/tasks.json
vendored
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "docker-compose up",
|
|
||||||
"command": "docker-compose up -d",
|
|
||||||
"type": "shell"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
21
README.md
21
README.md
@@ -2,4 +2,23 @@
|
|||||||
|
|
||||||
My CI testing pipeline for a django project.
|
My CI testing pipeline for a django project.
|
||||||
|
|
||||||
[](http://gitlab.ducoterra.net/ducoterra/ci_builder/-/commits/master)
|
[](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
|
||||||
|
```
|
||||||
|
|||||||
25
api/migrations/0002_auto_20200426_1517.py
Normal file
25
api/migrations/0002_auto_20200426_1517.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
23
api/migrations/0003_auto_20200426_1526.py
Normal file
23
api/migrations/0003_auto_20200426_1526.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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']
|
|
||||||
@@ -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
|
||||||
41
api/tests.py
41
api/tests.py
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
16
api/views.py
16
api/views.py
@@ -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
|
||||||
@@ -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'),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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:
|
|
||||||
@@ -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)
|
|
||||||
@@ -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/
|
|
||||||
@@ -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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: {{ .Release.Name }}
|
|
||||||
labels:
|
|
||||||
app: {{ .Release.Name }}
|
|
||||||
data:
|
|
||||||
ALLOWED_HOSTS: {{ .Release.Name }}.ducoterra.net
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
image: hub.ducoterra.net/ducoterra/button
|
|
||||||
tag: 1.0.2
|
|
||||||
secret: true
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -2,5 +2,4 @@ django
|
|||||||
djangorestframework
|
djangorestframework
|
||||||
pygments
|
pygments
|
||||||
gunicorn
|
gunicorn
|
||||||
whitenoise
|
whitenoise
|
||||||
psycopg2-binary
|
|
||||||
@@ -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;}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
43
ui/tests.py
43
ui/tests.py
@@ -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)
|
|
||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
70
ui/views.py
70
ui/views.py
@@ -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)
|
|
||||||
Reference in New Issue
Block a user