| @ -1 +1 @@ | |||||
| kubectl exec -ti deployment.apps/lyrics -- /bin/bash | |||||
| kubectl -n lyrics exec -ti deployment.apps/lyrics -- /bin/bash | |||||
| @ -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: lyrics | |||||
| namespace: lyrics | |||||
| spec: | |||||
| defaultBackend: | |||||
| service: | |||||
| name: lyrics | |||||
| port: | |||||
| number: 5000 | |||||
| ingressClassName: lyrics | |||||
| rules: | |||||
| - host: lyrics.rancher.reymota.lab | |||||
| status: | |||||
| loadBalancer: {} | |||||
| @ -0,0 +1,8 @@ | |||||
| apiVersion: v1 | |||||
| kind: Secret | |||||
| metadata: | |||||
| name: myregistrykey | |||||
| namespace: lyrics | |||||
| data: | |||||
| .dockerconfigjson: ewoJImF1dGhzIjogewoJCSJyZWdpc3RyeS5yZXltb3RhLmVzIjogewoJCQkiYXV0aCI6ICJZM0psZVd4dmNHVjZPbEpsZVMweE1UYzIiCgkJfQoJfQp9 | |||||
| type: kubernetes.io/dockerconfigjson | |||||
| @ -0,0 +1,3 @@ | |||||
| Dockerfile | |||||
| Makefile | |||||
| venv/ | |||||
| @ -0,0 +1,73 @@ | |||||
| # syntax=docker/dockerfile:1 | |||||
| ################## | |||||
| # BUILDER # | |||||
| ################## | |||||
| FROM python:3.11.4-slim-buster 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 . | |||||
| COPY ./requirements.txt . | |||||
| RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt | |||||
| ################## | |||||
| # FINAL # | |||||
| ################## | |||||
| FROM python:3.11.4-slim-buster | |||||
| # create directory for the app user | |||||
| RUN mkdir -p /app | |||||
| # create the app user | |||||
| #RUN addgroup --system app && adduser --system --group app | |||||
| # create the appropriate directories | |||||
| ENV APP_HOME=/app | |||||
| RUN mkdir -p $APP_HOME | |||||
| #RUN mkdir -p $APP_HOME/staticfiles | |||||
| #RUN mkdir -p $APP_HOME/mediafiles | |||||
| WORKDIR $APP_HOME | |||||
| # install system dependencies | |||||
| RUN apt-get update && apt-get install -y sqlite3 netcat vim procps curl jq | |||||
| 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 | |||||
| # chown all the files to the app user | |||||
| #RUN chown -R app:app $APP_HOME | |||||
| # change to the app user | |||||
| #USER app | |||||
| WORKDIR $APP_HOME/reymota | |||||
| # run entrypoint.sh | |||||
| ENTRYPOINT ["/app/entrypoint.sh"] | |||||
| @ -0,0 +1 @@ | |||||
| temp.json | |||||
| @ -0,0 +1,20 @@ | |||||
| apiVersion: v1 | |||||
| data: | |||||
| DEBUG: "True" | |||||
| ENTORNO: "Pruebas" | |||||
| DJANGO_ALLOWED_HOSTS: "ranchermota.es vmcluster k8s-server localhost 127.0.0.1 [::1]" | |||||
| CSRF_TRUSTED_ORIGINS: "https://ranchermota.es http://vmcluster" | |||||
| SECRET_KEY: change_me | |||||
| SQL_DATABASE: reymota | |||||
| SQL_ENGINE: django.db.backends.postgresql | |||||
| SQL_HOST: db | |||||
| SQL_PASSWORD: Dsa-0213 | |||||
| SQL_PORT: "5432" | |||||
| SQL_USER: creylopez | |||||
| DATABASE: postgres | |||||
| kind: ConfigMap | |||||
| metadata: | |||||
| labels: | |||||
| io.kompose.service: web-env-prod | |||||
| name: env-prod | |||||
| namespace: ranchermota | |||||
| @ -0,0 +1,20 @@ | |||||
| apiVersion: v1 | |||||
| data: | |||||
| DEBUG: "True" | |||||
| ENTORNO: "Producción" | |||||
| DJANGO_ALLOWED_HOSTS: "ranchermota.rancher.reymota.lab reymota.es vmcluster k8s-server localhost 127.0.0.1 [::1]" | |||||
| CSRF_TRUSTED_ORIGINS: "https://reymota.es http://vmcluster http://ranchermota.rancher.reymota.lab/ http://localhost http://127.0.0.1" | |||||
| SECRET_KEY: change_me | |||||
| SQL_DATABASE: reymota | |||||
| SQL_ENGINE: django.db.backends.postgresql | |||||
| SQL_HOST: db | |||||
| SQL_PASSWORD: Dsa-0213 | |||||
| SQL_PORT: "5432" | |||||
| SQL_USER: creylopez | |||||
| DATABASE: postgres | |||||
| kind: ConfigMap | |||||
| metadata: | |||||
| labels: | |||||
| io.kompose.service: web-env-prod | |||||
| name: env-prod | |||||
| namespace: ranchermota | |||||
| @ -0,0 +1,11 @@ | |||||
| apiVersion: v1 | |||||
| data: | |||||
| POSTGRES_DB: reymota | |||||
| POSTGRES_PASSWORD: Dsa-0213 | |||||
| POSTGRES_USER: creylopez | |||||
| kind: ConfigMap | |||||
| metadata: | |||||
| labels: | |||||
| io.kompose.service: db-env-prod-db | |||||
| name: env-prod-db | |||||
| namespace: ranchermota | |||||
| @ -0,0 +1,53 @@ | |||||
| apiVersion: apps/v1 | |||||
| kind: Deployment | |||||
| metadata: | |||||
| annotations: | |||||
| kompose.cmd: kompose convert | |||||
| kompose.version: 1.34.0 (cbf2835db) | |||||
| labels: | |||||
| io.kompose.service: db | |||||
| name: db | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| replicas: 1 | |||||
| selector: | |||||
| matchLabels: | |||||
| io.kompose.service: db | |||||
| strategy: | |||||
| type: Recreate | |||||
| template: | |||||
| metadata: | |||||
| annotations: | |||||
| kompose.cmd: kompose convert | |||||
| kompose.version: 1.34.0 (cbf2835db) | |||||
| labels: | |||||
| io.kompose.service: db | |||||
| 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: db | |||||
| volumeMounts: | |||||
| - mountPath: /var/lib/postgresql/data | |||||
| name: postgres-data | |||||
| subPath: pgdata | |||||
| restartPolicy: Always | |||||
| volumes: | |||||
| - name: postgres-data | |||||
| persistentVolumeClaim: | |||||
| claimName: postgres-data | |||||
| @ -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: ranchermota | |||||
| 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-reymota-$ARQUITECTURA:$IMG_NGINX_VERSION | |||||
| name: nginx | |||||
| ports: | |||||
| - containerPort: 80 | |||||
| protocol: TCP | |||||
| volumeMounts: | |||||
| - mountPath: /app/reymota/staticfiles | |||||
| name: static-volume | |||||
| - mountPath: /app/reymota/mediafiles | |||||
| name: reymota-media | |||||
| imagePullSecrets: | |||||
| - name: myregistrykey | |||||
| restartPolicy: Always | |||||
| volumes: | |||||
| - name: static-volume | |||||
| persistentVolumeClaim: | |||||
| claimName: static-volume | |||||
| - name: reymota-media | |||||
| persistentVolumeClaim: | |||||
| claimName: reymota-media | |||||
| @ -0,0 +1,126 @@ | |||||
| apiVersion: apps/v1 | |||||
| kind: Deployment | |||||
| metadata: | |||||
| name: reymota | |||||
| namespace: ranchermota | |||||
| labels: | |||||
| app: reymota | |||||
| spec: | |||||
| replicas: 1 | |||||
| selector: | |||||
| matchLabels: | |||||
| app: reymota | |||||
| strategy: | |||||
| type: Recreate | |||||
| template: | |||||
| metadata: | |||||
| labels: | |||||
| app: reymota | |||||
| spec: | |||||
| containers: | |||||
| - args: | |||||
| - gunicorn | |||||
| - reymota.wsgi:application | |||||
| - --bind | |||||
| - 0.0.0.0:8000 | |||||
| name: reymota | |||||
| image: $REGISTRY/reymota-$ARQUITECTURA:$IMG_VERSION | |||||
| env: | |||||
| - name: VERSION | |||||
| value: "$IMG_VERSION" | |||||
| - name: ENVIRONMENT | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: ENTORNO | |||||
| name: env-prod | |||||
| - name: DEBUG | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: DEBUG | |||||
| name: env-prod | |||||
| - name: DJANGO_ALLOWED_HOSTS | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: DJANGO_ALLOWED_HOSTS | |||||
| name: env-prod | |||||
| - name: CSRF_TRUSTED_ORIGINS | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: CSRF_TRUSTED_ORIGINS | |||||
| name: env-prod | |||||
| - name: SECRET_KEY | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SECRET_KEY | |||||
| name: env-prod | |||||
| - name: DATABASE | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: DATABASE | |||||
| name: env-prod | |||||
| - name: SQL_HOST | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SQL_HOST | |||||
| name: env-prod | |||||
| - name: SQL_PORT | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SQL_PORT | |||||
| name: env-prod | |||||
| - name: SQL_ENGINE | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SQL_ENGINE | |||||
| name: env-prod | |||||
| - name: SQL_DATABASE | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SQL_DATABASE | |||||
| name: env-prod | |||||
| - name: SQL_USER | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SQL_USER | |||||
| name: env-prod | |||||
| - name: SQL_PASSWORD | |||||
| valueFrom: | |||||
| configMapKeyRef: | |||||
| key: SQL_PASSWORD | |||||
| name: env-prod | |||||
| ports: | |||||
| - containerPort: 8000 | |||||
| protocol: TCP | |||||
| volumeMounts: | |||||
| - mountPath: /app/reymota/mediafiles | |||||
| name: reymota-media | |||||
| - mountPath: /app/reymota/lyrics/migrations | |||||
| name: reymota-lyrics-migrations | |||||
| - mountPath: /app/reymota/repostajes/migrations | |||||
| name: reymota-repostajes-migrations | |||||
| - mountPath: /app/reymota/reymotausers/migrations | |||||
| name: reymota-reymotausers-migrations | |||||
| - mountPath: /app/reymota/staticfiles | |||||
| name: static-volume | |||||
| imagePullSecrets: | |||||
| - name: myregistrykey | |||||
| restartPolicy: Always | |||||
| volumes: | |||||
| - name: reymota-media | |||||
| persistentVolumeClaim: | |||||
| claimName: reymota-media | |||||
| - name: reymota-lyrics-migrations | |||||
| persistentVolumeClaim: | |||||
| claimName: reymota-lyrics-migrations | |||||
| - name: reymota-repostajes-migrations | |||||
| persistentVolumeClaim: | |||||
| claimName: reymota-repostajes-migrations | |||||
| - name: reymota-reymotausers-migrations | |||||
| persistentVolumeClaim: | |||||
| claimName: reymota-reymotausers-migrations | |||||
| - name: static-volume | |||||
| persistentVolumeClaim: | |||||
| claimName: static-volume | |||||
| status: {} | |||||
| @ -0,0 +1,60 @@ | |||||
| export ARQUITECTURA := $(shell lscpu |grep itectur | tr -d ' '| cut -f2 -d':') | |||||
| #export REGISTRY=registry.cube.local | |||||
| export REGISTRY=registry.reymota.es | |||||
| export IMG_VERSION = 0.56 | |||||
| export IMG_NGINX_VERSION = 1.0 | |||||
| # limpia todo | |||||
| all: imagen clean install | |||||
| imagen: | |||||
| cd ../; make | |||||
| install: | |||||
| # Secreto y configmaps | |||||
| -kubectl create -f reg-secret.yaml | |||||
| -kubectl create -f ./ConfigMaps/env-prod-configmap.yaml | |||||
| -kubectl create -f ./ConfigMaps/env-prod-db-configmap.yaml | |||||
| # Deployments | |||||
| -kubectl create -f ./Deployments/db-deployment.yaml | |||||
| -envsubst < ./Deployments/reymota-deployment.yaml |kubectl create -f - | |||||
| -envsubst < ./Deployments/nginx-deployment.yaml |kubectl create -f - | |||||
| # Servicios | |||||
| -kubectl create -f ./Services/db-service.yaml | |||||
| -kubectl create -f ./Services/reymota-service.yaml | |||||
| -kubectl create -f ./Services/nginx-service.yaml | |||||
| -kubectl create -f ./Services/reymota-ingress.yaml | |||||
| clean: | |||||
| # Deployments | |||||
| -envsubst < ./Deployments/nginx-deployment.yaml |kubectl delete -f - | |||||
| -envsubst < ./Deployments/reymota-deployment.yaml |kubectl delete -f - | |||||
| -kubectl delete -f ./Deployments/db-deployment.yaml | |||||
| # Servicios | |||||
| -kubectl delete -f ./Services/reymota-ingress.yaml | |||||
| -kubectl delete -f ./Services/reymota-service.yaml | |||||
| -kubectl delete -f ./Services/nginx-service.yaml | |||||
| -kubectl delete -f ./Services/db-service.yaml | |||||
| # Secreto y configmaps | |||||
| -kubectl delete -f ./ConfigMaps/env-prod-configmap.yaml | |||||
| -kubectl delete -f ./ConfigMaps/env-prod-db-configmap.yaml | |||||
| nginx: | |||||
| cd ../nginx; make | |||||
| backup: | |||||
| kubectl --kubeconfig /home/creylopez/.kube/config -n reymota exec -ti deployment.apps/db -- /usr/lib/postgresql/15/bin/pg_dump --username=creylopez --dbname=reymota > reymota-$(IMG_VERSION).sql | |||||
| muestra: | |||||
| -envsubst < reymota-deployment.yaml > /tmp/deployment.yaml | |||||
| @ -0,0 +1,13 @@ | |||||
| apiVersion: v1 | |||||
| kind: PersistentVolumeClaim | |||||
| metadata: | |||||
| labels: | |||||
| io.kompose.service: postgres-data | |||||
| name: postgres-data | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| accessModes: | |||||
| - ReadWriteOnce | |||||
| resources: | |||||
| requests: | |||||
| storage: 100Mi | |||||
| @ -0,0 +1,63 @@ | |||||
| apiVersion: v1 | |||||
| kind: PersistentVolumeClaim | |||||
| metadata: | |||||
| creationTimestamp: null | |||||
| labels: | |||||
| io.kompose.service: reymota-media | |||||
| name: reymota-media | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| accessModes: | |||||
| - ReadWriteOnce | |||||
| resources: | |||||
| requests: | |||||
| storage: 100Mi | |||||
| status: {} | |||||
| --- | |||||
| apiVersion: v1 | |||||
| kind: PersistentVolumeClaim | |||||
| metadata: | |||||
| creationTimestamp: null | |||||
| labels: | |||||
| io.kompose.service: reymota-lyrics-migrations | |||||
| name: reymota-lyrics-migrations | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| accessModes: | |||||
| - ReadWriteOnce | |||||
| resources: | |||||
| requests: | |||||
| storage: 50Mi | |||||
| status: {} | |||||
| --- | |||||
| apiVersion: v1 | |||||
| kind: PersistentVolumeClaim | |||||
| metadata: | |||||
| creationTimestamp: null | |||||
| labels: | |||||
| io.kompose.service: reymota-repostajes-migrations | |||||
| name: reymota-repostajes-migrations | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| accessModes: | |||||
| - ReadWriteOnce | |||||
| resources: | |||||
| requests: | |||||
| storage: 52Mi | |||||
| status: {} | |||||
| --- | |||||
| apiVersion: v1 | |||||
| kind: PersistentVolumeClaim | |||||
| metadata: | |||||
| creationTimestamp: null | |||||
| labels: | |||||
| io.kompose.service: reymota-reymotausers-migrations | |||||
| name: reymota-reymotausers-migrations | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| accessModes: | |||||
| - ReadWriteOnce | |||||
| resources: | |||||
| requests: | |||||
| storage: 53Mi | |||||
| status: {} | |||||
| @ -0,0 +1,13 @@ | |||||
| apiVersion: v1 | |||||
| kind: PersistentVolumeClaim | |||||
| metadata: | |||||
| labels: | |||||
| io.kompose.service: static-volume | |||||
| name: static-volume | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| accessModes: | |||||
| - ReadWriteOnce | |||||
| resources: | |||||
| requests: | |||||
| storage: 70Mi | |||||
| @ -0,0 +1,17 @@ | |||||
| apiVersion: v1 | |||||
| kind: Service | |||||
| metadata: | |||||
| annotations: | |||||
| kompose.cmd: kompose convert | |||||
| kompose.version: 1.34.0 (cbf2835db) | |||||
| labels: | |||||
| io.kompose.service: db | |||||
| name: db | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| ports: | |||||
| - name: "5432" | |||||
| port: 5432 | |||||
| targetPort: 5432 | |||||
| selector: | |||||
| io.kompose.service: db | |||||
| @ -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: ranchermota | |||||
| spec: | |||||
| type: NodePort | |||||
| ports: | |||||
| - name: "1337" | |||||
| port: 1337 | |||||
| nodePort: 30343 | |||||
| targetPort: 80 | |||||
| selector: | |||||
| io.kompose.service: nginx | |||||
| @ -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: reymota | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| defaultBackend: | |||||
| service: | |||||
| name: nginx | |||||
| port: | |||||
| number: 1337 | |||||
| ingressClassName: nginx | |||||
| rules: | |||||
| - host: ranchermota.rancher.reymota.lab | |||||
| status: | |||||
| loadBalancer: {} | |||||
| @ -0,0 +1,12 @@ | |||||
| apiVersion: v1 | |||||
| kind: Service | |||||
| metadata: | |||||
| name: reymota | |||||
| namespace: ranchermota | |||||
| spec: | |||||
| ports: | |||||
| - name: "8000" | |||||
| port: 8000 | |||||
| targetPort: 8000 | |||||
| selector: | |||||
| app: reymota | |||||
| @ -0,0 +1,2 @@ | |||||
| ( NAMESPACE=ranchermota; kubectl proxy & kubectl get namespace $NAMESPACE -o json |jq '.spec = {"finalizers":[]}' >temp.json; curl -k -H "Content-Type: application/json" -X PUT --data-binary @temp.json 127.0.0.1:8001/api/v1/namespaces/$NAMESPACE/finalize; ) | |||||
| @ -0,0 +1 @@ | |||||
| kubectl -n ranchermota exec -ti deployment.apps/reymota -- /bin/bash | |||||
| @ -0,0 +1 @@ | |||||
| kubectl -n ranchermota exec -ti deployment.apps/db -- psql --username=creylopez --dbname=reymota | |||||
| @ -0,0 +1,7 @@ | |||||
| ################################################### | |||||
| # Namespace reymota | |||||
| ################################################### | |||||
| apiVersion: v1 | |||||
| kind: Namespace | |||||
| metadata: | |||||
| name: ranchermota | |||||
| @ -0,0 +1,8 @@ | |||||
| apiVersion: v1 | |||||
| kind: Secret | |||||
| metadata: | |||||
| name: myregistrykey | |||||
| namespace: ranchermota | |||||
| data: | |||||
| .dockerconfigjson: ewoJImF1dGhzIjogewoJCSJyZWdpc3RyeS5yZXltb3RhLmVzIjogewoJCQkiYXV0aCI6ICJZM0psZVd4dmNHVjZPbEpsZVMweE1UYzIiCgkJfQoJfQp9 | |||||
| type: kubernetes.io/dockerconfigjson | |||||
| @ -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}/reymota-${ARQUITECTURA}:${IMG_VERSION} . | |||||
| docker push ${REGISTRY}/reymota-${ARQUITECTURA}:${IMG_VERSION} | |||||
| @ -0,0 +1,49 @@ | |||||
| # Instalación | |||||
| Desde el directorio K8S ejecutar make (esto hace todo: la imagen, para los pods y los lanza otra vez) | |||||
| La primera vez, hay que entrar en el pod de vehículos con 'entra.sh' y | |||||
| python manage.py createsuperuser | |||||
| python manage.py makemigrations | |||||
| python manage.py migrate | |||||
| ## Comprobar la base de datos | |||||
| Con la shell entraPsql.sh: | |||||
| \l para listar las BD | |||||
| \c reymota para usar nuestra db | |||||
| \dt para ver las tablas | |||||
| # De dónde cogí ideas | |||||
| https://learndjango.com/tutorials/django-login-and-logout-tutorial | |||||
| Username: {{ user.username }} | |||||
| User Full name: {{ user.get_full_name }} | |||||
| User Group: {{ user.groups.all.0 }} | |||||
| Email: {{ user.email }} | |||||
| Session Started at: {{ user.last_login }} | |||||
| ## Para funcionar con gunicorn y nginx | |||||
| https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/ | |||||
| ## Cambiar la secuencia de lo sid | |||||
| ALTER SEQUENCE tablename_id_seq RESTART WITH nn; | |||||
| esto se hace cuando restauro un volcado de la bd sobre una instalación nueva. Si hay índices ya creados, hay que reinciar a partir del último. | |||||
| @ -0,0 +1,20 @@ | |||||
| #!/bin/bash | |||||
| 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 "$@" | |||||
| @ -0,0 +1,4 @@ | |||||
| FROM nginx:1.25 | |||||
| RUN rm /etc/nginx/conf.d/default.conf | |||||
| COPY nginx.conf /etc/nginx/conf.d | |||||
| @ -0,0 +1,8 @@ | |||||
| install: | |||||
| echo "Creando imagen con version '${IMG_NGINX_VERSION}' para la arquitectura '${ARQUITECTURA}' en el registry '${REGISTRY}'" | |||||
| docker build --no-cache -t ${REGISTRY}/nginx-reymota-${ARQUITECTURA}:${IMG_NGINX_VERSION} . | |||||
| docker push ${REGISTRY}/nginx-reymota-${ARQUITECTURA}:${IMG_NGINX_VERSION} | |||||
| @ -0,0 +1,25 @@ | |||||
| upstream reymota { | |||||
| server reymota:8000; | |||||
| } | |||||
| server { | |||||
| listen 80; | |||||
| location / { | |||||
| proxy_pass http://reymota; | |||||
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |||||
| proxy_set_header Host $http_host; | |||||
| proxy_redirect off; | |||||
| client_max_body_size 100M; | |||||
| } | |||||
| location /static/ { | |||||
| alias /app/reymota/staticfiles/; | |||||
| } | |||||
| location /media/ { | |||||
| alias /app/reymota/mediafiles/; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| asgiref==3.8.1 | |||||
| Django==4.2 | |||||
| django-calculation==1.0.0 | |||||
| djangorestframework==3.15.2 | |||||
| flake8==7.1.1 | |||||
| gunicorn==22.0.0 | |||||
| mccabe==0.7.0 | |||||
| numpy==2.2.2 | |||||
| packaging==24.1 | |||||
| pandas==2.2.3 | |||||
| pillow==10.4.0 | |||||
| psycopg2-binary==2.9.6 | |||||
| pycodestyle==2.12.1 | |||||
| pyflakes==3.2.0 | |||||
| python-dateutil==2.9.0.post0 | |||||
| pytz==2025.1 | |||||
| six==1.17.0 | |||||
| sqlparse==0.5.1 | |||||
| typing_extensions==4.12.2 | |||||
| tzdata==2025.1 | |||||
| @ -0,0 +1 @@ | |||||
| migrations/ | |||||
| @ -0,0 +1,3 @@ | |||||
| from django.contrib import admin | |||||
| # Register your models here. | |||||
| @ -0,0 +1,6 @@ | |||||
| from django.apps import AppConfig | |||||
| class AccountsConfig(AppConfig): | |||||
| default_auto_field = 'django.db.models.BigAutoField' | |||||
| name = 'accounts' | |||||
| @ -0,0 +1,3 @@ | |||||
| from django.db import models | |||||
| # Create your models here. | |||||
| @ -0,0 +1,3 @@ | |||||
| from django.test import TestCase | |||||
| # Create your tests here. | |||||
| @ -0,0 +1,8 @@ | |||||
| # accounts/urls.py | |||||
| from django.urls import path | |||||
| from django.contrib.auth import views as auth_views | |||||
| urlpatterns = [ | |||||
| path('login/', auth_views.LoginView.as_view(), name='login'), | |||||
| path('logout/', auth_views.LogoutView.as_view(), name='logout'), | |||||
| ] | |||||
| @ -0,0 +1 @@ | |||||
| from django.shortcuts import render | |||||
| @ -0,0 +1,6 @@ | |||||
| export CSRF_TRUSTED_ORIGINS="http://localhost" | |||||
| export DEBUG="True" | |||||
| export SECRET_KEY="hola" | |||||
| export DJANGO_ALLOWED_HOSTS="localhost" | |||||
| @ -0,0 +1,9 @@ | |||||
| from django.contrib import admin | |||||
| # Register your models here. | |||||
| from .models import Artista, Album, Song | |||||
| admin.site.register(Artista) | |||||
| admin.site.register(Album) | |||||
| admin.site.register(Song) | |||||
| @ -0,0 +1,6 @@ | |||||
| from django.apps import AppConfig | |||||
| class LyricsConfig(AppConfig): | |||||
| default_auto_field = 'django.db.models.BigAutoField' | |||||
| name = 'lyrics' | |||||
| @ -0,0 +1,41 @@ | |||||
| from django import forms | |||||
| from .models import Artista, Album, Song | |||||
| class ArtistaForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Artista | |||||
| fields = ['nombre', 'biografia', 'foto'] | |||||
| nombre = forms.CharField( | |||||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||||
| biografia = forms.CharField( | |||||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||||
| class AlbumForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Album | |||||
| fields = ['name', 'artist', 'year', 'cover_image'] | |||||
| # year = forms.DateField( | |||||
| # widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})) | |||||
| artist = forms.ModelChoiceField( | |||||
| queryset=Artista.objects.all(), | |||||
| widget=forms.Select(attrs={'class': 'form-control'})) | |||||
| class SongForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Song | |||||
| fields = ['title', 'artist', 'album', 'year', 'lyrics'] | |||||
| artist = forms.ModelChoiceField( | |||||
| queryset=Artista.objects.all(), | |||||
| widget=forms.Select(attrs={'class': 'form-control'})) | |||||
| album = forms.ModelChoiceField( | |||||
| queryset=Album.objects.all(), # habría que seleccionar los álbumes del artista | |||||
| widget=forms.Select(attrs={'class': 'form-control'})) | |||||
| @ -0,0 +1,48 @@ | |||||
| import json | |||||
| from django.core.management.base import BaseCommand | |||||
| from lyrics.models import Album, Artista | |||||
| class Command(BaseCommand): | |||||
| help = "Importa albumes desde un archivo JSON" | |||||
| def add_arguments(self, parser): | |||||
| parser.add_argument('archivo_json', type=str, help="Ruta del archivo JSON") | |||||
| def handle(self, *args, **kwargs): | |||||
| archivo_json = kwargs['archivo_json'] | |||||
| try: | |||||
| with open(archivo_json, 'r', encoding='utf-8') as file: | |||||
| datos = json.load(file) | |||||
| self.stdout.write(self.style.WARNING(f"\nSe encontraron {len(datos)} albumes en el archivo '{archivo_json}'.")) | |||||
| confirmar = input("¿Deseas continuar con la importación? (s/n): ").strip().lower() | |||||
| if confirmar != 's': | |||||
| self.stdout.write(self.style.ERROR("Importación cancelada.")) | |||||
| return | |||||
| albumes_creados = 0 | |||||
| for album_data in datos: | |||||
| try: | |||||
| artista = Artista.objects.get(nombre=album_data["artista_nombre"]) | |||||
| creado = Album.objects.create( | |||||
| artist=artista, | |||||
| name=album_data['name'], | |||||
| year=album_data['year'], | |||||
| cover_image=album_data['cover_image'], | |||||
| ) | |||||
| if creado: | |||||
| albumes_creados += 1 | |||||
| except Artista.DoesNotExist: | |||||
| self.stderr.write(self.style.ERROR(f"Artista '{album_data['artista_nombre']}' no encontrado.")) | |||||
| self.stdout.write(self.style.SUCCESS(f'Se importaron {albumes_creados} albumes correctamente.')) | |||||
| except FileNotFoundError: | |||||
| self.stderr.write(self.style.ERROR(f"El archivo {archivo_json} no se encontró.")) | |||||
| except json.JSONDecodeError: | |||||
| self.stderr.write(self.style.ERROR("Error al leer el archivo JSON. Asegúrate de que el formato sea correcto.")) | |||||
| @ -0,0 +1,42 @@ | |||||
| import json | |||||
| from django.core.management.base import BaseCommand | |||||
| from lyrics.models import Artista | |||||
| class Command(BaseCommand): | |||||
| help = "Importa artistas desde un archivo JSON" | |||||
| def add_arguments(self, parser): | |||||
| parser.add_argument('archivo_json', type=str, help="Ruta del archivo JSON") | |||||
| def handle(self, *args, **kwargs): | |||||
| archivo_json = kwargs['archivo_json'] | |||||
| try: | |||||
| with open(archivo_json, 'r', encoding='utf-8') as file: | |||||
| datos = json.load(file) | |||||
| self.stdout.write(self.style.WARNING(f"\nSe encontraron {len(datos)} artistas en el archivo '{archivo_json}'.")) | |||||
| confirmar = input("¿Deseas continuar con la importación? (s/n): ").strip().lower() | |||||
| if confirmar != 's': | |||||
| self.stdout.write(self.style.ERROR("Importación cancelada.")) | |||||
| return | |||||
| artistas_creados = 0 | |||||
| for artista_data in datos: | |||||
| creado = Artista.objects.create( | |||||
| id=artista_data['id'], | |||||
| nombre=artista_data['nombre'], | |||||
| biografia=artista_data['biografia'], | |||||
| foto=artista_data['foto'] | |||||
| ) | |||||
| if creado: | |||||
| artistas_creados += 1 | |||||
| self.stdout.write(self.style.SUCCESS(f'Se importaron {artistas_creados} artistas correctamente.')) | |||||
| except FileNotFoundError: | |||||
| self.stderr.write(self.style.ERROR(f"El archivo {archivo_json} no se encontró.")) | |||||
| except json.JSONDecodeError: | |||||
| self.stderr.write(self.style.ERROR("Error al leer el archivo JSON. Asegúrate de que el formato sea correcto.")) | |||||
| @ -0,0 +1,51 @@ | |||||
| import json | |||||
| from django.core.management.base import BaseCommand | |||||
| from lyrics.models import Song, Album, Artista | |||||
| class Command(BaseCommand): | |||||
| help = "Importa canciones desde un archivo JSON" | |||||
| def add_arguments(self, parser): | |||||
| parser.add_argument('archivo_json', type=str, help="Ruta del archivo JSON") | |||||
| def handle(self, *args, **kwargs): | |||||
| archivo_json = kwargs['archivo_json'] | |||||
| try: | |||||
| with open(archivo_json, 'r', encoding='utf-8') as file: | |||||
| datos = json.load(file) | |||||
| self.stdout.write(self.style.WARNING(f"\nSe encontraron {len(datos)} canciones en el archivo '{archivo_json}'.")) | |||||
| confirmar = input("¿Deseas continuar con la importación? (s/n): ").strip().lower() | |||||
| if confirmar != 's': | |||||
| self.stdout.write(self.style.ERROR("Importación cancelada.")) | |||||
| return | |||||
| canciones_creados = 0 | |||||
| for cancion_data in datos: | |||||
| try: | |||||
| album = Album.objects.get(name=cancion_data["album_nombre"]) | |||||
| artista = Artista.objects.get(nombre=cancion_data["artista_nombre"]) | |||||
| creado = Song.objects.create( | |||||
| album=album, | |||||
| artist=artista, | |||||
| title=cancion_data['title'], | |||||
| year=cancion_data['year'], | |||||
| lyrics=cancion_data['lyrics'], | |||||
| pista=cancion_data['pista'], | |||||
| ) | |||||
| if creado: | |||||
| canciones_creados += 1 | |||||
| except Album.DoesNotExist: | |||||
| self.stderr.write(self.style.ERROR(f"Album '{cancion_data['album']}' no encontrado.")) | |||||
| self.stdout.write(self.style.SUCCESS(f'Se importaron {canciones_creados} canciones correctamente.')) | |||||
| except FileNotFoundError: | |||||
| self.stderr.write(self.style.ERROR(f"El archivo {archivo_json} no se encontró.")) | |||||
| except json.JSONDecodeError: | |||||
| self.stderr.write(self.style.ERROR("Error al leer el archivo JSON. Asegúrate de que el formato sea correcto.")) | |||||
| @ -0,0 +1,52 @@ | |||||
| # Generated by Django 4.2 on 2024-09-10 13:23 | |||||
| import django.core.validators | |||||
| from django.db import migrations, models | |||||
| import django.db.models.deletion | |||||
| import lyrics.models | |||||
| class Migration(migrations.Migration): | |||||
| initial = True | |||||
| dependencies = [ | |||||
| ] | |||||
| operations = [ | |||||
| migrations.CreateModel( | |||||
| name='Album', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('name', models.CharField(max_length=200)), | |||||
| ('year', models.PositiveBigIntegerField(default=2024, validators=[django.core.validators.MinValueValidator(1984), lyrics.models.max_value_current_year])), | |||||
| ('cover_image', models.ImageField(blank=True, null=True, upload_to='cover_image/')), | |||||
| ], | |||||
| ), | |||||
| migrations.CreateModel( | |||||
| name='Artista', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('nombre', models.CharField(max_length=200)), | |||||
| ('biografia', models.TextField(blank=True, null=True)), | |||||
| ('foto', models.ImageField(blank=True, null=True, upload_to='artistas/')), | |||||
| ], | |||||
| ), | |||||
| migrations.CreateModel( | |||||
| name='Song', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('title', models.CharField(max_length=200)), | |||||
| ('year', models.PositiveBigIntegerField(default=2024, validators=[django.core.validators.MinValueValidator(1984), lyrics.models.max_value_current_year])), | |||||
| ('lyrics', models.CharField(max_length=1000)), | |||||
| ('pista', models.DecimalField(blank=True, decimal_places=0, max_digits=5, null=True)), | |||||
| ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lyrics.album')), | |||||
| ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lyrics.artista')), | |||||
| ], | |||||
| ), | |||||
| migrations.AddField( | |||||
| model_name='album', | |||||
| name='artist', | |||||
| field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lyrics.artista'), | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,42 @@ | |||||
| from django.db import models | |||||
| import datetime | |||||
| from django.core.validators import MaxValueValidator, MinValueValidator | |||||
| def current_year(): | |||||
| return datetime.date.today().year | |||||
| def max_value_current_year(value): | |||||
| return MaxValueValidator(current_year())(value) | |||||
| class Artista(models.Model): | |||||
| nombre = models.CharField(max_length=200) | |||||
| biografia = models.TextField(blank=True, null=True) | |||||
| foto = models.ImageField(upload_to='artistas/', blank=True, null=True) # Nuevo campo | |||||
| def __str__(self): | |||||
| return self.nombre | |||||
| class Album(models.Model): | |||||
| name = models.CharField(max_length=200) | |||||
| artist = models.ForeignKey(Artista, on_delete=models.CASCADE) | |||||
| year = models.PositiveBigIntegerField(default=current_year(), validators=[MinValueValidator(1945), max_value_current_year]) | |||||
| cover_image = models.ImageField(upload_to='cover_image/', blank=True, null=True) # Nuevo campo | |||||
| def __str__(self): | |||||
| return self.name | |||||
| class Song(models.Model): | |||||
| title = models.CharField(max_length=200) | |||||
| artist = models.ForeignKey(Artista, on_delete=models.CASCADE) | |||||
| album = models.ForeignKey(Album, on_delete=models.CASCADE, related_name='song') | |||||
| year = models.DecimalField(max_digits=4, decimal_places=0, blank=False, null=False) | |||||
| lyrics = models.TextField() | |||||
| pista = models.DecimalField(max_digits=5, decimal_places=0, blank=True, null=True) | |||||
| def __str__(self): | |||||
| return self.title | |||||
| @ -0,0 +1,39 @@ | |||||
| import os | |||||
| from rest_framework import serializers | |||||
| from .models import Artista, Album, Song | |||||
| class ArtistaSerializer(serializers.ModelSerializer): | |||||
| class Meta: | |||||
| model = Artista | |||||
| fields = '__all__' # Incluir todos los campos del modelo | |||||
| def to_representation(self, instance): | |||||
| ret = super().to_representation(instance) | |||||
| ret['foto'] = "artistas/" + os.path.basename(ret['foto']) | |||||
| return ret | |||||
| class AlbumSerializer(serializers.ModelSerializer): | |||||
| artista_nombre = serializers.CharField(source='artist.nombre', read_only=True) | |||||
| class Meta: | |||||
| model = Album | |||||
| fields = ['name', 'year', 'cover_image', 'artista_nombre'] | |||||
| def to_representation(self, instance): | |||||
| ret = super().to_representation(instance) | |||||
| ret['cover_image'] = "cover_image/" + os.path.basename(ret['cover_image']) | |||||
| return ret | |||||
| class CancionSerializer(serializers.ModelSerializer): | |||||
| artista_nombre = serializers.CharField(source='artist.nombre', read_only=True) | |||||
| album_nombre = serializers.CharField(source='album.name', read_only=True) | |||||
| class Meta: | |||||
| model = Song | |||||
| fields = ['title', 'year', 'lyrics', 'pista', 'artista_nombre', 'album_nombre'] | |||||
| @ -0,0 +1,3 @@ | |||||
| from django.test import TestCase | |||||
| # Create your tests here. | |||||
| @ -0,0 +1,39 @@ | |||||
| from django.urls import path | |||||
| from . import views | |||||
| from .views import api_lista_artistas, api_detalle_artista | |||||
| from .views import api_lista_albumes, api_detalle_album | |||||
| from .views import api_lista_canciones, api_detalle_cancion | |||||
| app_name = 'lyrics' | |||||
| urlpatterns = [ | |||||
| path('', views.principal, name='principal'), | |||||
| path('artistas/', views.lista_artistas, name='lista_artistas'), | |||||
| path('artistas/nuevo/', views.nuevo_artista, name='nuevo_artista'), | |||||
| path('artistas/<int:artista_id>/', views.detalle_artista, name='detalle_artista'), | |||||
| path('artistas/<int:artista_id>/editar/', views.editar_artista, name='editar_artista'), | |||||
| path('artistas/<int:artista_id>/eliminar/', views.eliminar_artista, name='eliminar_artista'), | |||||
| path('album/', views.lista_albumes, name='lista_albumes'), | |||||
| path('album/nuevo/', views.nuevo_album, name='nuevo_album'), | |||||
| path('album/<int:album_id>/', views.detalle_album, name='detalle_album'), | |||||
| path('album/<int:album_id>/editar/', views.editar_album, name='editar_album'), | |||||
| path('album/<int:album_id>/eliminar/', views.eliminar_album, name='eliminar_album'), | |||||
| path('song/', views.lista_songs, name='lista_songs'), | |||||
| path('song/nuevo/', views.nuevo_song, name='nuevo_song'), | |||||
| path('song/<int:song_id>/', views.detalle_song, name='detalle_song'), | |||||
| path('song/<int:song_id>/editar/', views.editar_song, name='editar_song'), | |||||
| path('song/<int:song_id>/eliminar/', views.eliminar_song, name='eliminar_song'), | |||||
| path('api/artistas/', api_lista_artistas, name='api_lista_artistas'), | |||||
| path('api/artistas/<int:artista_id>/', api_detalle_artista, name='api_detalle_artista'), | |||||
| path('api/albumes/', api_lista_albumes, name='api_lista_albumes'), | |||||
| path('api/albumes/<int:album_id>/', api_detalle_album, name='api_detalle_album'), | |||||
| path('api/canciones/', api_lista_canciones, name='api_lista_canciones'), | |||||
| path('api/canciones/<int:cancion_id>/', api_detalle_cancion, name='api_detalle_cancion'), | |||||
| ] | |||||
| @ -0,0 +1,251 @@ | |||||
| # Create your views here. | |||||
| from django.shortcuts import render, get_object_or_404, redirect | |||||
| from django.contrib.auth.decorators import login_required | |||||
| from rest_framework.response import Response | |||||
| from rest_framework.decorators import api_view | |||||
| from .models import Artista, Album, Song | |||||
| from .forms import ArtistaForm, AlbumForm, SongForm | |||||
| from .serializers import ArtistaSerializer, AlbumSerializer, CancionSerializer | |||||
| import logging | |||||
| logger = logging.getLogger(__name__) | |||||
| @login_required | |||||
| def principal(request): | |||||
| artistas = Artista.objects.all() | |||||
| albumes = Album.objects.all() | |||||
| return render(request, 'lyrics/index.html', {'artistas': artistas, 'albumes': albumes}) | |||||
| ######################### | |||||
| # Vistas para los artistas | |||||
| @login_required | |||||
| def lista_artistas(request): | |||||
| artistas = Artista.objects.all() | |||||
| return render(request, 'lyrics/lista_artistas.html', {'artistas': artistas}) | |||||
| @login_required | |||||
| def detalle_artista(request, artista_id): | |||||
| artista = get_object_or_404(Artista, pk=artista_id) | |||||
| albumes = Album.objects.filter(artist=artista_id) | |||||
| return render(request, 'lyrics/detalle_artista.html', {'artista': artista, 'albumes': albumes}) | |||||
| @login_required | |||||
| def nuevo_artista(request): | |||||
| if request.method == 'POST': | |||||
| form = ArtistaForm(request.POST, request.FILES) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lyrics:lista_artistas') | |||||
| else: | |||||
| form = ArtistaForm() | |||||
| return render(request, 'lyrics/form_artista.html', {'form': form}) | |||||
| @login_required | |||||
| def editar_artista(request, artista_id): | |||||
| artista = get_object_or_404(Artista, pk=artista_id) | |||||
| if request.method == 'POST': | |||||
| form = ArtistaForm(request.POST, request.FILES, instance=artista) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lyrics:lista_artistas') | |||||
| else: | |||||
| form = ArtistaForm(instance=artista) | |||||
| return render(request, 'lyrics/form_artista.html', {'form': form}) | |||||
| @login_required | |||||
| def eliminar_artista(request, artista_id): | |||||
| artista = get_object_or_404(Artista, pk=artista_id) | |||||
| artista.delete() | |||||
| return redirect('lyrics:lista_artistas') | |||||
| ######################### | |||||
| # Vistas para los albumes | |||||
| @login_required | |||||
| def lista_albumes(request): | |||||
| albumes = Album.objects.all() | |||||
| return render(request, 'lyrics/lista_albumes.html', {'albumes': albumes}) | |||||
| @login_required | |||||
| def detalle_album(request, album_id): | |||||
| album = get_object_or_404(Album, pk=album_id) | |||||
| songs = Song.objects.filter(album_id=album_id) | |||||
| return render(request, 'lyrics/detalle_album.html', {'album': album, 'songs': songs}) | |||||
| @login_required | |||||
| def nuevo_album(request): | |||||
| artistas = Artista.objects.all() # vamos a ver si hay vehículos dados de alta | |||||
| if artistas: | |||||
| if request.method == 'POST': | |||||
| form = AlbumForm(request.POST, request.FILES) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lyrics:lista_albumes') | |||||
| else: | |||||
| form = AlbumForm() | |||||
| return render(request, 'lyrics/form_album.html', {'form': form}) | |||||
| else: | |||||
| return render(request, 'lyrics/index.html') | |||||
| @login_required | |||||
| def editar_album(request, album_id): | |||||
| album = get_object_or_404(Album, pk=album_id) | |||||
| if request.method == 'POST': | |||||
| form = AlbumForm(request.POST, request.FILES, instance=album) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lyrics:lista_albumes') | |||||
| else: | |||||
| form = AlbumForm(instance=album) | |||||
| return render(request, 'lyrics/form_album.html', {'form': form}) | |||||
| @login_required | |||||
| def eliminar_album(request, album_id): | |||||
| album = Album.objects.get(pk=album_id) | |||||
| album.delete() | |||||
| return redirect('lyrics:lista_albumes') | |||||
| ######################### | |||||
| # Vistas para los songs | |||||
| @login_required | |||||
| def lista_songs(request): | |||||
| songs = Song.objects.all() | |||||
| return render(request, 'lyrics/lista_songs.html', {'songs': songs}) | |||||
| @login_required | |||||
| def detalle_song(request, song_id): | |||||
| song = get_object_or_404(Song, pk=song_id) | |||||
| albumes = Album.objects.filter(song=song_id) | |||||
| return render(request, 'lyrics/detalle_song.html', {'song': song, 'albumes': albumes}) | |||||
| @login_required | |||||
| def nuevo_song(request): | |||||
| album_id = request.GET.get('album_id') # Obtener el album_id de los parámetros de la URL | |||||
| if request.method == 'POST': | |||||
| form = SongForm(request.POST, request.FILES) | |||||
| if form.is_valid(): | |||||
| album = form.cleaned_data['album'] | |||||
| song_count = album.song.count() | |||||
| nueva_cancion = form.save(commit=False) | |||||
| nueva_cancion.pista = song_count + 1 | |||||
| nueva_cancion.save() | |||||
| logger.info("Canción creada %s", nueva_cancion.title) | |||||
| return redirect('lyrics:lista_songs') | |||||
| else: | |||||
| if album_id: | |||||
| # Si tenemos un album_id, preseleccionamos ese álbum en el formulario | |||||
| album = get_object_or_404(Album, id=album_id) | |||||
| form = SongForm(initial={'album': album, 'artist': album.artist, 'year': album.year}) | |||||
| else: | |||||
| form = SongForm() | |||||
| return render(request, 'lyrics/form_song.html', {'form': form}) | |||||
| @login_required | |||||
| def editar_song(request, song_id): | |||||
| song = get_object_or_404(Song, pk=song_id) | |||||
| if request.method == 'POST': | |||||
| form = SongForm(request.POST, request.FILES, instance=song) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lyrics:lista_songs') | |||||
| else: | |||||
| form = SongForm(instance=song) | |||||
| return render(request, 'lyrics/form_song.html', {'form': form}) | |||||
| @login_required | |||||
| def eliminar_song(request, song_id): | |||||
| song = get_object_or_404(Song, pk=song_id) | |||||
| song.delete() | |||||
| return redirect('lyrics:lista_songs') | |||||
| @api_view(['GET']) | |||||
| def api_lista_artistas(request): | |||||
| """Devuelve la lista de todos los artistas.""" | |||||
| artistas = Artista.objects.all() | |||||
| serializer = ArtistaSerializer(artistas, many=True) | |||||
| return Response(serializer.data) | |||||
| @api_view(['GET']) | |||||
| def api_detalle_artista(request, artista_id): | |||||
| """Devuelve los detalles de un artista específico.""" | |||||
| try: | |||||
| artista = Artista.objects.get(id=artista_id) | |||||
| serializer = ArtistaSerializer(artista) | |||||
| return Response(serializer.data) | |||||
| except Artista.DoesNotExist: | |||||
| return Response({'error': 'Artista no encontrado'}, status=404) | |||||
| @api_view(['GET']) | |||||
| def api_lista_albumes(request): | |||||
| """Devuelve la lista de todos los albumes.""" | |||||
| albumes = Album.objects.all() | |||||
| serializer = AlbumSerializer(albumes, many=True) | |||||
| return Response(serializer.data) | |||||
| @api_view(['GET']) | |||||
| def api_detalle_album(request, album_id): | |||||
| """Devuelve los detalles de un album específico.""" | |||||
| try: | |||||
| album = Album.objects.get(id=album_id) | |||||
| serializer = AlbumSerializer(album) | |||||
| return Response(serializer.data) | |||||
| except Album.DoesNotExist: | |||||
| return Response({'error': 'Album no encontrado'}, status=404) | |||||
| @api_view(['GET']) | |||||
| def api_lista_canciones(request): | |||||
| """Devuelve la lista de todos los canciones.""" | |||||
| canciones = Song.objects.all() | |||||
| serializer = CancionSerializer(canciones, many=True) | |||||
| return Response(serializer.data) | |||||
| @api_view(['GET']) | |||||
| def api_detalle_cancion(request, cancion_id): | |||||
| """Devuelve los detalles de un cancion específica.""" | |||||
| try: | |||||
| cancion = Song.objects.get(id=cancion_id) | |||||
| serializer = CancionSerializer(cancion) | |||||
| return Response(serializer.data) | |||||
| except Song.DoesNotExist: | |||||
| return Response({'error': 'Canción no encontrada'}, status=404) | |||||
| @ -0,0 +1,22 @@ | |||||
| #!/usr/bin/env python | |||||
| """Django's command-line utility for administrative tasks.""" | |||||
| import os | |||||
| import sys | |||||
| def main(): | |||||
| """Run administrative tasks.""" | |||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'reymota.settings') | |||||
| try: | |||||
| from django.core.management import execute_from_command_line | |||||
| except ImportError as exc: | |||||
| raise ImportError( | |||||
| "Couldn't import Django. Are you sure it's installed and " | |||||
| "available on your PYTHONPATH environment variable? Did you " | |||||
| "forget to activate a virtual environment?" | |||||
| ) from exc | |||||
| execute_from_command_line(sys.argv) | |||||
| if __name__ == '__main__': | |||||
| main() | |||||
| @ -0,0 +1 @@ | |||||
| migrations/ | |||||
| @ -0,0 +1,9 @@ | |||||
| from django.contrib import admin | |||||
| # Register your models here. | |||||
| from .models import Vehiculo, Repostaje | |||||
| admin.site.register(Vehiculo) | |||||
| admin.site.register(Repostaje) | |||||
| @ -0,0 +1,6 @@ | |||||
| from django.apps import AppConfig | |||||
| class RepostajesConfig(AppConfig): | |||||
| default_auto_field = 'django.db.models.BigAutoField' | |||||
| name = 'repostajes' | |||||
| @ -0,0 +1,41 @@ | |||||
| from django import forms | |||||
| from .models import Vehiculo, Repostaje | |||||
| class VehiculoForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Vehiculo | |||||
| fields = ['marca', 'modelo', 'matricula', 'foto'] | |||||
| marca = forms.CharField( | |||||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||||
| modelo = forms.CharField( | |||||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||||
| matricula = forms.CharField( | |||||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||||
| class RepostajeForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Repostaje | |||||
| fields = ['fecha', 'vehiculo', 'kms', 'litros', 'importe'] | |||||
| exclude = ['descuento', 'precioxlitro'] | |||||
| fecha = forms.DateField( | |||||
| widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})) | |||||
| vehiculo = forms.ModelChoiceField( | |||||
| queryset=Vehiculo.objects.all(), | |||||
| widget=forms.Select(attrs={'class': 'form-control'})) | |||||
| kms = forms.DecimalField( | |||||
| widget=forms.TextInput(attrs={'class': 'form-control'})) | |||||
| litros = forms.DecimalField( | |||||
| widget=forms.NumberInput(attrs={'class': 'form-control'})) | |||||
| importe = forms.DecimalField( | |||||
| widget=forms.NumberInput(attrs={'class': 'form-control'})) | |||||
| aplica_descuento = forms.BooleanField(initial=False, required=False) | |||||
| @ -0,0 +1,53 @@ | |||||
| import json | |||||
| from django.core.management.base import BaseCommand | |||||
| from repostajes.models import Repostaje, Vehiculo | |||||
| class Command(BaseCommand): | |||||
| help = "Importa repostajes desde un archivo JSON" | |||||
| def add_arguments(self, parser): | |||||
| parser.add_argument('archivo_json', type=str, help="Ruta del archivo JSON") | |||||
| def handle(self, *args, **kwargs): | |||||
| archivo_json = kwargs['archivo_json'] | |||||
| try: | |||||
| with open(archivo_json, 'r', encoding='utf-8') as file: | |||||
| datos = json.load(file) | |||||
| self.stdout.write(self.style.WARNING(f"\nSe encontraron {len(datos)} repostajes en el archivo '{archivo_json}'.")) | |||||
| confirmar = input("¿Deseas continuar con la importación? (s/n): ").strip().lower() | |||||
| if confirmar != 's': | |||||
| self.stdout.write(self.style.ERROR("Importación cancelada.")) | |||||
| return | |||||
| repostajes_creados = 0 | |||||
| for repostaje_data in datos: | |||||
| try: | |||||
| vehiculo = Vehiculo.objects.get(matricula=repostaje_data["vehiculo_matricula"]) | |||||
| creado = Repostaje.objects.create( | |||||
| vehiculo=vehiculo, | |||||
| fecha=repostaje_data['fecha'], | |||||
| kms=repostaje_data['kms'], | |||||
| litros=repostaje_data['litros'], | |||||
| descuento=repostaje_data['descuento'], | |||||
| importe=repostaje_data['importe'], | |||||
| precioxlitro=repostaje_data['precioxlitro'], | |||||
| kmsrecorridos=repostaje_data['kmsrecorridos'], | |||||
| consumo=repostaje_data['consumo'] | |||||
| ) | |||||
| if creado: | |||||
| repostajes_creados += 1 | |||||
| except Vehiculo.DoesNotExist: | |||||
| self.stderr.write(self.style.ERROR(f"Vehiculo con matrícula '{repostaje_data['vehiculo_matricula']}' no encontrado.")) | |||||
| self.stdout.write(self.style.SUCCESS(f'Se importaron {repostajes_creados} repostajes correctamente.')) | |||||
| except FileNotFoundError: | |||||
| self.stderr.write(self.style.ERROR(f"El archivo {archivo_json} no se encontró.")) | |||||
| except json.JSONDecodeError: | |||||
| self.stderr.write(self.style.ERROR("Error al leer el archivo JSON. Asegúrate de que el formato sea correcto.")) | |||||
| @ -0,0 +1,42 @@ | |||||
| import json | |||||
| from django.core.management.base import BaseCommand | |||||
| from repostajes.models import Vehiculo | |||||
| class Command(BaseCommand): | |||||
| help = "Importa vehiculos desde un archivo JSON" | |||||
| def add_arguments(self, parser): | |||||
| parser.add_argument('archivo_json', type=str, help="Ruta del archivo JSON") | |||||
| def handle(self, *args, **kwargs): | |||||
| archivo_json = kwargs['archivo_json'] | |||||
| try: | |||||
| with open(archivo_json, 'r', encoding='utf-8') as file: | |||||
| datos = json.load(file) | |||||
| self.stdout.write(self.style.WARNING(f"\nSe encontraron {len(datos)} vehiculos en el archivo '{archivo_json}'.")) | |||||
| confirmar = input("¿Deseas continuar con la importación? (s/n): ").strip().lower() | |||||
| if confirmar != 's': | |||||
| self.stdout.write(self.style.ERROR("Importación cancelada.")) | |||||
| return | |||||
| vehiculos_creados = 0 | |||||
| for vehiculo_data in datos: | |||||
| creado = Vehiculo.objects.create( | |||||
| marca=vehiculo_data['marca'], | |||||
| modelo=vehiculo_data['modelo'], | |||||
| matricula=vehiculo_data['matricula'], | |||||
| foto=vehiculo_data['foto'] | |||||
| ) | |||||
| if creado: | |||||
| vehiculos_creados += 1 | |||||
| self.stdout.write(self.style.SUCCESS(f'Se importaron {vehiculos_creados} vehiculos correctamente.')) | |||||
| except FileNotFoundError: | |||||
| self.stderr.write(self.style.ERROR(f"El archivo {archivo_json} no se encontró.")) | |||||
| except json.JSONDecodeError: | |||||
| self.stderr.write(self.style.ERROR("Error al leer el archivo JSON. Asegúrate de que el formato sea correcto.")) | |||||
| @ -0,0 +1,27 @@ | |||||
| from django.db import models | |||||
| from django.core.validators import MaxValueValidator | |||||
| class Vehiculo(models.Model): | |||||
| marca = models.CharField(max_length=200) | |||||
| modelo = models.CharField(max_length=200) | |||||
| matricula = models.CharField(max_length=200) | |||||
| foto = models.ImageField(upload_to='vehiculos/', blank=True, null=True) # Nuevo campo | |||||
| def __str__(self): | |||||
| return self.marca | |||||
| class Repostaje(models.Model): | |||||
| vehiculo = models.ForeignKey(Vehiculo, on_delete=models.CASCADE) | |||||
| fecha = models.DateField() | |||||
| kms = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True) | |||||
| litros = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) | |||||
| descuento = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) | |||||
| importe = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) | |||||
| precioxlitro = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) | |||||
| kmsrecorridos = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True) | |||||
| consumo = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) | |||||
| def __str__(self): | |||||
| return str(self.fecha) | |||||
| @ -0,0 +1,24 @@ | |||||
| import os | |||||
| from rest_framework import serializers | |||||
| from .models import Vehiculo, Repostaje | |||||
| class VehiculoSerializer(serializers.ModelSerializer): | |||||
| class Meta: | |||||
| model = Vehiculo | |||||
| fields = '__all__' # Incluir todos los campos del modelo | |||||
| def to_representation(self, instance): | |||||
| ret = super().to_representation(instance) | |||||
| ret['foto'] = "vehiculos/" + os.path.basename(ret['foto']) | |||||
| return ret | |||||
| class RepostajeSerializer(serializers.ModelSerializer): | |||||
| vehiculo_matricula = serializers.CharField(source='vehiculo.matricula', read_only=True) | |||||
| class Meta: | |||||
| model = Repostaje | |||||
| fields = ['id', 'fecha', 'kms', 'litros', 'descuento', 'importe', 'precioxlitro', 'kmsrecorridos', 'consumo', 'vehiculo', 'vehiculo_matricula'] | |||||
| @ -0,0 +1,21 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <svg width="215px" height="215px" viewBox="0 0 215 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||||
| <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch --> | |||||
| <title>portal-logo</title> | |||||
| <desc>Created with Sketch.</desc> | |||||
| <defs> | |||||
| <path d="M51.165,8.742 C54.505,12.619 56.876,17.365 57.892,22.588 C60.148,17.225 65.452,13.46 71.636,13.46 C79.867,13.46 86.541,20.134 86.541,28.365 C86.541,36.597 79.867,43.269 71.636,43.269 C63.404,43.269 56.728,36.597 56.728,28.365 C56.728,12.7 44.03,0 28.365,0 C12.7,0 0,12.7 0,28.365 C0,44.031 12.7,56.731 28.365,56.731 C36.419,56.731 43.695,53.393 48.858,48.003 C45.501,44.117 43.128,39.383 42.108,34.14 C39.852,39.504 34.548,43.269 28.365,43.269 C20.133,43.269 13.46,36.597 13.46,28.365 C13.46,20.134 20.133,13.46 28.365,13.46 C36.966,13.46 43.27,20.577 43.27,28.365 C43.27,44.031 55.97,56.731 71.636,56.731 C87.3,56.731 100,44.031 100,28.365 C100,12.7 87.3,0 71.636,0 C63.589,0 56.327,3.358 51.165,8.742 Z" id="path-1"></path> | |||||
| </defs> | |||||
| <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||||
| <g id="portal-logo"> | |||||
| <circle id="Oval" fill="#51B37F" fill-rule="nonzero" cx="107.5" cy="107.5" r="107.5"></circle> | |||||
| <g id="logo" transform="translate(58.000000, 79.000000)"> | |||||
| <mask id="mask-2" fill="white"> | |||||
| <use xlink:href="#path-1"></use> | |||||
| </mask> | |||||
| <g id="Clip-2"></g> | |||||
| <polygon id="Fill-1" fill="#FFFFFE" mask="url(#mask-2)" points="-5 61.73 105 61.73 105 -5 -5 -5"></polygon> | |||||
| </g> | |||||
| </g> | |||||
| </g> | |||||
| </svg> | |||||
| @ -0,0 +1,21 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <svg width="215px" height="215px" viewBox="0 0 215 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||||
| <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch --> | |||||
| <title>portal-logo</title> | |||||
| <desc>Created with Sketch.</desc> | |||||
| <defs> | |||||
| <path d="M51.165,8.742 C54.505,12.619 56.876,17.365 57.892,22.588 C60.148,17.225 65.452,13.46 71.636,13.46 C79.867,13.46 86.541,20.134 86.541,28.365 C86.541,36.597 79.867,43.269 71.636,43.269 C63.404,43.269 56.728,36.597 56.728,28.365 C56.728,12.7 44.03,0 28.365,0 C12.7,0 0,12.7 0,28.365 C0,44.031 12.7,56.731 28.365,56.731 C36.419,56.731 43.695,53.393 48.858,48.003 C45.501,44.117 43.128,39.383 42.108,34.14 C39.852,39.504 34.548,43.269 28.365,43.269 C20.133,43.269 13.46,36.597 13.46,28.365 C13.46,20.134 20.133,13.46 28.365,13.46 C36.966,13.46 43.27,20.577 43.27,28.365 C43.27,44.031 55.97,56.731 71.636,56.731 C87.3,56.731 100,44.031 100,28.365 C100,12.7 87.3,0 71.636,0 C63.589,0 56.327,3.358 51.165,8.742 Z" id="path-1"></path> | |||||
| </defs> | |||||
| <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||||
| <g id="portal-logo"> | |||||
| <circle id="Oval" fill="#09B6CA" fill-rule="nonzero" cx="107.5" cy="107.5" r="107.5"></circle> | |||||
| <g id="logo" transform="translate(58.000000, 79.000000)"> | |||||
| <mask id="mask-2" fill="white"> | |||||
| <use xlink:href="#path-1"></use> | |||||
| </mask> | |||||
| <g id="Clip-2"></g> | |||||
| <polygon id="Fill-1" fill="#FFFFFE" mask="url(#mask-2)" points="-5 61.73 105 61.73 105 -5 -5 -5"></polygon> | |||||
| </g> | |||||
| </g> | |||||
| </g> | |||||
| </svg> | |||||