Compare commits

..

19 Commits

Author SHA1 Message Date
ducoterra
6610c2896b add visitor model and api 2020-04-26 12:05:03 -04:00
ducoterra
15c5f5293a position fixed, overflow auto 2020-04-25 19:41:55 -04:00
ducoterra
f11f9a0d97 attempt fix with overflow 2020-04-25 19:35:09 -04:00
ducoterra
c80ef7441d change user-scalable 2020-04-25 19:28:56 -04:00
ducoterra
a2e7a92280 prod deploy 2020-04-25 19:17:52 -04:00
ducoterra
b4ef050e1f test vars 2020-04-25 19:11:27 -04:00
ducoterra
c09558c0ff manual approval to prod 2020-04-25 19:04:21 -04:00
ducoterra
c3666783e4 disable zooming 2020-04-25 18:25:26 -04:00
ducoterra
54c6336e22 proper slash handling in button.js 2020-04-25 17:07:45 -04:00
ducoterra
7da888aa09 proper / on button 2020-04-25 16:37:53 -04:00
ducoterra
07d98bf11d remove admin panel 2020-04-25 15:54:10 -04:00
ducoterra
4584ba0143 split out helper functions from button.js 2020-04-25 15:23:12 -04:00
ducoterra
6feac7ef2e apply to html and body, not button 2020-04-25 15:14:01 -04:00
ducoterra
5efb93ea68 don't zoom on double tap 2020-04-25 15:03:03 -04:00
ducoterra
1ee6b890ef remove snippets urls for now 2020-04-25 12:58:31 -04:00
ducoterra
18aab648c6 you have got to be kidding me 2020-04-25 12:34:03 -04:00
ducoterra
57328a7fc8 need secret mounts 2020-04-25 12:30:04 -04:00
ducoterra
948569a659 spacing... spacing 2020-04-25 12:21:31 -04:00
ducoterra
50cdfd8180 wrong configmap 2020-04-25 12:14:42 -04:00
20 changed files with 214 additions and 96 deletions

View File

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

13
.vscode/launch.json vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,4 +3,4 @@ kind: ConfigMap
metadata: metadata:
name: $DEPLOY name: $DEPLOY
data: data:
ALLOWED_HOSTS: localhost,test.ducoterra.net ALLOWED_HOSTS: localhost,$DEPLOY.ducoterra.net

View File

@@ -16,7 +16,9 @@ spec:
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
envFrom: envFrom:
- configMapRef: - configMapRef:
name: gitlab name: $DEPLOY
- secretRef:
name: django-secrets
volumeMounts: volumeMounts:
- mountPath: /app/db - mountPath: /app/db
name: $DEPLOY name: $DEPLOY

View File

@@ -7,10 +7,10 @@ metadata:
spec: spec:
tls: tls:
- hosts: - hosts:
- test.ducoterra.net - $DEPLOY.ducoterra.net
secretName: letsencrypt secretName: letsencrypt
rules: rules:
- host: test.ducoterra.net - host: $DEPLOY.ducoterra.net
http: http:
paths: paths:
- backend: - backend:
@@ -29,7 +29,7 @@ spec:
tls: tls:
secretName: letsencrypt secretName: letsencrypt
routes: routes:
- match: Host(`test.ducoterra.net`) - match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule kind: Rule
services: services:
- name: $DEPLOY - name: $DEPLOY
@@ -45,7 +45,7 @@ spec:
entryPoints: entryPoints:
- web - web
routes: routes:
- match: Host(`test.ducoterra.net`) - match: Host(`$DEPLOY.ducoterra.net`)
kind: Rule kind: Rule
services: services:
- name: $DEPLOY - name: $DEPLOY

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

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

View File

@@ -1,8 +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%;
}
.container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
text-align: center;
} }

View File

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

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

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

View File

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

View File

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

View File

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