return None
def update_popularity(self):
- count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
+ count = self.userlistitem_set.values('list__user').order_by('list__user').distinct().count()
try:
pop = self.popularity
pop.count = count
def ridero_link(self):
return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
- def like(self, user):
- from social.utils import likes, get_set, set_sets
- if not likes(user, self):
- tag = get_set(user, '')
- set_sets(user, self, [tag])
-
- def unlike(self, user):
- from social.utils import likes, set_sets
- if likes(user, self):
- set_sets(user, self, [])
-
def full_sort_key(self):
return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
@staticmethod
def get_tag_list(tag_str):
+ from social.models import UserList
+
if not tag_str:
return []
tags = []
tags_splitted = tag_str.split('/')
for name in tags_splitted:
if category:
- tags.append(Tag.objects.get(slug=name, category=category))
+ if category == 'set':
+ tags.append(UserList.objects.get(slug=name, deleted=False))
+ else:
+ tags.append(Tag.objects.get(slug=name, category=category))
category = None
elif name in Tag.categories_rev:
category = Tag.categories_rev[name]
+++ /dev/null
-{% spaceless %}
- {% load i18n %}
- {% load thumbnail %}
- {% load cache %}
- {% load catalogue_tags %}
- {% load book_shelf_tags from social_tags %}
- {% load static %}
-
- {% with ga=book.get_audiobooks %}
- {% with audiobooks=ga.0 %}
- <div class="{% block box-class %}book-box{% if audiobooks %} audiobook-box{% endif %}{% endblock %}">
- <div class="book-box-inner">
-
- {% with book.tags_by_category as tags %}
- <div class="book-left-column">
- <div class="book-box-body">
- {% block book-box-body-pre %}
- {% endblock %}
-
- <div class="cover-area">
- {% if book.cover_clean %}
- <a href="{% block cover-link %}{{ book.get_absolute_url }}{% endblock %}">
- <img src="{% thumbnail book.cover_clean '139x193' as th %}{{ th.url }}{% endthumbnail %}" alt="Cover" class="cover" />
- </a>
- {% endif %}
- {% block cover-area-extra %}{% endblock %}
- </div>
-
- {% get_current_language as LANGUAGE_CODE %}
- {% cache 86400 book_box_head_tags book.pk LANGUAGE_CODE %}
- <div class="book-box-head">
- <div class="author">
- {% for tag in tags.author %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>{% if not forloop.last %},
- {% endif %}{% endfor %}{% for parent in book.parents %},
- <a href="{{ parent.get_absolute_url }}">{{ parent.title }}</a>{% endfor %}
- </div>
- <div class="title">
- <a href="{{ book.get_absolute_url }}">{{ book.title }}</a>
- </div>
- {% if book.translator %}
- <div class="author">
- tłum. {{ book.translator }}
- </div>
- {% endif %}
- </div>
-
- <div class="tags">
- <span class="category">
- <span class="mono"> {% trans "Epoka" %}:</span> <span class="book-box-tag">
- {% for tag in tags.epoch %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
- {% if not forloop.last %}<span>, </span>{% endif %}
- {% endfor %}
- </span></span>
-
- <span class="category">
- <span class="mono"> {% trans "Rodzaj" %}:</span> <span class="book-box-tag">
- {% for tag in tags.kind %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
- {% if not forloop.last %}<span>, </span>{% endif %}
- {% endfor %}
- </span></span>
-
- <span class="category">
- <span class="mono"> {% trans "Gatunek" %}:</span> <span class="book-box-tag">
- {% for tag in tags.genre %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
- {% if not forloop.last %}<span>, </span>{% endif %}
- {% endfor %}
- </span></span>
-
- {% with extra_info=book.get_extra_info_json %}
- {% if extra_info.location %}
- <span class="category">
- <span class="mono"> {% trans "Region" %}:</span> <span class="book-box-tag">
- {{ extra_info.location }}
- </span></span>
- {% endif %}
- {% endwith %}
-
- {% if book.is_foreign %}
- <span class="category">
- <span class="mono"> {% trans "Język" %}:</span> <span class="book-box-tag">
- <a>{{ book.language_name }}</a>
- </span>
- </span>
- {% endif %}
-
- {% with stage_note=book.stage_note %}
- {% if stage_note.0 %}
- <br>
- <span class="category">
- <a{% if stage_note.1 %} href="{{ stage_note.1 }}"{% endif %}>{{ stage_note.0 }}</a>
- </span>
- {% endif %}
- {% endwith %}
- </div>
- {% endcache %}
- </div>
- {% book_shelf_tags book.pk %}
-
- {% cache 86400 book_box_tools book.pk book|status:request.user LANGUAGE_CODE %}
- {% if book|status:request.user != 'closed' %}
- <ul class="book-box-tools">
- <li class="book-box-read">
- {% if book.html_file %}
- <div>{% content_warning book %}</div>
- <a href="{% url 'book_text' book.slug %}" class="downarrow">{% trans "Czytaj online" %}</a>
- {% endif %}
- {% if book.print_on_demand %}
- <a href="{{ book.ridero_link }}" class="downarrow print tlite-tooltip" title="{% trans "Cena książki w druku cyfrowym jest zależna od liczby stron.<br>Przed zakupem upewnij się, że cena druku na żądanie jest dla Ciebie odpowiednia.<br>Wszystkie nasze zasoby w wersji elektronicznej są zawsze dostępne bezpłatnie." %}">{% trans "Druk na żądanie z" %}
- <img src="{% static 'img/ridero.png' %}" style="height: 0.8em;"/></a>
- {% endif %}
- </li>
- <li class="book-box-download">
- <div class="book-box-formats">
- {% trans "Pobierz ebook" %}:<br>
- {% if book.pdf_file %}
- <a href="{{ book.pdf_url}}">PDF</a>
- {% endif %}
- {% if book.epub_file %}
- <a href="{{ book.epub_url}}">EPUB</a>
- {% endif %}
- {% if book.mobi_file %}
- <a href="{{ book.mobi_url}}">MOBI</a>
- {% endif %}
- {% if book.fb2_file %}
- <a href="{{ book.fb2_url}}">FB2</a>
- {% endif %}
- {% if book.txt_file %}
- <a href="{{ book.txt_url}}">TXT</a>
- {% endif %}
- </div>
- {% if book.has_mp3_file %}
- <div class="book-box-formats">
- {% trans "Pobierz audiobook" %}:<br>
- {% download_audio book %}
- </div>
- {% endif %}
- <div class="book-box-formats">
- {% custom_pdf_link_li book %}
- </div>
- </li>
- </ul>
- {% else %}
- {% block preview-info %}
- <p class="book-box-tools book-box-tools-warn">
- Ten utwór jest na razie dostępny wyłącznie dla naszych Darczyńców.
- <a href="{% url 'club_join' %}">Wspieraj Wolne Lektury</a>
- </p>
- <div>{% content_warning book %}</div>
- {% endblock %}
- {% endif %}
- {% endcache %}
- {% block book-box-extra-info %}{% endblock %}
- {% block box-append %}{% endblock %}
- </div>
- {% endwith %}
-
- {% if book.abstract %}
- <div class="abstract more-expand">
- {{ book.abstract|safe }}
- </div>
- {% endif %}
-
- <div class="clearboth"></div>
- </div>
- </div>
- {% endwith %}
- {% endwith %}
-{% endspaceless %}
from catalogue.models import Book, BookMedia, Fragment, Tag, Source
from catalogue.constants import LICENSES
from club.models import Membership
+from social.models import UserList
register = template.Library()
def split_tags(tags):
result = {}
for tag in tags:
- result.setdefault(tag.category, []).append(tag)
+ if isinstance(tag, UserList):
+ result.setdefault('userlist', []).append(tag)
+ else:
+ result.setdefault(tag.category, []).append(tag)
return result
self = split_tags(tags)
from club.models import Club
from annoy.models import DynamicTextInsert
from pdcounter import views as pdcounter_views
+from social.models import UserList
from wolnelektury.utils import is_ajax
from catalogue import constants
from catalogue import forms
class TaggedObjectList(BookList):
def analyse(self):
super().analyse()
+
self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
- self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
- self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
+ self.ctx.update({
+ 'fragment_tags': [],
+ 'work_tags': [],
+ 'user_lists': [],
+ })
+ for tag in self.ctx['tags']:
+ if isinstance(tag, UserList):
+ self.ctx['user_lists'].append(tag)
+ elif tag.category == 'theme':
+ self.ctx['fragment_tags'].append(tag)
+ else:
+ self.ctx['work_tags'].append(tag)
+
self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
if self.is_themed:
self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
- elif self.ctx['tags']:
- self.ctx['main_tag'] = self.ctx['tags'][0]
+ elif self.ctx['work_tags']:
+ self.ctx['main_tag'] = self.ctx['work_tags'][0]
else:
self.ctx['main_tag'] = None
self.ctx['filtering_tags'] = [
t for t in self.ctx['tags']
if t is not self.ctx['main_tag']
]
- if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
+ if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None and self.ctx['main_tag'].category == 'author':
self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
def get_queryset(self):
- qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
+ if self.ctx['work_tags']:
+ qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
+ else:
+ qs = Book.objects.filter(findable=True)
+ for ul in self.ctx['user_lists']:
+ qs = qs.filter(id__in=[i.id for i in ul.get_books()])
qs = qs.exclude(ancestor__in=qs)
if self.is_themed:
fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
return qs
def get_suggested_tags(self, queryset):
+ if self.ctx['user_lists']:
+ # TODO
+ return []
tag_ids = [t.id for t in self.ctx['tags']]
if self.is_themed:
related_tags = []
containing_books,
).exclude(category='set').exclude(pk__in=tag_ids)
))
+ ### FIXME: These won't be tags
if self.request.user.is_authenticated:
related_tags.extend(list(
Tag.objects.usage_for_queryset(
Tag.objects.usage_for_queryset(
objects, counts=True
).exclude(category='set'))
+ ### FIXME: these won't be tags
if request.user.is_authenticated:
related_tag_lists.append(
Tag.objects.usage_for_queryset(
.only('name', 'sort_key', 'category', 'slug'))
if isinstance(objects, QuerySet):
objects = prefetch_relations(objects, 'author')
-
+
categories = split_tags(*related_tag_lists)
suggest = []
for c in ['set', 'author', 'epoch', 'kind', 'genre']:
}
template = 'catalogue/author_detail.html'
-
+
return render(
request, template, result,
)
from django.conf import settings
from django.urls import reverse
-from catalogue.models import Book, Tag
+from catalogue.models import Book
+from social.models import UserList
class Poem(models.Model):
conts)
@classmethod
- def for_set(cls, tag):
- books = Book.tagged_top_level([tag])
- cont_tabs = (cls.get(b) for b in books.iterator())
+ def for_userlist(cls, ul):
+ cont_tabs = (cls.get(b) for b in ul.get_books())
return reduce(cls.join_conts, cont_tabs)
@classmethod
def get(cls, sth):
object_type = ContentType.objects.get_for_model(sth)
should_keys = {sth.id}
- if isinstance(sth, Tag):
- should_keys = set(b.pk for b in Book.tagged.with_any((sth,)).iterator())
+ if isinstance(sth, UserList):
+ should_keys = set(b.pk for b in sth.get_books())
try:
obj = cls.objects.get(content_type=object_type, object_id=sth.id)
if not obj.pickle:
except cls.DoesNotExist:
if isinstance(sth, Book):
conts = cls.for_book(sth)
- elif isinstance(sth, Tag):
- conts = cls.for_set(sth)
+ elif isinstance(sth, UserList):
+ conts = cls.for_userlist(sth)
else:
raise NotImplementedError('Lesmianator continuations: only Book and Tag supported')
from django.views.decorators import cache
from catalogue.utils import get_random_hash
-from catalogue.models import Book, Tag
+from catalogue.models import Book
+from social.models import UserList
from lesmianator.models import Poem, Continuations
def main_page(request):
last = Poem.objects.all().order_by('-created_at')[:10]
- shelves = Tag.objects.filter(user__username='lesmianator')
+ shelves = UserList.objects.filter(user__username='lesmianator')
return render(
request,
@cache.never_cache
def poem_from_set(request, shelf):
user = request.user if request.user.is_authenticated else None
- tag = get_object_or_404(Tag, category='set', slug=shelf)
+ tag = get_object_or_404(UserList, slug=shelf)
text = Poem.write(Continuations.get(tag))
p = Poem(slug=get_random_hash(text), text=text, created_by=user)
- books = Book.tagged.with_any((tag,))
+ books = tag.get_books()
p.created_from = json.dumps([b.id for b in books])
p.save()
from basicauth import logged_in_or_basicauth, factory_decorator
from catalogue.models import Book, Tag
from search.utils import UnaccentSearchQuery, UnaccentSearchVector
+from social.models import UserList
import operator
import logging
return "Półki użytkownika %s" % user.username
def items(self, user):
- return Tag.objects.filter(category='set', user=user).exclude(items=None)
+ return UserList.objects.filter(user=user, deleted=False)
def item_title(self, item):
return item.name
return "Spis utworów na stronie http://WolneLektury.pl"
def get_object(self, request, slug):
- return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
+ return get_object_or_404(UserList, deleted=False, slug=slug, user=request.user)
def items(self, tag):
- return Book.tagged.with_any([tag])
+ return tag.get_books()
@piwik_track
]
etags.append(d)
- unused_tags = Tag.objects.exclude(category='set').filter(items=None, book=None)
+ unused_tags = Tag.objects.filter(items=None, book=None)
return render(request, 'reporting/main.html', {
'media_types': media_types,
import catalogue.models
import infopages.models
+import social.models
from .forms import SearchFilters
import re
import json
for author in authors[:limit - len(data)]
])
if request.user.is_authenticated and len(data) < limit:
- tags = catalogue.models.Tag.objects.filter(
- category='set', user=request.user, name_pl__iregex='\m' + prefix).only('name', 'id', 'slug', 'category')
+ tags = social.models.UserList.objects.filter(
+ user=request.user, name__iregex='\m' + prefix).only('name', 'id', 'slug')
data.extend([
{
'type': 'set',
from django.forms.widgets import TextInput
from admin_ordering.admin import OrderableAdmin
from social.models import Cite, BannerGroup, Carousel, CarouselItem
-
+from social import models
class CiteForm(ModelForm):
class Meta:
admin.site.register(Carousel, CarouselAdmin)
+
+admin.site.register(models.UserList)
from datetime import datetime
from pytz import utc
from django.http import Http404
-from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, DestroyAPIView, get_object_or_404
+from django.utils.timezone import now
+from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework import serializers
from catalogue.api.serializers import BookSerializer
from catalogue.models import Book
import catalogue.models
-from social.utils import likes, get_set
from social.views import get_sets_for_book_ids
from social import models
book = get_object_or_404(Book, slug=slug)
action = request.query_params.get('action', 'like')
if action == 'like':
- book.like(request.user)
+ models.UserList.like(request.user, book)
elif action == 'unlike':
- book.unlike(request.user)
+ models.UserList.unlike(request.user, book)
return Response({})
def put(self, request, slug):
book = get_object_or_404(Book, slug=slug)
- book.like(request.user)
+ models.UserList.like(request.user, book)
return Response({"likes": likes(request.user, book)})
def delete(self, request, slug):
book = get_object_or_404(Book, slug=slug)
- book.unlike(request.user)
+ models.UserList.unlike(request.user, book)
return Response({"likes": likes(request.user, book)})
permission_classes = [IsAuthenticated]
def get(self, request):
- ids = catalogue.models.tag.TagRelation.objects.filter(tag__user=request.user).values_list('object_id', flat=True).distinct()
- books = Book.objects.filter(id__in=ids)
- books = {b.id: b.slug for b in books}
- res = get_sets_for_book_ids(ids, request.user)
- res = {books[bid]: v for bid, v in res.items()}
-
- res = list(books.values())
- res.sort()
- return Response(res)
+ ul = models.UserList.get_favorites_list(request.user)
+ if ul is None:
+ return Response([])
+ return Response(
+ ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
+ )
-class TaggedBooksField(serializers.Field):
+class UserListItemsField(serializers.Field):
def to_representation(self, value):
- return catalogue.models.Book.tagged.with_all([value]).values_list('slug', flat=True)
+ return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
def to_internal_value(self, value):
return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
class UserListSerializer(serializers.ModelSerializer):
- books = TaggedBooksField(source='*')
+ books = UserListItemsField(source='*')
class Meta:
- model = catalogue.models.Tag
+ model = models.UserList
fields = ['name', 'slug', 'books']
read_only_fields = ['slug']
def create(self, validated_data):
- instance = get_set(validated_data['user'], validated_data['name'])
- catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete()
+ instance = models.UserList.get_by_name(
+ validated_data['user'],
+ validated_data['name'],
+ create=True
+ )
+ instance.userlistitem_set.all().delete()
for book in validated_data['books']:
- catalogue.models.Tag.objects.add_tag(book, instance)
+ instance.append(book)
return instance
def update(self, instance, validated_data):
- catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete()
+ instance.userlistitem_set.all().delete()
for book in validated_data['books']:
- catalogue.models.Tag.objects.add_tag(book, instance)
+ instance.append(instance)
return instance
class UserListBooksSerializer(UserListSerializer):
class Meta:
- model = catalogue.models.Tag
+ model = models.UserList
fields = ['books']
serializer_class = UserListSerializer
def get_queryset(self):
- return catalogue.models.Tag.objects.filter(user=self.request.user).exclude(name='')
+ return models.UserList.objects.filter(
+ user=self.request.user,
+ favorites=False,
+ deleted=False
+ )
def perform_create(self, serializer):
serializer.save(user=self.request.user)
serializer_class = UserListSerializer
def get_object(self):
- return get_object_or_404(catalogue.models.Tag, slug=self.kwargs['slug'], user=self.request.user)
+ return get_object_or_404(
+ models.UserList,
+ slug=self.kwargs['slug'],
+ user=self.request.user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
serializer.is_valid(raise_exception=True)
instance = self.get_object()
for book in serializer.validated_data['books']:
- catalogue.models.Tag.objects.add_tag(book, instance)
+ instance.append(book)
return Response(self.get_serializer(instance).data)
+ def perform_destroy(self, instance):
+ instance.update(
+ deleted=True,
+ updated_at=now()
+ )
+
@never_cache
class ListItemView(APIView):
permission_classes = [IsAuthenticated]
def delete(self, request, slug, book):
- instance = get_object_or_404(catalogue.models.Tag, slug=slug, user=self.request.user)
+ instance = get_object_or_404(
+ models.UserList, slug=slug, user=self.request.user)
book = get_object_or_404(catalogue.models.Book, slug=book)
- catalogue.models.Tag.objects.remove_tag(book, instance)
+ instance.remove(book=book)
return Response(UserListSerializer(instance).data)
after = self.request.query_params.get('after')
count = int(self.request.query_params.get('count', 50))
if state == 'likes':
- books = Book.tagged.with_any(self.request.user.tag_set.all())
+ books = Book.objects.filter(userlistitem__list__user=self.request.user)
else:
ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
.values_list('book_id', flat=True)
#
from django import forms
-from catalogue.models import Book, Tag
-from social.utils import get_set
-
-
-class UserSetsForm(forms.Form):
- def __init__(self, book, user, *args, **kwargs):
- super(UserSetsForm, self).__init__(*args, **kwargs)
- self.fields['set_ids'] = forms.ChoiceField(
- choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user).iterator()],
- )
+from catalogue.models import Book
+from . import models
class AddSetForm(forms.Form):
def save(self, user):
name = self.cleaned_data['name'].strip()
if not name: return
- tag = get_set(user, name)
+ ul = models.UserList.get_by_name(user, name, create=True)
try:
book = Book.objects.get(id=self.cleaned_data['book'])
except Book.DoesNotExist:
return
try:
- book.tag_relations.create(tag=tag)
+ ul.append(book=book)
except:
pass
- return book, tag
+ return book, ul
class RemoveSetForm(forms.Form):
def save(self, user):
slug = self.cleaned_data['slug']
try:
- tag = Tag.objects.get(user=user, slug=slug)
- except Tag.DoesNotExist:
+ ul = models.UserList.objects.get(user=user, slug=slug)
+ except models.UserList.DoesNotExist:
return
try:
book = Book.objects.get(id=self.cleaned_data['book'])
return
try:
- book.tag_relations.filter(tag=tag).delete()
+ ul.userlistitem_set.filter(book=book).delete()
except:
pass
- return book, tag
+ return book, ul
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-14 13:39
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('bookmarks', '0002_quote'),
+ ('social', '0019_progress_deleted'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserList',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('slug', models.SlugField(unique=True)),
+ ('name', models.CharField(max_length=1024)),
+ ('favorites', models.BooleanField(default=False)),
+ ('public', models.BooleanField(default=False)),
+ ('deleted', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField()),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UserListItem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order', models.IntegerField()),
+ ('deleted', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField()),
+ ('note', models.TextField(blank=True)),
+ ('book', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalogue.book')),
+ ('bookmark', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookmarks.bookmark')),
+ ('fragment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalogue.fragment')),
+ ('list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='social.userlist')),
+ ('quote', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookmarks.quote')),
+ ],
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-14 13:40
+
+from django.db import migrations
+from django.utils.timezone import now
+
+
+def move_sets_to_userlists(apps, schema_editor):
+ UserList = apps.get_model('social', 'UserList')
+ UserListItem = apps.get_model('social', 'UserListItem')
+ Tag = apps.get_model('catalogue', 'Tag')
+
+ for tag in Tag.objects.filter(category='set'):
+ print()
+ print(tag)
+ ul = UserList.objects.create(
+ slug=tag.slug,
+ user=tag.user,
+ name=tag.name,
+ favorites=not tag.name,
+ public=not tag.name,
+ created_at=tag.created_at,
+ updated_at=tag.changed_at,
+ )
+
+ for i, item in enumerate(tag.items.all()):
+ #assert item.content_type_id == 12, item.content_type_id
+ print(item)
+ ul.userlistitem_set.create(
+ order=i + 1,
+ created_at=ul.updated_at,
+ updated_at=ul.updated_at,
+ book_id=item.object_id
+ )
+
+ tag.delete()
+
+
+def rollback_userlists_to_sets(apps, schema_editor):
+ UserList = apps.get_model('social', 'UserList')
+ UserListItem = apps.get_model('social', 'UserListItem')
+ Tag = apps.get_model('catalogue', 'Tag')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0020_userlist_userlistitem'),
+ ('catalogue', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ move_sets_to_userlists,
+ rollback_userlists_to_sets
+ )
+ ]
from django.core.exceptions import ValidationError
from django.core.mail import send_mail
from django.urls import reverse
+from django.utils.timezone import now
from catalogue.models import Book
from wolnelektury.utils import cached_render, clear_cached_renders
self.implicit_text_percent = 60
self.implicit_text_anchor = 'f20'
return super().save(*args, **kwargs)
+
+
+class UserList(models.Model):
+ slug = models.SlugField(unique=True)
+ user = models.ForeignKey(User, models.CASCADE)
+ name = models.CharField(max_length=1024)
+ favorites = models.BooleanField(default=False)
+ public = models.BooleanField(default=False)
+ deleted = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField()
+
+ def get_absolute_url(self):
+ return reverse(
+ 'tagged_object_list',
+ args=[f'polka/{self.slug}']
+ )
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def url_chunk(self):
+ return f'polka/{self.slug}'
+
+ @classmethod
+ def create(cls, user, name):
+ return cls.objects.create(
+ user=user,
+ name=name,
+ slug=get_random_hash(name),
+ updated_at=now()
+ )
+
+ @classmethod
+ def get_by_name(cls, user, name, create=False):
+ l = cls.objects.filter(
+ user=user,
+ name=name
+ ).first()
+ if l is None and create:
+ l = cls.create(user, name)
+ return l
+
+ @classmethod
+ def get_favorites_list(cls, user, create=False):
+ try:
+ return cls.objects.get(
+ user=user,
+ favorites=True
+ )
+ except cls.DoesNotExist:
+ if create:
+ return cls.objects.create(
+ user=user,
+ favorites=True,
+ slug=get_random_hash(name),
+ updated_at=now()
+ )
+ else:
+ return None
+ except cls.MultipleObjectsReturned:
+ # merge?
+ lists = list(cls.objects.filter(user=user, favorites=True))
+ for l in lists[1:]:
+ t.userlistitem_set.all().update(
+ list=lists[0]
+ )
+ l.delete()
+ return lists[0]
+
+ @classmethod
+ def likes(cls, user, book):
+ ls = cls.get_favorites_list(user)
+ if ls is None:
+ return False
+ return ls.userlistitem_set.filter(deleted=False, book=book).exists()
+
+ def append(self, book):
+ # TODO: check for duplicates?
+ self.userlistitem_set.create(
+ book=book,
+ order=self.userlistitem_set.aggregate(m=models.Max('order'))['m'] + 1,
+ updated_at=now(),
+ )
+ book.update_popularity()
+
+ def remove(self, book):
+ self.userlistitem_set.filter(book=book).update(
+ deleted=True,
+ updated_at=now()
+ )
+ book.update_popularity()
+
+ @classmethod
+ def like(cls, user, book):
+ ul = cls.get_favorites_list(user, create=True)
+ ul.append(book)
+
+ @classmethod
+ def unlike(cls, user, book):
+ ul = cls.get_favorites_list(user)
+ if ul is not None:
+ ul.remove(book)
+
+ def get_books(self):
+ return [item.book for item in self.userlistitem_set.exclude(deleted=True).exclude(book=None)]
+
+
+class UserListItem(models.Model):
+ list = models.ForeignKey(UserList, models.CASCADE)
+ order = models.IntegerField()
+ deleted = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField()
+
+ book = models.ForeignKey('catalogue.Book', models.SET_NULL, null=True, blank=True)
+ fragment = models.ForeignKey('catalogue.Fragment', models.SET_NULL, null=True, blank=True)
+ quote = models.ForeignKey('bookmarks.Quote', models.SET_NULL, null=True, blank=True)
+ bookmark = models.ForeignKey('bookmarks.Bookmark', models.SET_NULL, null=True, blank=True)
+
+ note = models.TextField(blank=True)
+++ /dev/null
-{% spaceless %}
- <ul class='social-shelf-tags'>
- {% for tag in tags %}
- <li><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></li>
- {% endfor %}
- </ul>
-{% endspaceless %}
\ No newline at end of file
#
import re
from django import template
-from django.utils.functional import lazy
from django.utils.cache import add_never_cache_headers
from catalogue.models import Book, Fragment
from social.utils import likes, get_or_choose_cite, choose_cite as cs
return Fragment.tagged.with_all([author]).order_by('?')[:number]
-@register.simple_tag(takes_context=True)
-def book_shelf_tags(context, book_id):
- request = context['request']
- if not request.user.is_authenticated:
- return ''
- book = Book.objects.get(pk=book_id)
- lks = likes(request.user, book, request)
-
- def get_value():
- if not lks:
- return ''
- tags = book.tags.filter(category='set', user=request.user).exclude(name='')
- if not tags:
- return ''
- ctx = {'tags': tags}
- return template.loader.render_to_string('social/shelf_tags.html', ctx)
- return lazy(get_value, str)()
-
-
@register.inclusion_tag('social/carousel.html', takes_context=True)
def carousel(context, placement):
banners = Carousel.get(placement).carouselitem_set.all()#first().get_banner()
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.functional import lazy
-from catalogue.models import Book, Tag
-from catalogue import utils
-from catalogue.tasks import touch_tag
+from catalogue.models import Book
from social.models import Cite
+from social import models
def likes(user, work, request=None):
return False
if request is None:
- return work.tags.filter(category='set', user=user).exists()
+ return models.UserList.likes(user, work)
if not hasattr(request, 'social_likes'):
# tuple: unchecked, checked, liked
if likes_t[0]:
ids = tuple(likes_t[0])
likes_t[0].clear()
- likes_t[2].update(Tag.intermediary_table_model.objects.filter(
- content_type_id=ct.pk, tag__user_id=user.pk,
- object_id__in=ids
- ).distinct().values_list('object_id', flat=True))
+ ls = models.UserList.get_favorites_list(user)
+ likes_t[2].update(
+ ls.userlistitem_set.filter(deleted=False).filter(
+ book_id__in=ids).values_list('book_id', flat=True))
likes_t[1].update(ids)
return work.pk in likes_t[2]
return lazy(_likes, bool)()
-def get_set(user, name):
- """Returns a tag for use by the user. Creates it, if necessary."""
- try:
- tag = Tag.objects.get(category='set', user=user, name=name)
- except Tag.DoesNotExist:
- tag = Tag.objects.create(
- category='set', user=user, name=name, slug=utils.get_random_hash(name), sort_key=name.lower())
- except Tag.MultipleObjectsReturned:
- # fix duplicated noname shelf
- tags = list(Tag.objects.filter(category='set', user=user, name=name))
- tag = tags[0]
- for other_tag in tags[1:]:
- for item in other_tag.items.all():
- Tag.objects.remove_tag(item, other_tag)
- Tag.objects.add_tag(item, tag)
- other_tag.delete()
- return tag
-
-
-def set_sets(user, work, sets):
- """Set tags used for given work by a given user."""
-
- old_sets = list(work.tags.filter(category='set', user=user))
-
- work.tags = sets + list(
- work.tags.filter(~Q(category='set') | ~Q(user=user)))
-
- for shelf in [shelf for shelf in old_sets if shelf not in sets]:
- touch_tag(shelf)
- for shelf in [shelf for shelf in sets if shelf not in old_sets]:
- touch_tag(shelf)
-
- # delete empty tags
- Tag.objects.filter(category='set', user=user, items=None).delete()
-
- if isinstance(work, Book):
- work.update_popularity()
-
-
def cites_for_tags(tags):
"""Returns a QuerySet with all Cites for books with given tags."""
return Cite.objects.filter(book__in=Book.tagged.with_all(tags))
from django.views.decorators.http import require_POST
from django.views.generic.edit import FormView
-from catalogue.models import Book, Tag
-import catalogue.models.tag
+from catalogue.models import Book
from social import forms, models
from wolnelektury.utils import is_ajax
return HttpResponseForbidden('Login required.')
book = get_object_or_404(Book, slug=slug)
- book.like(request.user)
+ models.UserList.like(request.user, book)
if is_ajax(request):
return JsonResponse({"success": True, "msg": "ok", "like": True})
return HttpResponseForbidden('Login required.')
book = get_object_or_404(Book, slug=slug)
- book.unlike(request.user)
+ models.UserList.unlike(request.user, book)
if is_ajax(request):
return JsonResponse({"success": True, "msg": "ok", "like": False})
@login_required
def my_shelf(request):
template_name = 'social/my_shelf.html'
- tags = list(request.user.tag_set.all())
- suggest = [t for t in tags if t.name]
- print(suggest)
+ ulists = list(request.user.userlist_set.all())
+ suggest = [t for t in ulists if t.name]
return render(request, template_name, {
- 'tags': tags,
- 'books': Book.tagged.with_any(tags),
+ 'tags': ulists,
+ 'books': Book.objects_filter(userlistitem__list__user=request.user),
'suggest': suggest,
})
def get_sets_for_book_ids(book_ids, user):
data = {}
- tagged = catalogue.models.tag.TagRelation.objects.filter(
- tag__user=user,
- #content_type= # for books,
- object_id__in=book_ids
- ).order_by('tag__sort_key')
+ tagged = models.UserListItem.objects.filter(
+ list__user=user,
+ book_id__in=book_ids
+ ).order_by('list__name')
for t in tagged:
- # related?
- item = data.setdefault(t.object_id, [])
- if t.tag.name:
+ item = data.setdefault(t.book_id, [])
+ if t.list.name:
item.append({
- "slug": t.tag.slug,
- "url": t.tag.get_absolute_url(),
- "name": t.tag.name,
+ "slug": t.list.slug,
+ "url": t.list.get_absolute_url(),
+ "name": t.list.name,
})
for b in book_ids:
if b not in data:
@login_required
def my_tags(request):
term = request.GET.get('term', '')
- tags = Tag.objects.filter(user=request.user).order_by('sort_key')
+ tags = models.UserList.objects.filter(user=request.user).order_by('name')
if term:
- tags = tags.filter(name__icontains=term)
+ ulists = tags.filter(name__icontains=term)
return JsonResponse(
[
- t.name for t in tags
+ ul.name for ul in ulists
], safe=False
)