Browse Source

Preparando para independizar proyecto

Celestino Rey 1 year ago
parent
commit
4e6766cdfa
46 changed files with 922 additions and 28 deletions
  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 pip --upgrade && \
     /opt/venv/bin/python -m pip install -r requirements.txt
     /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
 RUN echo "export PATH=/opt/venv/bin/:$PATH" >> /root/.bashrc
 
 
 # make our entrypoint.sh executable
 # make our entrypoint.sh executable
 RUN chmod +x config/entrypoint.sh
 RUN chmod +x config/entrypoint.sh
 
 
-EXPOSE 8080
+#EXPOSE 8080
 
 
 # execute our entrypoint.sh file
 # execute our entrypoint.sh file
 CMD ["./config/entrypoint.sh"]
 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>/',
     path('reservar/<int:evento_id>/',
          views.reservar_evento, name='reservar_evento'),
          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/', api_lista_eventos, name='api_lista_eventos'),
     path('api/eventos/<int:evento_id>/', api_detalle_evento, name='api_detalle_evento'),
     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/', api_lista_noticias, name='api_lista_noticias'),
     path('api/noticias/<int:noticia_id>/', api_detalle_noticia, name='api_detalle_noticia'),
     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.shortcuts import render, get_object_or_404, redirect
 from django.contrib.auth.decorators import login_required, user_passes_test
 from django.contrib.auth.decorators import login_required, user_passes_test
 from django.contrib import messages
 from django.contrib import messages
+from django.contrib.admin.views.decorators import staff_member_required
+
 from django.core.mail import EmailMultiAlternatives
 from django.core.mail import EmailMultiAlternatives
 from django.conf import settings
 from django.conf import settings
 from django.template.loader import render_to_string
 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 .models import Evento, Reserva, ListaEspera, Noticia
 from .forms import ListaEsperaForm, EventoForm, MensajeCorreoForm
 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__)
 logger = logging.getLogger(__name__)
 
 
 
 
@@ -335,11 +342,12 @@ def enviar_correo_inscritos(request, evento_id):
             firma = "\n\n\nSaludos, \n\nPádel entre amigos"
             firma = "\n\n\nSaludos, \n\nPádel entre amigos"
             mensaje = f"{mensaje_usuario}{firma}"
             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:
             try:
-                send_mass_mail(mensaje_final, fail_silently=False)
+                send_mass_mail(mensajes, fail_silently=False)
             except BadHeaderError:
             except BadHeaderError:
                 return HttpResponse('Encabezado inválido encontrado.')
                 return HttpResponse('Encabezado inválido encontrado.')
             except SMTPException as e:
             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})
     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
 from django.conf import settings
 
 
