Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e66f3e2d2 | ||
|
|
b43927c384 | ||
|
|
11dc31660d | ||
|
|
438ae0fa93 | ||
|
|
f9017ad302 | ||
|
|
7bd7bde188 | ||
|
|
5f9f4762a9 | ||
|
|
cead9d5aea | ||
|
|
03de5aacfb |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
db/
|
db/
|
||||||
staticfiles/
|
staticfiles/
|
||||||
|
.vscode/settings.json
|
||||||
@@ -36,8 +36,6 @@ test:
|
|||||||
- python manage.py test
|
- python manage.py test
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
variables:
|
|
||||||
DEPLOY: test
|
|
||||||
stage: deploy
|
stage: deploy
|
||||||
only:
|
only:
|
||||||
variables:
|
variables:
|
||||||
@@ -50,6 +48,7 @@ deploy:
|
|||||||
- 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 -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 /usr/bin/kubectl
|
- chmod +x /usr/bin/kubectl
|
||||||
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||||
- helm install --upgrade $DEPLOY ./helm --set image=$CI_REGISTRY_IMAGE --set tag=$CI_COMMIT_TAG
|
- helm upgrade --install $DEPLOY ./helm --set image=$CI_REGISTRY_IMAGE --set tag=$CI_COMMIT_TAG
|
||||||
- POD=$(./kubectl get pods --selector=app=$DEPLOY --output=jsonpath='{.items[*].metadata.name}')
|
- sleep 10
|
||||||
- ./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
|
||||||
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -12,8 +12,11 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"test",
|
"test",
|
||||||
],
|
],
|
||||||
|
"env": {
|
||||||
|
"DB_HOST": "localhost"
|
||||||
|
},
|
||||||
"django": true,
|
"django": true,
|
||||||
"preLaunchTask": "Migrate"
|
"preLaunchTask": "docker-compose up"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Server",
|
"name": "Run Server",
|
||||||
@@ -22,10 +25,12 @@
|
|||||||
"program": "${workspaceFolder}/manage.py",
|
"program": "${workspaceFolder}/manage.py",
|
||||||
"args": [
|
"args": [
|
||||||
"runserver",
|
"runserver",
|
||||||
"--noreload"
|
|
||||||
],
|
],
|
||||||
|
"env": {
|
||||||
|
"DB_HOST": "localhost"
|
||||||
|
},
|
||||||
"django": true,
|
"django": true,
|
||||||
"preLaunchTask": "Migrate"
|
"preLaunchTask": "docker-compose up"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
20
.vscode/tasks.json
vendored
20
.vscode/tasks.json
vendored
@@ -2,23 +2,9 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Collect Static",
|
"label": "docker-compose up",
|
||||||
"command": "venv/bin/python manage.py collectstatic --no-input",
|
"command": "docker-compose up -d",
|
||||||
"type": "shell",
|
"type": "shell"
|
||||||
"presentation": {
|
|
||||||
"reveal": "always"
|
|
||||||
},
|
|
||||||
"group": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dependsOn": "Collect Static",
|
|
||||||
"label": "Migrate",
|
|
||||||
"command": "venv/bin/python manage.py migrate",
|
|
||||||
"type": "shell",
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always"
|
|
||||||
},
|
|
||||||
"group": "build"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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"]
|
||||||
|
|||||||
@@ -2,4 +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)
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
version: '3.5'
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
button:
|
button:
|
||||||
build: .
|
build: .
|
||||||
|
image: site:local
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.button.rule=Host(`button.localhost`)"
|
- "traefik.http.routers.button.rule=Host(`button.localhost`)"
|
||||||
- "traefik.http.services.button-service.loadbalancer.server.port=8000"
|
- "traefik.http.services.button-service.loadbalancer.server.port=8000"
|
||||||
@@ -18,37 +19,27 @@ services:
|
|||||||
- SECRET_KEY=secret
|
- SECRET_KEY=secret
|
||||||
- ALLOWED_HOSTS=button.localhost
|
- ALLOWED_HOSTS=button.localhost
|
||||||
- DJANGO_SUPERUSER_PASSWORD=django
|
- DJANGO_SUPERUSER_PASSWORD=django
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12
|
image: postgres:13
|
||||||
volumes:
|
volumes:
|
||||||
- data:/var/lib/postgresql/data
|
- data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
pgadmin:
|
- 5432:5432
|
||||||
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:
|
traefik:
|
||||||
image: traefik:v2.2
|
image: traefik:v2.4
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.traefik.rule=Host(`traefik.localhost`)"
|
- "traefik.http.routers.traefik.rule=Host(`traefik.localhost`)"
|
||||||
- "traefik.http.services.traefik-service.loadbalancer.server.port=8080"
|
- "traefik.http.services.traefik-service.loadbalancer.server.port=8080"
|
||||||
command: --api.insecure=true --providers.docker --log.level=ERROR --accesslog=true
|
command: --api.insecure=true --providers.docker --log.level=ERROR --accesslog=true
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- 80:80
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
pgadmin:
|
|
||||||
@@ -13,8 +13,8 @@ else:
|
|||||||
burst = False
|
burst = False
|
||||||
sleep = 1 / rate
|
sleep = 1 / rate
|
||||||
print(f"Beginning load test at {rate} calls/second")
|
print(f"Beginning load test at {rate} calls/second")
|
||||||
# url = 'https://button.ducoterra.net/button/'
|
url = 'https://button.ducoterra.net/'
|
||||||
url = 'http://button.localhost/button/'
|
# url = 'http://button.localhost/'
|
||||||
|
|
||||||
def timer(func, *args, **kwargs):
|
def timer(func, *args, **kwargs):
|
||||||
then = time.time()
|
then = time.time()
|
||||||
@@ -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,79 +0,0 @@
|
|||||||
apiVersion: traefik.containo.us/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY-internal-tls
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: traefik-internal
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: myresolver
|
|
||||||
domains:
|
|
||||||
- main: "*.ducoterra.net"
|
|
||||||
routes:
|
|
||||||
- match: Host(`$DEPLOY.ducoterra.net`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: $DEPLOY
|
|
||||||
port: 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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY-external-tls
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: traefik-external
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: myresolver
|
|
||||||
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
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: traefik-external
|
|
||||||
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,39 +0,0 @@
|
|||||||
apiVersion: traefik.containo.us/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: $DEPLOY-internal-tls
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: traefik-internal
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: myresolver
|
|
||||||
domains:
|
|
||||||
- main: "*.ducoterra.net"
|
|
||||||
routes:
|
|
||||||
- match: Host(`$DEPLOY.ducoterra.net`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: $DEPLOY
|
|
||||||
port: 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
|
|
||||||
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
|
||||||
|
|
||||||
|
$@
|
||||||
10
ui/tests.py
10
ui/tests.py
@@ -13,28 +13,26 @@ class SimpleTest(TestCase):
|
|||||||
def test_button(self):
|
def test_button(self):
|
||||||
# Test initial load
|
# Test initial load
|
||||||
c = Client()
|
c = Client()
|
||||||
response = c.get('/button')
|
response = c.get('/')
|
||||||
self.assertEqual(response.status_code, 301)
|
|
||||||
response = c.get('/button/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.context.get("achievement"), {})
|
self.assertEqual(response.context.get("achievement"), {})
|
||||||
|
|
||||||
# Test first achievement
|
# Test first achievement
|
||||||
response = c.post('/button/', {})
|
response = c.post('/', {})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json().get("pressed"), 1)
|
self.assertEqual(response.json().get("pressed"), 1)
|
||||||
self.assertEqual(response.json().get("achievement"), "Clicked!")
|
self.assertEqual(response.json().get("achievement"), "Clicked!")
|
||||||
self.assertEqual(c.session.get('pressed'), 1)
|
self.assertEqual(c.session.get('pressed'), 1)
|
||||||
|
|
||||||
# Test second achievement
|
# Test second achievement
|
||||||
response = c.post('/button/', {})
|
response = c.post('/', {})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json().get("pressed"), 2)
|
self.assertEqual(response.json().get("pressed"), 2)
|
||||||
self.assertEqual(response.json().get("achievement"), "Clicked Twice!")
|
self.assertEqual(response.json().get("achievement"), "Clicked Twice!")
|
||||||
self.assertEqual(c.session.get('pressed'), 2)
|
self.assertEqual(c.session.get('pressed'), 2)
|
||||||
|
|
||||||
# Test no achievement
|
# Test no achievement
|
||||||
response = c.post('/button/', {})
|
response = c.post('/', {})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json().get("pressed"), 3)
|
self.assertEqual(response.json().get("pressed"), 3)
|
||||||
self.assertEqual(response.json().get("achievement"), None)
|
self.assertEqual(response.json().get("achievement"), None)
|
||||||
|
|||||||
12
ui/views.py
12
ui/views.py
@@ -10,7 +10,6 @@ achievements = {
|
|||||||
24: "I'm that old",
|
24: "I'm that old",
|
||||||
32: "2^5",
|
32: "2^5",
|
||||||
64: "2^6",
|
64: "2^6",
|
||||||
69: "Nice",
|
|
||||||
100: "one hundred",
|
100: "one hundred",
|
||||||
128: "2^7",
|
128: "2^7",
|
||||||
200: "two hundred",
|
200: "two hundred",
|
||||||
@@ -18,7 +17,6 @@ achievements = {
|
|||||||
256: "2^8",
|
256: "2^8",
|
||||||
300: "three hundred",
|
300: "three hundred",
|
||||||
400: "four hundred",
|
400: "four hundred",
|
||||||
420: "Blaze it",
|
|
||||||
500: "half thousand",
|
500: "half thousand",
|
||||||
512: "2^9",
|
512: "2^9",
|
||||||
600: "six hundred",
|
600: "six hundred",
|
||||||
@@ -55,17 +53,17 @@ achievements = {
|
|||||||
def button(request):
|
def button(request):
|
||||||
PRESSED = 'pressed'
|
PRESSED = 'pressed'
|
||||||
ACHIEVE = 'achievement'
|
ACHIEVE = 'achievement'
|
||||||
current = request.session.get(PRESSED, 0)
|
pressed = request.session.get(PRESSED, 0)
|
||||||
request.session[PRESSED] = current + 1
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
pressed = pressed + 1
|
||||||
|
request.session[PRESSED] = pressed
|
||||||
response = {
|
response = {
|
||||||
PRESSED: current,
|
PRESSED: pressed,
|
||||||
ACHIEVE: achievements.get(current)
|
ACHIEVE: achievements.get(pressed)
|
||||||
}
|
}
|
||||||
return JsonResponse(response)
|
return JsonResponse(response)
|
||||||
|
|
||||||
pressed = current
|
|
||||||
response = {PRESSED: pressed}
|
response = {PRESSED: pressed}
|
||||||
achieved = {k:v for k,v in achievements.items() if k <= pressed}
|
achieved = {k:v for k,v in achievements.items() if k <= pressed}
|
||||||
response.update({ACHIEVE: achieved})
|
response.update({ACHIEVE: achieved})
|
||||||
|
|||||||
Reference in New Issue
Block a user