Browse Source

Primer commit

main
Celestino Rey 8 months ago
commit
6ab596bc92
2404 changed files with 436641 additions and 0 deletions
  1. +40
    -0
      Dockerfile
  2. +98
    -0
      README.md
  3. +11
    -0
      Yamls/env-prod-db-configmap.yaml
  4. +52
    -0
      Yamls/postgresql-deployment.yaml
  5. +17
    -0
      Yamls/postgresql-service.yaml
  6. +17
    -0
      Yamls/pvc-postgresql.yaml
  7. +13
    -0
      Yamls/reymota-service.yaml
  8. +3
    -0
      borraDB.sh
  9. +4
    -0
      creaDB.sh
  10. +32
    -0
      nginx/default.conf
  11. +1
    -0
      nuevaapp.sh
  12. +1
    -0
      src/accounts/.gitignore
  13. +0
    -0
      src/accounts/__init__.py
  14. +3
    -0
      src/accounts/admin.py
  15. +6
    -0
      src/accounts/apps.py
  16. +3
    -0
      src/accounts/models.py
  17. +3
    -0
      src/accounts/tests.py
  18. +8
    -0
      src/accounts/urls.py
  19. +1
    -0
      src/accounts/views.py
  20. +27
    -0
      src/config/entrypoint.sh
  21. +0
    -0
      src/entrenadores/__init__.py
  22. +16
    -0
      src/entrenadores/asgi.py
  23. +6
    -0
      src/entrenadores/context_processors.py
  24. +209
    -0
      src/entrenadores/settings.py
  25. +0
    -0
      src/entrenadores/templatetags/__init__.py
  26. +9
    -0
      src/entrenadores/templatetags/filtros_de_entorno.py
  27. +43
    -0
      src/entrenadores/urls.py
  28. +31
    -0
      src/entrenadores/views.py
  29. +16
    -0
      src/entrenadores/wsgi.py
  30. +0
    -0
      src/lyrics/__init__.py
  31. +9
    -0
      src/lyrics/admin.py
  32. +6
    -0
      src/lyrics/apps.py
  33. +41
    -0
      src/lyrics/forms.py
  34. +0
    -0
      src/lyrics/management/__init__.py
  35. +0
    -0
      src/lyrics/management/commands/__init__.py
  36. +48
    -0
      src/lyrics/management/commands/importar_albumes.py
  37. +42
    -0
      src/lyrics/management/commands/importar_artistas.py
  38. +51
    -0
      src/lyrics/management/commands/importar_canciones.py
  39. +42
    -0
      src/lyrics/models.py
  40. +39
    -0
      src/lyrics/serializers.py
  41. +3
    -0
      src/lyrics/tests.py
  42. +39
    -0
      src/lyrics/urls.py
  43. +251
    -0
      src/lyrics/views.py
  44. +22
    -0
      src/manage.py
  45. +0
    -0
      src/mediafiles/artistas/.noborrar
  46. +0
    -0
      src/mediafiles/cover_image/.noborrar
  47. BIN
      src/mediafiles/profile_images/default.jpg
  48. BIN
      src/mediafiles/profile_images/gravatar-tino.jpeg
  49. BIN
      src/mediafiles/profile_images/gravatar-tino_jqB0Vq4.jpeg
  50. +0
    -0
      src/mediafiles/vehiculos/.noborrar
  51. +1
    -0
      src/repostajes/.gitignore
  52. +0
    -0
      src/repostajes/__init__.py
  53. +9
    -0
      src/repostajes/admin.py
  54. +6
    -0
      src/repostajes/apps.py
  55. +41
    -0
      src/repostajes/forms.py
  56. +0
    -0
      src/repostajes/management/__init__.py
  57. +0
    -0
      src/repostajes/management/commands/__init__.py
  58. +53
    -0
      src/repostajes/management/commands/importar_repostajes.py
  59. +42
    -0
      src/repostajes/management/commands/importar_vehiculos.py
  60. +27
    -0
      src/repostajes/models.py
  61. +16
    -0
      src/repostajes/serializers.py
  62. +12
    -0
      src/repostajes/static/css/portal.css
  63. +21
    -0
      src/repostajes/static/images/app-logo.svg
  64. BIN
      src/repostajes/static/images/background/background-1.jpg
  65. BIN
      src/repostajes/static/images/background/background-2.jpg
  66. BIN
      src/repostajes/static/images/background/background-3.jpg
  67. BIN
      src/repostajes/static/images/background/background-palmeras.jpg
  68. BIN
      src/repostajes/static/images/doc-thumb-1.jpg
  69. BIN
      src/repostajes/static/images/doc-thumb-2.jpg
  70. BIN
      src/repostajes/static/images/favicon.ico
  71. BIN
      src/repostajes/static/images/favicon.png
  72. +21
    -0
      src/repostajes/static/images/portal-logo.svg
  73. BIN
      src/repostajes/static/images/profiles/profile-1.png
  74. BIN
      src/repostajes/static/images/profiles/profile-2.png
  75. BIN
      src/repostajes/static/images/profiles/profile-3.png
  76. BIN
      src/repostajes/static/images/profiles/profile-4.png
  77. BIN
      src/repostajes/static/images/profiles/profile-5.png
  78. BIN
      src/repostajes/static/images/profiles/profile-6.png
  79. +18
    -0
      src/repostajes/static/images/reymota-logo.svg
  80. BIN
      src/repostajes/static/images/user.png
  81. BIN
      src/repostajes/static/images/users/user-1.jpg
  82. BIN
      src/repostajes/static/images/users/user-2.jpg
  83. BIN
      src/repostajes/static/images/users/user-3.jpg
  84. BIN
      src/repostajes/static/images/users/user-4.jpg
  85. BIN
      src/repostajes/static/images/users/user-5.jpg
  86. BIN
      src/repostajes/static/images/users/user-6.jpg
  87. BIN
      src/repostajes/static/images/users/user-7.jpg
  88. BIN
      src/repostajes/static/images/users/user-8.jpg
  89. BIN
      src/repostajes/static/images/users/user-9.jpg
  90. +96
    -0
      src/repostajes/static/js/app.js
  91. +366
    -0
      src/repostajes/static/js/charts-demo.js
  92. +224
    -0
      src/repostajes/static/js/index-charts.js
  93. +4085
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.css
  94. +1
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.css.map
  95. +6
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.min.css
  96. +1
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.min.css.map
  97. +4084
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.css
  98. +1
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.css.map
  99. +6
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.min.css
  100. +1
    -0
      src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.min.css.map