+
 def app_version(request):
 def app_version(request):
     return {
     return {
         'APP_VERSION': settings.APP_VERSION
         'APP_VERSION': settings.APP_VERSION

+ 35 - 16
src/gestion_reservas/settings.py

@@ -14,7 +14,10 @@ from pathlib import Path
 import os
 import os
 import logging
 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'.
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
 BASE_DIR = Path(__file__).resolve().parent.parent
 BASE_DIR = Path(__file__).resolve().parent.parent
@@ -27,10 +30,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent
 SECRET_KEY = 'hey'
 SECRET_KEY = 'hey'
 
 
 # SECURITY WARNING: don't run with debug turned on in production!
 # 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
 # Application definition
 
 
@@ -87,16 +93,26 @@ WSGI_APPLICATION = 'gestion_reservas.wsgi.application'
 # Database
 # Database
 # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
 # 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
 # Password validation
 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -136,8 +152,13 @@ THOUSAND_SEPARATOR = '.'
 # Static files (CSS, JavaScript, Images)
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/5.1/howto/static-files/
 # https://docs.djangoproject.com/en/5.1/howto/static-files/
 
 
-STATIC_URL = '/static/'
+STATIC_URL = 'static/'
 STATIC_ROOT = BASE_DIR / "staticfiles"
 STATIC_ROOT = BASE_DIR / "staticfiles"
+STATICFILES_DIRS = [BASE_DIR / "static",]
+
+# Mediafiles
+MEDIA_ROOT = BASE_DIR / "mediafiles"
+MEDIA_URL = '/media/'
 
 
 # Default primary key field type
 # Default primary key field type
 # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
 # 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"
 AUTH_USER_MODEL = "reymotausers.ReyMotaUser"
 
 
-MEDIA_ROOT = BASE_DIR / "mediafiles"
-MEDIA_URL = '/media/'
 
 
 # Configuración de correo con Gmail
 # Configuración de correo con Gmail
 if DEBUG is True:
 if DEBUG is True:

+ 1 - 0
src/gestion_reservas/urls.py

@@ -38,6 +38,7 @@ urlpatterns = [
     path('ayuda/', views.ayuda, name='ayuda'),
     path('ayuda/', views.ayuda, name='ayuda'),
 
 
     path('api/ayuda/', api_lista_ayuda, name='api_lista_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)
 ] + 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
 import os
+
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.decorators import user_passes_test
 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.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.decorators import api_view
+from rest_framework.response import Response
 
 
 from eventos.models import Noticia, Evento
 from eventos.models import Noticia, Evento
 from .serializers import AyudaSerializer
 from .serializers import AyudaSerializer
@@ -64,10 +68,19 @@ def ayuda(request):
     return render(request, 'ayuda.html', {'elementos': elementos_ayuda, 'apartados': apartados})
     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
 #
 #
 
 
+
 @api_view(['GET'])
 @api_view(['GET'])
 def api_lista_ayuda(request):
 def api_lista_ayuda(request):
     """Devuelve la lista de toda la ayuda."""
     """Devuelve la lista de toda la ayuda."""

+ 1 - 1
src/reymotausers/admin.py

@@ -56,7 +56,7 @@ class ReyMotaUserAdmin(UserAdmin):
         ("Personal", {"fields": ("nombre",)}),
         ("Personal", {"fields": ("nombre",)}),
         ("Varios", {"fields": ("foto",)}),
         ("Varios", {"fields": ("foto",)}),
     )
     )
-    search_fields = ("email",)
+    search_fields = ("email", "nombre",)
     ordering = ("email",)
     ordering = ("email",)
     actions = [enviar_email_prueba]  # Añadir la acción a la lista de acciones
     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 rest_framework import serializers
 from .models import ReyMotaUser
 from .models import ReyMotaUser
 
 
@@ -6,3 +8,9 @@ class ReyMotaUserSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = ReyMotaUser
         model = ReyMotaUser
         fields = ['email', 'password', 'foto', 'is_superuser', 'is_staff', 'is_active', 'nombre', 'last_login']
         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 %}
 {% load filtros_de_entorno %}
 
 
 <div class="app-branding">
 <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--> 
 </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>
     </style>
 
 
-    <h1>Centro de Ayuda</h1>
+    <h1>Centro de ayuda</h1>
 
 
     {% for code, nombre in apartados %}
     {% for code, nombre in apartados %}
         <div class="apartado">
         <div class="apartado">

+ 25 - 0
src/templates/base.html

@@ -125,6 +125,17 @@
                     
                     
                     {% endif %}
                     {% 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">
                     <li class="nav-item">
                         <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->
                         <!--//Bootstrap Icons: https://icons.getbootstrap.com/ -->
                         <a class="nav-link" href="{% url 'ayuda' %}">
                         <a class="nav-link" href="{% url 'ayuda' %}">
@@ -137,6 +148,20 @@
                                 <span class="nav-link-text">Ayuda</span>
                                 <span class="nav-link-text">Ayuda</span>
                         </a><!--//nav-link-->
                         </a><!--//nav-link-->
                     </li><!--//nav-item-->
                     </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-->
                 </ul><!--//app-menu-->
             </nav><!--//app-nav-->           
             </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 %}
+