Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions backend/users/migrations/0002_userpreferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.1.15 on 2025-12-18 18:22

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='UserPreferences',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language', models.CharField(choices=[('es', 'Español'), ('en', 'English'), ('fr', 'Français'), ('de', 'Deutsch')], default='es', max_length=5)),
('theme', models.CharField(choices=[('light', 'Claro'), ('dark', 'Oscuro'), ('auto', 'Automatico')], default='light', max_length=10)),
('favorite_station', models.IntegerField(blank=True, help_text='ID de la estacion meteorologica favorita', null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='preferences', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Preferencia de Usuario',
'verbose_name_plural': 'Preferencias de Usuario',
},
),
]
50 changes: 49 additions & 1 deletion backend/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,52 @@ def invalidate_user_tokens(cls, user):
"""
Invalida todos los tokens activos de un usuario
"""
cls.objects.filter(user=user, is_used=False).update(is_used=True)
cls.objects.filter(user=user, is_used=False).update(is_used=True)


class UserPreferences(models.Model):
"""
modelo para almacenar preferencias del usuario (idioma, tema, estacion favorita).
"""
LANGUAGE_CHOICES = [
('es', 'Español'),
('en', 'English'),
('fr', 'Français'),
('de', 'Deutsch'),
]

THEME_CHOICES = [
('light', 'Claro'),
('dark', 'Oscuro'),
('auto', 'Automatico'),
]

user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="preferences"
)
language = models.CharField(
max_length=5,
choices=LANGUAGE_CHOICES,
default='es'
)
theme = models.CharField(
max_length=10,
choices=THEME_CHOICES,
default='light'
)
favorite_station = models.IntegerField(
null=True,
blank=True,
help_text="ID de la estacion meteorologica favorita"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
verbose_name = "Preferencia de Usuario"
verbose_name_plural = "Preferencias de Usuario"

def __str__(self):
return f"Preferencias de {self.user.username}"
52 changes: 50 additions & 2 deletions backend/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from .models import PasswordResetToken
from .models import PasswordResetToken, UserPreferences
from .errors import PasswordResetError
from django.utils import timezone

Expand Down Expand Up @@ -395,4 +395,52 @@ def save(self):
PasswordResetToken.invalidate_user_tokens(user)

return user



class UserPreferencesSerializer(serializers.Serializer):
"""
serializer para las preferencias del usuario (idioma, tema, estacion favorita).
"""
language = serializers.ChoiceField(
choices=['es', 'en', 'fr', 'de'],
required=False,
default='es'
)
theme = serializers.ChoiceField(
choices=['light', 'dark', 'auto'],
required=False,
default='light'
)
favorite_station = serializers.IntegerField(
required=False,
allow_null=True
)

def validate_favorite_station(self, value):
"""
valida que la estacion favorita sea valida si se proporciona.
"""
if value is not None and value <= 0:
raise serializers.ValidationError("El ID de la estación debe ser positivo.")
return value

def update(self, instance, validated_data):
"""
actualiza las preferencias del usuario.
"""
instance.language = validated_data.get('language', instance.language)
instance.theme = validated_data.get('theme', instance.theme)
instance.favorite_station = validated_data.get('favorite_station', instance.favorite_station)
instance.save()
return instance

def to_representation(self, instance):
"""
serializa la instancia de preferencias para la respuesta.
"""
return {
'language': instance.language,
'theme': instance.theme,
'favorite_station': instance.favorite_station,
}

6 changes: 4 additions & 2 deletions backend/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from .views import (
RegisterView, MeView, ProfileView,
AdminOnlyView, SuperuserOnlyView, PublicView,
LoginView, ChangePasswordView, PasswordResetRequestView
LoginView, ChangePasswordView, PasswordResetRequestView,
UserPreferencesView
)

urlpatterns = [
Expand All @@ -14,6 +15,7 @@
path("superuser-only/", SuperuserOnlyView.as_view(), name="superuser-only"),
path("public/", PublicView.as_view(), name="public"),
path("change-password/", ChangePasswordView.as_view(), name="change-password"),
path("password-reset/request/", PasswordResetRequestView.as_view(), name="password-reset")
path("password-reset/request/", PasswordResetRequestView.as_view(), name="password-reset"),
path("preferences/", UserPreferencesView.as_view(), name="preferences"),
]

55 changes: 53 additions & 2 deletions backend/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
LoginSerializer,
ChangePasswordSerializer,
PasswordResetRequestSerializer,
PasswordResetConfirmSerializer
PasswordResetConfirmSerializer,
UserPreferencesSerializer
)
from .models import UserPreferences
from .permissions import IsSuperUser

# Solo importar FWT si está disponible
Expand Down Expand Up @@ -288,4 +290,53 @@ def post(self, request):
'success': False,
'error': 'Datos inválidos',
'detail': serializer.errors
}, status=status.HTTP_400_BAD_REQUEST)
}, status=status.HTTP_400_BAD_REQUEST)


class UserPreferencesView(APIView):
"""
vista para obtener y actualizar las preferencias del usuario autenticado.
GET: recupera las preferencias
PUT: actualiza las preferencias
"""
permission_classes = [permissions.IsAuthenticated]

def get(self, request):
"""
recupera las preferencias del usuario.
"""
try:
preferences = UserPreferences.objects.get(user=request.user)
except UserPreferences.DoesNotExist:
# crear preferencias por defecto si no existen
preferences = UserPreferences.objects.create(user=request.user)

serializer = UserPreferencesSerializer(preferences)
return Response(serializer.data, status=status.HTTP_200_OK)

def put(self, request):
"""
actualiza las preferencias del usuario.
"""
try:
preferences = UserPreferences.objects.get(user=request.user)
except UserPreferences.DoesNotExist:
preferences = UserPreferences.objects.create(user=request.user)

serializer = UserPreferencesSerializer(
preferences,
data=request.data,
partial=True
)

if serializer.is_valid():
serializer.save()
return Response(
serializer.data,
status=status.HTTP_200_OK
)

return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
16 changes: 11 additions & 5 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import "./styles/styles.css";
import "./components/features/auth/auth.css";
import "./components/ui/HamburgerMenu/HamburgerMenu.css";
import "./components/features/theme/ClaroOscuro.css";
import "./components/features/settings/settings.css";

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ThemeProvider } from "./context/ThemeContext";
import { PreferencesProvider } from "./context/PreferencesContext";

import Navbar from "./components/layout/Navbar";
import Footer from "./components/layout/Footer";
Expand All @@ -21,13 +23,15 @@ import WeatherHistoryPage from "./pages/WeatherHistoryPage";
import ForecastPage from "./pages/ForecastPage";
import ForecastExtendedPage from "./pages/ForecastExtendedPage";
import PasswordResetPage from "./pages/PasswordResetPage";
import SettingsPage from "./pages/SettingsPage";

function App() {
return (
<ThemeProvider>
<BrowserRouter>
<div className="app">
<Navbar />
<PreferencesProvider>
<BrowserRouter>
<div className="app">
<Navbar />

<Routes>
<Route path="/" element={<DashboardPage />} />
Expand All @@ -41,11 +45,13 @@ function App() {
<Route path="/forecast-extended" element={<ForecastExtendedPage />} />
<Route path="/password-reset" element={<PasswordResetPage />} />
<Route path="/password-reset/:token" element={<PasswordResetPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>

<Footer />
</div>
</BrowserRouter>
</div>
</BrowserRouter>
</PreferencesProvider>
</ThemeProvider>
);
}
Expand Down
Loading