Initial commit
266
.gitignore
vendored
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python,django
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python,django
|
||||||
|
|
||||||
|
### Django ###
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
media
|
||||||
|
|
||||||
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
|
# <django-project-name>/staticfiles/
|
||||||
|
|
||||||
|
### Django.Python Stack ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python,django
|
25
README.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# caFICtería-aaS
|
||||||
|
|
||||||
|
<img src="web/main/static/main/img/logo.svg" alt="Project logo" width=200/>
|
||||||
|
|
||||||
|
Welcome to caFICtería-aaS, an **unofficial** page for the _Facultade de Informática da Coruña_ (FIC)'s cafeteria. Here, you can easily check the **daily menu** offerings (scraped from Telegram), enjoy memorable **quotes from our beloved waiter Lázaro**, and more!
|
||||||
|
|
||||||
|
## 🐳 Install with Docker
|
||||||
|
|
||||||
|
If you want to host your own instance of caFICtería-aaS, it is as easy as a few docker containers. You can use the `docker-compose.yml` file at the root of this repo. You know: `docker-compose up -d`.
|
||||||
|
|
||||||
|
If you want to get the history of menus since 2018 you can do `docker-compose exec bot python3 get_history.py` (I guess you can skip this if you don't love data 😔).
|
||||||
|
|
||||||
|
### 🔧 Environment Variables
|
||||||
|
|
||||||
|
You can check each folder (`bot` and `web`) and look at the `README.md` for information about the individual parts of the project and evironment variables.
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
This project is not endorsed by, directly affiliated with, maintained by, sponsored by or in any way officially related with _Universidade da Coruña_ or _Facultade de Informática da Coruña_. I'm just a student there 😅.
|
||||||
|
|
||||||
|
## ❤️ Credits
|
||||||
|
|
||||||
|
- Logo
|
||||||
|
- Base cup by [OpenClipart-Vectors](https://pixabay.com/users/openclipart-vectors-30363) from Pixabay ([here](https://pixabay.com/vectors/coffee-cup-silhouette-steam-tea-158980/))
|
||||||
|
- The original FIC logo is the property of _Universidade da Coruña_ and _Facultade de Informática da Coruña_.
|
179
bot/.dockerignore
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
|
||||||
|
# Ignore Telethon session files
|
||||||
|
*.session
|
13
bot/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
COPY requirements.txt /
|
||||||
|
RUN pip3 install --no-cache-dir -r /requirements.txt
|
||||||
|
RUN rm /requirements.txt
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
CMD ["python3", "bot_listen.py"]
|
29
bot/README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# caFICtería-aaS (bot)
|
||||||
|
|
||||||
|
This is the bot that actually scrapes the menus from Telegram and puts them in a JSON file.
|
||||||
|
|
||||||
|
## ⚙️ Usage
|
||||||
|
|
||||||
|
- `get_history.py` will download all messages on the channel and parse them (2018-now). Should be run when you install everything (I guess you can skip this if you don't love data 😔).
|
||||||
|
- This will also include the menus sent in photos during a few days in July 2022 (manually parsed in `menu_photos_history.json`).
|
||||||
|
- `bot_listen.py` will listen for new messages on the channel, and parse them when received. This is what should be running to fetch the messages received after the initial run of `get_history.py`.
|
||||||
|
|
||||||
|
## 🔧 Environment Variables
|
||||||
|
|
||||||
|
| Name | Required | Description |
|
||||||
|
|--------------------------|----------|-------------|
|
||||||
|
| `TG_API_ID` | YES | Your telegram API Id |
|
||||||
|
| `TG_API_HASH` | YES | Your telegram API Hash |
|
||||||
|
| `TG_SESSION_NAME` | NO | The telethon session name/path _(Default: "default")_ |
|
||||||
|
| `TG_CHANNEL_NAME` | NO | The channel where the menu info is stored _(Default: "CafeteriaFIC")_ |
|
||||||
|
| `MENU_HISTORY_FILE_PATH` | NO | The place where the JSON file with the menu history will be stored. _(Default: "/tmp/menu_history.json")_ |
|
||||||
|
|
||||||
|
_**Tip💡:**_ Look at [Telethon documentation](https://docs.telethon.dev/en/stable/basic/signing-in.html#signing-in) for info about the telegram authentication process
|
||||||
|
|
||||||
|
## 🐳 Building the Docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.peprolinbot.com/peprolinbot/caFICteria-aaS.git
|
||||||
|
cd caFICteria-aaS/bot
|
||||||
|
docker build -t caficteria-bot .
|
||||||
|
```
|
32
bot/bot_listen.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
import config
|
||||||
|
from utils import parse_menu_message, InvalidMenuMessageError
|
||||||
|
|
||||||
|
|
||||||
|
client = TelegramClient(config.session_name, config.api_id, config.api_hash)
|
||||||
|
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(chats=config.channel_name))
|
||||||
|
async def handler(event):
|
||||||
|
msg = event.message
|
||||||
|
if not msg.message is None:
|
||||||
|
try:
|
||||||
|
courses = parse_menu_message(msg.message)
|
||||||
|
except InvalidMenuMessageError as e:
|
||||||
|
print(e)
|
||||||
|
return
|
||||||
|
with open(config.menu_history_file_path, "rw", encoding='utf-8') as f:
|
||||||
|
menus = json.load(f)
|
||||||
|
menus[msg.date.strftime("%d-%m-%Y")] = {"courses": courses,
|
||||||
|
"message": msg.message}
|
||||||
|
json.dump(menus, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await client.start()
|
||||||
|
await client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import asyncio
|
||||||
|
asyncio.run(main())
|
9
bot/config.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from os import getenv
|
||||||
|
|
||||||
|
api_id = getenv("TG_API_ID")
|
||||||
|
api_hash = getenv("TG_API_HASH")
|
||||||
|
session_name = getenv("TG_SESSION_NAME", "default")
|
||||||
|
channel_name = getenv("TG_CHANNEL_NAME", "CafeteriaFIC")
|
||||||
|
|
||||||
|
menu_history_file_path = getenv(
|
||||||
|
"MENU_HISTORY_FILE_PATH", "/tmp/menu_history.json")
|
48
bot/get_history.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from telethon import TelegramClient
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from utils import parse_menu_message, InvalidMenuMessageError
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
async def get_message_history(chat_name, limit=None):
|
||||||
|
async with TelegramClient(config.session_name, config.api_id, config.api_hash) as client:
|
||||||
|
chat_info = await client.get_entity(chat_name)
|
||||||
|
|
||||||
|
messages = await client.get_messages(entity=chat_info, limit=limit)
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def parse_message_history(chat_name, limit=None):
|
||||||
|
messages = asyncio.run(get_message_history(
|
||||||
|
chat_name=chat_name, limit=limit))
|
||||||
|
|
||||||
|
menus = {}
|
||||||
|
for msg in messages:
|
||||||
|
# Ignore messages without text conent and info messages (these usually include ! symbol)
|
||||||
|
if msg.message is None or "!" in msg.message:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
courses = parse_menu_message(msg.message)
|
||||||
|
except InvalidMenuMessageError as e:
|
||||||
|
print(e)
|
||||||
|
continue
|
||||||
|
menus[msg.date.strftime("%d-%m-%Y")] = {"courses": courses,
|
||||||
|
"message": msg.message}
|
||||||
|
|
||||||
|
return menus
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
menus = parse_message_history(config.channel_name)
|
||||||
|
|
||||||
|
# We manually include menus in photos from July 2022
|
||||||
|
with open("menu_photos_history.json", 'r') as file:
|
||||||
|
data = json.load(file)
|
||||||
|
menus.update(data)
|
||||||
|
|
||||||
|
with open(config.menu_history_file_path, "w", encoding='utf-8') as f:
|
||||||
|
json.dump(menus, f, ensure_ascii=False, indent=4)
|
98
bot/menu_photos_history.json
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"14-07-2022": [
|
||||||
|
[
|
||||||
|
"Espaguetis boloñesa",
|
||||||
|
"Ensaladilla de gambas",
|
||||||
|
"Ensalada mixta"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Lirios fritos",
|
||||||
|
"Codillo asado"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"15-07-2022": [
|
||||||
|
[
|
||||||
|
"Tortilla",
|
||||||
|
"Pastel de york y queso",
|
||||||
|
"Espaguetis con salsa de tomate y atún",
|
||||||
|
"Gazpacho"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Varitas de merluza",
|
||||||
|
"Merluza a la plancha",
|
||||||
|
"Raxo de cerdo"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"18-07-2022": [
|
||||||
|
[
|
||||||
|
"Ensalada de pasta",
|
||||||
|
"Croquetas"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Lasaña",
|
||||||
|
"Filete de caballa a la plancha"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"19-07-2022": [
|
||||||
|
[
|
||||||
|
"Champiñones con jamón",
|
||||||
|
"Raviolis con salsa de tomate casera"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Gallo a la plancha",
|
||||||
|
"Milanesa de pollo"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"20-07-2022": [
|
||||||
|
[
|
||||||
|
"Ensalada de pasta",
|
||||||
|
"Crema de zanahoria"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Raxo de pollo",
|
||||||
|
"Arroz con bacalao"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"21-07-2022": [
|
||||||
|
[
|
||||||
|
"Melón con jamón",
|
||||||
|
"Espaguetis a la arrabbiata"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hamburguesa de ternera con patatas fritas",
|
||||||
|
"Raya a la gallega"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"26-07-2022": [
|
||||||
|
[
|
||||||
|
"Ensaladilla rusa",
|
||||||
|
"Judías con chorizo",
|
||||||
|
"Huevos rellenos"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Rabas de calamar",
|
||||||
|
"Codillo asado"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"27-07-2022": [
|
||||||
|
[
|
||||||
|
"Espinacas salteadas",
|
||||||
|
"Raviolis con salsa de tomate casera"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Rosada al horno",
|
||||||
|
"Albóndigas con arroz"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"28-07-2022": [
|
||||||
|
[
|
||||||
|
"Champiñones con jamón",
|
||||||
|
"Raviolis con salsa de tomate casera",
|
||||||
|
"Melón con jamón"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Merluza a la plancha",
|
||||||
|
"Chuletas de cerdo"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
1
bot/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Telethon==1.38.1
|
97
bot/utils.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMenuMessageError(Exception):
|
||||||
|
"""
|
||||||
|
Custom exception raised when a menu can not be parsed correctly.
|
||||||
|
Usually happens when trying to parse a random message like "Estamos de vuelta!!!")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = f"Invalid message while parsing into a menu, message: '{message}'"
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_menu_message(message: str) -> list[list]:
|
||||||
|
"""
|
||||||
|
Using `menu=parse_menu_message(my_message)`, `menu[0]` are the options for the first course (`menu[0]=["Pasta a la boloñesa", "Ensaladilla rusa"]` for example). `menu[1]` is the same but for the second course
|
||||||
|
|
||||||
|
:param message: Will receive a message's content as sent in the telegram channel.
|
||||||
|
|
||||||
|
:returns: A list of lists with the options for each course.
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = re.sub(r'\n\s*\n+', '\n\n', message)
|
||||||
|
lines = message.splitlines()
|
||||||
|
|
||||||
|
if lines == []:
|
||||||
|
raise InvalidMenuMessageError(message)
|
||||||
|
|
||||||
|
first_line = lines[0].lower()
|
||||||
|
first_line_is_header = "menu" in first_line or "menú" in first_line
|
||||||
|
if first_line_is_header:
|
||||||
|
lines.pop(0)
|
||||||
|
# Remove the newline after the header if it exists
|
||||||
|
if not lines[0].strip():
|
||||||
|
lines.pop(0)
|
||||||
|
|
||||||
|
# Initialize this to none in case we don't get it
|
||||||
|
course_separator_index = None
|
||||||
|
|
||||||
|
# Check if course separator is a blank line
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if not line.strip():
|
||||||
|
course_separator_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if course_separator_index is None:
|
||||||
|
# Old menus use - for first course and > for second course, or otherwise
|
||||||
|
second_course_char = '-' if lines[0][0] == '>' else '>' if lines[0][0] == '-' else None
|
||||||
|
if not second_course_char is None:
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line[0] == second_course_char:
|
||||||
|
course_separator_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if course_separator_index is None:
|
||||||
|
raise InvalidMenuMessageError(message)
|
||||||
|
|
||||||
|
# Some messages have "TAMBIEN PARA LLEVAR" at the end of them
|
||||||
|
last_line = lines[-1].lower()
|
||||||
|
if "llevar" in last_line or "preguntar" in last_line:
|
||||||
|
lines.pop()
|
||||||
|
|
||||||
|
def fix_line(line):
|
||||||
|
# Remove first character if it is not a letter (- and > are common in 2023). Leading whitespace might appear, will be deleted later
|
||||||
|
line = re.sub(r'^[^\w]+', '', line)
|
||||||
|
|
||||||
|
# Strips the line (leading and trailing whitespaces on menu items are common)
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Replace occurrences of "c/" with "con " (if extra space was added it will be removed after)
|
||||||
|
line = line.replace("c/", "con ")
|
||||||
|
|
||||||
|
# Remove extra whitespaces in the middle of the string
|
||||||
|
line = ' '.join(line.split())
|
||||||
|
|
||||||
|
# Remove trailing dot
|
||||||
|
if line[-1] == '.':
|
||||||
|
line = line[:-1]
|
||||||
|
|
||||||
|
# Capitalize
|
||||||
|
line = line.capitalize()
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
lines = [fix_line(line) for line in lines if line.strip()]
|
||||||
|
|
||||||
|
# First two lines are first course, and second two ones are second course
|
||||||
|
first_course = lines[:course_separator_index]
|
||||||
|
second_course = lines[course_separator_index:]
|
||||||
|
|
||||||
|
if len(first_course) <= 1 or len(second_course) <= 1:
|
||||||
|
raise InvalidMenuMessageError(message)
|
||||||
|
|
||||||
|
courses = [first_course, second_course]
|
||||||
|
|
||||||
|
return courses
|
22
docker-compose.yml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: quay.io/peprolinbot/caficteria-web:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
environment:
|
||||||
|
- DJANGO_ALLOWED_HOSTS=example.com
|
||||||
|
- DJANGO_SECRET_KEY=changemetosomethingsecureplease
|
||||||
|
volumes:
|
||||||
|
- /data/caficteria-aas/web_db.sqlite3:/app/db.sqlite3
|
||||||
|
- /data/caficteria-aas/menu_history.json:/tmp/menu_history.json:ro
|
||||||
|
|
||||||
|
bot:
|
||||||
|
image: quay.io/peprolinbot/caficteria-bot:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TG_API_ID=yourtgapiid
|
||||||
|
- TG_API_HASH=yourtgapihash
|
||||||
|
- TG_CHANNEL_NAME=CafeteriaFIC
|
||||||
|
volumes:
|
||||||
|
- /data/caficteria-aas/menu_history.json:/tmp/menu_history.json
|
266
web/.dockerignore
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python,django
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python,django
|
||||||
|
|
||||||
|
### Django ###
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
media
|
||||||
|
|
||||||
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
|
# <django-project-name>/staticfiles/
|
||||||
|
|
||||||
|
### Django.Python Stack ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python,django
|
22
web/Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache nginx supervisor
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
COPY requirements.txt /
|
||||||
|
RUN pip3 install --no-cache-dir -r /requirements.txt
|
||||||
|
RUN rm /requirements.txt
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN python manage.py collectstatic
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/http.d/default.conf
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
ENV DJANGO_DEBUG=false
|
||||||
|
EXPOSE 80 8080
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
27
web/README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# caFICtería-aaS (web)
|
||||||
|
|
||||||
|
This is the [Django](https://www.djangoproject.com/) web that serves the daily menu data and other functionalities.
|
||||||
|
|
||||||
|
## 🔧 Environment Variables
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|--------------------------|-------------|
|
||||||
|
| `DJANGO_DEBUG` (bool) | Whether to enable Django's debug mode. Leave it false in production. _(Default: False)_ |
|
||||||
|
| `DJANGO_ALLOWED_HOSTS` | Space-separated list of host/domain names that Django can serve. Not needed in debug mode (**otherwise is needed**). _(Default: "")_ |
|
||||||
|
| `DJANGO_SECRET_KEY` | The key to securing signed data. Must be randomly generated and kept secure. _(Default: "django-insecure-(krka)#p79n81tjf-)dy9f1!k^4*j&+qf5_eurt7)o%8%mr1ce")_ |
|
||||||
|
| `DJANGO_CSRF_TRUSTED_ORIGINS` | Space-separated list of trusted origins for unsafe requests. Not needed in debug mode, and when running on port 80/443. _(Default: "")_ |
|
||||||
|
| `MENU_HISTORY_FILE_PATH` | The place where the JSON file with the menu history is stored. _(Default: "/tmp/menu_history.json")_ |
|
||||||
|
|
||||||
|
_**Note🗒️:**_ Booleans are only true when their value is the string "true" (not case sensitive)
|
||||||
|
|
||||||
|
_**Tip💡:**_ You can generate a secret key with `openssl rand -hex 32`
|
||||||
|
|
||||||
|
_**Tip💡:**_ You can check [Django's documentation](https://docs.djangoproject.com/en/5.1/) to better understand these variables
|
||||||
|
|
||||||
|
## 🐳 Building the Docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.peprolinbot.com/peprolinbot/caFICteria-aaS.git
|
||||||
|
cd caFICteria-aaS/web
|
||||||
|
docker build -t caficteria-web .
|
||||||
|
```
|
0
web/caFICteria_aaS/__init__.py
Normal file
16
web/caFICteria_aaS/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for caFICteria_aaS 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', 'caFICteria_aaS.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
143
web/caFICteria_aaS/settings.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
"""
|
||||||
|
Django settings for caFICteria_aaS project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.1.3.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 = os.getenv('DJANGO_SECRET_KEY',
|
||||||
|
'django-insecure-uq7od9!2_9e840883ydo%$))piue&d!xye26=(v^avv_^ny@1(')
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv('DJANGO_DEBUG', "true").lower() == "true"
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', "").split()
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'main',
|
||||||
|
'daily_menu',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django_bootstrap5'
|
||||||
|
]
|
||||||
|
|
||||||
|
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 = 'caFICteria_aaS.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'caFICteria_aaS.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# For HTTPS to work
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS', "").split()
|
||||||
|
|
||||||
|
# Bootstrap5
|
||||||
|
BOOTSTRAP5 = {
|
||||||
|
"css_url": f"/{STATIC_URL}main/css/libs/bootstrap.min.css",
|
||||||
|
"javascript_url": f"/{STATIC_URL}main/js/libs/bootstrap.bundle.min.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
MENU_HISTORY_FILE_PATH = os.getenv(
|
||||||
|
"MENU_HISTORY_FILE_PATH", "/tmp/menu_history.json")
|
25
web/caFICteria_aaS/urls.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
URL configuration for caFICteria_aaS 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 include, path
|
||||||
|
from main import views as main_views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('menu/', include("daily_menu.urls")),
|
||||||
|
path('', main_views.index, name="index"),
|
||||||
|
]
|
16
web/caFICteria_aaS/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for caFICteria_aaS 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', 'caFICteria_aaS.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
web/daily_menu/__init__.py
Normal file
6
web/daily_menu/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DailyMenuConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'daily_menu'
|
10
web/daily_menu/forms.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django import forms
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class DateSelectionForm(forms.Form):
|
||||||
|
def get_current_date_formatted():
|
||||||
|
return datetime.date.today().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
date = forms.DateField(required=True, label='Fecha', initial=datetime.date.today, widget=forms.DateInput(attrs={
|
||||||
|
'class': 'bg-white text-black', 'type': 'date', 'max': get_current_date_formatted})) # type=date for the bootstrap+system datepicker to appear
|
0
web/daily_menu/migrations/__init__.py
Normal file
52
web/daily_menu/templates/daily_menu/show_menu.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends 'main/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h1 class="text-center mb-4">Menú del día 🗓️ {{ date|date:"d/m/Y" }}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<h2>Primero</h2>
|
||||||
|
<ul class="list-group mb-4">
|
||||||
|
{% for course in first_courses %}
|
||||||
|
<li class="list-group-item">{{ course }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<h2>Segundo</h2>
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for course in second_courses %}
|
||||||
|
<li class="list-group-item">{{ course }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<h2>Mensaje original</h2>
|
||||||
|
<div id="tgMsgDiv" class="border pt-3 px-3 pb-0" style="display: none;">
|
||||||
|
{{ tg_message|linebreaks }}
|
||||||
|
</div>
|
||||||
|
<button id="toggleTgMsgButton" class="btn btn-primary mt-2 mb-4">Mostrar mensaje</button>
|
||||||
|
<h2>API</h2>
|
||||||
|
<a href="{{ request.path|add:'/json' }}" target="_blank" class="btn btn-warning mt-2">Ver JSON</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#toggleTgMsgButton').click(function () {
|
||||||
|
var textDiv = $('#tgMsgDiv');
|
||||||
|
textDiv.toggle();
|
||||||
|
|
||||||
|
if (textDiv.is(':visible')) {
|
||||||
|
$(this).text('Ocultar');
|
||||||
|
$(this).removeClass('btn-primary');
|
||||||
|
$(this).addClass('btn-danger');
|
||||||
|
} else {
|
||||||
|
$(this).text('Mostrar mensaje');
|
||||||
|
$(this).removeClass('btn-danger');
|
||||||
|
$(this).addClass('btn-primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
3
web/daily_menu/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
11
web/daily_menu/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "daily_menu"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("historic", views.select_menu_form, name="select_menu_form"),
|
||||||
|
path("<str:date>", views.show_menu, name="show_menu"),
|
||||||
|
path("<str:date>/json", views.get_menu_json, name="get_menu_json"),
|
||||||
|
]
|
15
web/daily_menu/utils.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_menu(date="today"):
|
||||||
|
if date == "today":
|
||||||
|
date = datetime.now()
|
||||||
|
else:
|
||||||
|
date = datetime.strptime(date, "%d-%m-%Y")
|
||||||
|
|
||||||
|
with open(settings.MENU_HISTORY_FILE_PATH, "r", encoding='utf-8') as f:
|
||||||
|
menus = json.load(f)
|
||||||
|
|
||||||
|
return date, menus[date.strftime("%d-%m-%Y")]
|
47
web/daily_menu/views.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import JsonResponse, Http404
|
||||||
|
from .utils import get_menu
|
||||||
|
from .forms import DateSelectionForm
|
||||||
|
|
||||||
|
|
||||||
|
def select_menu_form(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = DateSelectionForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
return redirect('daily_menu:show_menu', form.cleaned_data['date'].strftime("%d-%m-%Y"))
|
||||||
|
else:
|
||||||
|
form = DateSelectionForm()
|
||||||
|
|
||||||
|
return render(request, 'main/show_form.html', {'form_header': 'Introduce una fecha para ver el menú', 'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
def show_menu(request, date):
|
||||||
|
try:
|
||||||
|
date, menu = get_menu(date)
|
||||||
|
except KeyError:
|
||||||
|
if date == "today":
|
||||||
|
messages.error(
|
||||||
|
request, "El menú de hoy (aún) no está disponible")
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
request, "No hay ningún menú ese día")
|
||||||
|
|
||||||
|
return redirect('daily_menu:select_menu_form')
|
||||||
|
|
||||||
|
courses = menu["courses"]
|
||||||
|
|
||||||
|
return render(request, 'daily_menu/show_menu.html', {'date': date,
|
||||||
|
'first_courses': courses[0],
|
||||||
|
'second_courses': courses[1],
|
||||||
|
'tg_message': menu["message"]})
|
||||||
|
|
||||||
|
|
||||||
|
def get_menu_json(request, date):
|
||||||
|
try:
|
||||||
|
date, menu = get_menu(date)
|
||||||
|
except KeyError:
|
||||||
|
return JsonResponse({"error": "There is no menu for that date"}, status=404)
|
||||||
|
|
||||||
|
return JsonResponse(menu)
|
0
web/lazaro_quotes/__init__.py
Normal file
3
web/lazaro_quotes/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
web/lazaro_quotes/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LazaroQuotesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'lazaro_quotes'
|
0
web/lazaro_quotes/migrations/__init__.py
Normal file
13
web/lazaro_quotes/models.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Quote(models.Model):
|
||||||
|
text = models.TextField()
|
||||||
|
quoted_name = models.CharField(null=True, blank=True, max_length=254)
|
||||||
|
suggester = models.ForeignKey(
|
||||||
|
Suggester, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Suggester(models.model):
|
||||||
|
name = models.CharField(null=True, blank=True, max_length=254)
|
||||||
|
email = models.CharField(null=True, blank=True, max_length=254)
|
3
web/lazaro_quotes/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
web/lazaro_quotes/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
0
web/main/__init__.py
Normal file
6
web/main/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MainConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'main'
|
0
web/main/migrations/__init__.py
Normal file
15
web/main/static/main/css/base.css
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-wrapper {
|
||||||
|
padding-bottom: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
height: 75px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
6
web/main/static/main/css/libs/bootstrap.min.css
vendored
Normal file
1
web/main/static/main/css/libs/bootstrap.min.css.map
Normal file
3
web/main/static/main/css/libs/select2-bootstrap-5-theme.min.css
vendored
Normal file
1
web/main/static/main/css/libs/select2.min.css
vendored
Normal file
27
web/main/static/main/css/navbar.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.bi {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-modes {
|
||||||
|
.dropdown-menu {
|
||||||
|
padding: .25rem;
|
||||||
|
|
||||||
|
li+li {
|
||||||
|
margin-top: .125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
@include border-radius(.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
display: block !important; // stylelint-disable-line declaration-no-important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
web/main/static/main/img/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
web/main/static/main/img/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
web/main/static/main/img/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
web/main/static/main/img/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 692 B |
BIN
web/main/static/main/img/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
web/main/static/main/img/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
web/main/static/main/img/favicon/site.webmanifest
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
85
web/main/static/main/img/logo.svg
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="1080"
|
||||||
|
height="1080"
|
||||||
|
viewBox="0 0 1080 1080"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="0.2848069"
|
||||||
|
inkscape:cx="651.31849"
|
||||||
|
inkscape:cy="677.65213"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1028"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
d="m 1055.6796,496.49715 c -16.5445,-29.2184 -42.5065,-53.92117 -77.93633,-66.34015 -14.953,-5.26117 -30.65539,-7.92267 -46.22257,-8.04627 h -1.04683 c -42.04298,0.008 -80.903,17.1857 -112.2537,43.39497 -31.71765,26.5917 -56.99984,63.18038 -72.28504,106.45174 l -3.86282,10.92794 h 0.28198 c -7.24665,23.70615 -10.81205,47.4123 -11.03995,70.29955 -0.0193,0.73779 -0.0193,1.47559 -0.0193,2.18635 0.0193,35.64615 8.07717,69.44973 24.33967,98.16984 16.54447,29.25317 42.52583,53.95594 77.93635,66.35947 l 2.78896,-7.72565 -2.78896,7.74883 c 14.88733,5.20708 30.5395,7.84925 46.09123,7.99218 h 0.15065 0.197 c 0.18542,-0.0193 0.49831,0 0.88073,0 42.04685,-0.0193 80.8721,-17.17798 112.18803,-43.36408 31.6172,-26.54146 56.8685,-63.01426 72.2,-106.16974 l 0.066,-0.13134 0.1005,-0.28971 c 9.5682,-27.26382 14.2538,-54.71692 14.5551,-81.1116 v -2.08592 c 0,-35.68092 -8.054,-69.50381 -24.3203,-98.26641 z M 801.56367,653.86478 c 0.1661,-20.50002 3.84352,-42.25544 11.48805,-63.8834 l 0.0193,-0.0695 0.0502,-0.13905 0.0348,-0.0618 c 12.10223,-34.53752 32.2121,-62.57776 54.19545,-80.93005 21.76701,-18.22866 44.44566,-26.39468 62.6975,-26.36763 h 0.8112 c 7.06511,0.0888 13.60873,1.17815 20.39185,3.54607 15.69853,5.45431 30.28841,17.57199 41.31291,36.92861 10.66147,18.6922 17.20887,43.71945 17.17417,71.25753 v 2.0782 c -0.1661,20.39185 -3.8474,42.08934 -11.42242,63.71343 l -0.0155,0.0657 -0.10048,0.23563 -0.0502,0.15065 c -12.10224,34.49117 -32.2121,62.52755 -54.19544,80.82962 -21.76701,18.2364 -44.48043,26.42558 -62.68593,26.39855 h -0.89231 c -6.96467,-0.0812 -13.56237,-1.20135 -20.4073,-3.53835 -15.6174,-5.435 -30.20729,-17.59132 -41.2318,-36.90543 -10.69615,-18.75789 -17.22434,-43.81603 -17.20889,-71.38502 v -1.88891 l -8.28961,-0.11585 z"
|
||||||
|
fill="#231f20"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#006e77;fill-opacity:1;stroke-width:3.86283" />
|
||||||
|
<path
|
||||||
|
d="m 893.5723,714.22142 c 35.95904,-93.59239 43.86625,-195.77956 43.89715,-315.97522 0,-5.77107 -2.38336,-11.4301 -6.56295,-15.4938 -4.17571,-4.12163 -9.912,-6.46637 -15.81826,-6.46637 H 103.54733 c -5.925575,0 -11.685046,2.34474 -15.841446,6.42389 -4.195028,4.10231 -6.566803,9.76521 -6.566803,15.53241 0.02318,120.58582 10.927931,227.49336 48.308489,323.25279 18.26344,46.90242 43.03187,90.89227 75.60707,132.05067 h 606.84982 c 36.39168,-44.43793 62.88293,-90.4944 81.63694,-139.32437 z"
|
||||||
|
fill="#231f20"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#006e77;fill-opacity:1;stroke-width:3.86283" />
|
||||||
|
<path
|
||||||
|
d="m 1020.3888,894.30632 c -3.3799,-8.28575 -11.6386,-13.76325 -20.77422,-13.76325 H 22.397101 c -9.062188,0 -17.2397885,5.33843 -20.6738399,13.55466 -3.488131,8.19692 -1.58375824,17.63766 4.8323941,23.9302 38.7518598,37.63167 59.2286958,71.38117 100.2441748,100.74627 41.16613,29.0949 98.66041,48.7992 206.18214,61.0597 L 315.5662,1080 h 390.87927 l 3.21774,-0.2318 c 105.65598,-15.0225 161.87168,-34.6881 202.77127,-62.8365 40.80302,-28.4303 62.08719,-60.7352 102.68932,-98.59088 6.5861,-6.19983 8.6721,-15.74102 5.2573,-24.0345 z"
|
||||||
|
fill="#231f20"
|
||||||
|
id="path3"
|
||||||
|
style="fill:#006e77;fill-opacity:1;stroke-width:3.86283" />
|
||||||
|
<path
|
||||||
|
d="m 417.76886,345.21808 c -28.68788,-33.20459 -41.81228,-63.21706 -41.81228,-91.57167 0.071,-30.01974 14.58503,-53.42618 29.38276,-72.37168 14.99271,-19.06909 31.06318,-35.45143 40.22402,-50.87758 6.16158,-10.32168 9.33455,-19.41086 9.36646,-29.31079 0.0532,-14.626305 -7.36696,-35.567771 -36.46609,-66.143773 -7.84911,-8.223882 -7.57613,-21.323212 0.60977,-29.187166 8.20718,-7.8930467 21.21811,-7.6458248 29.07786,0.607152 32.79679,34.320739 47.83915,65.067614 47.89942,94.723787 -0.0993,29.4453 -14.77292,52.20097 -29.4643,70.80106 -14.92535,18.77098 -30.86464,35.12424 -40.01838,50.92486 -6.19351,10.5798 -9.45511,20.14888 -9.47992,30.83049 -0.007,14.97898 6.69338,35.38964 31.76869,64.52228 7.41658,8.65289 6.47356,21.68314 -2.12358,29.13989 -3.87848,3.38845 -8.6468,5.05724 -13.42574,5.05724 -5.7468,0.003 -11.47587,-2.42501 -15.53869,-7.1441 z"
|
||||||
|
fill="#999"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#f29111;fill-opacity:1;stroke-width:3.59016" />
|
||||||
|
<path
|
||||||
|
d="m 510.14301,345.23877 c -28.70562,-33.23003 -41.79809,-63.2425 -41.80518,-91.59347 0.0674,-30.03064 14.58501,-53.41165 29.35084,-72.36441 14.99271,-19.07637 31.06318,-35.42962 40.2382,-50.88484 6.17931,-10.30715 9.31683,-19.38904 9.34164,-29.30352 0.0815,-14.637211 -7.35632,-35.538696 -36.43418,-66.114695 -7.84912,-8.2457 -7.57612,-21.359572 0.62396,-29.2271632 8.19299,-7.8966743 21.20038,-7.6312724 29.07077,0.614428 32.7897,34.3134732 47.80369,65.0748932 47.86041,94.7237802 -0.0815,29.4744 -14.75164,52.17189 -29.43592,70.81926 -14.936,18.74915 -30.87174,35.10241 -40.03968,50.90667 -6.16867,10.57254 -9.46218,20.14526 -9.46218,30.82686 -0.0248,14.97169 6.7111,35.38963 31.75451,64.55135 7.44141,8.62018 6.49837,21.67951 -2.06687,29.13992 -3.8891,3.3957 -8.67514,5.04993 -13.45056,5.04993 -5.75388,0.003 -11.49712,-2.42135 -15.54576,-7.1441 z"
|
||||||
|
fill="#999"
|
||||||
|
id="path5"
|
||||||
|
style="fill:#f29111;fill-opacity:1;stroke-width:3.59016" />
|
||||||
|
<path
|
||||||
|
d="m 611.73117,345.21697 c -28.66661,-33.17915 -41.77682,-63.18798 -41.77682,-91.53167 0.0248,-30.0452 14.57083,-53.45165 29.35084,-72.40441 14.98918,-19.05818 31.0809,-35.42962 40.2382,-50.88484 6.16159,-10.29987 9.3381,-19.38904 9.35938,-29.30352 0.0567,-14.615403 -7.35633,-35.538696 -36.48028,-66.114695 -7.82784,-8.2457 -7.55485,-21.359572 0.63459,-29.2271632 8.18591,-7.8966743 21.18976,-7.6312724 29.07432,0.614428 32.76842,34.3134732 47.83914,65.0748932 47.87106,94.7237802 -0.0922,29.4744 -14.74457,52.17189 -29.43594,70.81926 -14.936,18.74915 -30.87174,35.12421 -40.04675,50.93939 -6.16159,10.56526 -9.45156,20.11254 -9.47282,30.83413 0,14.9317 6.71818,35.34964 31.76867,64.515 7.44142,8.62018 6.49839,21.6795 -2.0775,29.1399 -3.89618,3.39571 -8.67868,5.02451 -13.44345,5.02451 -5.75388,0.003 -11.51841,-2.39955 -15.5635,-7.1441 z"
|
||||||
|
fill="#999"
|
||||||
|
id="path6"
|
||||||
|
style="fill:#f29111;fill-opacity:1;stroke-width:3.59016" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:467.286px;font-family:'Ubuntu Nerd Font';-inkscape-font-specification:'Ubuntu Nerd Font Bold';fill:#ffffff;fill-opacity:1;stroke-width:5.47605"
|
||||||
|
x="258.03479"
|
||||||
|
y="791.28717"
|
||||||
|
id="text6"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="258.03479"
|
||||||
|
y="791.28717"
|
||||||
|
id="tspan7"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Ubuntu Nerd Font';-inkscape-font-specification:'Ubuntu Nerd Font';fill:#ffffff;fill-opacity:1;stroke-width:5.47605"><tspan
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;fill:#ffffff;fill-opacity:1;stroke-width:5.47605"
|
||||||
|
id="tspan8">f</tspan>ic</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.9 KiB |
62
web/main/static/main/js/color-modes.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*!
|
||||||
|
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2022 The Bootstrap Authors
|
||||||
|
* Licensed under the Creative Commons Attribution 3.0 Unported License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const storedTheme = localStorage.getItem('theme')
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTheme = function (theme) {
|
||||||
|
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', 'dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
const showActiveTheme = theme => {
|
||||||
|
const activeThemeIcon = document.querySelector('.theme-icon-active use')
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
|
||||||
|
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
||||||
|
element.classList.remove('active')
|
||||||
|
})
|
||||||
|
|
||||||
|
btnToActive.classList.add('active')
|
||||||
|
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
if (storedTheme !== 'light' || storedTheme !== 'dark') {
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
showActiveTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]')
|
||||||
|
.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
const theme = toggle.getAttribute('data-bs-theme-value')
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
setTheme(theme)
|
||||||
|
showActiveTheme(theme)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
7
web/main/static/main/js/libs/bootstrap.bundle.min.js
vendored
Normal file
1
web/main/static/main/js/libs/bootstrap.bundle.min.js.map
Normal file
2
web/main/static/main/js/libs/jquery.min.js
vendored
Normal file
6
web/main/static/main/js/libs/popper.min.js
vendored
Normal file
2
web/main/static/main/js/libs/select2.min.js
vendored
Normal file
13
web/main/templates/404.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'main/base.html' %}
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container text-center mt-5">
|
||||||
|
<div style="font-size: 8rem;"">😕</div>
|
||||||
|
<h1>Oops! 404</h1>
|
||||||
|
<p>No hemos encontrado la página que buscabas.</p>
|
||||||
|
<a class=" btn btn-primary" href="/">Volver al Inicio</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
13
web/main/templates/500.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'main/base.html' %}
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container text-center mt-5">
|
||||||
|
<div style="font-size: 8rem;"">🤔</div>
|
||||||
|
<h1>Oops! 500</h1>
|
||||||
|
<p>El servidor ha tenido problemas procesando tu petición.</p>
|
||||||
|
<a class=" btn btn-primary" href="/">Volver al Inicio</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
120
web/main/templates/main/_items/navbar.html
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!-- Bootstrap icons-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||||
|
<symbol id="bootstrap" viewBox="0 0 512 408" fill="currentcolor">
|
||||||
|
<path
|
||||||
|
d="M106.342 0c-29.214 0-50.827 25.58-49.86 53.32.927 26.647-.278 61.165-8.966 89.31C38.802 170.862 24.07 188.707 0 191v26c24.069 2.293 38.802 20.138 47.516 48.37 8.688 28.145 9.893 62.663 8.965 89.311C55.515 382.42 77.128 408 106.342 408h299.353c29.214 0 50.827-25.58 49.861-53.319-.928-26.648.277-61.166 8.964-89.311 8.715-28.232 23.411-46.077 47.48-48.37v-26c-24.069-2.293-38.765-20.138-47.48-48.37-8.687-28.145-9.892-62.663-8.964-89.31C456.522 25.58 434.909 0 405.695 0H106.342zm236.559 251.102c0 38.197-28.501 61.355-75.798 61.355h-87.202a2 2 0 01-2-2v-213a2 2 0 012-2h86.74c39.439 0 65.322 21.354 65.322 54.138 0 23.008-17.409 43.61-39.594 47.219v1.203c30.196 3.309 50.532 24.212 50.532 53.085zm-84.58-128.125h-45.91v64.814h38.669c29.888 0 46.373-12.03 46.373-33.535 0-20.151-14.174-31.279-39.132-31.279zm-45.91 90.53v71.431h47.605c31.12 0 47.605-12.482 47.605-35.941 0-23.46-16.947-35.49-49.608-35.49h-45.602z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="check2" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="circle-half" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="moon-stars-fill" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
|
||||||
|
<path
|
||||||
|
d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="sun-fill" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<link href="{% static 'main/css/navbar.css' %}" rel="stylesheet">
|
||||||
|
<script src="{% static 'main/js/color-modes.js' %}"></script>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg border-bottom">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
<img class="d-inline-block align-text-center me-2" src="{% static 'main/img/logo.svg' %}" width="40"
|
||||||
|
height="40" alt="GaliBus logo">
|
||||||
|
caFICtería-aaS
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
|
||||||
|
<div class="dropdown nav-item">
|
||||||
|
<a class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"
|
||||||
|
aria-haspopup="true">Menú
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item nav-link"
|
||||||
|
href="{% url 'daily_menu:show_menu' date='today' %}">Hoy</a></li>
|
||||||
|
<li><a class="dropdown-item nav-link"
|
||||||
|
href="{% url 'daily_menu:select_menu_form' %}">Histórico</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown nav-item">
|
||||||
|
<a class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"
|
||||||
|
aria-haspopup="true">Lázaro-aaS
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item nav-link" href="">Ver frases</a></li>
|
||||||
|
<li><a class="dropdown-item nav-link" href="">Sugerir frases</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Theme selector -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<button class="btn btn-link px-0 text-decoration-none dropdown-toggle d-flex align-items-center"
|
||||||
|
id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown"
|
||||||
|
data-bs-display="static">
|
||||||
|
<svg class="bi my-1 me-2 theme-icon-active">
|
||||||
|
<use href="#circle-half"></use>
|
||||||
|
</svg>
|
||||||
|
Toggle theme
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme"
|
||||||
|
style="--bs-dropdown-min-width: 8rem;">
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-theme-value="light">
|
||||||
|
<svg class="bi me-2 opacity-50 theme-icon">
|
||||||
|
<use href="#sun-fill"></use>
|
||||||
|
</svg>
|
||||||
|
Light
|
||||||
|
<svg class="bi ms-auto d-none">
|
||||||
|
<use href="#check2"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-theme-value="dark">
|
||||||
|
<svg class="bi me-2 opacity-50 theme-icon">
|
||||||
|
<use href="#moon-stars-fill"></use>
|
||||||
|
</svg>
|
||||||
|
Dark
|
||||||
|
<svg class="bi ms-auto d-none">
|
||||||
|
<use href="#check2"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center active"
|
||||||
|
data-bs-theme-value="auto">
|
||||||
|
<svg class="bi me-2 opacity-50 theme-icon">
|
||||||
|
<use href="#circle-half"></use>
|
||||||
|
</svg>
|
||||||
|
Auto
|
||||||
|
<svg class="bi ms-auto d-none">
|
||||||
|
<use href="#check2"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
111
web/main/templates/main/base.html
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="light">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content='maximum-scale=1.0, initial-scale=1.0, width=device-width' name='viewport'>
|
||||||
|
<meta name="description"
|
||||||
|
content="Página para consultar el menú del día de la fic, frases de nuestro querido camarero Lázaro, y otras funcionalidades">
|
||||||
|
<meta name="author" content="peprolinbot">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'main/img/favicon/apple-touch-icon.png' %}">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'main/img/favicon/favicon-32x32.png' %}">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'main/img/favicon/favicon-16x16.png' %}">
|
||||||
|
<link rel="manifest" href="{% static 'main/img/favicon/site.webmanifest' %}">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>{% block title %}caFICtería-aaS{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- CSS stuff
|
||||||
|
================================================== -->
|
||||||
|
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
{% bootstrap_css %}
|
||||||
|
|
||||||
|
<!-- Select2 -->
|
||||||
|
<link href="{% static 'main/css/libs/select2.min.css' %}" rel="stylesheet">
|
||||||
|
<link href="{% static 'main/css/libs/select2-bootstrap-5-theme.min.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link href="{% static 'main/css/base.css' %}" rel="stylesheet">
|
||||||
|
{% block css %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- JavaScript stuff
|
||||||
|
================================================== -->
|
||||||
|
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="{% static 'main/js/libs/jquery.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
{% bootstrap_javascript %}
|
||||||
|
|
||||||
|
<!-- Select2 -->
|
||||||
|
<script src="{% static 'main/js/libs/select2.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Popper -->
|
||||||
|
<script src="{% static 'main/js/libs/popper.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS -->
|
||||||
|
{% block js %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block navbar %}
|
||||||
|
{% include 'main/_items/navbar.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div id="content-wrapper" class="container-fluid">
|
||||||
|
<div id="bootstrapMessagesDiv" class="px-2 pt-3">{% bootstrap_messages %}</div>
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer text-center">
|
||||||
|
<hr class="mx-5">
|
||||||
|
<p>Hecho con ❤️ por <b>peprolinbot</b> - <a href="https://git.peprolinbot.com/peprolinbot/caFICteria-aaS"
|
||||||
|
target="_blank">Código fuente</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Enable popovers -->
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function ($) {
|
||||||
|
const popoverTriggerList = $('[data-bs-toggle="popover"]').toArray()
|
||||||
|
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
|
||||||
|
|
||||||
|
|
||||||
|
// For django alerts to not take space
|
||||||
|
function checkIfAlertsEmpty() {
|
||||||
|
if ($.trim($('#bootstrapMessagesDiv').text()) === '') { // Check if the div is empty
|
||||||
|
$('#bootstrapMessagesDiv').css('display', 'none'); // Set height to 0 if empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intial check
|
||||||
|
checkIfAlertsEmpty();
|
||||||
|
|
||||||
|
const alertsDivObserver = new MutationObserver(checkIfAlertsEmpty);
|
||||||
|
|
||||||
|
// Start observing the target node for configured mutations
|
||||||
|
alertsDivObserver.observe($('#bootstrapMessagesDiv')[0], {
|
||||||
|
childList: true, // Observe direct children
|
||||||
|
subtree: true // Observe all descendants
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide popovers when you open another one
|
||||||
|
$('[data-bs-toggle="popover"]').click(function () {
|
||||||
|
$('[data-bs-toggle="popover"]').not(this).popover('hide'); //all but this
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
14
web/main/templates/main/show_form.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'main/base.html' %}
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container col-md-6 mt-5">
|
||||||
|
<h1 class="text-center">{{ form_header }}</h1>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% bootstrap_button button_type="submit" content="Enviar" extra_classes="w-100 mt-3" %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
5
web/main/views.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
return redirect('daily_menu:show_menu', "today")
|
22
web/manage.py
Executable 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', 'caFICteria_aaS.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()
|
15
web/nginx.conf
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
alias /app/staticfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
3
web/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Django==5.1.3
|
||||||
|
django-bootstrap5==24.3
|
||||||
|
gunicorn==23.0.0
|
21
web/supervisord.conf
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
redirect_stderr=true
|
||||||
|
|
||||||
|
[program:gunicorn]
|
||||||
|
command=gunicorn --preload --bind 0.0.0.0:8000 caFICteria_aaS.wsgi
|
||||||
|
directory=/app
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
redirect_stderr=true
|