Merge branch 'main' of https://github.com/neodb-social/neodb
Some checks are pending
code check / lint (3.12) (push) Waiting to run
code check / type-checker (3.12) (push) Waiting to run
Mirror to Codeberg / to_codeberg (push) Waiting to run
unit test / django (3.12) (push) Waiting to run

This commit is contained in:
gesang 2025-03-14 21:07:03 +01:00
commit 576e1e0c30
Signed by: gesang
GPG key ID: 6CE35141D31CEAFB
9 changed files with 225 additions and 16 deletions

View file

@ -103,7 +103,7 @@
<a href="{% url 'users:info' %}">{% trans 'Account' %}</a>
</li>
<li>
<a href="{% url 'users:logout' %}">{% trans 'Logout' %}</a>
<a href="#" onclick="$('#logout').submit()">{% trans 'Logout' %}</a>
</li>
{% if request.user.is_superuser %}
<li>
@ -149,3 +149,6 @@ _search_cat_change();
{% endfor %}
</ul>
{% endif %}
<form id="logout" action="{% url 'users:logout' %}" method="post">
{% csrf_token %}
</form>

View file

@ -62,10 +62,7 @@ def list_marks_on_user_shelf(
target = APIdentity.get_by_handle(handle)
except APIdentity.DoesNotExist:
return ShelfMember.objects.none()
viewer = request.user.identity
if target.is_blocking(viewer) or target.is_blocked_by(viewer):
return ShelfMember.objects.none()
qv = q_owned_piece_visible_to_user(request.user, target)
qv = q_owned_piece_visible_to_user(request.user, target, True)
queryset = (
target.shelf_manager.get_latest_members(
type, ItemCategory(category) if category else None

View file

@ -41,7 +41,12 @@ class VisibilityType(models.IntegerChoices):
Private = 2, _("Mentioned Only") # type:ignore[reportCallIssue]
def q_owned_piece_visible_to_user(viewing_user: User, owner: APIdentity):
def q_owned_piece_visible_to_user(
viewing_user: User | None, owner: APIdentity, check_blocking: bool = False
) -> Q:
"""return a Q object to filter pieces that are visible to the viewing user"""
if check_blocking and owner.restricted:
return Q(pk__in=[])
if not viewing_user or not viewing_user.is_authenticated:
if owner.anonymous_viewable:
return Q(owner=owner, visibility=0)
@ -50,8 +55,8 @@ def q_owned_piece_visible_to_user(viewing_user: User, owner: APIdentity):
viewer = viewing_user.identity
if viewer == owner:
return Q(owner=owner)
# elif viewer.is_blocked_by(owner):
# return Q(pk__in=[])
elif check_blocking and viewer.is_blocked_by(owner):
return Q(pk__in=[])
elif viewer.is_following(owner):
return Q(owner=owner, visibility__in=[0, 1])
else:

View file

@ -7,8 +7,9 @@ from django.db import connection, models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from loguru import logger
from polymorphic.models import PolymorphicManager
from polymorphic.models import ContentType, PolymorphicManager
from catalog.common.models import item_categories
from catalog.models import Item, ItemCategory
from takahe.utils import Takahe
from users.models import APIdentity
@ -743,6 +744,45 @@ class ShelfManager:
def get_manager_for_user(owner: APIdentity):
return ShelfManager(owner)
def get_stats(self, q: models.Q | None = None) -> dict:
from .review import Review
if not q:
q = models.Q(owner=self.owner)
qs = (
ShelfMember.objects.filter(q)
.values("parent__shelf_type", "item__polymorphic_ctype_id")
.annotate(num=models.Count("item"))
)
qs2 = (
Review.objects.filter(q)
.values("item__polymorphic_ctype_id")
.annotate(num=models.Count("item"))
)
stats = {
cat: {t: 0 for t in (ShelfType.values + ["reviewed"])}
for cat in ItemCategory.values
}
for cat, item_classes in item_categories().items():
ct_ids = [
ContentType.objects.get_for_model(item_cls).pk
for item_cls in item_classes
]
for typ in ShelfType.values:
stats[cat][typ] = sum(
[
s["num"]
for s in qs
if s["item__polymorphic_ctype_id"] in ct_ids
and s["parent__shelf_type"] == typ
],
0,
)
stats[cat]["reviewed"] = sum(
[s["num"] for s in qs2 if s["item__polymorphic_ctype_id"] in ct_ids], 0
)
return stats
def get_calendar_data(self, max_visiblity: int):
shelf_id = self.get_shelf(ShelfType.COMPLETE).pk
timezone_offset = timezone.localtime(timezone.now()).strftime("%z")

View file

@ -3,3 +3,4 @@ from .ndjson import *
from .piece import *
from .rating import *
from .search import *
from .shelf import *

162
journal/tests/shelf.py Normal file
View file

@ -0,0 +1,162 @@
from django.test import TestCase
from catalog.common.models import ItemCategory
from catalog.models import Edition, Game, IdType, Movie, TVShow
from journal.models.common import q_owned_piece_visible_to_user
from journal.models.review import Review
from journal.models.shelf import ShelfManager, ShelfMember, ShelfType
from users.models import User
class ShelfManagerTest(TestCase):
databases = "__all__"
def setUp(self):
# Create a user
self.user = User.register(email="user@example.com", username="testuser")
self.identity = self.user.identity
# Create items of different categories
self.book = Edition.objects.create(
localized_title=[{"lang": "en", "text": "Test Book"}],
primary_lookup_id_type=IdType.ISBN,
primary_lookup_id_value="9780553283686",
author=["Test Author"],
)
self.movie = Movie.objects.create(
localized_title=[{"lang": "en", "text": "Test Movie"}],
primary_lookup_id_type=IdType.IMDB,
primary_lookup_id_value="tt1234567",
director=["Test Director"],
year=2020,
)
self.tvshow = TVShow.objects.create(
localized_title=[{"lang": "en", "text": "Test Show"}],
primary_lookup_id_type=IdType.IMDB,
primary_lookup_id_value="tt9876543",
)
self.game = Game.objects.create(
localized_title=[{"lang": "en", "text": "Test Game"}],
primary_lookup_id_type=IdType.IGDB,
primary_lookup_id_value="12345",
developer=["Test Developer"],
)
# Initialize shelf manager for user
self.shelf_manager = ShelfManager(self.identity)
self._add_items_to_shelves()
def _add_items_to_shelves(self):
"""Helper to add items to different shelves"""
# Add books to shelves
shelves = {
ShelfType.WISHLIST: [self.book],
ShelfType.PROGRESS: [
self.movie,
self.tvshow,
], # Add same book twice to test count
ShelfType.COMPLETE: [self.game],
ShelfType.DROPPED: [],
}
# Create shelf members
for shelf_type, items in shelves.items():
shelf = self.shelf_manager.get_shelf(shelf_type)
for item in items:
ShelfMember.objects.update_or_create(
owner=self.identity,
item=item,
defaults={"visibility": 1, "position": 0, "parent": shelf},
)
# Add reviews for some items
Review.objects.create(
owner=self.identity,
item=self.book,
body="Book review",
title="Book Review",
visibility=0,
)
Review.objects.create(
owner=self.identity,
item=self.movie,
body="Movie review",
title="Movie Review",
visibility=1,
)
# Add two reviews for the game to test counts
Review.objects.create(
owner=self.identity,
item=self.game,
body="Game review 1",
title="Game Review 1",
visibility=2,
)
def test_get_stats(self):
"""Test that ShelfManager.get_stats() returns correct statistics"""
# Get stats
stats = self.shelf_manager.get_stats()
# Verify structure: stats should have keys for each ItemCategory
for category in ItemCategory.values:
self.assertIn(category, stats)
# Verify each category has counts for each shelf type
for category in ItemCategory.values:
for shelf_type in ShelfType.values:
self.assertIn(shelf_type, stats[category])
self.assertIn("reviewed", stats[category])
# Verify expected counts for book category
self.assertEqual(stats[ItemCategory.Book][ShelfType.WISHLIST], 1)
self.assertEqual(stats[ItemCategory.Book][ShelfType.PROGRESS], 0)
self.assertEqual(stats[ItemCategory.Book][ShelfType.COMPLETE], 0)
self.assertEqual(stats[ItemCategory.Book][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.Book]["reviewed"], 1)
# Verify expected counts for movie category
self.assertEqual(stats[ItemCategory.Movie][ShelfType.WISHLIST], 0)
self.assertEqual(stats[ItemCategory.Movie][ShelfType.PROGRESS], 1)
self.assertEqual(stats[ItemCategory.Movie][ShelfType.COMPLETE], 0)
self.assertEqual(stats[ItemCategory.Movie][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.Movie]["reviewed"], 1)
# Verify expected counts for TV category
self.assertEqual(stats[ItemCategory.TV][ShelfType.WISHLIST], 0)
self.assertEqual(stats[ItemCategory.TV][ShelfType.PROGRESS], 1)
self.assertEqual(stats[ItemCategory.TV][ShelfType.COMPLETE], 0)
self.assertEqual(stats[ItemCategory.TV][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.TV]["reviewed"], 0)
# Verify expected counts for game category
self.assertEqual(stats[ItemCategory.Game][ShelfType.WISHLIST], 0)
self.assertEqual(stats[ItemCategory.Game][ShelfType.PROGRESS], 0)
self.assertEqual(stats[ItemCategory.Game][ShelfType.COMPLETE], 1)
self.assertEqual(stats[ItemCategory.Game][ShelfType.DROPPED], 0)
self.assertEqual(stats[ItemCategory.Game]["reviewed"], 1)
def test_get_stats_with_filter(self):
"""Test ShelfManager.get_stats() with a filter"""
q1 = q_owned_piece_visible_to_user(None, self.identity)
stats1 = self.shelf_manager.get_stats(q=q1)
self.assertEqual(stats1[ItemCategory.Book][ShelfType.WISHLIST], 0)
self.assertEqual(stats1[ItemCategory.Book]["reviewed"], 1)
self.assertEqual(stats1[ItemCategory.Movie]["reviewed"], 0)
self.assertEqual(stats1[ItemCategory.Game]["reviewed"], 0)
# Create a second user to make sure filtering works
user2 = User.register(email="user2@example.com", username="testuser2")
user2.identity.follow(self.user.identity, True)
q2 = q_owned_piece_visible_to_user(user2, self.identity)
stats2 = self.shelf_manager.get_stats(q=q2)
self.assertEqual(stats2[ItemCategory.Book][ShelfType.WISHLIST], 1)
self.assertEqual(stats2[ItemCategory.Book]["reviewed"], 1)
self.assertEqual(stats2[ItemCategory.Movie]["reviewed"], 1)
self.assertEqual(stats2[ItemCategory.Game]["reviewed"], 0)

View file

@ -57,6 +57,7 @@ def profile(request: AuthedHttpRequest, user_name):
ItemCategory.Game,
ItemCategory.Performance,
]
stats = target.shelf_manager.get_stats()
for category in visbile_categories:
shelf_list[category] = {}
for shelf_type in ShelfType:
@ -69,7 +70,7 @@ def profile(request: AuthedHttpRequest, user_name):
).filter(qv)
shelf_list[category][shelf_type] = {
"title": label,
"count": members.count(),
"count": stats[category][shelf_type],
"members": members[:10].prefetch_related("item"),
}
reviews = (
@ -79,7 +80,7 @@ def profile(request: AuthedHttpRequest, user_name):
)
shelf_list[category]["reviewed"] = {
"title": target.shelf_manager.get_label("reviewed", category),
"count": reviews.count(),
"count": stats[category].get("reviewed", 0),
"members": reviews[:10].prefetch_related("item"),
}
collections = Collection.objects.filter(qv).order_by("-created_time")

View file

@ -24,10 +24,9 @@
{{ site_name }} is flourishing because of collaborations and contributions from users like you. Please read our <a href="/pages/rules">rules</a>, and feel free to <a href="{{ support_link }}">contact us</a> if you have any question or feedback.
{% endblocktrans %}
</p>
<form action="{{ request.session.next_url | default:'/' }}" method="post">
{% csrf_token %}
<input type="submit" value="{% trans 'Accept' %}">
</form>
<input type="submit"
value="{% trans 'Accept' %}"
onclick="location='{{ request.session.next_url | default:'/' }}'">
</article>
</div>
</body>

View file

@ -51,6 +51,7 @@ def login(request):
)
@require_http_methods(["POST"])
@login_required
def logout(request):
return auth_logout(request)
@ -216,7 +217,7 @@ def logout_takahe(response: HttpResponse):
def auth_logout(request):
auth.logout(request)
return logout_takahe(redirect("/"))
return logout_takahe(redirect(request.GET.get("next", "/")))
def initiate_user_deletion(user):