Răsfoiți Sursa

Preparando para independizar proyecto

Celestino Rey 1 an în urmă
părinte
comite
4e6766cdfa
46 a modificat fișierele cu 922 adăugiri și 28 ștergeri
  1. 3 1
      Dockerfile
  2. 62 0
      Dockerfile.k8s
  3. 74 0
      K8S/Makefile
  4. 5 0
      K8S/creaDirs.sh
  5. 52 0
      K8S/db-deployment.yaml
  6. 17 0
      K8S/db-service.yaml
  7. 1 0
      K8S/entra.sh
  8. 1 0
      K8S/entraPsql.sh
  9. 11 0
      K8S/env-prod-configmap.yaml
  10. 11 0
      K8S/env-prod-db-configmap.yaml
  11. 84 0
      K8S/jugaralpadel-deployment.yaml
  12. 31 0
      K8S/jugaralpadel-ingress.yaml
  13. 50 0
      K8S/jugaralpadel-topolvm-pvc.yaml
  14. 7 0
      K8S/namespace.yaml
  15. 46 0
      K8S/nginx-deployment.yaml
  16. 20 0
      K8S/nginx-service.yaml
  17. 74 0
      K8S/pv-local-jugaralpadel.yaml
  18. 47 0
      K8S/pvc-jugaralpadel.yaml
  19. 13 0
      K8S/pvc-postgresql.yaml
  20. 13 0
      K8S/pvc-static.yaml
  21. 8 0
      K8S/reg-secret.yaml
  22. 14 0
      K8S/static-topolvm-pvc.yaml
  23. 1 0
      K8S/verImg.sh
  24. 8 0
      Makefile
  25. 0 0
      Shells/borraDB.sh
  26. 0 0
      Shells/creaDB.sh
  27. 0 0
      Shells/entra.sh
  28. 0 0
      Shells/lanza.sh
  29. 0 0
      Shells/nuevaapp.sh
  30. 15 0
      Yamls/jugaralpadel-service.yaml
  31. 22 0
      entrypoint.sh
  32. 6 0
      src/entornoPruebas.sh
  33. 7 0
      src/eventos/urls.py
  34. 34 3
      src/eventos/views.py
  35. 1 0
      src/gestion_reservas/context_processors.py
  36. 35 16
      src/gestion_reservas/settings.py
  37. 1 0
      src/gestion_reservas/urls.py
  38. 17 4
      src/gestion_reservas/views.py
  39. 1 1
      src/reymotausers/admin.py
  40. 8 0
      src/reymotausers/serializers.py
  41. 1 2
      src/templates/_branding.html
  42. 29 0
      src/templates/acerca_de.html
  43. 1 1
      src/templates/ayuda.html
  44. 25 0
      src/templates/base.html
  45. 22 0
      src/templates/eventos/estadisticas_usuarios.html
  46. 44 0
      src/templates/eventos/eventos_por_usuario.html

+ 3 - 1
Dockerfile

@@ -26,12 +26,14 @@ RUN python -m venv /opt/venv && \
     /opt/venv/bin/python -m pip install pip --upgrade && \
     /opt/venv/bin/python -m pip install -r requirements.txt
 
+# Añade path a .bashrc
+
 RUN echo "export PATH=/opt/venv/bin/:$PATH" >> /root/.bashrc
 
 # make our entrypoint.sh executable
 RUN chmod +x config/entrypoint.sh
 
-EXPOSE 8080
+#EXPOSE 8080
 
 # execute our entrypoint.sh file
 CMD ["./config/entrypoint.sh"]

+ 62 - 0
Dockerfile.k8s

