Portfolio Creator
Your professional information is usually scattered: code on GitHub, projects on your university's or company's GitLab, papers in scientific databases. Portfolio Creator brings it all together in one place, lets you choose what to show, and generates a CV ready to download as PDF or HTML, with AI-written summaries.
- HTML
- CSS
- Python
- Django
- Docker
- AWS
# ─── CV Profesional Unificado ──────────────────────────────
def _get_cv_data(user):
"""
Recupera el estado de la 'cesta' del CV.
Guarda los datos serializados como texto JSON en la tabla ContenidoData,
usando un recurso especial con nombre 'cv_profesional_username'.
"""
recurso_name = f"cv_profesional_{user.username}"
# Busca el registro o lo crea vacío si no existe
contenido_obj, created = ContenidoData.objects.get_or_create(
recurso=recurso_name,
defaults={'usuario': user.username, 'contenido': '{}'}
)
try:
# Deserializar JSON (texto a diccionario Python)
data = json.loads(contenido_obj.contenido)
except json.JSONDecodeError:
data = {}
if not isinstance(data, dict):
data = {}
# Estructura inicial obligatoria
if 'personal_info' not in data:
data['personal_info'] = {'photo': '', 'name': '', 'linkedin': '', 'phone': '', 'email': '', 'about': ''}
if 'items' not in data:
data['items'] = []
return contenido_obj, data
@login_required
def cv_builder(request):
"""Renderiza el panel de control del CV unificado (la vista de la 'cesta')"""
contenido_obj, data = _get_cv_data(request.user) # Lee la base de datos
if request.method == 'POST':
# El usuario ha enviado el formulario de "Información Personal"
import base64
if 'photo' in request.FILES: # Si ha subido una foto
try:
photo_file = request.FILES['photo']
# Convierte la foto a Base64: un string gigante que la incrusta directamente en el HTML
photo_b64 = base64.b64encode(photo_file.read()).decode('utf-8')
data['personal_info']['photo'] = f"data:{photo_file.content_type};base64,{photo_b64}"
except Exception:
pass
# Guarda los textos
data['personal_info']['name'] = request.POST.get('name', '').strip()
data['personal_info']['linkedin'] = request.POST.get('linkedin', '').strip()
data['personal_info']['phone'] = request.POST.get('phone', '').strip()
data['personal_info']['email'] = request.POST.get('email', '').strip()
data['personal_info']['about'] = request.POST.get('about', '').strip()
# Vuelve a convertir el diccionario a texto JSON y lo guarda en la BD
contenido_obj.contenido = json.dumps(data)
contenido_obj.save()
messages.success(request, 'Información del CV guardada correctamente.')
return redirect('cv_builder')
return render(request, 'portfolioCV/cv_builder.html', {
'cv_data': data
})
@login_required
def agregar_al_cv(request):
"""Agrega un repositorio u obra a la cesta del CV (se llama desde un formulario POST)"""
if request.method == 'POST':
# Se reciben los datos básicos del repositorio para añadirlos a la 'cesta'
platform = request.POST.get('platform')
item_id = request.POST.get('id')
name = request.POST.get('name')
if platform and item_id and name:
contenido_obj, data = _get_cv_data(request.user)
# Evitar duplicados (no añadir la misma repo dos veces)
if not any(str(item['id']) == str(item_id) and item['platform'] == platform for item in data['items']):
data['items'].append({ # Añadir al array de JSON
'platform': platform,
'id': item_id,
'name': name
})
contenido_obj.contenido = json.dumps(data) # Guardar
contenido_obj.save()
messages.success(request, f'"{name}" añadido a tu CV Profesional.')
else:
messages.info(request, f'"{name}" ya estaba en tu CV Profesional.')
# Vuelve a la página anterior (HTTP Referer) o al CV Builder si no hay
return redirect(request.META.get('HTTP_REFERER', 'cv_builder'))
return redirect('index')# ─── CV Profesional Unificado ──────────────────────────────
def _get_cv_data(user):
"""
Recupera el estado de la 'cesta' del CV.
Guarda los datos serializados como texto JSON en la tabla ContenidoData,
usando un recurso especial con nombre 'cv_profesional_username'.
"""
recurso_name = f"cv_profesional_{user.username}"
# Busca el registro o lo crea vacío si no existe
contenido_obj, created = ContenidoData.objects.get_or_create(
recurso=recurso_name,
defaults={'usuario': user.username, 'contenido': '{}'}
)
try:
# Deserializar JSON (texto a diccionario Python)
data = json.loads(contenido_obj.contenido)
except json.JSONDecodeError:
data = {}
if not isinstance(data, dict):
data = {}
# Estructura inicial obligatoria
if 'personal_info' not in data:
data['personal_info'] = {'photo': '', 'name': '', 'linkedin': '', 'phone': '', 'email': '', 'about': ''}
if 'items' not in data:
data['items'] = []
return contenido_obj, data
@login_required
def cv_builder(request):
"""Renderiza el panel de control del CV unificado (la vista de la 'cesta')"""
contenido_obj, data = _get_cv_data(request.user) # Lee la base de datos
if request.method == 'POST':
# El usuario ha enviado el formulario de "Información Personal"
import base64
if 'photo' in request.FILES: # Si ha subido una foto
try:
photo_file = request.FILES['photo']
# Convierte la foto a Base64: un string gigante que la incrusta directamente en el HTML
photo_b64 = base64.b64encode(photo_file.read()).decode('utf-8')
data['personal_info']['photo'] = f"data:{photo_file.content_type};base64,{photo_b64}"
except Exception:
pass
# Guarda los textos
data['personal_info']['name'] = request.POST.get('name', '').strip()
data['personal_info']['linkedin'] = request.POST.get('linkedin', '').strip()
data['personal_info']['phone'] = request.POST.get('phone', '').strip()
data['personal_info']['email'] = request.POST.get('email', '').strip()
data['personal_info']['about'] = request.POST.get('about', '').strip()
# Vuelve a convertir el diccionario a texto JSON y lo guarda en la BD
contenido_obj.contenido = json.dumps(data)
contenido_obj.save()
messages.success(request, 'Información del CV guardada correctamente.')
return redirect('cv_builder')
return render(request, 'portfolioCV/cv_builder.html', {
'cv_data': data
})
@login_required
def agregar_al_cv(request):
"""Agrega un repositorio u obra a la cesta del CV (se llama desde un formulario POST)"""
if request.method == 'POST':
# Se reciben los datos básicos del repositorio para añadirlos a la 'cesta'
platform = request.POST.get('platform')
item_id = request.POST.get('id')
name = request.POST.get('name')
if platform and item_id and name:
contenido_obj, data = _get_cv_data(request.user)
# Evitar duplicados (no añadir la misma repo dos veces)
if not any(str(item['id']) == str(item_id) and item['platform'] == platform for item in data['items']):
data['items'].append({ # Añadir al array de JSON
'platform': platform,
'id': item_id,
'name': name
})
contenido_obj.contenido = json.dumps(data) # Guardar
contenido_obj.save()
messages.success(request, f'"{name}" añadido a tu CV Profesional.')
else:
messages.info(request, f'"{name}" ya estaba en tu CV Profesional.')
# Vuelve a la página anterior (HTTP Referer) o al CV Builder si no hay
return redirect(request.META.get('HTTP_REFERER', 'cv_builder'))
return redirect('index')