Merge branch 'main' of https://github.com/neodb-social/neodb
This commit is contained in:
commit
576e1e0c30
9 changed files with 225 additions and 16 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
162
journal/tests/shelf.py
Normal 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)
|
|
@ -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")
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue