From 6e9d24e060cc435efb34b8968e253f38983be8d1 Mon Sep 17 00:00:00 2001 From: Bogdan2005-19 Date: Thu, 30 Apr 2026 16:12:19 +0200 Subject: [PATCH 001/100] refactor: improved header --- Templates/base.html | 2 -- static/css/base.css | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Templates/base.html b/Templates/base.html index c7f5a31..8530d62 100644 --- a/Templates/base.html +++ b/Templates/base.html @@ -32,8 +32,6 @@ {% if user.is_authenticated %} {% trans "Welcome" %} {{ user.username }} {% endif %} - -
+ +
+

Valoracions de la comunitat

+ +
+ {% if total_reviews > 0 %} + ★ {{ avg_rating|floatformat:1 }} + ({{ total_reviews }}) + {% else %} + Sense valoracions encara + {% endif %} +
+ + {% if user.is_authenticated %} +
+

Deixa la teva opinió

+
+ {% csrf_token %} +
+ {% for i in "54321" %} + + + {% endfor %} +
+ + +
+
+ {% endif %} + +
+ {% for review in reviews %} +
+
+ {{ review.user.username }} + + {% for i in "12345" %}{% if forloop.counter <= review.puntuacio %}★{% else %}{% endif %}{% endfor %} + +
+

{{ review.comentari }}

+
+ {% empty %} +

Sigues el primer en valorar aquest contingut!

+ {% endfor %} +
+
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/Templates/Details/details_movie.html b/Templates/Details/details_movie.html index 4b0c5ba..ad3b9cc 100644 --- a/Templates/Details/details_movie.html +++ b/Templates/Details/details_movie.html @@ -7,4 +7,26 @@ {% endblock %} -{% block favorite_url %}{% url 'toggle_movie_favorite' content.id %}{% endblock %} \ No newline at end of file +{% block favorite_url %}{% url 'toggle_movie_favorite' content.id %}{% endblock %} + +{% block extra_main_content %} +
+

També et pot agradar...

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

{{ rec.title }}

+
+ {% empty %} +

No hi ha recomanacions disponibles.