+ 40
- 0
Dockerfile View File

@ -0,0 +1,40 @@
# yup, python 3.11!
FROM python:3.11-slim
# install nginx y otras cosas
RUN apt-get update && apt-get install nginx netcat-openbsd curl vim jq rsync -y
# copy our nginx configuration to overwrite nginx defaults
RUN rm /etc/nginx/sites-enabled/default
RUN rm /etc/nginx/sites-available/default
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
# link nginx logs to container stdout
RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
# copy the django code
COPY ./src ./app
RUN chgrp -R 0 ./app && chmod -R g=u ./app
RUN chgrp -R 0 /var/lib/nginx && chmod -R g=u /var/lib/nginx
# change our working directory to the django projcet roo
WORKDIR /app
# create virtual env (notice the location?)
# update pip
# install requirements
RUN python -m venv /opt/venv && \
/opt/venv/bin/python -m pip install pip --upgrade && \
/opt/venv/bin/python -m pip install -r requirements.txt
# Añade path a .bashrc
RUN echo "export PATH=/opt/venv/bin/:$PATH" >> /root/.bashrc
# make our entrypoint.sh executable
RUN chmod +x config/entrypoint.sh
#EXPOSE 8080
# execute our entrypoint.sh file
CMD ["./config/entrypoint.sh"]

+ 98
- 0
README.md View File

@ -0,0 +1,98 @@
# Versión para openshift
## Instalación
Crear un proyecto.
### Running Commands as Root in OpenShift
oc adm policy add-scc-to-user anyuid -z default
### app trainersapp
A continuación añadir una app desde Agregar, importar desde git.
oc new-app https://gogs.reymota.es/creylopez/TrainersAppDj.git -e DEBUG="False" --name='trainersapp'
Tal y como está la estructura de directorios, deberia detectar automáticamente una compilación Python
## asignación de los volúmenes
### Si la pvc no está creada
oc set volume deployment.apps/reymota --add -t pvc --claim-size=300M --name=reymota-lyrics-migrations --claim-name='reymota-lyrics-migrations' --mount-path='/app/lyrics/migrations'
oc set volume deployment.apps/reymota --add -t pvc --claim-size=300M --name=reymota-media --claim-name='reymota-media' --mount-path='/app/mediafiles'
oc set volume deployment.apps/reymota --add -t pvc --claim-size=300M --name=reymota-repostajes-migrations --claim-name='reymota-repostajes-migrations' --mount-path='/app/repostajes/migrations'
oc set volume deployment.apps/reymota --add -t pvc --claim-size=300M --name=reymota-reymotausers-migrations --claim-name='reymota-reymotausers-migrations' --mount-path='/app/reymotausers/migrations'
oc set volume deployment.apps/reymota --add -t pvc --claim-size=50G --name=static-volume --claim-name='static-volume' --mount-path='/app/staticfiles'
### Si la pvc ya está creada
oc set volume deployment.apps/reymota --add -t pvc --name=reymota-lyrics-migrations --claim-name='reymota-lyrics-migrations' --mount-path='/app/lyrics/migrations'
oc set volume deployment.apps/reymota --add -t pvc --name=reymota-media --claim-name='reymota-media' --mount-path='/app/mediafiles'
oc set volume deployment.apps/reymota --add -t pvc --name=reymota-repostajes-migrations --claim-name='reymota-repostajes-migrations' --mount-path='/app/repostajes/migrations'
oc set volume deployment.apps/reymota --add -t pvc --name=reymota-reymotausers-migrations --claim-name='reymota-reymotausers-migrations' --mount-path='/app/reymotausers/migrations'
oc set volume deployment.apps/reymota --add -t pvc --name=static-volume --claim-name='static-volume' --mount-path='/app/staticfiles'
## Exponer el servicio
oc expose deployment.apps/reymota --type=NodePort --port=8080
### postgresql
Se hace desde el yaml
oc create -f postgresql-deployment.yaml
## Modificaciones al código
Una vez realizadas las modificaciones al código y se hayan subido a gitea, hay que reconstruir el proyecto.
oc start-build reymota
## Comandos a ejecutar la primera vez o cuando haya cambios en las bases de datos
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.

