diff --git a/.github/workflows/quality-pipeline.yml b/.github/workflows/quality-pipeline.yml index 5aaadf7..a9826c9 100644 --- a/.github/workflows/quality-pipeline.yml +++ b/.github/workflows/quality-pipeline.yml @@ -645,7 +645,7 @@ jobs: FAIL=0 if find . \( -name "*.js" -o -name "*.ts" -o -name "*.vue" -o -name "*.py" -o -name "*.java" \) \ - -not -path "*/node_modules/*" -not -path "*/dist/*" 2>/dev/null | grep -q .; then + -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/tests/*" -not -path "*/.venv/*" 2>/dev/null | grep -q .; then DETECTED=1 else DETECTED=0 @@ -654,7 +654,7 @@ jobs: # Unconditional frontend router guard bypass if grep -rEn "next\(true\)|beforeEach.*return true|navigate\(.*\{.*replace.*\}\)" \ --include="*.js" --include="*.ts" --include="*.vue" . 2>/dev/null \ - | grep -v node_modules | grep -v "//"; then + | grep -vE "node_modules|tests/|\.venv|dist/" | grep -v "//"; then echo "::warning::Possible unconditional navigation guard bypass detected" fi @@ -662,14 +662,14 @@ jobs: if grep -rEn "password\s*[=:]\s*['\"][^'\"]{4,}['\"]" \ --include="*.js" --include="*.ts" --include="*.vue" --include="*.py" --include="*.java" \ . 2>/dev/null \ - | grep -v node_modules \ + | grep -vE "node_modules|tests/|\.venv|dist/" \ | grep -v "//" \ | grep -vE "[[:space:]]:[a-zA-Z][a-zA-Z0-9_-]*=" \ | grep -q .; then grep -rEn "password\s*[=:]\s*['\"][^'\"]{4,}['\"]" \ --include="*.js" --include="*.ts" --include="*.vue" --include="*.py" --include="*.java" \ . 2>/dev/null \ - | grep -v node_modules | grep -v "//" \ + | grep -vE "node_modules|tests/|\.venv|dist/" | grep -v "//" \ | grep -vE "[[:space:]]:[a-zA-Z][a-zA-Z0-9_-]*=" echo "::error::Hardcoded password value detected in source code" FAIL=1 @@ -678,14 +678,14 @@ jobs: # eval() / exec() usage if grep -rEn "\beval\(|\bexec\(" \ --include="*.js" --include="*.ts" --include="*.vue" --include="*.py" \ - . 2>/dev/null | grep -v node_modules | grep -v "//"; then + . 2>/dev/null | grep -vE "node_modules|tests/|\.venv|dist/" | grep -v "//"; then echo "::warning::eval()/exec() usage detected — review for potential code injection risk" fi # Disabled SSL/TLS certificate verification if grep -rEn "rejectUnauthorized\s*:\s*false|verify\s*=\s*False|ssl_verify\s*=\s*false" \ --include="*.js" --include="*.ts" --include="*.py" --include="*.java" \ - . 2>/dev/null | grep -v node_modules | grep -v "//"; then + . 2>/dev/null | grep -vE "node_modules|tests/|\.venv|dist/" | grep -v "//"; then echo "::error::SSL/TLS certificate verification disabled — man-in-the-middle attack risk" FAIL=1 fi @@ -693,7 +693,7 @@ jobs: # Sensitive tokens in URL query strings if grep -rEn "[?&](token|api_key|apikey|access_token|secret)=" \ --include="*.js" --include="*.ts" --include="*.vue" --include="*.py" \ - . 2>/dev/null | grep -v node_modules | grep -v "//"; then + . 2>/dev/null | grep -vE "node_modules|tests/|\.venv|dist/" | grep -v "//"; then echo "::warning::Sensitive token/key passed in URL query string — use Authorization header instead." fi diff --git a/Templates/Details/details_base.html b/Templates/Details/details_base.html index 3412911..8eefb20 100644 --- a/Templates/Details/details_base.html +++ b/Templates/Details/details_base.html @@ -48,7 +48,6 @@

{{ content.title }}

data-contingut-id="{{ content.contingut.id }}" data-api-id="{{ api.id }}"> {{ api.name|default:api.port }} - {% empty %} {% trans "No hay información de API disponible." %} @@ -137,15 +136,35 @@

{% trans "Deja tu opinión" %}

✅ {% trans "Añadido a tu historial" %}
{% endblock %} \ No newline at end of file diff --git a/Templates/Details/details_serie.html b/Templates/Details/details_serie.html index 9eaa8e6..e6fc94a 100644 --- a/Templates/Details/details_serie.html +++ b/Templates/Details/details_serie.html @@ -17,4 +17,27 @@ {% endif %} {% endblock %} -{% block favorite_url %}{% url 'toggle_series_favorite' content.id %}{% endblock %} \ No newline at end of file +{% block favorite_url %}{% url 'toggle_series_favorite' content.id %}{% endblock %} + +{% block extra_main_content %} +
+

{% trans "También te puede gustar..." %}

+
+ {% for rec in recommendations %} + + {% if rec.poster_path %} + {{ rec.title }} + {% else %} +
+ {{ rec.title }} +
+ {% endif %} +

{{ rec.title }}

+
+ {% empty %} + +

{% trans "No hay recomendaciones disponibles." %}

+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/Templates/base.html b/Templates/base.html index 9f42415..ededc38 100644 --- a/Templates/base.html +++ b/Templates/base.html @@ -125,7 +125,7 @@ {% endif %} - + @@ -174,7 +175,11 @@
{% trans "Legal" %}
{% trans "Contact" %}
-

info@StreamSync.com

+

+ + info@StreamSync.com + +

+34 600 000 000

diff --git a/Templates/paginator.html b/Templates/paginator.html new file mode 100644 index 0000000..3cb8853 --- /dev/null +++ b/Templates/paginator.html @@ -0,0 +1,44 @@ +{% load i18n %} + + + + \ No newline at end of file diff --git a/users/middleware.py b/users/middleware.py index 7109573..3ee5712 100644 --- a/users/middleware.py +++ b/users/middleware.py @@ -22,6 +22,7 @@ def __call__(self, request): 'update_user_role', 'gestion_cartelleres', 'editar_cartellera', + 'system_logs', ] if current_url_name not in exempt_url_names and not request.path.startswith('/static/'): diff --git a/users/static/users/css/history.css b/users/static/users/css/history.css index 34dc29c..a556053 100644 --- a/users/static/users/css/history.css +++ b/users/static/users/css/history.css @@ -1,72 +1,115 @@ -/* ---- Movie cards ---- */ -.movie-card { - background: rgba(31, 40, 51, 0.6); +/* ---- Filtros y Selects ---- */ +.filter-select { + background: rgba(0, 0, 0, 0.4); + color: var(--text-primary); border: 1px solid var(--border-glass); - border-radius: 8px; - overflow: hidden; - transition: transform 0.25s, border-color 0.25s, box-shadow 0.25s; - height: 100%; + border-radius: 6px; + padding: 6px 12px; + font-size: 0.85rem; + outline: none; + transition: all 0.3s ease; } -.movie-card:hover { - transform: translateY(-5px); - border-color: rgba(102, 252, 241, 0.25); - box-shadow: 0 8px 24px rgba(102, 252, 241, 0.08); +.filter-select:focus { + border-color: var(--neon-cyan); + box-shadow: 0 0 8px rgba(102, 252, 241, 0.2); } -.movie-card-body { - padding: 1rem 1.1rem; +.filter-select option { + background: var(--bg-deep); + color: var(--text-primary); } -.movie-title { +/* ---- Agrupación por Fechas ---- */ +.history-group-title { color: var(--text-primary); - font-size: 14px; - font-weight: 600; - margin-bottom: 4px; font-family: 'Syne', sans-serif; - letter-spacing: -0.01em; + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-glass); } -.movie-year { - color: var(--text-muted); - font-size: 11px; - margin-bottom: 4px; +/* ---- Tarjeta Horizontal (Lista de Historial) ---- */ +.history-item-card { + transition: transform 0.2s, border-color 0.2s; + border: 1px solid transparent; + background: rgba(31, 40, 51, 0.4); } -.movie-genre { - color: var(--neon-cyan); - font-size: 11px; - margin-bottom: 8px; - opacity: 0.8; +.history-item-card:hover { + transform: translateX(5px); + border-color: rgba(102, 252, 241, 0.2); + background: rgba(31, 40, 51, 0.6); +} + +/* Miniatura del Póster */ +.history-poster-container { + width: 60px; + height: 90px; + flex-shrink: 0; + border-radius: 4px; + overflow: hidden; + background: rgba(0,0,0,0.5); } -.movie-rating { - color: #f5c518; - font-size: 12px; - margin-bottom: 8px; +.history-poster { + width: 100%; + height: 100%; + object-fit: cover; } -.movie-synopsis { +.history-poster-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; color: var(--text-muted); - font-size: 12px; - line-height: 1.5; + font-size: 1.5rem; + font-weight: bold; +} + +/* Información y Badges */ +.history-title { + color: var(--text-primary); + font-size: 1rem; + font-weight: 600; margin: 0; } -/* ---- Empty state ---- */ +.type-badge { + font-size: 0.7rem; + padding: 2px 8px; + border-radius: 12px; +} + +.platform-badge { + font-size: 0.75rem; + color: var(--text-muted); + background: rgba(255,255,255,0.05); + padding: 3px 8px; + border-radius: 4px; + border: 1px solid rgba(255,255,255,0.1); +} + +.history-time { + font-size: 0.8rem; + opacity: 0.8; +} + +/* ---- Estado Vacío (Empty state) ---- */ .history-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; - min-height: 200px; - color: var(--text-muted); - font-size: 14px; - gap: 8px; + text-align: center; } .history-empty-icon { - font-size: 32px; - opacity: 0.3; - margin-bottom: 8px; + font-size: 48px; + margin-bottom: 1rem; + opacity: 0.5; } \ No newline at end of file diff --git a/users/static/users/css/user_settings.css b/users/static/users/css/user_settings.css index 73a61db..a2d7009 100644 --- a/users/static/users/css/user_settings.css +++ b/users/static/users/css/user_settings.css @@ -1,9 +1,16 @@ -¡/* ---- Tabs ---- */ +/* ---- Tabs wrapper ---- */ +.nav-tabs-wrapper { + border: 1px solid var(--border-glass); + border-bottom: none; + border-radius: 8px 8px 0 0; +} + +/* ---- Tabs ---- */ .nav-tabs { - border-bottom: 1px solid var(--border-glass) !important; + border: none !important; background: var(--bg-glass); border-radius: 8px 8px 0 0; - padding: 0 0.5rem; + padding: 0; margin-bottom: 0 !important; } @@ -12,23 +19,30 @@ border: none !important; border-bottom: 2px solid transparent !important; border-radius: 0 !important; + margin-bottom: 0 !important; padding: 14px 18px; font-size: 13px; font-weight: 500; - transition: color 0.2s, border-color 0.2s; + transition: color 0.2s, background 0.2s; background: transparent !important; font-family: 'DM Sans', sans-serif; letter-spacing: 0.02em; } .nav-tabs .nav-link:hover { - color: var(--neon-cyan) !important; + color: var(--text-primary) !important; } .nav-tabs .nav-link.active { - color: var(--neon-cyan) !important; - border-bottom: 2px solid var(--neon-cyan) !important; - text-shadow: 0 0 10px rgba(102, 252, 241, 0.4); + color: #0B0C10 !important; + background: #E0E6ED !important; + border-bottom: 2px solid transparent !important; + border-radius: 6px 6px 0 0 !important; + text-shadow: none; +} + +.nav-item:first-child .nav-link.active { + border-radius: 8px 6px 0 0 !important; } /* ---- Tab content area ---- */ diff --git a/users/templates/users/parts/dashboard_age_ratings.html b/users/templates/users/parts/dashboard_age_ratings.html index 85c41eb..c65a0d1 100644 --- a/users/templates/users/parts/dashboard_age_ratings.html +++ b/users/templates/users/parts/dashboard_age_ratings.html @@ -7,7 +7,7 @@
-
Age Rating Distribution
+
Views by Age Rating
@@ -27,7 +27,7 @@
Age Rating Table
Rating - Views + Visualizations @@ -54,7 +54,7 @@
Age Rating Table
data: { labels: {{ age_stats.labels|safe }}, datasets: [{ - label: 'Views', + label: 'Visualizations', data: {{ age_stats.values|safe }}, backgroundColor: '#3498db', borderRadius: 5 diff --git a/users/templates/users/parts/dashboard_directors.html b/users/templates/users/parts/dashboard_directors.html index ab4abc3..9c62a15 100644 --- a/users/templates/users/parts/dashboard_directors.html +++ b/users/templates/users/parts/dashboard_directors.html @@ -7,7 +7,7 @@
-
Top Directors
+
Top Directors by Views
@@ -27,7 +27,7 @@
Top Directors Table
Director - Views + Visualizations @@ -54,7 +54,7 @@
Top Directors Table
data: { labels: {{ director_stats.labels|safe }}, datasets: [{ - label: 'Views', + label: 'Visualizations', data: {{ director_stats.values|safe }}, backgroundColor: '#2c3e50', borderRadius: 5 diff --git a/users/templates/users/parts/dashboard_genres.html b/users/templates/users/parts/dashboard_genres.html index ab8fc21..d494d15 100644 --- a/users/templates/users/parts/dashboard_genres.html +++ b/users/templates/users/parts/dashboard_genres.html @@ -7,7 +7,7 @@
-
Genre Distribution
+
Views by Genre
@@ -27,7 +27,7 @@
Top Genres Data
Genre - Views + Visualizations diff --git a/users/templates/users/parts/dashboard_overview.html b/users/templates/users/parts/dashboard_overview.html index f66f0f2..5148afd 100644 --- a/users/templates/users/parts/dashboard_overview.html +++ b/users/templates/users/parts/dashboard_overview.html @@ -5,7 +5,7 @@ {% include 'users/parts/filter_form.html' %}
-
Global Content Analytics Summary
+
Views per Content
Export CSV @@ -22,7 +22,7 @@
Global Content Analytics Summary
Year Director Platform - Views + Visualizations Favorites diff --git a/users/templates/users/parts/history.html b/users/templates/users/parts/history.html index 0ec0b64..7559471 100644 --- a/users/templates/users/parts/history.html +++ b/users/templates/users/parts/history.html @@ -1,20 +1,91 @@ {% extends 'users/types/user_client.html' %} {% load i18n %} {% load static %} + {% block extra_css %} {{ block.super }} {% endblock %} {% block parts_content %} -
- {% for movie in movies %} - {% include 'part/cardlite.html' with movie=movie %} - {% empty %} -
-
-

{% trans "No movies in history yet." %}

+
+ - {% endfor %} + + {% if grouped_history %} + {% for group_name, items in grouped_history.items %} +
+

{{ group_name }}

+
+ {% for vis in items %} +
+
+ {% if vis.contingut.poster_path %} + {{ vis.contingut.titol }} + {% else %} +
{{ vis.contingut.titol|first|upper }}
+ {% endif %} +
+ +
+
{{ vis.contingut.titol }}
+
+ {{ vis.content_type }} + + {{ vis.api.name|default:vis.api.port }} + + + {{ vis.data_visualitzacio|date:"d M Y, H:i" }} + +
+
+
+ {% endfor %} +
+
+ {% endfor %} + + {% if page_obj.paginator.num_pages > 1 %} +
+ {% include 'paginator.html' with items=page_obj %} +
+ {% endif %} + {% else %} +
+
📺
+

{% trans "No s'han trobat visualitzacions amb aquests criteris." %}

+ {% if selected_platform or selected_date %} + {% trans "Tornar a veure-ho tot" %} + {% endif %} +
+ {% endif %}
{% endblock %} \ No newline at end of file diff --git a/users/templates/users/parts/system_logs.html b/users/templates/users/parts/system_logs.html new file mode 100644 index 0000000..e7e2100 --- /dev/null +++ b/users/templates/users/parts/system_logs.html @@ -0,0 +1,78 @@ +{% extends 'users/types/staff_admin.html' %} +{% load i18n %} + +{% block staff_admin_content %} +
+
+
+

{% trans "Logs de Sincronización" %}

+

{% trans "Registro histórico del Scheduler con las APIs externas." %}

+
+
+ +
+
+ + + + + + + + + + + {% for log in logs %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Fecha de Inicio" %}{% trans "Fecha Fin" %}{% trans "Estado" %}{% trans "Resumen" %}
+ {{ log.start_time|date:"d M Y, H:i:s" }} + + {% if log.end_time %} + {{ log.end_time|date:"d M Y, H:i:s" }} + {% else %} + {% trans "En curso..." %} + {% endif %} + + {% if log.status == 'SUCCESS' %} + + Éxito + + {% elif log.status == 'ERROR' %} + + Error + + {% else %} + + {{ log.status }} + + {% endif %} + + {% if log.summary %} + {{ log.summary|truncatechars:80 }} + {% else %} + - + {% endif %} +
+ {% trans "Todavía no hay ningún registro de sincronización." %} +
+
+
+ + {% if logs.has_other_pages %} +
+ {% include 'paginator.html' with items=logs %} +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/users/templates/users/settings/user_settings.html b/users/templates/users/settings/user_settings.html deleted file mode 100644 index fabc638..0000000 --- a/users/templates/users/settings/user_settings.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends 'users/base_profile.html' %} -{% load static %} -{% load i18n %} - -{% block content_user_setings %} - {% if user.type == 'Consumer' %} -
- - -
- -
-
{% trans "Bio" %}
-

- {% if user.bio %} - {{ user.bio }} - {% else %} - {% trans "Write a few words about you" %} - {% endif %} -

-
- -
-
{% trans "Member since" %}
-

{{ user.date_joined|date:"d/m/Y" }}

-
- -
-
{% trans "Last access" %}
-

{{ user.last_login|date:"d/m/Y" }}

-
- -
-
{% trans "Location" %}
-

- {% if user.location %} - {{ user.location }} - {% else %} - {% trans "Add your location" %} - {% endif %} -

-
- -
- - {% endif %} - - - {% if user.type == 'Consumer' %} -
- {% else %} -
- {% endif %} - {% block user_content %}{% endblock %} -
- -
-{% endblock %} \ No newline at end of file diff --git a/users/templates/users/types/staff_admin.html b/users/templates/users/types/staff_admin.html index 713f841..6136a70 100644 --- a/users/templates/users/types/staff_admin.html +++ b/users/templates/users/types/staff_admin.html @@ -1,4 +1,4 @@ -{% extends 'users/settings/user_settings.html' %} +{% extends 'users/base_profile.html' %} {% load i18n %} {% block content_staff_admin %} @@ -12,10 +12,14 @@ {% trans 'Carteleras' %} + -
+
{% block staff_admin_content %}{% endblock %}
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/users/templates/users/types/user_admin.html b/users/templates/users/types/user_admin.html index 5883179..5774e99 100644 --- a/users/templates/users/types/user_admin.html +++ b/users/templates/users/types/user_admin.html @@ -1,4 +1,4 @@ -{% extends 'users/settings/user_settings.html' %} +{% extends 'users/base_profile.html' %} {% load i18n %} {% block content_admin %} diff --git a/users/templates/users/types/user_client.html b/users/templates/users/types/user_client.html index dfce223..e4e2aa9 100644 --- a/users/templates/users/types/user_client.html +++ b/users/templates/users/types/user_client.html @@ -1,31 +1,75 @@ -{% extends 'users/settings/user_settings.html' %} +{% extends 'users/base_profile.html' %} {% load i18n %} {% block content_user %} - - - - -
- {% block parts_content %}{% endblock %} +
+ +
+ +
+
{% trans "Bio" %}
+

+ {% if user.bio %} + {{ user.bio }} + {% else %} + {% trans "Write a few words about you" %} + {% endif %} +

+
+ +
+
{% trans "Member since" %}
+

{{ user.date_joined|date:"d/m/Y" }}

+
+ +
+
{% trans "Last access" %}
+

{{ user.last_login|date:"d/m/Y" }}

+
+ +
+
{% trans "Location" %}
+

+ {% if user.location %} + {{ user.location }} + {% else %} + {% trans "Add your location" %} + {% endif %} +

+
+
-{% endblock %} \ No newline at end of file +
+ + + +
+ {% block parts_content %}{% endblock %} +
+ +
+ +
+ +{% endblock %} diff --git a/users/urls.py b/users/urls.py index 6c7af8c..d435512 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,5 +1,6 @@ from django.urls import path from . import views +from web_app.views import system_logs urlpatterns = [ # Staff Admin URLs @@ -10,7 +11,7 @@ path('api/update-role/', views.update_user_role, name='update_user_role'), path('staff-admin/cartelleres/', views.gestion_cartelleres, name='gestion_cartelleres'), path('staff-admin/cartelleres/editar//', views.editar_cartellera, name='editar_cartellera'), - + path('staff-admin/logs/', system_logs, name='system_logs'), # User URLs path('profile/', views.user_profile, name='user_profile'), path('history/', views.history, name='history'), diff --git a/users/views.py b/users/views.py index 4747a35..849f821 100644 --- a/users/views.py +++ b/users/views.py @@ -9,6 +9,7 @@ from django.utils.crypto import get_random_string from django.contrib.sessions.models import Session from django.utils import timezone +from datetime import timedelta import json import gettext @@ -85,12 +86,71 @@ def user_profile(request): @login_required(login_url='login') @user_passes_test(is_consumer) def history(request): - # OPTIMIZACIÓN: Buscamos en el modelo real de visualizaciones y hacemos Eager Loading - visualizaciones = Visualitzacio.objects.filter(user=request.user).select_related( - 'contingut__movie', 'contingut__series' - ).prefetch_related('contingut__apis').order_by('-data_visualitzacio') - - return render(request, 'users/parts/history.html', {'visualizaciones': visualizaciones}) + user = request.user + visualizaciones = Visualitzacio.objects.filter(user=user).select_related( + 'contingut__movie', 'contingut__series', 'api' + ).order_by('-data_visualitzacio') + + platform_id = request.GET.get('platform') + if platform_id: + visualizaciones = visualizaciones.filter(api_id=platform_id) + + date_filter = request.GET.get('date') + now = timezone.now() + if date_filter == 'today': + visualizaciones = visualizaciones.filter(data_visualitzacio__date=now.date()) + elif date_filter == 'week': + visualizaciones = visualizaciones.filter(data_visualitzacio__gte=now - timedelta(days=7)) + elif date_filter == 'month': + visualizaciones = visualizaciones.filter(data_visualitzacio__gte=now - timedelta(days=30)) + + paginator = Paginator(visualizaciones, 20) + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + grouped_history = { + 'Avui': [], + 'Aquesta setmana': [], + 'Aquest mes': [], + 'Més antic': [] + } + + today = now.date() + week_ago = (now - timedelta(days=7)).date() + month_ago = (now - timedelta(days=30)).date() + + for vis in page_obj: + vis_date = vis.data_visualitzacio.date() + + if hasattr(vis.contingut, 'movie'): + vis.content_type = 'Movie' + elif hasattr(vis.contingut, 'series'): + vis.content_type = 'Series' + else: + vis.content_type = 'Desconegut' + + if vis_date == today: + grouped_history['Avui'].append(vis) + elif vis_date >= week_ago: + grouped_history['Aquesta setmana'].append(vis) + elif vis_date >= month_ago: + grouped_history['Aquest mes'].append(vis) + else: + grouped_history['Més antic'].append(vis) + + grouped_history = {k: v for k, v in grouped_history.items() if v} + + platforms = API.objects.all() + + context = { + 'page_obj': page_obj, + 'grouped_history': grouped_history, + 'platforms': platforms, + 'selected_platform': platform_id, + 'selected_date': date_filter, + } + + return render(request, 'users/parts/history.html', context) @login_required(login_url='login') @user_passes_test(is_consumer) @@ -234,7 +294,7 @@ def export_analytics_csv(request): response['Content-Disposition'] = 'attachment; filename="analytics_report.csv"' writer = csv.writer(response) - writer.writerow(['Title', 'Genre', 'Age Rating', 'Year', 'Director', 'Platform', 'Total Views', 'Total Favorites'])#TODO translate this + writer.writerow(['Title', 'Genre', 'Age Rating', 'Year', 'Director', 'Platform', 'Total Visualizations', 'Total Favorites'])#TODO translate this for item in clean_data: writer.writerow([ @@ -420,3 +480,4 @@ def editar_cartellera(request, contingut_id): return render(request, 'users/parts/editar_cartellera.html', { 'contingut': contingut, }) + diff --git a/web_app/apps.py b/web_app/apps.py index c071959..a9bd4d5 100644 --- a/web_app/apps.py +++ b/web_app/apps.py @@ -42,7 +42,6 @@ def create_default_user(sender, **kwargs): username = 'admin' email = 'admin@admin.com' password = 'adminpassword' - if not User.objects.filter(username=username).exists(): User.objects.create_superuser( username=username, diff --git a/web_app/models.py b/web_app/models.py index 2f3c49a..caa3cdb 100644 --- a/web_app/models.py +++ b/web_app/models.py @@ -130,7 +130,7 @@ def get_similar_by_genre(self, limit=4): return Movie.objects.filter( contingut__genere=self.contingut.genere - ).exclude(id=self.id).order_by('?')[:limit] + ).exclude(id=self.id).distinct().order_by('?')[:limit] class Series(models.Model): contingut = models.OneToOneField(Contingut, on_delete=models.CASCADE, related_name='series') @@ -187,6 +187,14 @@ def total_seasons(self): def poster_path(self): return self.contingut.poster_path + def get_similar_by_genre(self, limit=4): + if not self.contingut.genere: + return Series.objects.none() + + return Series.objects.filter( + contingut__genere=self.contingut.genere + ).exclude(id=self.id).distinct().order_by('?')[:limit] + class CustomUser(AbstractUser): """ diff --git a/web_app/views.py b/web_app/views.py index dde9023..b4a32b8 100644 --- a/web_app/views.py +++ b/web_app/views.py @@ -4,7 +4,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from web_app.forms import CustomUserAdminCreationForm, CustomUserCreationForm from web_app.models import AgeRating, API, CustomUser, Director, Genre, Movie, UserProfile, Series, Contingut, \ - AgeRating, Valoracio, Visualitzacio + AgeRating, Valoracio, Visualitzacio, SyncLog from web_app import utils import json from itertools import chain @@ -161,6 +161,7 @@ def series_detail(request, pk): 'reviews': reviews, 'avg_rating': review_stats['avg_rating'], 'total_reviews': review_stats['total_reviews'], + 'recommendations': series.get_similar_by_genre(limit=4) }) @@ -240,6 +241,7 @@ def terms_use(request): def privacy_policy(request): return render(request, 'footer_legal/privacy_policy.html') +@login_required def register_platform_click(request): if request.method == 'POST': try: @@ -247,22 +249,33 @@ def register_platform_click(request): contingut_id = data.get('contingut_id') api_id = data.get('api_id') - contingut = get_object_or_404(Contingut, id=contingut_id) - api = get_object_or_404(API, id=api_id) - - user = request.user if request.user.is_authenticated else None - Visualitzacio.objects.create( - user=user, - contingut=contingut, - api=api + # MAGIA DE DJANGO: Si existe, actualiza la fecha. Si no, lo crea. + vis, created = Visualitzacio.objects.update_or_create( + user=request.user, + contingut_id=contingut_id, + api_id=api_id, + defaults={'data_visualitzacio': timezone.now()} ) - total_clicks = Visualitzacio.objects.filter(contingut=contingut, api=api).count() - - return JsonResponse({'status': 'success', 'total_clicks': total_clicks}) - + return JsonResponse({ + 'status': 'success', + 'message': 'Historial actualizado correctamente' + }) except Exception as e: return JsonResponse({'status': 'error', 'message': str(e)}, status=400) return JsonResponse({'status': 'error', 'message': 'Método no permitido'}, status=405) + +@user_passes_test(lambda u: u.is_superuser) +def system_logs(request): + if not request.user.is_authenticated: + return redirect('login') + + logs = SyncLog.objects.all().order_by('-start_time') + paginator = Paginator(logs, 20) + page_obj = paginator.get_page(request.GET.get('page', 1)) + + return render(request, 'users/parts/system_logs.html', { + 'logs': page_obj + }) \ No newline at end of file