Upload o carga de archivos en Django

Video thumbnail

Subir archivos en Django puede parecer complicado la primera vez, pero en realidad no lo es… si sabes qué pasos seguir y qué errores evitar.

Vamos a aprender a cargar un archivo empleando Django, específicamente el avatar pero, los pasos señalados e implementados aquí los puedes emplear y adaptar para la carga de cualquier tipo de archivo como PDFs.

El proceso de carga de archivos no es un proceso difícil, pero sí consta de varios pasos que debemos de tener en cuenta; veremos paso por paso cada uno de estos pasos para que la implementación sea lo más entendible posible para el lector.

⚙️ Introducción: subir archivos en Django no es complicado (si sabes esto antes)

Antes de comenzar, vale aclarar algo: Django ya está preparado para manejar archivos, solo hay que configurarlo bien.

Este tutorial te servirá tanto para subir una imagen de perfil como para cargar documentos en PDF u otros formatos.

Cuando probé esto por primera vez, me topé con errores tontos —como el clásico “Pillow is not installed”— pero te mostraré cómo resolverlos rápido.

Configuración inicial: prepara tu entorno Django

Instalar dependencias y definir rutas

Primero, instala Pillow, la librería que Django usa para manejar imágenes:

$ pip install Pillow

En tu archivo settings.py, define las rutas donde se guardarán los archivos:

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

Crea la carpeta /media en el directorio raíz del proyecto y asegúrate de tener permisos de escritura.

Ovidé crearla la primera vez y Django no generó el directorio automáticamente.

Error típico: Pillow is not installed

Si ves este error:

Cannot use ImageField because Pillow is not installed.

simplemente instala la dependencia y vuelve a ejecutar:

$ python manage.py makemigrations
$ python manage.py migrate

Creación del modelo: dónde guardar el archivo del upload

Para adaptar el modelo de usuarios para que tenga el campo del avatar, pudiéramos modificar el modelo actual, pero, para evitar modificar un modelo de sistema, lo más recomendado sería crear una relación aparte de tipo uno a uno que contenga los campos adicionales que en este ejemplo, es el del avatar.

Para este ejemplo, usaremos un modelo que extienda al usuario mediante una relación OneToOneField.

Así evitamos modificar el modelo base del sistema, algo que siempre recomiendo:

account\models.py

# Create your models here.
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to='user/avatar')

✨ Opté por crear un modelo separado para modificar la entidad de User de Django; es más limpio y seguro a largo plazo.

Creamos y ejecutamos las migraciones:

$ python manage.py makemigrations
$ python manage.py migrate

Ahora creamos un ModelForm de Django que se encargue de manejar tanto los datos normales (request.POST) como los archivos (request.FILES):

account\forms.py

from django import forms
from .models import UserProfile

class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ('avatar', 'user')
        widgets = {'user': forms.HiddenInput()}

El manejo del campo de usuario aplicaremos varios campos en los siguientes apartados ya que por defecto es de tipo selección obligatoria comportamiento que no queremos.

Cargar un archivo, implementación mínima

Para evitar un error del formulario en el cual indique el el usuario es requerido, establecemos en el modelo que dicho campo puede ser nulo:

account\models.py

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True)

También podríamos modificar el formulario como mostramos anteriormente; el siguiente punto a tratar, es colocar el campo de tipo oculto:

account\forms.py

class UserProfileForm(forms.ModelForm):
    class Meta:
        ***
        widgets = {'user': forms.HiddenInput() }

Vista y lógica para procesar la carga

Para manejar el formulario, la implementación queda similares a los anteriores casos:

account\views.py

@login_required
def profile(request):
    form = UserProfileForm()

    if request.method == "POST":
        form = UserProfileForm(request.POST, request.FILES)
        if form.is_valid():
            userprofile = form.save(commit=False)
            userprofile.user = request.user
            userprofile.save()

    return render(request, 'profile.html', {'form':form})

Como consideraciones importantes tenemos:

UserProfileForm(request.POST, request.FILES)

Para establecer los archivos enviados por el usuario, que en este ejemplo es solamente uno, también se conserva la petición con datos planos:

request.POST

Ya que en el formulario anterior pueden haber otros campos de tipo textos o similares.