+ 11
- 0
Yamls/env-prod-db-configmap.yaml View File

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

+ 52
- 0
Yamls/postgresql-deployment.yaml View File

@ -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: trainersapp
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: postgresql
restartPolicy: Always
volumes:
- name: postgresql
persistentVolumeClaim:
claimName: postgresql

+ 17
- 0
Yamls/postgresql-service.yaml View File

@ -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: trainersapp
spec:
ports:
- name: "5432"
port: 5432
targetPort: 5432
selector:
io.kompose.service: postgresql

+ 17
- 0
Yamls/pvc-postgresql.yaml View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
finalizers:
- kubernetes.io/pvc-protection
labels:
template: postgresql-persistent-template
name: postgresql
namespace: reymota
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: lvms-vg1
volumeMode: Filesystem

+ 13
- 0
Yamls/reymota-service.yaml View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: reymota
namespace: reymota
spec:
type: NodePort
ports:
- name: "8080"
port: 8080
nodePort: 30341
targetPort: 8080

+ 3
- 0
borraDB.sh View File

@ -0,0 +1,3 @@
oc delete -f Yamls/env-prod-db-configmap.yaml
oc delete -f Yamls/postgresql-deployment.yaml
oc delete -f Yamls/postgresql-service.yaml

+ 4
- 0
creaDB.sh View File

@ -0,0 +1,4 @@
oc create -f Yamls/env-prod-db-configmap.yaml
#oc create -f Yamls/pvc-postgresql.yaml
oc create -f Yamls/postgresql-deployment.yaml
oc create -f Yamls/postgresql-service.yaml

+ 32
- 0
nginx/default.conf View File

@ -0,0 +1,32 @@
upstream projecto_entrenadores {
server localhost:8000;
}
error_log /var/log/nginx/error.log;
server {
listen 8080;
access_log /var/log/nginx/access.log;
location / {
proxy_pass http://projecto_entrenadores;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
client_max_body_size 100M;
}
location /static/ {
alias /app/staticfiles/;
}
location /media/ {
alias /app/mediafiles/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

+ 1
- 0
nuevaapp.sh View File

@ -0,0 +1 @@
oc new-app https://gogs.reymota.es/creylopez/TrainersAppDj.git -e DEBUG=True

+ 1
- 0
src/accounts/.gitignore View File

@ -0,0 +1 @@
migrations/

+ 0
- 0
src/accounts/__init__.py View File


+ 3
- 0
src/accounts/admin.py View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

+ 6
- 0
src/accounts/apps.py View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

+ 3
- 0
src/accounts/models.py View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

+ 3
- 0
src/accounts/tests.py View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

+ 8
- 0
src/accounts/urls.py View File

@ -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'),
]

+ 1
- 0
src/accounts/views.py View File

@ -0,0 +1 @@
from django.shortcuts import render

+ 27
- 0
src/config/entrypoint.sh View File

@ -0,0 +1,27 @@
#!/bin/bash
RUN_PORT="8000"
DATABASE=postgres
SQL_HOST=postgresql
SQL_PORT=5432
export PATH=/opt/venv/bin:$PATH
if [ "$DATABASE" = "postgres" ]
then
echo "Waiting for postgres..."
while ! nc -z $SQL_HOST $SQL_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
/opt/venv/bin/python manage.py migrate --no-input
/opt/venv/bin/python manage.py collectstatic --no-input
/opt/venv/bin/gunicorn entrenadores.wsgi:application --bind "0.0.0.0:${RUN_PORT}" --daemon
nginx -g 'daemon off;'
else
echo "la base de datos no es postgres: '$DATABASE'"
fi

+ 0
- 0
src/entrenadores/__init__.py View File


+ 16
- 0
src/entrenadores/asgi.py View File

@ -0,0 +1,16 @@
"""
ASGI config for reymota project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'reymota.settings')
application = get_asgi_application()

+ 6
- 0
src/entrenadores/context_processors.py View File

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

+ 209
- 0
src/entrenadores/settings.py View File

@ -0,0 +1,209 @@
"""
Django settings for entrenadores project.
Generated by 'django-admin startproject' using Django 5.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import os
import logging
APP_VERSION = "11.0.2"
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-vu#zk4g8pj-qoov#8^i$&s8n_ipp2r3h+o$z1w(1%d=6+i@erm'
# SECURITY WARNING: don't run with debug turned on in production!
# DEBUG = True
DEBUG = os.environ["DEBUG"] == 'True'
ALLOWED_HOSTS = [".ocp-cluster.reymota.lab", "reymota.es"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'repostajes',
'reymotausers',
'lyrics',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'entrenadores.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'entrenadores.context_processors.app_version',
],
'libraries': {
'filtros_de_entorno': 'entrenadores.templatetags.filtros_de_entorno',
}
},
},
]
WSGI_APPLICATION = 'entrenadores.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "entrenadores",
"USER": "creylopez",
"PASSWORD": "Dsa-0213",
"HOST": "postgresql",
"PORT": "5432",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'es-es'
TIME_ZONE = 'Europe/Madrid'
USE_I18N = True
USE_TZ = True
I18N = True
L10N = True
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / "staticfiles"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = 'principal'
LOGOUT_REDIRECT_URL = 'principal'
AUTH_USER_MODEL = "reymotausers.ReyMotaUser"
MEDIA_ROOT = BASE_DIR / "mediafiles"
MEDIA_URL = '/media/'
CSRF_TRUSTED_ORIGINS = ["https://*.ocp-cluster.reymota.lab"]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': '/dev/null',
'formatter': 'verbose',
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True,
},
'repostajes': {
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': False,
},
'lyrics': {
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': False,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
},
}