+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/static/css/details.css b/static/css/details.css index 6f8f878..4823d65 100644 --- a/static/css/details.css +++ b/static/css/details.css @@ -149,6 +149,117 @@ box-shadow: 0 0 20px var(--cyan-glow); } +/* Sistema de Ressenyes */ + +.community-reviews-section { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.rating-summary { + display: flex; + align-items: center; + gap: 10px; + font-size: 1.2rem; + margin-bottom: 25px; +} + +.rating-summary .stars-avg { + color: #f5c518; + font-weight: bold; + font-size: 1.5rem; +} + +.rating-summary .count { + color: rgba(255, 255, 255, 0.6); + font-size: 1rem; +} + +.review-form-container { + background: rgba(255, 255, 255, 0.03); + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; +} + +.star-rating-input { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + gap: 5px; + font-size: 2rem; + margin-bottom: 15px; +} + +.star-rating-input input { + display: none; +} + +.star-rating-input label { + color: rgba(255, 255, 255, 0.2); + cursor: pointer; + transition: color 0.2s; +} + +.star-rating-input label:hover, +.star-rating-input label:hover ~ label, +.star-rating-input input:checked ~ label { + color: #f5c518; +} + +.review-textarea { + width: 100%; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + color: white; + padding: 15px; + border-radius: 6px; + resize: vertical; + min-height: 100px; +} + +.review-textarea:focus { + outline: none; + border-color: #00ffff; +} + +.review-card { + background: rgba(255, 255, 255, 0.02); + border-left: 3px solid #00ffff; + padding: 15px 20px; + margin-bottom: 15px; + border-radius: 4px; +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.review-user { + font-weight: bold; + color: #fff; +} + +.review-stars { + color: #f5c518; +} + +.review-date { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 10px; +} + +.review-text { + line-height: 1.5; + color: rgba(255, 255, 255, 0.9); + margin: 0; +} + /* Responsive */ @media (max-width: 900px) { .details-wrapper { diff --git a/web_app/models.py b/web_app/models.py index ce82307..e7c382a 100644 --- a/web_app/models.py +++ b/web_app/models.py @@ -125,6 +125,13 @@ def rating(self): def poster_path(self): return self.contingut.poster_path + def get_similar_by_genre(self, limit=4): + if not self.contingut.genere: + return Movie.objects.none() + + return Movie.objects.filter( + contingut__genere=self.contingut.genere + ).exclude(id=self.id).order_by('?')[:limit] class Series(models.Model): contingut = models.OneToOneField(Contingut, on_delete=models.CASCADE, related_name='series') diff --git a/web_app/urls.py b/web_app/urls.py index 2272065..938479f 100644 --- a/web_app/urls.py +++ b/web_app/urls.py @@ -14,6 +14,7 @@ path("user_setting/", views.user_setting, name="user_setting"), path("movie//", views.movie_detail, name="movie_detail"), path("series//", views.series_detail, name="series_detail"), + path('review/submit//', views.submit_review, name='submit_review'), path("api/user/profile/", views.api_user_profile, name="api_user_profile"), path('favorite/movie//', views.toggle_movie_favorite, name='toggle_movie_favorite'), diff --git a/web_app/views.py b/web_app/views.py index 7b0fc09..b5a9336 100644 --- a/web_app/views.py +++ b/web_app/views.py @@ -1,12 +1,13 @@ from django.shortcuts import render, redirect, get_object_or_404 from web_app.forms import CustomUserCreationForm -from web_app.models import AgeRating, API, Director, Genre, Movie, UserProfile, Series, Contingut +from web_app.models import AgeRating, API, Director, Genre, Movie, UserProfile, Series, Contingut, Valoracio from web_app import utils from itertools import chain from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.contrib.auth import login from django.core.paginator import Paginator +from django.db.models import Avg, Count def home(request): movies = Movie.objects.select_related('contingut', 'contingut__genere', 'contingut__director', 'contingut__age_rating').prefetch_related('contingut__api').all() @@ -109,10 +110,17 @@ def movie_detail(request, pk): profile, _ = UserProfile.objects.get_or_create(user=request.user) is_favorite = movie.contingut in profile.preferits.all() + reviews = Valoracio.objects.filter(contingut=movie.contingut).order_by('-creada_a') + review_stats = reviews.aggregate(avg_rating=Avg('puntuacio'), total_reviews=Count('id')) + return render(request, 'Details/details_movie.html', { 'content': movie, 'is_favorite': is_favorite, - 'available_apis': available_apis + 'available_apis': available_apis, + 'reviews': reviews, + 'avg_rating': review_stats['avg_rating'], + 'total_reviews': review_stats['total_reviews'], + 'recommendations': movie.get_similar_by_genre(limit=4) }) def series_detail(request, pk): @@ -125,10 +133,16 @@ def series_detail(request, pk): profile, _ = UserProfile.objects.get_or_create(user=request.user) is_favorite = series.contingut in profile.preferits.all() + reviews = Valoracio.objects.filter(contingut=series.contingut).order_by('-creada_a') + review_stats = reviews.aggregate(avg_rating=Avg('puntuacio'), total_reviews=Count('id')) + return render(request, 'Details/details_serie.html', { 'content': series, 'is_favorite': is_favorite, - 'available_apis': available_apis + 'available_apis': available_apis, + 'reviews': reviews, + 'avg_rating': review_stats['avg_rating'], + 'total_reviews': review_stats['total_reviews'], }) @@ -181,6 +195,27 @@ def toggle_series_favorite(request, pk): return JsonResponse({'status': status}) return JsonResponse({'error': 'Método no permitido'}, status=405) + +@login_required +def submit_review(request, contingut_id): + if request.method == 'POST': + puntuacio = request.POST.get('rating') + comentari = request.POST.get('comment', '') + + contingut = get_object_or_404(Contingut, id=contingut_id) + + if puntuacio: + Valoracio.objects.update_or_create( + user=request.user, + contingut=contingut, + defaults={ + 'puntuacio': int(puntuacio), + 'comentari': comentari + } + ) + + return redirect(request.META.get('HTTP_REFERER', 'home')) + def terms_use(request): return render(request, 'footer_legal/terms_use.html') From 77cb93baf11a15923d21383dbbf636db8b80cbe4 Mon Sep 17 00:00:00 2001 From: Bogdan2005-19 Date: Wed, 13 May 2026 20:12:29 +0200 Subject: [PATCH 066/100] feat: experimenting for better page charging --- Templates/home/home.html | 126 +++++++++++++------ Templates/home/home_parts/paginator.html | 6 +- Templates/home/includes/result_fragment.html | 6 + Templates/part/card.html | 31 ++--- web_app/scheduler.py | 1 - web_app/urls.py | 1 + web_app/views.py | 123 ++++++++---------- 7 files changed, 165 insertions(+), 129 deletions(-) create mode 100644 Templates/home/includes/result_fragment.html diff --git a/Templates/home/home.html b/Templates/home/home.html index 1ae97c1..cb53107 100644 --- a/Templates/home/home.html +++ b/Templates/home/home.html @@ -1,13 +1,38 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} + {% block extra_css %} + {% endblock %} + {% block search-menu %} - -
- -
@@ -72,31 +81,78 @@ - {% endblock %} + {% block content %}
-
- -
+

+ {% if request.GET.q %} + {% trans "Results for" %}: {{ request.GET.q }} + {% else %} + {% trans "Catálogo completo" %} + {% endif %} +

-

{% trans "Results" %}: {{ search_query }}

- -
- {% for item in items %} - {% include 'part/card.html' with item=item %} - {% empty %} -

- {% trans "No content found matching those filters." %} -

- {% endfor %} +
+
+
+
+

{% trans "Cargando contenido..." %}

+
+
+ +
+
- - {% include 'home/home_parts/paginator.html' with items=items %}
+ + {% endblock %} \ No newline at end of file diff --git a/Templates/home/home_parts/paginator.html b/Templates/home/home_parts/paginator.html index d31354d..9bdf48c 100644 --- a/Templates/home/home_parts/paginator.html +++ b/Templates/home/home_parts/paginator.html @@ -13,15 +13,15 @@ {% endif %}
- {% trans "Pàgina" %} + {% trans "Página" %} {{ items.number }} {% trans "de" %} {{ items.paginator.num_pages }}
{% if items.has_next %} - - {% trans "següent" %} + + {% trans "siguiente" %} diff --git a/Templates/home/includes/result_fragment.html b/Templates/home/includes/result_fragment.html new file mode 100644 index 0000000..2836268 --- /dev/null +++ b/Templates/home/includes/result_fragment.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% for item in items %} + {% include 'part/card.html' with item=item %} +{% empty %} +

{% trans "No se han encontrado resultados." %}

+{% endfor %} \ No newline at end of file diff --git a/Templates/part/card.html b/Templates/part/card.html index ecabcd9..73a98e8 100644 --- a/Templates/part/card.html +++ b/Templates/part/card.html @@ -1,22 +1,16 @@ {% load i18n %} {% if item.content_type == 'movie' %} -
+
{% elif item.content_type == 'series' %} -
+
+{% else %} +
{% endif %}
{% if item.poster_path %} {{ item.title }} {% else %} -
- {{ item.title|slice:":1" }} -
+
{{ item.title|slice:":1" }}
{% endif %}

{{ item.title }}

@@ -28,12 +22,8 @@

{{ item.title }}

{{ item.title }}

{% if item.year %}{{ item.year }}{% endif %}
- {% if item.genre %} -

{% trans "Genre" %}: {{ item.genre.name|default:_("N/A") }}

- {% endif %} - {% if item.director %} -

{% trans "Director" %}: {{ item.director.name|default:_("N/A") }}

- {% endif %} +

{% trans "Género" %}: {{ item.genre.name|default:"N/A" }}

+

{% trans "Director" %}: {{ item.director.name|default:"N/A" }}

{% if item.rating %} @@ -46,12 +36,11 @@

{{ item.title }}

{{ item.age_rating.description }} {% endif %}
- {% if item.synopsis %} -

{{ item.synopsis|truncatewords:15 }}

- {% endif %} + {% if item.synopsis %}

{{ item.synopsis|truncatewords:15 }}

{% endif %} + {% if item.available_platforms %}
- {% trans "Available on" %}: + {% trans "Disponible en" %}:
{% for platform in item.available_platforms %} {{ platform.name|default:platform.port }} diff --git a/web_app/scheduler.py b/web_app/scheduler.py index 10e13b8..2e06b83 100644 --- a/web_app/scheduler.py +++ b/web_app/scheduler.py @@ -4,7 +4,6 @@ def start(): - return scheduler = BackgroundScheduler(timezone="Europe/Madrid") scheduler.add_jobstore(DjangoJobStore(), "default") diff --git a/web_app/urls.py b/web_app/urls.py index 1a0087b..a061957 100644 --- a/web_app/urls.py +++ b/web_app/urls.py @@ -11,6 +11,7 @@ path("login/", auth_views.LoginView.as_view(template_name="identify/login.html"), name="login"), path("logout/", LogoutView.as_view(), name="logout"), path('dashboard_redirect/', views.redirect_by_role, name='go_to_dashboard'), + path("api/search-content/", views.search_content_ajax, name="search_ajax"), path("user_setting/", views.user_setting, name="user_setting"), path("movie//", views.movie_detail, name="movie_detail"), diff --git a/web_app/views.py b/web_app/views.py index f7cc441..b671375 100644 --- a/web_app/views.py +++ b/web_app/views.py @@ -7,81 +7,66 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth import login from django.core.paginator import Paginator +from django.template.loader import render_to_string def home(request): - movies = Movie.objects.select_related('contingut', 'contingut__genere', 'contingut__director', 'contingut__age_rating').prefetch_related('contingut__api').all() - series = Series.objects.select_related('contingut', 'contingut__genere', 'contingut__director', 'contingut__age_rating').prefetch_related('contingut__api').all() - - search_query = request.GET.get('q', '') - genre_filter = request.GET.get('genre', '') - director_filter = request.GET.get('director', '') - age_rating_filter = request.GET.get('age_rating', '') - platform_filter = request.GET.get('platform', '') # value = API port - - if search_query: - movies = movies.filter(contingut__titol__icontains=search_query) - series = series.filter(contingut__titol__icontains=search_query) - - if genre_filter: - movies = movies.filter(contingut__genere__name=genre_filter) - series = series.filter(contingut__genere__name=genre_filter) - - if director_filter: - movies = movies.filter(contingut__director__name=director_filter) - series = series.filter(contingut__director__name=director_filter) - - if age_rating_filter: - movies = movies.filter(contingut__age_rating__codi=age_rating_filter) - series = series.filter(contingut__age_rating__codi=age_rating_filter) - - if platform_filter: - movies = movies.filter(contingut__api__port=platform_filter) - series = series.filter(contingut__api__port=platform_filter) - - unique_results = [] - seen_keys = set() - - for cont in chain(movies, series): - is_movie = isinstance(cont, Movie) - type_str = 'movie' if is_movie else 'series' - key = (cont.title.lower(), type_str) - if key not in seen_keys: - cont.content_type = type_str - if is_movie: - related_continguts = Contingut.objects.filter( - titol__iexact=cont.title, - movie__isnull=False - ) - else: - related_continguts = Contingut.objects.filter( - titol__iexact=cont.title, - series__isnull=False - ) - cont.available_platforms = list( - API.objects.filter(contingut__in=related_continguts).distinct() - ) - unique_results.append(cont) - seen_keys.add(key) - paginator = Paginator(unique_results, 20) - page_number = request.GET.get('page') - page_obj = paginator.get_page(page_number) - - genres = Genre.objects.values_list('name', flat=True).distinct() - directors = Director.objects.values_list('name', flat=True).distinct() - age_ratings = AgeRating.objects.values_list('codi', flat=True).distinct() - platforms = API.objects.all() - + """Carga instantánea del esqueleto de la página.""" context = { - 'items': page_obj, - 'genres': genres, - 'directors': directors, - 'age_ratings': age_ratings, - 'platforms': platforms, - 'search_query': search_query, + 'genres': Genre.objects.values_list('name', flat=True).distinct(), + 'directors': Director.objects.values_list('name', flat=True).distinct(), + 'platforms': API.objects.all(), + # Añadimos age_ratings que faltaba en tu home.html + 'age_ratings': AgeRating.objects.values_list('codi', flat=True).distinct(), } - return render(request, "home/home.html", context) +def search_content_ajax(request): + """Procesamiento asíncrono optimizado.""" + try: + # 1. Filtros + search_query = request.GET.get('q', '') + genre_filter = request.GET.get('genre', '') + director_filter = request.GET.get('director', '') + platform_filter = request.GET.get('platform', '') + + movies = Movie.objects.select_related('contingut__genere', 'contingut__director', 'contingut__age_rating', 'contingut__api').all() + series = Series.objects.select_related('contingut__genere', 'contingut__director', 'contingut__age_rating', 'contingut__api').all() + + if search_query: + movies = movies.filter(contingut__titol__icontains=search_query) + series = series.filter(contingut__titol__icontains=search_query) + # ... aplicar otros filtros si es necesario ... + + # 2. Unión y deduplicación + all_content = list(chain(movies, series)) + unique_results = [] + seen = set() + for item in all_content: + title_key = item.contingut.titol.lower() + if title_key not in seen: + item.content_type = 'movie' if isinstance(item, Movie) else 'series' + # Calculamos las plataformas para este item específico + item.available_platforms = [m.contingut.api for m in type(item).objects.filter(contingut__titol__iexact=item.title).select_related('contingut__api') if m.contingut.api] + unique_results.append(item) + seen.add(title_key) + + # 3. Paginación + paginator = Paginator(unique_results, 20) + page_obj = paginator.get_page(request.GET.get('page', 1)) + + # 4. Renderizado (RUTAS CORREGIDAS SEGÚN TU CAPTURA) + html_cards = render_to_string('home/includes/result_fragment.html', {'items': page_obj}, request=request) + html_pagination = render_to_string('home/home_parts/paginator.html', {'items': page_obj}, request=request) + + return JsonResponse({ + 'html': html_cards, + 'pagination_html': html_pagination + }) + except Exception as e: + import logging + logging.error(f"Error en AJAX: {str(e)}") + return JsonResponse({'error': str(e)}, status=500) + def register_view(request): form = CustomUserCreationForm(request.POST or None) if form.is_valid(): From 36cbc3e7c7d7dc76ce7e7245b3dcb0f52ab9d510 Mon Sep 17 00:00:00 2001 From: Bogdan2005-19 Date: Wed, 13 May 2026 20:33:18 +0200 Subject: [PATCH 067/100] fix: render now charging properly --- Templates/home/home.html | 42 +++++++++++++++++++++------------------- web_app/views.py | 23 +++++++++++++++------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Templates/home/home.html b/Templates/home/home.html index cb53107..b3f27af 100644 --- a/Templates/home/home.html +++ b/Templates/home/home.html @@ -6,7 +6,7 @@ {% endblock %} {% block content_staff_admin %} @@ -20,41 +75,109 @@

{% trans 'Registrar Nuevo Miembro' %}

{% for field in form %} + {% if field.name != 'subscriptions' %} +
+
+ + + {{ field }} + + {% if field.help_text %} + + {{ field.help_text|safe }} + + {% endif %} + + {% if field.errors %} + {% for error in field.errors %} +
+ {{ error }} +
+ {% endfor %} + {% endif %} +
+
+ {% endif %} + {% endfor %} +
-
- - - {{ field }} - - {% if field.help_text %} - - {{ field.help_text|safe }} - - {% endif %} - - {% if field.errors %} - {% for error in field.errors %} -
- {{ error }} +
- {% endfor %}
- + {% trans 'Cancelar' %}
+ + {% endblock %} \ No newline at end of file diff --git a/users/templates/users/types/user_client.html b/users/templates/users/types/user_client.html index beefc6b..b916452 100644 --- a/users/templates/users/types/user_client.html +++ b/users/templates/users/types/user_client.html @@ -1,7 +1,7 @@ {% extends 'users/settings/user_settings.html' %} {% load i18n %} -{% block user_content %} +{% block content_user %}