@@ -0,0 +1,62 @@
+# syntax=docker/dockerfile:1
+##################
+# BUILDER        #
+##################
+
+FROM python:3.11-slim AS builder
+
+# set work directory
+WORKDIR /app
+
+# set environment variables
+ENV PYTHONDONTWRITEBYTECODE=1
+ENV PYTHONUNBUFFERED=1
+
+# install system dependencies
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends gcc
+
+# lint
+RUN pip install --upgrade pip
+RUN pip install flake8==6.0.0
+COPY . /app/
+RUN flake8 --ignore=E501,F401,E126,E722 .
+
+COPY ./src/requirements.txt .
+
+
+RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
+
+##################
+# FINAL          #
+##################
+
+FROM python:3.11-slim
+
+# create the appropriate directories
+ENV APP_HOME=/app
+RUN mkdir -p $APP_HOME
+
+WORKDIR $APP_HOME
+
+# install system dependencies
+RUN apt-get update && apt-get install nginx netcat-openbsd curl vim jq rsync libpango-1.0-0 libpangocairo-1.0-0 libcairo2 -y
+
+COPY --from=builder /app/wheels /wheels
+COPY --from=builder /app/requirements.txt .
+
+RUN pip install --upgrade pip
+RUN pip install --no-cache /wheels/*
+
+# copy entrypoint.sh
+COPY ./entrypoint.sh .
+
+# copy project
+COPY . $APP_HOME
+
+# change to the app user
+
+WORKDIR $APP_HOME/src
+
+# run entrypoint.sh
+ENTRYPOINT ["/app/entrypoint.sh"]

+ 74 - 0
K8S/Makefile

@@ -0,0 +1,74 @@
+export ARQUITECTURA := $(shell lscpu |grep itectur | tr -d ' '| cut -f2 -d':')
+export REGISTRY=registry.reymota.es
+
+export IMG_VERSION = 0.70.40
+export IMG_NGINX_VERSION = 2.3
+
+# limpia todo
+all: imagen clean install
+
+imagen:
+	cd ../; make
+
+install:
+	-kubectl create -f namespace.yaml
+	-kubectl create -f reg-secret.yaml
+	-kubectl create -f env-prod-configmap.yaml 
+	-kubectl create -f env-prod-db-configmap.yaml 
+
+	-kubectl create -f pv-local-jugaralpadel.yaml
+	-kubectl create -f pvc-jugaralpadel.yaml
+	-kubectl create -f pvc-static.yaml
+	-kubectl create -f pvc-postgresql.yaml
+
+	-kubectl create -f db-deployment.yaml
+	-kubectl create -f db-service.yaml
+
+	-envsubst < jugaralpadel-deployment.yaml |kubectl create -f -
+	-envsubst < nginx-deployment.yaml |kubectl create -f -
+	-kubectl create -f nginx-service.yaml
+	-kubectl create -f jugaralpadel-ingress.yaml
+
+
+clean:
+	-envsubst < nginx-deployment.yaml |kubectl delete -f -
+	-kubectl delete -f nginx-service.yaml
+	-envsubst < jugaralpadel-deployment.yaml |kubectl delete -f -
+
+	-kubectl delete -f db-deployment.yaml
+	-kubectl delete -f db-service.yaml
+
+	-kubectl delete -f env-prod-configmap.yaml 
+	-kubectl delete -f env-prod-db-configmap.yaml 
+
+	-kubectl delete -f pvc-jugaralpadel.yaml
+	-kubectl delete -f pvc-static.yaml
+	-kubectl delete -f pvc-postgresql.yaml
+	-kubectl delete -f pv-local-jugaralpadel.yaml
+	-kubectl delete -f reg-secret.yaml
+	-kubectl delete -f namespace.yaml
+
+nginx:
+	cd ../nginx; make
+
+verimg:
+	docker run -it  ${REGISTRY}/jugaralpadel-${ARQUITECTURA}:${IMG_VERSION} bash
+
+backup:
+	kubectl --kubeconfig /home/creylopez/.kube/config -n jugaralpadel exec -ti deployment.apps/db -- /usr/lib/postgresql/15/bin/pg_dump --username=creylopez --dbname=jugaralpadel > jugaralpadel-$(IMG_VERSION).sql
+   
+fleet:
+	-cp namespace.yaml ../Fleet/15-namespace.yaml
+	-cp reg-secret.yaml ../Fleet/20-reg-secret.yaml
+	-cp env-prod-configmap.yaml  ../Fleet/25-env-prod-configmap.yaml
+	-cp env-prod-db-configmap.yaml  ../Fleet/30-env-prod-db-configmap.yaml
+	-cp pv-local-jugaralpadel.yaml ../Fleet/35-pv-local-jugaralpadel.yaml
+	-cp pvc-jugaralpadel.yaml ../Fleet/40-pvc-jugaralpadel.yaml
+	-cp pvc-static.yaml ../Fleet/45-pvc-static.yaml
+	-cp pvc-postgresql.yaml ../Fleet/50-pvc-postgresql.yaml
+	-cp db-deployment.yaml ../Fleet/55-db-deployment.yaml
+	-cp db-service.yaml ../Fleet/60-db-service.yaml
+	-cp nginx-service.yaml ../Fleet/65-nginx-service.yaml
+	-envsubst < jugaralpadel-deployment.yaml > ../Fleet/70-jugaralpadel-deployment.yaml
+	-envsubst < nginx-deployment.yaml > ../Fleet/75-nginx-deployment.yaml
+	-cp jugaralpadel-ingress.yaml ../Fleet/80-jugaralpadel-ingress.yaml

+ 5 - 0
K8S/creaDirs.sh

@@ -0,0 +1,5 @@
+sudo mkdir -p /mnt/Externo/jugaralpadel/media
+sudo mkdir -p /mnt/Externo/jugaralpadel/migrations/eventos
+sudo mkdir -p /mnt/Externo/jugaralpadel/migrations/reymotausers
+sudo mkdir -p /mnt/Externo/jugaralpadel/static
+sudo mkdir -p /mnt/Externo/jugaralpadel/pg

+ 52 - 0
K8S/db-deployment.yaml

@@ -0,0 +1,52 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  annotations:
+    kompose.cmd: kompose convert
+    kompose.version: 1.34.0 (cbf2835db)
+  labels:
+    io.kompose.service: postgresql
+  name: postgresql
+  namespace: jugaralpadel
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      io.kompose.service: postgresql
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      annotations:
+        kompose.cmd: kompose convert
+        kompose.version: 1.34.0 (cbf2835db)
+      labels:
+        io.kompose.service: postgresql
+    spec:
+      containers:
+        - env:
+            - name: POSTGRES_DB
+              valueFrom:
+                configMapKeyRef:
+                  key: POSTGRES_DB
+                  name: env-prod-db
+            - name: POSTGRES_PASSWORD
+              valueFrom:
+                configMapKeyRef:
+                  key: POSTGRES_PASSWORD
+                  name: env-prod-db
+            - name: POSTGRES_USER
+              valueFrom:
+                configMapKeyRef:
+                  key: POSTGRES_USER
+                  name: env-prod-db
+          image: postgres:15
+          name: postgresql
+          volumeMounts:
+            - mountPath: /var/lib/postgresql/data
+              name: postgres-data
+      restartPolicy: Always
+      volumes:
+        - name: postgres-data
+          persistentVolumeClaim:
+            claimName: postgres-data

+ 17 - 0
K8S/db-service.yaml

@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+  annotations:
+    kompose.cmd: kompose convert
+    kompose.version: 1.34.0 (cbf2835db)
+  labels:
+    io.kompose.service: postgresql
+  name: postgresql
+  namespace: jugaralpadel
+spec:
+  ports:
+    - name: "5432"
+      port: 5432
+      targetPort: 5432
+  selector:
+    io.kompose.service: postgresql

+ 1 - 0
K8S/entra.sh

@@ -0,0 +1 @@
+kubectl -n jugaralpadel exec -ti deployment.apps/jugaralpadel -- /bin/bash

+ 1 - 0
K8S/entraPsql.sh

@@ -0,0 +1 @@
+kubectl -n jugaralpadel exec -ti deployment.apps/db -- psql --username=creylopez --dbname=jugaralpadel

+ 11 - 0
K8S/env-prod-configmap.yaml

@@ -0,0 +1,11 @@
+apiVersion: v1
+data:
+  DEBUG: "False"
+  APP_VERSION: 14.0.1
+  DATABASE: postgres
+kind: ConfigMap
+metadata:
+  labels:
+    io.kompose.service: web-env-prod
+  name: env-prod
+  namespace: jugaralpadel

+ 11 - 0
K8S/env-prod-db-configmap.yaml

@@ -0,0 +1,11 @@
+apiVersion: v1
+data:
+  POSTGRES_DB: jugaralpadel
+  POSTGRES_PASSWORD: Dsa-0213
+  POSTGRES_USER: creylopez
+kind: ConfigMap
+metadata:
+  labels:
+    io.kompose.service: db-env-prod-db
+  name: env-prod-db
+  namespace: jugaralpadel

+ 84 - 0
K8S/jugaralpadel-deployment.yaml

@@ -0,0 +1,84 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: jugaralpadel
+  namespace: jugaralpadel
+spec:
+  ports:
+    - name: "8000"
+      port: 8000
+      targetPort: 8000
+  selector:
+    app: jugaralpadel
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: jugaralpadel
+  namespace: jugaralpadel
+  labels:
+    app: jugaralpadel
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: jugaralpadel
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        app: jugaralpadel
+    spec:
+      containers:
+      - args:
+          - gunicorn
+          - gestion_reservas.wsgi:application
+          - --bind
+          - 0.0.0.0:8000
+        name: jugaralpadel
+        image: $REGISTRY/jugaralpadel-$ARQUITECTURA:$IMG_VERSION
+        env:
+        - name: IMG_VERSION
+          value: "$IMG_VERSION"
+        - name: DEBUG
+          valueFrom:
+            configMapKeyRef:
+              key: DEBUG
+              name: env-prod
+        - name: APP_VERSION
+          valueFrom:
+            configMapKeyRef:
+              key: APP_VERSION
+              name: env-prod
+        ports:
+        - containerPort: 8000
+          protocol: TCP
+        volumeMounts:
+        - mountPath: /app/gestion_reservas/mediafiles
+          name: jugaralpadel-media
+
+        - mountPath: /app/gestion_reservas/eventos/migrations
+          name: jugaralpadel-eventos-migrations
+        - mountPath: /app/gestion_reservas/reymotausers/migrations
+          name: jugaralpadel-reymotausers-migrations
+
+        - mountPath: /app/gestion_reservas/staticfiles
+          name: static-volume
+      imagePullSecrets:
+      - name: myregistrykey
+      restartPolicy: Always
+      volumes:
+      - name: jugaralpadel-media
+        persistentVolumeClaim:
+          claimName: jugaralpadel-media
+      - name: jugaralpadel-eventos-migrations
+        persistentVolumeClaim:
+          claimName: jugaralpadel-eventos-migrations
+      - name: jugaralpadel-reymotausers-migrations
+        persistentVolumeClaim:
+          claimName: jugaralpadel-reymotausers-migrations
+      - name: static-volume
+        persistentVolumeClaim:
+          claimName: static-volume
+status: {}

+ 31 - 0
K8S/jugaralpadel-ingress.yaml

@@ -0,0 +1,31 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  generation: 1
+  managedFields:
+    - apiVersion: networking.k8s.io/v1
+      fieldsType: FieldsV1
+      fieldsV1:
+        f:spec:
+          f:defaultBackend:
+            .: {}
+            f:service:
+              .: {}
+              f:name: {}
+              f:port: {}
+          f:rules: {}
+      manager: rancher
+      operation: Update
+  name: jugaralpadel
+  namespace: jugaralpadel
+spec:
+  defaultBackend:
+    service:
+      name: nginx
+      port:
+        number: 1337
+  ingressClassName: nginx
+  rules:
+    - host: jugaralpadel.rancher.reymota.lab
+status:
+  loadBalancer: {}

+ 50 - 0
K8S/jugaralpadel-topolvm-pvc.yaml

@@ -0,0 +1,50 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  creationTimestamp: null
+  labels:
+    io.kompose.service: jugaralpadel-media
+  name: jugaralpadel-media
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 100Mi
+  storageClassName: topolvm-provisioner
+status: {}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  creationTimestamp: null
+  labels:
+    io.kompose.service: jugaralpadel-eventos-migrations
+  name: jugaralpadel-eventos-migrations
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 50Mi
+  storageClassName: topolvm-provisioner
+status: {}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  creationTimestamp: null
+  labels:
+    io.kompose.service: jugaralpadel-reymotausers-migrations
+  name: jugaralpadel-reymotausers-migrations
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 53Mi
+  storageClassName: topolvm-provisioner
+status: {}

+ 7 - 0
K8S/namespace.yaml

@@ -0,0 +1,7 @@
+###################################################
+# Namespace jugaralpadel
+###################################################
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: jugaralpadel

+ 46 - 0
K8S/nginx-deployment.yaml

@@ -0,0 +1,46 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  annotations:
+    kompose.cmd: kompose convert
+    kompose.version: 1.34.0 (cbf2835db)
+  labels:
+    io.kompose.service: nginx
+  name: nginx
+  namespace: jugaralpadel
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      io.kompose.service: nginx
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      annotations:
+        kompose.cmd: kompose convert
+        kompose.version: 1.34.0 (cbf2835db)
+      labels:
+        io.kompose.service: nginx
+    spec:
+      containers:
+        - image: $REGISTRY/nginx-jugaralpadel-$ARQUITECTURA:$IMG_NGINX_VERSION
+          name: nginx
+          ports:
+            - containerPort: 80
+              protocol: TCP
+          volumeMounts:
+            - mountPath: /app/gestion_reservas/staticfiles
+              name: static-volume
+            - mountPath: /app/gestion_reservas/mediafiles
+              name: jugaralpadel-media
+      imagePullSecrets:
+      - name: myregistrykey
+      restartPolicy: Always
+      volumes:
+        - name: static-volume
+          persistentVolumeClaim:
+            claimName: static-volume
+        - name: jugaralpadel-media
+          persistentVolumeClaim:
+            claimName: jugaralpadel-media

+ 20 - 0
K8S/nginx-service.yaml

@@ -0,0 +1,20 @@
+apiVersion: v1
+kind: Service
+metadata:
+  annotations:
+    kompose.cmd: kompose convert
+    kompose.version: 1.34.0 (cbf2835db)
+  labels:
+    io.kompose.service: nginx
+  name: nginx
+  namespace: jugaralpadel
+spec:
+  type: NodePort
+  ports:
+    - name: "1337"
+      port: 1337
+      nodePort: 30345
+      targetPort: 80
+  selector:
+    io.kompose.service: nginx
+

+ 74 - 0
K8S/pv-local-jugaralpadel.yaml

@@ -0,0 +1,74 @@
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: jugaralpadel-media-folder
+  namespace: jugaralpadel
+  labels:
+    app: jugaralpadel
+spec:
+  capacity:
+    storage: 100Mi
+  accessModes:
+    - ReadWriteOnce
+  hostPath:
+    path: "/mnt/Externo/jugaralpadel/media"
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: jugaralpadel-eventos-migrations-folder
+  namespace: jugaralpadel
+  labels:
+    app: jugaralpadel
+spec:
+  capacity:
+    storage: 50Mi
+  accessModes:
+    - ReadWriteOnce
+  hostPath:
+    path: "/mnt/Externo/jugaralpadel/migrations/eventos"
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: jugaralpadel-reymotausers-migrations-folder
+  namespace: jugaralpadel
+  labels:
+    app: jugaralpadel
+spec:
+  capacity:
+    storage: 53Mi
+  accessModes:
+    - ReadWriteOnce
+  hostPath:
+    path: "/mnt/Externo/jugaralpadel/migrations/reymotausers"
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: jugaralpadel-static-folder
+  namespace: jugaralpadel
+  labels:
+    app: jugaralpadel
+spec:
+  capacity:
+    storage: 70Mi
+  accessModes:
+    - ReadWriteOnce
+  hostPath:
+    path: "/mnt/Externo/jugaralpadel/static"
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: jugaralpadel-pg-folder
+  namespace: jugaralpadel
+  labels:
+    app: jugaralpadel
+spec:
+  capacity:
+    storage: 150Mi
+  accessModes:
+    - ReadWriteOnce
+  hostPath:
+    path: "/mnt/Externo/jugaralpadel/pg"

+ 47 - 0
K8S/pvc-jugaralpadel.yaml

@@ -0,0 +1,47 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  creationTimestamp: null
+  labels:
+    io.kompose.service: jugaralpadel-media
+  name: jugaralpadel-media
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 100Mi
+status: {}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  creationTimestamp: null
+  labels:
+    io.kompose.service: jugaralpadel-eventos-migrations
+  name: jugaralpadel-eventos-migrations
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 50Mi
+status: {}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  creationTimestamp: null
+  labels:
+    io.kompose.service: jugaralpadel-reymotausers-migrations
+  name: jugaralpadel-reymotausers-migrations
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 53Mi
+status: {}

+ 13 - 0
K8S/pvc-postgresql.yaml

@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  labels:
+    io.kompose.service: postgres-data
+  name: postgres-data
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 150Mi

+ 13 - 0
K8S/pvc-static.yaml

@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  labels:
+    io.kompose.service: static-volume
+  name: static-volume
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 70Mi

+ 8 - 0
K8S/reg-secret.yaml

@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: myregistrykey
+  namespace: jugaralpadel
+data:
+  .dockerconfigjson: ewoJImF1dGhzIjogewoJCSJyZWdpc3RyeS5yZXltb3RhLmVzIjogewoJCQkiYXV0aCI6ICJZM0psZVd4dmNHVjZPbEpsZVMweE1UYzIiCgkJfQoJfQp9
+type: kubernetes.io/dockerconfigjson

+ 14 - 0
K8S/static-topolvm-pvc.yaml

@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  labels:
+    io.kompose.service: static-volume
+  name: static-volume
+  namespace: jugaralpadel
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 70Mi
+  storageClassName: topolvm-provisioner

+ 1 - 0
K8S/verImg.sh

@@ -0,0 +1 @@
+docker run -it registry.jugaralpadel.es/jugaralpadel-x86_64:0.1 bash

+ 8 - 0
Makefile

@@ -0,0 +1,8 @@
+install:
+
+	echo "Creando imagen con version '${IMG_VERSION}' para la arquitectura '${ARQUITECTURA}' en el registry '${REGISTRY}'"
+
+	docker build --no-cache -t ${REGISTRY}/jugaralpadel-${ARQUITECTURA}:${IMG_VERSION} -f Dockerfile.k8s .
+	docker push ${REGISTRY}/jugaralpadel-${ARQUITECTURA}:${IMG_VERSION}
+
+

+ 0 - 0
borraDB.sh → Shells/borraDB.sh


+ 0 - 0
creaDB.sh → Shells/creaDB.sh


+ 0 - 0
entra.sh → Shells/entra.sh


+ 0 - 0
lanza.sh → Shells/lanza.sh


+ 0 - 0
nuevaapp.sh → Shells/nuevaapp.sh


+ 15 - 0
Yamls/jugaralpadel-service.yaml

@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: jugaralpadel
+  namespace: jugaralpadel
+spec:
+  type: LoadBalancer
+  ports:
+    - name: "8080"
+      port: 8080
+      nodePort: 30345
+      targetPort: 8080
+  selector:
+    deployment: jugaralpadel
+

+ 22 - 0
entrypoint.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# Este es para K8S
+
+if [ "$DATABASE" = "postgres" ]
+then
+    echo "Waiting for postgres..."
+
+    while ! nc -z $SQL_HOST $SQL_PORT; do
+      sleep 0.1
+    done
+
+    echo "PostgreSQL started"
+else
+    echo "la base de datos no es postgres: '$DATABASE'"
+fi
+
+python manage.py collectstatic --noinput
+#python manage.py flush --no-input
+#python manage.py migrate
+
+exec "$@"

+ 6 - 0
src/entornoPruebas.sh

@@ -0,0 +1,6 @@
+export CSRF_TRUSTED_ORIGINS="http://localhost"
+export DEBUG="True"
+export SECRET_KEY="hola"
+export DJANGO_ALLOWED_HOSTS="vmcluster localhost imac.reymota.lab"
+
+

+ 7 - 0
src/eventos/urls.py

@@ -25,6 +25,8 @@ urlpatterns = [
     path('reservar/<int:evento_id>/',
          views.reservar_evento, name='reservar_evento'),
 
+    path("reservar/<int:reserva_id>/eliminar/", views.cancelar_reserva_admin, name="cancelar_reserva_admin"),
+
     path('api/eventos/', api_lista_eventos, name='api_lista_eventos'),
     path('api/eventos/<int:evento_id>/', api_detalle_evento, name='api_detalle_evento'),
 
@@ -36,4 +38,9 @@ urlpatterns = [
 
     path('api/noticias/', api_lista_noticias, name='api_lista_noticias'),
     path('api/noticias/<int:noticia_id>/', api_detalle_noticia, name='api_detalle_noticia'),
+
+    path("estadisticas/usuarios/", views.estadisticas_por_usuario, name="estadisticas_por_usuario"),
+    path("estadisticas/usuarios/<int:usuario_id>/", views.eventos_por_usuario, name="eventos_por_usuario"),
+
+
 ]

+ 34 - 3
src/eventos/views.py

@@ -1,6 +1,8 @@
 from django.shortcuts import render, get_object_or_404, redirect
 from django.contrib.auth.decorators import login_required, user_passes_test
 from django.contrib import messages
+from django.contrib.admin.views.decorators import staff_member_required
+
 from django.core.mail import EmailMultiAlternatives
 from django.conf import settings
 from django.template.loader import render_to_string
@@ -21,6 +23,11 @@ from .serializers import EventoSerializer, ReservaSerializer, ListaEsperaSeriali
 from .models import Evento, Reserva, ListaEspera, Noticia
 from .forms import ListaEsperaForm, EventoForm, MensajeCorreoForm
 
+from django.contrib.auth import get_user_model
+from django.db.models import Count
+
+User = get_user_model()
+
 logger = logging.getLogger(__name__)
 
 
@@ -335,11 +342,12 @@ def enviar_correo_inscritos(request, evento_id):
             firma = "\n\n\nSaludos, \n\nPádel entre amigos"
             mensaje = f"{mensaje_usuario}{firma}"
 
-            mensaje_comun = (asunto, mensaje, settings.DEFAULT_FROM_EMAIL, destinatarios)
-            mensaje_final = [mensaje_comun]
+            mensajes = [
+                (asunto, mensaje, settings.DEFAULT_FROM_EMAIL, [email]) for email in destinatarios
+            ]
 
             try:
-                send_mass_mail(mensaje_final, fail_silently=False)
+                send_mass_mail(mensajes, fail_silently=False)
             except BadHeaderError:
                 return HttpResponse('Encabezado inválido encontrado.')
             except SMTPException as e:
@@ -356,3 +364,26 @@ def enviar_correo_inscritos(request, evento_id):
 
     return render(request, 'eventos/enviar_correo_inscritos.html', {'form': form, 'evento': evento})
 
+
+def estadisticas_por_usuario(request):
+    usuarios = User.objects.annotate(num_eventos=Count("reserva__evento")).order_by("-num_eventos")
+    return render(request, "eventos/estadisticas_usuarios.html", {"usuarios": usuarios})
+
+
+def eventos_por_usuario(request, usuario_id):
+    usuario = get_object_or_404(User, pk=usuario_id)
+    reservas = Reserva.objects.filter(usuario=usuario).select_related("evento")
+
+    return render(request, "eventos/eventos_por_usuario.html", {
+        "usuario": usuario,
+        "reservas": reservas
+    })
+
+
+@staff_member_required
+def cancelar_reserva_admin(request, reserva_id):
+    reserva = get_object_or_404(Reserva, id=reserva_id)
+    usuario_id = reserva.usuario.id
+    if request.method == "POST":
+        reserva.delete()
+    return redirect("eventos:eventos_por_usuario", usuario_id)

+ 1 - 0
src/gestion_reservas/context_processors.py

@@ -1,5 +1,6 @@
 from django.conf import settings
 
+
 def app_version(request):
     return {
         'APP_VERSION': settings.APP_VERSION

+ 35 - 16
src/gestion_reservas/settings.py

@@ -14,7 +14,10 @@ from pathlib import Path
 import os
 import logging
 
-APP_VERSION = "11.0.2"
+APP_NAME = "jugaralpadel.es"
+APP_VERSION = os.environ.get("APP_VERSION", "15.0.0-dev")  # Valor por defecto si no está definido
+IMG_VERSION = os.environ.get("IMG_VERSION", "0.0.0-dev")  # Valor por defecto si no está definido
+
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
 BASE_DIR = Path(__file__).resolve().parent.parent
@@ -27,10 +30,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent
 SECRET_KEY = 'hey'
 
 # SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = os.environ["DEBUG"] == 'True'
+DEBUG = os.environ.get('DEBUG', 'False') == 'True'
+OPENSHIFT = os.environ.get('OPENSHIFT', 'False') == 'True'
+LOCAL = os.environ.get('LOCAL', 'False') == 'True'
+
+ALLOWED_HOSTS = [".ocp-cluster.reymota.lab", "jugaralpadel.rancher.reymota.lab", "jugaralpadel.es", ".reymota.es"]
 
-ALLOWED_HOSTS = [".ocp-cluster.reymota.lab", "jugaralpadel.rancher.reymota.lab", "jugaralpadel.es"]
-CSRF_TRUSTED_ORIGINS = ["https://*.ocp-cluster.reymota.lab", "http://jugaralpadel.rancher.reymota.lab", " https://jugaralpadel.rancher.reymota.lab", " https://jugaralpadel.es"]
+CSRF_TRUSTED_ORIGINS = ["https://*.ocp-cluster.reymota.lab", "http://jugaralpadel.rancher.reymota.lab", "https://jugaralpadel.rancher.reymota.lab", "https://jugaralpadel.es", "https://*.reymota.es"]
 
 # Application definition
 
@@ -87,16 +93,26 @@ WSGI_APPLICATION = 'gestion_reservas.wsgi.application'
 # Database
 # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
 
-DATABASES = {
-    "default": {
-        "ENGINE": "django.db.backends.postgresql",
-        "NAME": "jugaralpadel",
-        "USER": "creylopez",
-        "PASSWORD": "Dsa-0213",
-        "HOST": "postgresql",
-        "PORT": "5432",
+if LOCAL is False:
+	DATABASES = {
+		"default": {
+			"ENGINE": "django.db.backends.postgresql",
+			"NAME": "jugaralpadel",
+			"USER": "creylopez",
+			"PASSWORD": "Dsa-0213",
+			"HOST": "postgresql",
+			"PORT": "5432",
+		}
+	}
+else:
+    DATABASES = {
+        'default': {
+            "ENGINE": "django.db.backends.sqlite3",
+            "NAME": BASE_DIR / "db.sqlite3",
+        }
     }
-}
+
+
 
 # Password validation
 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -136,8 +152,13 @@ THOUSAND_SEPARATOR = '.'
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/5.1/howto/static-files/
 
-STATIC_URL = '/static/'
+STATIC_URL = 'static/'
 STATIC_ROOT = BASE_DIR / "staticfiles"
+STATICFILES_DIRS = [BASE_DIR / "static",]
+
+# Mediafiles
+MEDIA_ROOT = BASE_DIR / "mediafiles"
+MEDIA_URL = '/media/'
 
 # Default primary key field type
 # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
@@ -150,8 +171,6 @@ LOGOUT_REDIRECT_URL = 'principal'
 
 AUTH_USER_MODEL = "reymotausers.ReyMotaUser"
 
-MEDIA_ROOT = BASE_DIR / "mediafiles"
-MEDIA_URL = '/media/'
 
 # Configuración de correo con Gmail
 if DEBUG is True:

+ 1 - 0
src/gestion_reservas/urls.py

@@ -38,6 +38,7 @@ urlpatterns = [
     path('ayuda/', views.ayuda, name='ayuda'),
 
     path('api/ayuda/', api_lista_ayuda, name='api_lista_ayuda'),
+    path("acerca-de/", views.acerca_de, name="acerca_de"),
 
 ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 

+ 17 - 4
src/gestion_reservas/views.py

@@ -1,14 +1,18 @@
-from rest_framework.response import Response
-from django.utils import timezone
-from rest_framework import status
+# Imports
 
 import os
+
 from django.conf import settings
 from django.contrib.auth.decorators import user_passes_test
-from django.shortcuts import render, get_object_or_404
 from django.http import HttpResponseForbidden
+from django.shortcuts import render, get_object_or_404
+from django.utils import timezone
 
+from datetime import date
+
+from rest_framework import status
 from rest_framework.decorators import api_view
+from rest_framework.response import Response
 
 from eventos.models import Noticia, Evento
 from .serializers import AyudaSerializer
@@ -64,10 +68,19 @@ def ayuda(request):
     return render(request, 'ayuda.html', {'elementos': elementos_ayuda, 'apartados': apartados})
 
 
+def acerca_de(request):
+    context = {
+        "app_name": settings.APP_NAME,
+        "app_version": settings.APP_VERSION,
+        "img_version": settings.IMG_VERSION
+    }
+    return render(request, "acerca_de.html", context)
+
 #
 # API
 #
 
+
 @api_view(['GET'])
 def api_lista_ayuda(request):
     """Devuelve la lista de toda la ayuda."""

+ 1 - 1
src/reymotausers/admin.py

@@ -56,7 +56,7 @@ class ReyMotaUserAdmin(UserAdmin):
         ("Personal", {"fields": ("nombre",)}),
         ("Varios", {"fields": ("foto",)}),
     )
-    search_fields = ("email",)
+    search_fields = ("email", "nombre",)
     ordering = ("email",)
     actions = [enviar_email_prueba]  # Añadir la acción a la lista de acciones
 

+ 8 - 0
src/reymotausers/serializers.py

@@ -1,3 +1,5 @@
+import os
+
 from rest_framework import serializers
 from .models import ReyMotaUser
 
@@ -6,3 +8,9 @@ class ReyMotaUserSerializer(serializers.ModelSerializer):
     class Meta:
         model = ReyMotaUser
         fields = ['email', 'password', 'foto', 'is_superuser', 'is_staff', 'is_active', 'nombre', 'last_login']
+
+    def to_representation(self, instance):
+        ret = super().to_representation(instance)
+        ret['foto'] = "profile_images/" + os.path.basename(ret['foto'])
+
+        return ret

+ 1 - 2
src/templates/_branding.html

@@ -3,7 +3,6 @@
 {% load filtros_de_entorno %}
 
 <div class="app-branding">
-    <a class="app-logo" href="{% url 'principal' %}"><img class="logo-icon me-2" src="{% static 'images/palapadel.svg' %}" alt="logo"><span class="logo-text">JUGAR AL PADEL</span>
-	<span style="color: blue; font-size: 14px;">Versión: {{ APP_VERSION }}</span></a>
+    <a class="app-logo" href="{% url 'principal' %}"><img class="logo-icon me-2" src="{% static 'images/palapadel.svg' %}" alt="logo"><span class="logo-text">JUGAR AL PADEL</span></a>
 
 </div><!--//app-branding--> 

+ 29 - 0
src/templates/acerca_de.html

@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% load filtros_de_entorno %}
+
+{% block content %}
+<div class="container-xl">
+    <h1 class="app-page-title">Acerca de...</h1>
+    <div class="row g-4">
+        <div class="col-12 col-md-6">
+            <div class="app-card app-card-basic d-flex flex-column align-items-start shadow-sm">
+                <div class="app-card-body px-4">
+                    <ul class="list-unstyled">
+                        <li><strong>Nombre de la aplicación:</strong> {{ app_name }}</li>
+                        <li><strong>Versión:</strong> {{ app_version }}</li>
+
+                    {% if 'OPENSHIFT'|muestra_version == 'True' %}
+                        <li><strong>Versión de la imagen:</strong> openshift </li>
+                    {% else %}
+                        <li><strong>Versión de la imagen:</strong> {{ img_version }}</li>
+                    {% endif %}
+
+                    </ul>
+                </div><!--//app-card-body-->
+            </div><!--//app-card-->
+        </div><!--//col-->
+    </div><!--//row-->
+</div>
+{% endblock %}
+

+ 1 - 1
src/templates/ayuda.html

@@ -34,7 +34,7 @@
         }
     </style>
 
-    <h1>Centro de Ayuda</h1>
+    <h1>Centro de ayuda</h1>
 
     {% for code, nombre in apartados %}
         <div class="apartado">

+ 25 - 0
src/templates/base.html

@@ -125,6 +125,17 @@
                     
                     {% endif %}
 
+                    <li class="nav-item"></li>
+                        <a class="nav-link" href="{% url 'eventos:estadisticas_por_usuario' %}">
+                           <span class="nav-icon">
+                            <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-bar-chart-line" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+                             <path fill-rule="evenodd" d="M11 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h1V7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7h1V2zm1 12h2V2h-2v12zm-3 0V7H7v7h2zm-5 0v-3H2v3h2z"/>
+                           </svg>
+                            </span>
+                                <span class="nav-link-text">Estadísticas por usuario</span>
+                        </a><!--//nav-link-->
+                    </li><!--//nav-item-->
+
                     <li class="nav-item">
                         <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->
                         <a class="nav-link" href="{% url 'ayuda' %}">
@@ -137,6 +148,20 @@
                                 <span class="nav-link-text">Ayuda</span>
                         </a><!--//nav-link-->
                     </li><!--//nav-item-->
+{% if request.user.is_authenticated and request.user.email == "creylopez@yahoo.es" %}
+                    <li class="nav-item">
+                        <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->
+                        <a class="nav-link" href="{% url 'acerca_de' %}">
+                           <span class="nav-icon">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-square" viewBox="0 0 16 16">
+                              <path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
+                              <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
+                            </svg>
+                            </span>
+                                <span class="nav-link-text">Acerca de...</span>
+                        </a><!--//nav-link-->
+                    </li><!--//nav-item-->
+{% endif %}
  
                 </ul><!--//app-menu-->
             </nav><!--//app-nav-->           

+ 22 - 0
src/templates/eventos/estadisticas_usuarios.html

@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h2>Estadísticas de Inscripciones por Usuario</h2>
+
+<table class="table table-bordered">
+    <thead>
+        <tr>
+            <th>Usuario</th>
+            <th>Eventos inscritos</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for usuario in usuarios %}
+        <tr>
+            <td><a href="{% url 'eventos:eventos_por_usuario' usuario.id %}">{{ usuario.get_full_name|default:usuario.nombre }}</a></td>
+            <td>{{ usuario.num_eventos }}</td>
+        </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% endblock %}

+ 44 - 0
src/templates/eventos/eventos_por_usuario.html

@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h2>Eventos en los que participa o ha participado {{ usuario.get_full_name|default:usuario.nombre }}</h2>
+
+{% if reservas %}
+    <table class="table table-striped">
+        <thead>
+            <tr>
+                <th>Evento</th>
+                <th>Fecha</th>
+
+                {% if user.is_staff %}
+                <th>Acciones</th>
+                {% endif %}
+            </tr>
+        </thead>
+        <tbody>
+            {% for reserva in reservas %}
+            <tr>
+                <td><a href="{% url 'eventos:detalle_evento' reserva.evento.id %}">{{ reserva.evento.nombre }}</a></td>
+                <td>{{ reserva.evento.fecha }}</td>
+                {% if user.is_staff %}
+                <td>
+                    <form action="{% url 'eventos:cancelar_reserva_admin' reserva.id %}" method="post" style="display:inline;">
+                        {% csrf_token %}
+                        <button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('¿Eliminar esta inscripción?')">
+                            Eliminar
+                        </button>
+                    </form>
+                </td>
+                {% endif %}
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+
+{% else %}
+    <p>No está inscrito en ningún evento.</p>
+{% endif %}
+
+<a href="{% url 'eventos:estadisticas_por_usuario' %}">← Volver a estadísticas</a>
+{% endblock %}
+