+ 0
- 0
src/entrenadores/templatetags/__init__.py View File


+ 9
- 0
src/entrenadores/templatetags/filtros_de_entorno.py View File

@ -0,0 +1,9 @@
import os
from django import template
register = template.Library()
@register.filter
def muestra_version(clave):
return os.getenv(clave, '')

+ 43
- 0
src/entrenadores/urls.py View File

@ -0,0 +1,43 @@
"""
URL configuration for entrenadores project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.views.generic.base import TemplateView # new
from . import views
urlpatterns = [
path('obreros/', admin.site.urls),
path('repostajes/', include('repostajes.urls')),
path('lyrics/', include('lyrics.urls')),
path("accounts/", include("accounts.urls")), # new
path("accounts/", include("django.contrib.auth.urls")),
path("", TemplateView.as_view(template_name="index.html"),
name="principal"), # new
path('entorno/', views.ver_variables_entorno, name='ver_variables_entorno'),
path('usuarios/', include("reymotausers.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 31
- 0
src/entrenadores/views.py View File

@ -0,0 +1,31 @@
import os
from django.conf import settings
from django.contrib.auth.decorators import user_passes_test
from django.shortcuts import render
from django.http import HttpResponseForbidden
@user_passes_test(lambda u: u.is_staff)
def ver_variables_entorno(request):
if not settings.DEBUG:
return HttpResponseForbidden("Acceso prohibido")
# Variables a excluir por motivos de seguridad
variables_excluidas = {'SECRET_KEY', 'DATABASES', 'EMAIL_HOST_PASSWORD', 'API_KEY'}
# Obtiene todas las variables de entorno
entorno = {key: os.getenv(key) for key in os.environ.keys() if key not in variables_excluidas}
# Obtiene todas las variables de settings excluyendo las confidenciales
configuracion = {
key: getattr(settings, key) for key in dir(settings)
if key.isupper() and key not in variables_excluidas
}
# Combina ambas en un solo diccionario
contexto = {
'entorno': entorno,
'configuracion': configuracion
}
return render(request, 'ver_entorno.html', contexto)

+ 16
- 0
src/entrenadores/wsgi.py View File

@ -0,0 +1,16 @@
"""
WSGI config for reymota project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'entrenadores.settings')
application = get_wsgi_application()

+ 0
- 0
src/lyrics/__init__.py View File


+ 9
- 0
src/lyrics/admin.py View File

@ -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)

+ 6
- 0
src/lyrics/apps.py View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class LyricsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'lyrics'

+ 41
- 0
src/lyrics/forms.py View File

@ -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
src/lyrics/management/__init__.py View File


+ 0
- 0
src/lyrics/management/commands/__init__.py View File


+ 48
- 0
src/lyrics/management/commands/importar_albumes.py View File

@ -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."))

+ 42
- 0
src/lyrics/management/commands/importar_artistas.py View File

@ -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."))

+ 51
- 0
src/lyrics/management/commands/importar_canciones.py View File

@ -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."))

+ 42
- 0
src/lyrics/models.py View File

@ -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

+ 39
- 0
src/lyrics/serializers.py View File

@ -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']

+ 3
- 0
src/lyrics/tests.py View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

+ 39
- 0
src/lyrics/urls.py View File

