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
|