Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/lffiesco-svg/gastromovil/llms.txt

Use this file to discover all available pages before exploring further.

GastroMóvil offloads all user-uploaded media to Cloudinary through the django-cloudinary-storage package. This means uploaded images never touch the server’s local filesystem in production — they are streamed directly to Cloudinary and served from Cloudinary’s CDN. Static assets (CSS, JavaScript, Tailwind output) follow a different path and are served locally by WhiteNoise, keeping the two concerns cleanly separated.

Cloudinary Configuration

Cloudinary is initialised in settings.py using credentials loaded from environment variables:
import cloudinary
import cloudinary.uploader
import cloudinary.api
from decouple import config

cloudinary.config(
    cloud_name=config('CLOUDINARY_CLOUD_NAME'),
    api_key=config('CLOUDINARY_API_KEY'),
    api_secret=config('CLOUDINARY_API_SECRET'),
)

DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
Setting DEFAULT_FILE_STORAGE to MediaCloudinaryStorage means every Django ImageField and FileField that calls .save() automatically uploads to Cloudinary without any per-field configuration. The local media paths are kept as a fallback for development tooling:
MEDIA_URL  = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
In production MEDIA_ROOT is never written to — all uploads go to Cloudinary. The path is retained so that local management commands that reference MEDIA_ROOT do not error out.

Required Environment Variables

VariableDescription
CLOUDINARY_CLOUD_NAMEYour cloud name, visible in the Cloudinary console URL
CLOUDINARY_API_KEYAPI key from Settings → Access Keys
CLOUDINARY_API_SECRETAPI secret — treat it like a database password
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=123456789012345
CLOUDINARY_API_SECRET=your-cloudinary-api-secret

Image Fields and Validators

Two models in restaurantes/models.py use CloudinaryField. Each has distinct upload requirements enforced by Django validators that run before the image is uploaded.

Restaurante.imagen — Strict PNG, 400 × 250 px

Restaurant banner images have two validators applied in sequence:
from cloudinary.models import CloudinaryField
from PIL import Image as PilImage
from django.core.exceptions import ValidationError

def validar_png(imagen):
    if not imagen.name.lower().endswith('.png'):
        raise ValidationError('Solo se permiten imágenes en formato PNG.')

def validar_dimensiones_restaurante(imagen):
    img = PilImage.open(imagen)
    ancho, alto = img.size
    if ancho != 400 or alto != 250:
        raise ValidationError(
            f'La imagen debe ser exactamente 400×250 px. '
            f'La tuya es {ancho}×{alto} px.'
        )

class Restaurante(models.Model):
    imagen = CloudinaryField(
        'image',
        blank=True,
        null=True,
        validators=[validar_png, validar_dimensiones_restaurante],
        help_text='PNG de exactamente 400×250 px con fondo transparente.'
    )
validar_png checks the filename extension. validar_dimensiones_restaurante opens the uploaded file with Pillow and verifies that both dimensions are exact — the image is rejected if it is even one pixel off.
Restaurant banners must be exactly 400 × 250 pixels and in PNG format. Uploading a JPEG or a differently-sized PNG will raise a ValidationError before the file reaches Cloudinary. Use a tool like GIMP, Figma, or ImageMagick to crop and export your banner to the correct dimensions.

Producto.imagen — Any format, no size restrictions

Product images have no format or dimension constraints:
class Producto(models.Model):
    imagen = CloudinaryField('image', blank=True, null=True)
Any image format that Pillow and Cloudinary accept (JPEG, PNG, WebP, etc.) can be uploaded. Both fields are blank=True, null=True, so a restaurant or product can be created without an image.

Uploading Images via the Restaurant Panel

Restaurant owners manage their images through the web-based restaurant panel. To update a restaurant banner:
1

Log in as a restaurant owner

Navigate to /usuarios/login and authenticate with a user account whose rol is restaurante.
2

Open the restaurant panel

After login you are redirected to panel_restaurante. Click Editar next to the restaurant whose banner you want to change.
3

Upload the banner

In the edit form, select a PNG file that is exactly 400 × 250 px. The form will reject the upload and display a validation error if either requirement is not met.
4

Save

Submit the form. Django runs both validators, then django-cloudinary-storage streams the file to Cloudinary and stores the returned public ID in the Restaurante.imagen column.
To generate a correctly-sized banner quickly with ImageMagick:
convert input.jpg -resize 400x250^ -gravity Center -extent 400x250 banner.png
The ^ flag fills the canvas before cropping so the output is always exactly 400 × 250 px.

Static Files (WhiteNoise)

Static assets are handled entirely separately from media uploads. WhiteNoise serves pre-collected static files directly from the Django process with aggressive caching headers — no CDN or object-storage bucket is needed for CSS and JS.
STATICFILES_STORAGE = 'whitenoise.storage.StaticFilesStorage'

STATIC_URL  = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles_prod')

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
    os.path.join(BASE_DIR, 'theme/static'),
]
WhiteNoise is registered as the second middleware entry (after CorsMiddleware) so that static files are served without hitting the Django view layer:
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    ...
]

Building and Collecting Static Files

The Railway build command (from railway.toml) runs both steps automatically on every deploy:
python manage.py tailwind build && python manage.py collectstatic --noinput
tailwind build compiles the Tailwind CSS output into theme/static/ using the app defined by TAILWIND_APP_NAME = 'theme'. collectstatic --noinput then gathers all static files from STATICFILES_DIRS and every installed app into staticfiles_prod/. For local development, run the two commands manually whenever you change CSS:
python manage.py tailwind build
python manage.py collectstatic --noinput
Tailwind’s JIT watcher (python manage.py tailwind start) is also available during development and will rebuild CSS on file changes automatically, so you only need to run collectstatic once before the first local server start.

Build docs developers (and LLMs) love