@ -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'),
]

+ 251
- 0
src/lyrics/views.py View File

@ -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)

+ 22
- 0
src/manage.py View File

@ -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', 'entrenadores.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
src/mediafiles/artistas/.noborrar View File


+ 0
- 0
src/mediafiles/cover_image/.noborrar View File


BIN
src/mediafiles/profile_images/default.jpg View File

Before After
Width: 826  |  Height: 826  |  Size: 20 KiB

BIN
src/mediafiles/profile_images/gravatar-tino.jpeg View File

Before After
Width: 256  |  Height: 256  |  Size: 29 KiB

BIN
src/mediafiles/profile_images/gravatar-tino_jqB0Vq4.jpeg View File

Before After
Width: 256  |  Height: 256  |  Size: 29 KiB

+ 0
- 0
src/mediafiles/vehiculos/.noborrar View File


+ 1
- 0
src/repostajes/.gitignore View File

@ -0,0 +1 @@
migrations/

+ 0
- 0
src/repostajes/__init__.py View File


+ 9
- 0
src/repostajes/admin.py View File

@ -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)

+ 6
- 0
src/repostajes/apps.py View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RepostajesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'repostajes'

+ 41
- 0
src/repostajes/forms.py View File

@ -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
src/repostajes/management/__init__.py View File


+ 0
- 0
src/repostajes/management/commands/__init__.py View File


+ 53
- 0
src/repostajes/management/commands/importar_repostajes.py View File

@ -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."))

+ 42
- 0
src/repostajes/management/commands/importar_vehiculos.py View File

@ -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."))

+ 27
- 0
src/repostajes/models.py View File

@ -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)

+ 16
- 0
src/repostajes/serializers.py View File

@ -0,0 +1,16 @@
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
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']

+ 12
- 0
src/repostajes/static/css/portal.css
File diff suppressed because it is too large
View File


+ 21
- 0
src/repostajes/static/images/app-logo.svg View File

@ -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>

BIN
src/repostajes/static/images/background/background-1.jpg View File

Before After
Width: 1000  |  Height: 1139  |  Size: 150 KiB

BIN
src/repostajes/static/images/background/background-2.jpg View File

Before After
Width: 1200  |  Height: 781  |  Size: 232 KiB

BIN
src/repostajes/static/images/background/background-3.jpg View File

Before After
Width: 1200  |  Height: 789  |  Size: 256 KiB

BIN
src/repostajes/static/images/background/background-palmeras.jpg View File

Before After
Width: 1000  |  Height: 1139  |  Size: 360 KiB

BIN
src/repostajes/static/images/doc-thumb-1.jpg View File

Before After
Width: 600  |  Height: 432  |  Size: 75 KiB

BIN
src/repostajes/static/images/doc-thumb-2.jpg View File

Before After
Width: 600  |  Height: 472  |  Size: 75 KiB

BIN
src/repostajes/static/images/favicon.ico View File

Before After

BIN
src/repostajes/static/images/favicon.png View File

Before After
Width: 32  |  Height: 32  |  Size: 1.7 KiB

+ 21
- 0
src/repostajes/static/images/portal-logo.svg View File

@ -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>

BIN
src/repostajes/static/images/profiles/profile-1.png View File

Before After
Width: 120  |  Height: 120  |  Size: 34 KiB

BIN
src/repostajes/static/images/profiles/profile-2.png View File

Before After
Width: 120  |  Height: 120  |  Size: 35 KiB

BIN
src/repostajes/static/images/profiles/profile-3.png View File

Before After
Width: 120  |  Height: 120  |  Size: 37 KiB

BIN
src/repostajes/static/images/profiles/profile-4.png View File

Before After
Width: 120  |  Height: 120  |  Size: 22 KiB

BIN
src/repostajes/static/images/profiles/profile-5.png View File

Before After
Width: 120  |  Height: 120  |  Size: 36 KiB

BIN
src/repostajes/static/images/profiles/profile-6.png View File

Before After
Width: 120  |  Height: 120  |  Size: 29 KiB

+ 18
- 0
src/repostajes/static/images/reymota-logo.svg View File

@ -0,0 +1,18 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<!-- Fondo -->
<rect width="400" height="400" fill="#ffffff" />
<!-- Corona -->
<g transform="translate(100, 100) scale(2)">
<polygon points="50,150 75,50 100,150" fill="#FFD700" />
<polygon points="0,150 50,0 100,150" fill="#FFD700" />
<polygon points="100,150 125,50 150,150" fill="#FFD700" />
</g>
<!-- Letra R -->
<!--
<text x="100" y="360" font-family="Arial, sans-serif" font-size="400" fill="#000000" font-weight="bold">R</text>
-->
<text x="100" y="360" font-family="Open Sans" font-size="400" fill="#000000" font-weight="bold">R</text>
</svg>

BIN
src/repostajes/static/images/user.png View File

Before After
Width: 120  |  Height: 120  |  Size: 38 KiB

