Initial commit

This commit is contained in:
Pedro Rey Anca 2024-12-02 18:19:54 +01:00
commit 8f112c1bb5
Signed by: peprolinbot
GPG key ID: 053EA6E00116533A
69 changed files with 2068 additions and 0 deletions

266
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View 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
View file

@ -0,0 +1 @@
Telethon==1.38.1

97
bot/utils.py Normal file
View 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
View 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
View 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
View 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
View 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 .
```

View file

View 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()

View 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")

View 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"),
]

View 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()

View file

6
web/daily_menu/apps.py Normal file
View 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
View 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

View file

View 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
View file

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

11
web/daily_menu/urls.py Normal file
View 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
View 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
View 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)

View file

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
web/main/__init__.py Normal file
View file

6
web/main/apps.py Normal file
View file

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

View file

View 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%;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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"}

View 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

View 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)
})
})
})
})()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 %}

View 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 %}

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

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

View 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
View 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
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '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
View 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
View file

@ -0,0 +1,3 @@
Django==5.1.3
django-bootstrap5==24.3
gunicorn==23.0.0

21
web/supervisord.conf Normal file
View 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