En cuanto al:

commit=False

Se emplea para solamente generar la instancia del modelo y NO guardar en la base de datos, ya que, antes de guardar a la base de dato se debe de establecer el usuario autenticado:

userprofile = form.save(commit=False)
userprofile.user = request.user
userprofile.save()

Definimos el template con el formulario y las clases de Bootstrap y mostrar los errores:

Template HTML para mostrar el formulario

account\templates\profile.html

{% extends "base_user_account.html" %}

{% load widget_tweaks %}

{% block content %}

{% if form.errors %}
    <div class="alert alert-warning">
        {{ form.errors }}
    </div>
{% endif %}

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <div class="mt-3">
        {{ form.avatar|add_label_class:"form-label" }}
        {{ form.avatar|add_class:"form-control" }}
    </div>

    {{ form.user }}
    <button type="submit" class="mt-3 btn btn-primary">Send</button>
</form>

{% endblock %}

Recuerda el atributo enctype="multipart/form-data"; sin él, los archivos no se envían correctamente.

Con el proceso anterior, se pueden cargar archivos en el sistema, veremos que se genera una carpeta con el archivo cargado en:

user\avatar

Que es la ruta definida en el campo de tipo imagen:

models.ImageField(upload_to='user/avatar')

Y el registro correspondiente en la base de datos; el problema que tenemos actualmente es que, al ser una relación de tipo uno a uno, solamente se puede procesar los datos una sola vez cuando no existe el registro del perfil del usuario en la base de datos, pero, esto lo solventamos en el siguiente apartado.

Otra forma de modificar los atributos del formulario, es directamente desde la clase:

class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ('avatar','user')
        
    def __init__(self,*args,**kwargs):
        super(UserProfileForm, self).__init__(*args,**kwargs)
        self.fields['user'].widget = forms.HiddenInput()
        self.fields['user'].required = False
        self.fields['avatar'].widget.attrs['class'] = "custom-file-input"
        self.fields['avatar'].widget.attrs['id'] = "customFile"
      ***

Verificar que la carga funciona

Tras enviar el formulario:

  • El archivo se guarda dentro de /media/user/avatar/.
  • En la base de datos se crea un registro UserProfile.
  • Puedes comprobarlo desde el Django admin o explorando la carpeta media.

Abre directamente la carpeta media/user/avatar y confirmé que la imagen se había copiado correctamente.

Errores comunes al subir archivos en Django

Error    Causa    Solución
request.FILES vacío    Falta enctype="multipart/form-data"    Añadir al formulario
“MEDIA_ROOT not defined”    Configuración faltante    Definir en settings.py
“Pillow is not installed”    Falta librería    pip install Pillow
Archivo no visible    Ruta mal definida    Verificar MEDIA_URL y MEDIA_ROOT

Consejos y buenas prácticas

Mantén rutas limpias (upload_to='user/avatar' es claro y ordenado).

Controla el tamaño máximo de los archivos.

Usa validaciones personalizadas en el formulario.

Si subes imágenes, optimízalas con Pillow antes de guardarlas.

Protege las rutas de media en entornos productivos (Nginx/Apache).

❓ Preguntas frecuentes (FAQs)

¿Dónde se guardan los archivos subidos en Django?
En la carpeta definida por MEDIA_ROOT dentro del proyecto.

¿Cómo permitir múltiples archivos?
Usa un FileField con multiple=True y procesa request.FILES.getlist().

¿Qué pasa si el usuario ya tiene un archivo anterior?
Puedes sobreescribirlo o eliminar el viejo antes de guardar el nuevo.

Conclusión

Subir archivos en Django es mucho más fácil cuando entiendes la lógica detrás de request.FILES, el formulario y la ruta de almacenamiento.

En mi experiencia, los errores más comunes vienen de pequeñas omisiones (como olvidar el enctype o no instalar Pillow).
Siguiendo esta guía, tendrás una implementación funcional y profesional lista para producción.

El siguiente paso es emplear la vista de detalle.

Acepto recibir anuncios de interes sobre este Blog.

Veremos como realizar la carga de archivos, especificamente del avatar empleando Django 5 y componentes de Bootstrap 5.

| 👤 Andrés Cruz

🇺🇸 In english