BIN
src/repostajes/static/images/users/user-1.jpg View File

Before After
Width: 200  |  Height: 200  |  Size: 48 KiB

BIN
src/repostajes/static/images/users/user-2.jpg View File

Before After
Width: 350  |  Height: 350  |  Size: 28 KiB

BIN
src/repostajes/static/images/users/user-3.jpg View File

Before After
Width: 200  |  Height: 200  |  Size: 40 KiB

BIN
src/repostajes/static/images/users/user-4.jpg View File

Before After
Width: 240  |  Height: 240  |  Size: 7.2 KiB

BIN
src/repostajes/static/images/users/user-5.jpg View File

Before After
Width: 240  |  Height: 240  |  Size: 22 KiB

BIN
src/repostajes/static/images/users/user-6.jpg View File

Before After
Width: 240  |  Height: 240  |  Size: 9.1 KiB

BIN
src/repostajes/static/images/users/user-7.jpg View File

Before After
Width: 240  |  Height: 240  |  Size: 22 KiB

BIN
src/repostajes/static/images/users/user-8.jpg View File

Before After
Width: 240  |  Height: 240  |  Size: 12 KiB

BIN
src/repostajes/static/images/users/user-9.jpg View File

Before After
Width: 240  |  Height: 240  |  Size: 32 KiB

+ 96
- 0
src/repostajes/static/js/app.js View File

@ -0,0 +1,96 @@
'use strict';
/* ===== Enable Bootstrap Popover (on element ====== */
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
/* ==== Enable Bootstrap Alert ====== */
//var alertList = document.querySelectorAll('.alert')
//alertList.forEach(function (alert) {
// new bootstrap.Alert(alert)
//});
const alertList = document.querySelectorAll('.alert')
const alerts = [...alertList].map(element => new bootstrap.Alert(element))
/* ===== Responsive Sidepanel ====== */
const sidePanelToggler = document.getElementById('sidepanel-toggler');
const sidePanel = document.getElementById('app-sidepanel');
const sidePanelDrop = document.getElementById('sidepanel-drop');
const sidePanelClose = document.getElementById('sidepanel-close');
window.addEventListener('load', function(){
responsiveSidePanel();
});
window.addEventListener('resize', function(){
responsiveSidePanel();
});
function responsiveSidePanel() {
let w = window.innerWidth;
if(w >= 1200) {
// if larger
//console.log('larger');
sidePanel.classList.remove('sidepanel-hidden');
sidePanel.classList.add('sidepanel-visible');
} else {
// if smaller
//console.log('smaller');
sidePanel.classList.remove('sidepanel-visible');
sidePanel.classList.add('sidepanel-hidden');
}
};
sidePanelToggler.addEventListener('click', () => {
if (sidePanel.classList.contains('sidepanel-visible')) {
console.log('visible');
sidePanel.classList.remove('sidepanel-visible');
sidePanel.classList.add('sidepanel-hidden');
} else {
console.log('hidden');
sidePanel.classList.remove('sidepanel-hidden');
sidePanel.classList.add('sidepanel-visible');
}
});
sidePanelClose.addEventListener('click', (e) => {
e.preventDefault();
sidePanelToggler.click();
});
sidePanelDrop.addEventListener('click', (e) => {
sidePanelToggler.click();
});
/* ====== Mobile search ======= */
const searchMobileTrigger = document.querySelector('.search-mobile-trigger');
const searchBox = document.querySelector('.app-search-box');
searchMobileTrigger.addEventListener('click', () => {
searchBox.classList.toggle('is-visible');
let searchMobileTriggerIcon = document.querySelector('.search-mobile-trigger-icon');
if(searchMobileTriggerIcon.classList.contains('fa-magnifying-glass')) {
searchMobileTriggerIcon.classList.remove('fa-magnifying-glass');
searchMobileTriggerIcon.classList.add('fa-xmark');
} else {
searchMobileTriggerIcon.classList.remove('fa-xmark');
searchMobileTriggerIcon.classList.add('fa-magnifying-glass');
}
});

+ 366
- 0
src/repostajes/static/js/charts-demo.js View File

