Compare commits
20 Commits
visitor_tr
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e66f3e2d2 | ||
|
|
b43927c384 | ||
|
|
11dc31660d | ||
|
|
438ae0fa93 | ||
|
|
f9017ad302 | ||
|
|
7bd7bde188 | ||
|
|
5f9f4762a9 | ||
|
|
cead9d5aea | ||
|
|
03de5aacfb | ||
|
|
5b9e568d54 | ||
|
|
eafbffce25 | ||
|
|
06c49d551d | ||
|
|
8c9389ba1f | ||
|
|
b9b948a8b5 | ||
|
|
a0e0afff1f | ||
|
|
8db4d5afed | ||
|
|
f45e289ed8 | ||
|
|
43c48d5216 | ||
|
|
0cb3896e4b | ||
|
|
e102db9f38 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
db/
|
db/
|
||||||
|
staticfiles/
|
||||||
|
.vscode/settings.json
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
variables:
|
variables:
|
||||||
CI_PROJECT_DIR: "."
|
CI_PROJECT_DIR: "."
|
||||||
CI_REGISTRY_IMAGE: hub.ducoterra.net/ducoterra/mysite
|
CI_REGISTRY_IMAGE: hub.ducoterra.net/ducoterra/button
|
||||||
|
DEPLOY: button
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
@@ -16,23 +17,25 @@ 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_to_test:
|
deploy:
|
||||||
variables:
|
|
||||||
DEPLOY: test
|
|
||||||
stage: deploy
|
stage: deploy
|
||||||
only:
|
only:
|
||||||
variables:
|
variables:
|
||||||
@@ -42,35 +45,10 @@ deploy_to_test:
|
|||||||
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 -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
|
- 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
|
||||||
- chmod +x ./kubectl
|
- chmod +x /usr/bin/kubectl
|
||||||
- mkdir /deploy
|
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||||
- for f in $(find k8s -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
|
- helm upgrade --install $DEPLOY ./helm --set image=$CI_REGISTRY_IMAGE --set tag=$CI_COMMIT_TAG
|
||||||
- for f in $(find k8s/test -regex '.*\.ya*ml'); do envsubst < $f > "/deploy/$(basename $f)"; done
|
- sleep 10
|
||||||
- ./kubectl apply -f /deploy
|
- POD=$(kubectl get pods --selector=app=$DEPLOY --output=jsonpath='{.items[*].metadata.name}')
|
||||||
- ./kubectl rollout status deploy $DEPLOY
|
- kubectl exec $POD -- 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
13
.vscode/launch.json
vendored
@@ -12,7 +12,11 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"test",
|
"test",
|
||||||
],
|
],
|
||||||
"django": true
|
"env": {
|
||||||
|
"DB_HOST": "localhost"
|
||||||
|
},
|
||||||
|
"django": true,
|
||||||
|
"preLaunchTask": "docker-compose up"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Server",
|
"name": "Run Server",
|
||||||
@@ -21,9 +25,12 @@
|
|||||||
"program": "${workspaceFolder}/manage.py",
|
"program": "${workspaceFolder}/manage.py",
|
||||||
"args": [
|
"args": [
|
||||||
"runserver",
|
"runserver",
|
||||||
"--noreload"
|
|
||||||
],
|
],
|
||||||
"django": true
|
"env": {
|
||||||
|
"DB_HOST": "localhost"
|
||||||
|
},
|
||||||
|
"django": true,
|
||||||
|
"preLaunchTask": "docker-compose up"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
10
.vscode/tasks.json
vendored
Normal file
10
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "docker-compose up",
|
||||||
|
"command": "docker-compose up -d",
|
||||||
|
"type": "shell"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
Dockerfile
13
Dockerfile
@@ -1,4 +1,6 @@
|
|||||||
FROM python:3.8.2
|
FROM python:3
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY config config
|
COPY config config
|
||||||
@@ -8,10 +10,13 @@ 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
|
||||||
|
|
||||||
|
COPY scripts scripts
|
||||||
|
RUN chmod +x scripts/*
|
||||||
|
ENTRYPOINT ["scripts/entrypoint.sh"]
|
||||||
|
|
||||||
RUN useradd -ms /bin/bash django
|
RUN useradd -ms /bin/bash django
|
||||||
RUN chown -R django .
|
RUN chown -R django .
|
||||||
|
|
||||||
USER django
|
USER django
|
||||||
RUN python manage.py collectstatic
|
RUN python manage.py collectstatic --no-input
|
||||||
|
|
||||||
CMD ["gunicorn","-b",":8000", "-w", "4", "config.wsgi"]
|
CMD ["scripts/cmd.sh"]
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -2,23 +2,4 @@
|
|||||||
|
|
||||||
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)
|
[](https://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
|
|
||||||
```
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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,11 +1,19 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from datetime import datetime
|
from pygments.lexers import get_all_lexers
|
||||||
|
from pygments.styles import get_all_styles
|
||||||
|
|
||||||
class Visitor(models.Model):
|
LEXERS = [item for item in get_all_lexers() if item[1]]
|
||||||
name = models.CharField(
|
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
|
||||||
primary_key=True,
|
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])
|
||||||
max_length = 255
|
|
||||||
)
|
|
||||||
clicked = models.IntegerField(default = 0, blank=True)
|
class Snippet(models.Model):
|
||||||
first_pressed = models.DateTimeField(blank=True, null=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
last_pressed = models.DateTimeField(blank=True, null=True)
|
title = models.CharField(max_length=100, blank=True, default='')
|
||||||
|
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,23 +1,29 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import *
|
from .models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
|
||||||
|
|
||||||
|
|
||||||
class VisitorSerializer(serializers.Serializer):
|
class SnippetSerializer(serializers.Serializer):
|
||||||
name = serializers.CharField(required=True)
|
id = serializers.IntegerField(read_only=True)
|
||||||
clicked = serializers.IntegerField(read_only=True)
|
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
||||||
first_pressed = serializers.DateTimeField(read_only=True)
|
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
||||||
last_pressed = serializers.DateTimeField(read_only=True)
|
linenos = serializers.BooleanField(required=False)
|
||||||
|
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 `Visitor` instance, given the validated data.
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
"""
|
"""
|
||||||
return Visitor.objects.create(**validated_data)
|
return Snippet.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.name = validated_data.get('name', instance.name)
|
instance.title = validated_data.get('title', instance.title)
|
||||||
|
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
|
||||||
39
api/tests.py
39
api/tests.py
@@ -1,53 +1,42 @@
|
|||||||
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 *
|
from .views import SnippetList, SnippetDetail
|
||||||
|
|
||||||
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_list_visitors(self):
|
def test_snippets(self):
|
||||||
# Create an instance of a GET request.
|
# Create an instance of a GET request.
|
||||||
request = self.factory.get('/visitors')
|
request = self.factory.get('/snippets')
|
||||||
|
|
||||||
# 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 = VisitorList.as_view()(request)
|
response = SnippetList.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)
|
||||||
|
|
||||||
|
|
||||||
def test_add_visitor(self):
|
request = self.factory.post('/snippets', data={
|
||||||
request = self.factory.post('/visitors', data={
|
'title': 'test1',
|
||||||
'name': 'test',
|
'code': '() => {console.log("hello")};',
|
||||||
|
'lineos': False,
|
||||||
|
'language': 'js',
|
||||||
|
'style': 'abap'
|
||||||
},
|
},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = VisitorList.as_view()(request)
|
response = SnippetList.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('visitor/', views.VisitorList.as_view()),
|
path('snippets/', views.SnippetList.as_view()),
|
||||||
path('visitor/<str:pk>/', views.VisitorDetail.as_view()),
|
path('snippets/<int:pk>/', views.SnippetDetail.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 *
|
from .models import Snippet
|
||||||
from .serializers import *
|
from .serializers import SnippetSerializer
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
|
|
||||||
class VisitorList(generics.ListCreateAPIView):
|
class SnippetList(generics.ListCreateAPIView):
|
||||||
# Add comments here
|
# Add comments here
|
||||||
queryset = Visitor.objects.all()
|
queryset = Snippet.objects.all()
|
||||||
serializer_class = VisitorSerializer
|
serializer_class = SnippetSerializer
|
||||||
|
|
||||||
|
|
||||||
class VisitorDetail(generics.RetrieveUpdateDestroyAPIView):
|
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
# Add comments here
|
# Add comments here
|
||||||
queryset = Visitor.objects.all()
|
queryset = Snippet.objects.all()
|
||||||
serializer_class = VisitorSerializer
|
serializer_class = SnippetSerializer
|
||||||
@@ -24,6 +24,7 @@ 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(",")
|
||||||
|
|
||||||
@@ -78,8 +79,12 @@ WSGI_APPLICATION = 'config.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': os.path.join(BASE_DIR, 'db/db.sqlite3'),
|
'NAME': os.getenv('DB_NAME', 'postgres'),
|
||||||
|
'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),
|
||||||
]
|
]
|
||||||
|
|||||||
45
docker-compose.yml
Executable file
45
docker-compose.yml
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
button:
|
||||||
|
build: .
|
||||||
|
image: site:local
|
||||||
|
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:13
|
||||||
|
volumes:
|
||||||
|
- data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.4
|
||||||
|
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:
|
||||||
38
heavy_load.py
Normal file
38
heavy_load.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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)
|
||||||
23
helm/.helmignore
Normal file
23
helm/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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/
|
||||||
23
helm/Chart.yaml
Normal file
23
helm/Chart.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
8
helm/templates/configmap.yaml
Normal file
8
helm/templates/configmap.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ .Release.Name }}
|
||||||
|
labels:
|
||||||
|
app: {{ .Release.Name }}
|
||||||
|
data:
|
||||||
|
ALLOWED_HOSTS: {{ .Release.Name }}.ducoterra.net
|
||||||
33
helm/templates/deploy.yaml
Normal file
33
helm/templates/deploy.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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"
|
||||||
18
helm/templates/hpa.yaml
Normal file
18
helm/templates/hpa.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
78
helm/templates/ingress.yaml
Executable file
78
helm/templates/ingress.yaml
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
10
helm/templates/secret.yaml
Normal file
10
helm/templates/secret.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{{ 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 }}
|
||||||
15
helm/templates/service.yaml
Normal file
15
helm/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
3
helm/values.yaml
Normal file
3
helm/values.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
image: hub.ducoterra.net/ducoterra/button
|
||||||
|
tag: 1.0.2
|
||||||
|
secret: true
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY
|
|
||||||
data:
|
|
||||||
ALLOWED_HOSTS: localhost,$DEPLOY.ducoterra.net
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: $DEPLOY
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: $DEPLOY
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: $DEPLOY
|
|
||||||
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: $DEPLOY
|
|
||||||
- secretRef:
|
|
||||||
name: django-secrets
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /app/db
|
|
||||||
name: $DEPLOY
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "256Mi"
|
|
||||||
cpu: "250m"
|
|
||||||
requests:
|
|
||||||
memory: "1Mi"
|
|
||||||
cpu: "1m"
|
|
||||||
ports:
|
|
||||||
- containerPort: 8000
|
|
||||||
volumes:
|
|
||||||
- name: $DEPLOY
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: $DEPLOY
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY-external-tls
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
secretName: letsencrypt
|
|
||||||
routes:
|
|
||||||
- match: Host(`$DEPLOY.ducoterra.net`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: $DEPLOY
|
|
||||||
port: 8000
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY-external-web
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
routes:
|
|
||||||
- match: Host(`$DEPLOY.ducoterra.net`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: $DEPLOY
|
|
||||||
port: 8000
|
|
||||||
middlewares:
|
|
||||||
- name: httpsredirect
|
|
||||||
11
k8s/pvc.yaml
11
k8s/pvc.yaml
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY
|
|
||||||
spec:
|
|
||||||
storageClassName: nfs-encrypted
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteMany
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 8Gi
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: $DEPLOY
|
|
||||||
ports:
|
|
||||||
- port: 8000
|
|
||||||
targetPort: 8000
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -3,3 +3,4 @@ djangorestframework
|
|||||||
pygments
|
pygments
|
||||||
gunicorn
|
gunicorn
|
||||||
whitenoise
|
whitenoise
|
||||||
|
psycopg2-binary
|
||||||
3
scripts/cmd.sh
Normal file
3
scripts/cmd.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
gunicorn -b :8000 -w 4 config.wsgi
|
||||||
22
scripts/entrypoint.sh
Executable file
22
scripts/entrypoint.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if ! $SKIP_ENTRYPOINT || [ -z $SKIP_ENTRYPOINT ]; then
|
||||||
|
|
||||||
|
MIGRATED=false
|
||||||
|
|
||||||
|
while ! $MIGRATED; do
|
||||||
|
echo "Migrating..."
|
||||||
|
python manage.py migrate 2> /dev/null
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
MIGRATED=true
|
||||||
|
else
|
||||||
|
echo "ERROR - $(date) - Migrate failed."
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
$@
|
||||||
41
ui/static/ui/achievement.css
Normal file
41
ui/static/ui/achievement.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.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,18 +1,32 @@
|
|||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: fixed;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section, .container {
|
.section, .container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.columns {
|
||||||
|
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,12 +1,31 @@
|
|||||||
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/', {
|
fetch(button.dataset.action, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -18,6 +37,7 @@ 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,6 +4,7 @@
|
|||||||
|
|
||||||
{% 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 %}
|
||||||
@@ -13,17 +14,34 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
</div>
|
||||||
|
<div class="button-column column">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
The Button
|
The Button
|
||||||
</h1>
|
</h1>
|
||||||
<button class="button is-primary" id="BUTTON">Press</button>
|
<button class="button is-danger" id="BUTTON" data-action = "{% url 'button' %}">Press</button>
|
||||||
</div>
|
</div>
|
||||||
<div><br></div>
|
<div><br></div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="title" id="COUNT">{{ pressed }}</h1>
|
<h1 class="title" id="COUNT">{{ pressed }}</h1>
|
||||||
</div>
|
</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>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
39
ui/tests.py
39
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
|
from django.test import RequestFactory, TestCase, Client
|
||||||
|
|
||||||
from .views import button
|
from .views import button
|
||||||
|
|
||||||
@@ -11,24 +11,29 @@ 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 button page returns 200
|
# Test initial load
|
||||||
request = self.factory.get('/button')
|
c = Client()
|
||||||
request.session = self.client.session
|
response = c.get('/')
|
||||||
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
|
# Test first achievement
|
||||||
request = self.factory.post(
|
response = c.post('/', {})
|
||||||
'/button',
|
|
||||||
data={},
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
request.session = self.client.session
|
|
||||||
response = button(request)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(request.session.get('pressed'), 1)
|
self.assertEqual(response.json().get("pressed"), 1)
|
||||||
|
self.assertEqual(response.json().get("achievement"), "Clicked!")
|
||||||
|
self.assertEqual(c.session.get('pressed'), 1)
|
||||||
|
|
||||||
# Test second click increments button to 2
|
# Test second achievement
|
||||||
response = button(request)
|
response = c.post('/', {})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(request.session.get('pressed'), 2)
|
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('button/', views.button, name = 'button'),
|
path('', views.button, name = 'button'),
|
||||||
]
|
]
|
||||||
70
ui/views.py
70
ui/views.py
@@ -1,14 +1,70 @@
|
|||||||
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'
|
||||||
try:
|
ACHIEVE = 'achievement'
|
||||||
request.session[PRESSED]
|
pressed = request.session.get(PRESSED, 0)
|
||||||
except KeyError:
|
|
||||||
request.session[PRESSED] = 0
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
request.session[PRESSED] += 1
|
pressed = pressed + 1
|
||||||
return JsonResponse({PRESSED: request.session[PRESSED]})
|
request.session[PRESSED] = pressed
|
||||||
return render(request, "ui/button.html", {PRESSED: request.session[PRESSED]})
|
response = {
|
||||||
|
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