@ -0,0 +1,366 @@
'use strict';
/* Chart.js docs: https://www.chartjs.org/ */
window.chartColors = {
green: '#75c181', // rgba(117,193,129, 1)
blue: '#5b99ea', // rgba(91,153,234, 1)
gray: '#a9b5c9',
text: '#252930',
border: '#e7e9ed'
};
/* Random number generator for demo purpose */
var randomDataPoint = function(){ return Math.round(Math.random()*100)};
//Area line Chart Demo
var lineChartConfig = {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],
datasets: [{
label: 'Dataset',
backgroundColor: "rgba(117,193,129,0.2)",
borderColor: "rgba(117,193,129, 0.8)",
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint()
],
}]
},
options: {
responsive: true,
legend: {
display: true,
position: 'bottom',
align: 'end',
},
tooltips: {
mode: 'index',
intersect: false,
titleMarginBottom: 10,
bodySpacing: 10,
xPadding: 16,
yPadding: 16,
borderColor: window.chartColors.border,
borderWidth: 1,
backgroundColor: '#fff',
bodyFontColor: window.chartColors.text,
titleFontColor: window.chartColors.text,
callbacks: {
label: function(tooltipItem, data) {
return tooltipItem.value + '%';
}
},
},
hover: {
mode: 'nearest',
intersect: true
},
scales: {
xAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.border,
},
scaleLabel: {
display: false,
}
}],
yAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.border,
},
scaleLabel: {
display: false,
},
ticks: {
beginAtZero: true,
userCallback: function(value, index, values) {
return value.toLocaleString() + '%';
}
},
}]
}
}
};
//Bar Chart Demo
var barChartConfig = {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],
datasets: [{
label: 'Dataset 1',
backgroundColor: "rgba(117,193,129,0.8)",
hoverBackgroundColor: "rgba(117,193,129,1)",
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint()
]
},
{
label: 'Dataset 2',
backgroundColor: "rgba(91,153,234,0.8)",
hoverBackgroundColor: "rgba(91,153,234,1)",
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint()
]
}
]
},
options: {
responsive: true,
legend: {
position: 'bottom',
align: 'end',
},
tooltips: {
mode: 'index',
intersect: false,
titleMarginBottom: 10,
bodySpacing: 10,
xPadding: 16,
yPadding: 16,
borderColor: window.chartColors.border,
borderWidth: 1,
backgroundColor: '#fff',
bodyFontColor: window.chartColors.text,
titleFontColor: window.chartColors.text,
callbacks: {
label: function(tooltipItem, data) {
return tooltipItem.value + '%';
}
},
},
scales: {
xAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.border,
},
}],
yAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.borders,
},
ticks: {
beginAtZero: true,
userCallback: function(value, index, values) {
return value + '%';
}
},
}]
}
}
}
// Pie Chart Demo
var pieChartConfig = {
type: 'pie',
data: {
datasets: [{
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
],
backgroundColor: [
window.chartColors.green,
window.chartColors.blue,
window.chartColors.gray,
],
label: 'Dataset 1'
}],
labels: [
'Green',
'Blue',
'Gray',
]
},
options: {
responsive: true,
legend: {
display: true,
position: 'bottom',
align: 'center',
},
tooltips: {
titleMarginBottom: 10,
bodySpacing: 10,
xPadding: 16,
yPadding: 16,
borderColor: window.chartColors.border,
borderWidth: 1,
backgroundColor: '#fff',
bodyFontColor: window.chartColors.text,
titleFontColor: window.chartColors.text,
/* Display % in tooltip - https://stackoverflow.com/questions/37257034/chart-js-2-0-doughnut-tooltip-percentages */
callbacks: {
label: function(tooltipItem, data) {
//get the concerned dataset
var dataset = data.datasets[tooltipItem.datasetIndex];
//calculate the total of this data set
var total = dataset.data.reduce(function(previousValue, currentValue, currentIndex, array) {
return previousValue + currentValue;
});
//get the current items value
var currentValue = dataset.data[tooltipItem.index];
//calculate the precentage based on the total and current item, also this does a rough rounding to give a whole number
var percentage = Math.floor(((currentValue/total) * 100)+0.5);
return percentage + "%";
},
},
},
}
};
// Doughnut Chart Demo
var doughnutChartConfig = {
type: 'doughnut',
data: {
datasets: [{
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
],
backgroundColor: [
window.chartColors.green,
window.chartColors.blue,
window.chartColors.gray,
],
label: 'Dataset 1'
}],
labels: [
'Green',
'Blue',
'Gray',
]
},
options: {
responsive: true,
legend: {
display: true,
position: 'bottom',
align: 'center',
},
tooltips: {
titleMarginBottom: 10,
bodySpacing: 10,
xPadding: 16,
yPadding: 16,
borderColor: window.chartColors.border,
borderWidth: 1,
backgroundColor: '#fff',
bodyFontColor: window.chartColors.text,
titleFontColor: window.chartColors.text,
animation: {
animateScale: true,
animateRotate: true
},
/* Display % in tooltip - https://stackoverflow.com/questions/37257034/chart-js-2-0-doughnut-tooltip-percentages */
callbacks: {
label: function(tooltipItem, data) {
//get the concerned dataset
var dataset = data.datasets[tooltipItem.datasetIndex];
//calculate the total of this data set
var total = dataset.data.reduce(function(previousValue, currentValue, currentIndex, array) {
return previousValue + currentValue;
});
//get the current items value
var currentValue = dataset.data[tooltipItem.index];
//calculate the precentage based on the total and current item, also this does a rough rounding to give a whole number
var percentage = Math.floor(((currentValue/total) * 100)+0.5);
return percentage + "%";
},
},
},
}
};
// Generate charts on load
window.addEventListener('load', function(){
var lineChart = document.getElementById('chart-line').getContext('2d');
window.myLine = new Chart(lineChart, lineChartConfig);
var barChart = document.getElementById('chart-bar').getContext('2d');
window.myBar = new Chart(barChart, barChartConfig);
var pieChart = document.getElementById('chart-pie').getContext('2d');
window.myPie = new Chart(pieChart, pieChartConfig);
var doughnutChart = document.getElementById('chart-doughnut').getContext('2d');
window.myDoughnut = new Chart(doughnutChart, doughnutChartConfig);
});

+ 224
- 0
src/repostajes/static/js/index-charts.js View File

@ -0,0 +1,224 @@
'use strict';
/* Chart.js docs: https://www.chartjs.org/ */
window.chartColors = {
green: '#75c181',
gray: '#a9b5c9',
text: '#252930',
border: '#e7e9ed'
};
/* Random number generator for demo purpose */
var randomDataPoint = function(){ return Math.round(Math.random()*10000)};
//Chart.js Line Chart Example
var lineChartConfig = {
type: 'line',
data: {
labels: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'],
datasets: [{
label: 'Current week',
fill: false,
backgroundColor: window.chartColors.green,
borderColor: window.chartColors.green,
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint()
],
}, {
label: 'Previous week',
borderDash: [3, 5],
backgroundColor: window.chartColors.gray,
borderColor: window.chartColors.gray,
data: [
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint(),
randomDataPoint()
],
fill: false,
}]
},
options: {
responsive: true,
aspectRatio: 1.5,
legend: {
display: true,
position: 'bottom',
align: 'end',
},
title: {
display: true,
text: 'Chart.js Line Chart Example',
},
tooltips: {
mode: 'index',
intersect: false,
titleMarginBottom: 10,
bodySpacing: 10,
xPadding: 16,
yPadding: 16,
borderColor: window.chartColors.border,
borderWidth: 1,
backgroundColor: '#fff',
bodyFontColor: window.chartColors.text,
titleFontColor: window.chartColors.text,
callbacks: {
//Ref: https://stackoverflow.com/questions/38800226/chart-js-add-commas-to-tooltip-and-y-axis
label: function(tooltipItem, data) {
if (parseInt(tooltipItem.value) >= 1000) {
return "$" + tooltipItem.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
} else {
return '$' + tooltipItem.value;
}
}
},
},
hover: {
mode: 'nearest',
intersect: true
},
scales: {
xAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.border,
},
scaleLabel: {
display: false,
}
}],
yAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.border,
},
scaleLabel: {
display: false,
},
ticks: {
beginAtZero: true,
userCallback: function(value, index, values) {
return '$' + value.toLocaleString(); //Ref: https://stackoverflow.com/questions/38800226/chart-js-add-commas-to-tooltip-and-y-axis
}
},
}]
}
}
};
// Chart.js Bar Chart Example
var barChartConfig = {
type: 'bar',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: [{
label: 'Orders',
backgroundColor: window.chartColors.green,
borderColor: window.chartColors.green,
borderWidth: 1,
maxBarThickness: 16,
data: [
23,
45,
76,
75,
62,
37,
83
]
}]
},
options: {
responsive: true,
aspectRatio: 1.5,
legend: {
position: 'bottom',
align: 'end',
},
title: {
display: true,
text: 'Chart.js Bar Chart Example'
},
tooltips: {
mode: 'index',
intersect: false,
titleMarginBottom: 10,
bodySpacing: 10,
xPadding: 16,
yPadding: 16,
borderColor: window.chartColors.border,
borderWidth: 1,
backgroundColor: '#fff',
bodyFontColor: window.chartColors.text,
titleFontColor: window.chartColors.text,
},
scales: {
xAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.border,
},
}],
yAxes: [{
display: true,
gridLines: {
drawBorder: false,
color: window.chartColors.borders,
},
}]
}
}
}
// Generate charts on load
window.addEventListener('load', function(){
var lineChart = document.getElementById('canvas-linechart').getContext('2d');
window.myLine = new Chart(lineChart, lineChartConfig);
var barChart = document.getElementById('canvas-barchart').getContext('2d');
window.myBar = new Chart(barChart, barChartConfig);
});

+ 4085
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.css
File diff suppressed because it is too large
View File


+ 1
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.css.map
File diff suppressed because it is too large
View File


+ 6
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.min.css
File diff suppressed because it is too large
View File


+ 1
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.min.css.map
File diff suppressed because it is too large
View File


+ 4084
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.css
File diff suppressed because it is too large
View File


+ 1
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.css.map
File diff suppressed because it is too large
View File


+ 6
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.min.css
File diff suppressed because it is too large
View File


+ 1
- 0
src/repostajes/static/plugins/bootstrap/css/bootstrap-grid.rtl.min.css.map
File diff suppressed because it is too large
View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save