Move sets to user lists.
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 15 Jul 2025 13:49:20 +0000 (15:49 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 15 Jul 2025 13:49:20 +0000 (15:49 +0200)
20 files changed:
src/catalogue/models/book.py
src/catalogue/models/tag.py
src/catalogue/templates/catalogue/book_short.html [deleted file]
src/catalogue/templatetags/catalogue_tags.py
src/catalogue/views.py
src/lesmianator/models.py
src/lesmianator/views.py
src/opds/views.py
src/reporting/views.py
src/search/views.py
src/social/admin.py
src/social/api/views.py
src/social/forms.py
src/social/migrations/0020_userlist_userlistitem.py [new file with mode: 0644]
src/social/migrations/0021_move_sets.py [new file with mode: 0644]
src/social/models.py
src/social/templates/social/shelf_tags.html [deleted file]
src/social/templatetags/social_tags.py
src/social/utils.py
src/social/views.py

index 15b1d2f..0f53baf 100644 (file)
@@ -993,7 +993,7 @@ class Book(models.Model):
             return None
 
     def update_popularity(self):
             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
         try:
             pop = self.popularity
             pop.count = count
@@ -1004,17 +1004,6 @@ class Book(models.Model):
     def ridero_link(self):
         return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
 
     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)))
 
     def full_sort_key(self):
         return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
 
index 07a43f4..fb90a7b 100644 (file)
@@ -161,6 +161,8 @@ class Tag(models.Model):
 
     @staticmethod
     def get_tag_list(tag_str):
 
     @staticmethod
     def get_tag_list(tag_str):
+        from social.models import UserList
+
         if not tag_str:
             return []
         tags = []
         if not tag_str:
             return []
         tags = []
@@ -170,7 +172,10 @@ class Tag(models.Model):
         tags_splitted = tag_str.split('/')
         for name in tags_splitted:
             if category:
         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]
                 category = None
             elif name in Tag.categories_rev:
                 category = Tag.categories_rev[name]
diff --git a/src/catalogue/templates/catalogue/book_short.html b/src/catalogue/templates/catalogue/book_short.html
deleted file mode 100644 (file)
index 190bdc8..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-{% 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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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 %}
index d2298d9..ddf1a8f 100644 (file)
@@ -17,6 +17,7 @@ from catalogue.helpers import get_audiobook_tags
 from catalogue.models import Book, BookMedia, Fragment, Tag, Source
 from catalogue.constants import LICENSES
 from club.models import Membership
 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()
 
 
 register = template.Library()
 
@@ -73,7 +74,10 @@ def nice_title_from_tags(tags, related_tags):
     def split_tags(tags):
         result = {}
         for tag in tags:
     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)
         return result
 
     self = split_tags(tags)
index 3c4a55f..a56286d 100644 (file)
@@ -23,6 +23,7 @@ from club.forms import DonationStep1Form
 from club.models import Club
 from annoy.models import DynamicTextInsert
 from pdcounter import views as pdcounter_views
 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
 from wolnelektury.utils import is_ajax
 from catalogue import constants
 from catalogue import forms
@@ -211,25 +212,42 @@ class AudiobooksView(LiteratureView):
 class TaggedObjectList(BookList):
     def analyse(self):
         super().analyse()
 class TaggedObjectList(BookList):
     def analyse(self):
         super().analyse()
+
         self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
         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]
         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']
         ]
         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):
             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'])
         qs = qs.exclude(ancestor__in=qs)
         if self.is_themed:
             fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
@@ -241,6 +259,9 @@ class TaggedObjectList(BookList):
         return qs
 
     def get_suggested_tags(self, queryset):
         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 = []
         tag_ids = [t.id for t in self.ctx['tags']]
         if self.is_themed:
             related_tags = []
@@ -252,6 +273,7 @@ class TaggedObjectList(BookList):
                     containing_books,
                 ).exclude(category='set').exclude(pk__in=tag_ids)
             ))
                     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(
             if self.request.user.is_authenticated:
                 related_tags.extend(list(
                     Tag.objects.usage_for_queryset(
@@ -292,6 +314,7 @@ def object_list(request, objects, list_type='books'):
             Tag.objects.usage_for_queryset(
                 objects, counts=True
             ).exclude(category='set'))
             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(
         if request.user.is_authenticated:
             related_tag_lists.append(
                 Tag.objects.usage_for_queryset(
@@ -309,7 +332,7 @@ def object_list(request, objects, list_type='books'):
             .only('name', 'sort_key', 'category', 'slug'))
         if isinstance(objects, QuerySet):
             objects = prefetch_relations(objects, 'author')
             .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']:
     categories = split_tags(*related_tag_lists)
     suggest = []
     for c in ['set', 'author', 'epoch', 'kind', 'genre']:
@@ -324,7 +347,7 @@ def object_list(request, objects, list_type='books'):
     }
 
     template = 'catalogue/author_detail.html'
     }
 
     template = 'catalogue/author_detail.html'
-        
+
     return render(
         request, template, result,
     )
     return render(
         request, template, result,
     )
index fbcee04..bcf271c 100644 (file)
@@ -16,7 +16,8 @@ from django.contrib.contenttypes.fields import GenericForeignKey
 from django.conf import settings
 from django.urls import reverse
 
 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):
 
 
 class Poem(models.Model):
@@ -138,17 +139,16 @@ class Continuations(models.Model):
                       conts)
 
     @classmethod
                       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}
         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:
         try:
             obj = cls.objects.get(content_type=object_type, object_id=sth.id)
             if not obj.pickle:
@@ -162,8 +162,8 @@ class Continuations(models.Model):
         except cls.DoesNotExist:
             if isinstance(sth, Book):
                 conts = cls.for_book(sth)
         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')
 
             else:
                 raise NotImplementedError('Lesmianator continuations: only Book and Tag supported')
 
index 938ba4c..0efeba3 100644 (file)
@@ -6,13 +6,14 @@ from django.shortcuts import render, get_object_or_404
 from django.views.decorators import cache
 
 from catalogue.utils import get_random_hash
 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]
 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,
 
     return render(
         request,
@@ -50,10 +51,10 @@ def poem_from_book(request, slug):
 @cache.never_cache
 def poem_from_set(request, shelf):
     user = request.user if request.user.is_authenticated else None
 @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)
     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()
 
     p.created_from = json.dumps([b.id for b in books])
     p.save()
 
index 38f34ef..561eb99 100644 (file)
@@ -17,6 +17,7 @@ from django.utils.functional import lazy
 from basicauth import logged_in_or_basicauth, factory_decorator
 from catalogue.models import Book, Tag
 from search.utils import UnaccentSearchQuery, UnaccentSearchVector
 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
 
 import operator
 import logging
@@ -318,7 +319,7 @@ class UserFeed(Feed):
         return "Półki użytkownika %s" % user.username
 
     def items(self, user):
         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
 
     def item_title(self, item):
         return item.name
@@ -343,10 +344,10 @@ class UserSetFeed(AcquisitionFeed):
         return "Spis utworów na stronie http://WolneLektury.pl"
 
     def get_object(self, request, slug):
         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):
 
     def items(self, tag):
-        return Book.tagged.with_any([tag])
+        return tag.get_books()
 
 
 @piwik_track
 
 
 @piwik_track
index 553371e..eb64746 100644 (file)
@@ -60,7 +60,7 @@ def stats_page(request):
         ]
         etags.append(d)
 
         ]
         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,
         
     return render(request, 'reporting/main.html', {
         'media_types': media_types,
index e439c41..459190d 100644 (file)
@@ -9,6 +9,7 @@ from sorl.thumbnail import get_thumbnail
 
 import catalogue.models
 import infopages.models
 
 import catalogue.models
 import infopages.models
+import social.models
 from .forms import SearchFilters
 import re
 import json
 from .forms import SearchFilters
 import re
 import json
@@ -53,8 +54,8 @@ def hint(request, mozhint=False, param='term'):
             for author in authors[:limit - len(data)]
         ])
     if request.user.is_authenticated and len(data) < limit:
             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',
         data.extend([
             {
                 'type': 'set',
index ac5fbd2..9d1ee45 100644 (file)
@@ -6,7 +6,7 @@ from django.forms import ModelForm
 from django.forms.widgets import TextInput
 from admin_ordering.admin import OrderableAdmin
 from social.models import Cite, BannerGroup, Carousel, CarouselItem
 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:
 
 class CiteForm(ModelForm):
     class Meta:
@@ -79,3 +79,5 @@ class CarouselAdmin(admin.ModelAdmin):
 
 admin.site.register(Carousel, CarouselAdmin)
 
 
 admin.site.register(Carousel, CarouselAdmin)
 
+
+admin.site.register(models.UserList)
index c58efd1..4f4937b 100644 (file)
@@ -4,7 +4,8 @@
 from datetime import datetime
 from pytz import utc
 from django.http import Http404
 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 rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
 from rest_framework.response import Response
 from rest_framework import serializers
@@ -15,7 +16,6 @@ from catalogue.api.helpers import order_books, books_after
 from catalogue.api.serializers import BookSerializer
 from catalogue.models import Book
 import catalogue.models
 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
 
 from social.views import get_sets_for_book_ids
 from social import models
 
@@ -32,9 +32,9 @@ class LikeView(APIView):
         book = get_object_or_404(Book, slug=slug)
         action = request.query_params.get('action', 'like')
         if action == 'like':
         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':
         elif action == 'unlike':
-            book.unlike(request.user)
+            models.UserList.unlike(request.user, book)
         return Response({})
 
 
         return Response({})
 
 
@@ -48,12 +48,12 @@ class LikeView2(APIView):
 
     def put(self, request, slug):
         book = get_object_or_404(Book, slug=slug)
 
     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)
         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)})
 
 
         return Response({"likes": likes(request.user, book)})
 
 
@@ -77,49 +77,50 @@ class MyLikesView(APIView):
     permission_classes = [IsAuthenticated]
 
     def get(self, request):
     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):
     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):
 
     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:
 
     class Meta:
-        model = catalogue.models.Tag
+        model = models.UserList
         fields = ['name', 'slug', 'books']
         read_only_fields = ['slug']
 
     def create(self, validated_data):
         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']:
         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):
         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']:
         for book in validated_data['books']:
-            catalogue.models.Tag.objects.add_tag(book, instance)
+            instance.append(instance)
         return instance
 
 class UserListBooksSerializer(UserListSerializer):
     class Meta:
         return instance
 
 class UserListBooksSerializer(UserListSerializer):
     class Meta:
-        model = catalogue.models.Tag
+        model = models.UserList
         fields = ['books']
 
 
         fields = ['books']
 
 
@@ -130,7 +131,11 @@ class ListsView(ListCreateAPIView):
     serializer_class = UserListSerializer
 
     def get_queryset(self):
     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)
 
     def perform_create(self, serializer):
         serializer.save(user=self.request.user)
@@ -143,7 +148,10 @@ class ListView(RetrieveUpdateDestroyAPIView):
     serializer_class = UserListSerializer
 
     def get_object(self):
     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)
 
     def perform_update(self, serializer):
         serializer.save(user=self.request.user)
@@ -153,18 +161,25 @@ class ListView(RetrieveUpdateDestroyAPIView):
         serializer.is_valid(raise_exception=True)
         instance = self.get_object()
         for book in serializer.validated_data['books']:
         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)
 
         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):
 
 @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)
         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)
 
 
         return Response(UserListSerializer(instance).data)
 
 
@@ -182,7 +197,7 @@ class ShelfView(ListAPIView):
         after = self.request.query_params.get('after')
         count = int(self.request.query_params.get('count', 50))
         if state == 'likes':
         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)
         else:
             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
                 .values_list('book_id', flat=True)
index f82e27c..4d39f08 100644 (file)
@@ -3,16 +3,8 @@
 #
 from django import forms
 
 #
 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):
 
 
 class AddSetForm(forms.Form):
@@ -22,18 +14,18 @@ class AddSetForm(forms.Form):
     def save(self, user):
         name = self.cleaned_data['name'].strip()
         if not name: return
     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:
         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
 
         except:
             pass
 
-        return book, tag
+        return book, ul
 
 
 class RemoveSetForm(forms.Form):
 
 
 class RemoveSetForm(forms.Form):
@@ -43,8 +35,8 @@ class RemoveSetForm(forms.Form):
     def save(self, user):
         slug = self.cleaned_data['slug']
         try:
     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 = Book.objects.get(id=self.cleaned_data['book'])
@@ -52,8 +44,8 @@ class RemoveSetForm(forms.Form):
             return
 
         try:
             return
 
         try:
-            book.tag_relations.filter(tag=tag).delete()
+            ul.userlistitem_set.filter(book=book).delete()
         except:
             pass
 
         except:
             pass
 
-        return book, tag
+        return book, ul
diff --git a/src/social/migrations/0020_userlist_userlistitem.py b/src/social/migrations/0020_userlist_userlistitem.py
new file mode 100644 (file)
index 0000000..f97f833
--- /dev/null
@@ -0,0 +1,48 @@
+# 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')),
+            ],
+        ),
+    ]
diff --git a/src/social/migrations/0021_move_sets.py b/src/social/migrations/0021_move_sets.py
new file mode 100644 (file)
index 0000000..64e9ccc
--- /dev/null
@@ -0,0 +1,56 @@
+# 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
+        )
+    ]
index c63879b..dc9a09a 100644 (file)
@@ -9,6 +9,7 @@ from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.core.mail import send_mail
 from django.urls import reverse
 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
 
 from catalogue.models import Book
 from wolnelektury.utils import cached_render, clear_cached_renders
 
@@ -252,3 +253,125 @@ class Progress(models.Model):
                 self.implicit_text_percent = 60
                 self.implicit_text_anchor = 'f20'
         return super().save(*args, **kwargs)
                 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)
diff --git a/src/social/templates/social/shelf_tags.html b/src/social/templates/social/shelf_tags.html
deleted file mode 100644 (file)
index 47e0b0a..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-{% 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
index a4b0f3e..ae7c5a0 100644 (file)
@@ -3,7 +3,6 @@
 #
 import re
 from django import template
 #
 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
 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
@@ -32,25 +31,6 @@ def choose_cites(number, book=None, author=None):
         return Fragment.tagged.with_all([author]).order_by('?')[:number]
 
 
         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()
 @register.inclusion_tag('social/carousel.html', takes_context=True)
 def carousel(context, placement):
     banners = Carousel.get(placement).carouselitem_set.all()#first().get_banner()
index 6ab303a..f4d9445 100644 (file)
@@ -7,10 +7,9 @@ from random import randint
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.utils.functional import lazy
 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.models import Cite
+from social import models
 
 
 def likes(user, work, request=None):
 
 
 def likes(user, work, request=None):
@@ -18,7 +17,7 @@ def likes(user, work, request=None):
         return False
 
     if request is 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 not hasattr(request, 'social_likes'):
         # tuple: unchecked, checked, liked
@@ -35,54 +34,15 @@ def likes(user, work, request=None):
             if likes_t[0]:
                 ids = tuple(likes_t[0])
                 likes_t[0].clear()
             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)()
 
 
                 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))
 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))
index 3dfcd9e..e3d60ca 100644 (file)
@@ -8,8 +8,7 @@ from django.views.decorators.cache import never_cache
 from django.views.decorators.http import require_POST
 from django.views.generic.edit import FormView
 
 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
 
 from social import forms, models
 from wolnelektury.utils import is_ajax
 
@@ -25,7 +24,7 @@ def like_book(request, slug):
         return HttpResponseForbidden('Login required.')
     book = get_object_or_404(Book, slug=slug)
 
         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})
 
     if is_ajax(request):
         return JsonResponse({"success": True, "msg": "ok", "like": True})
@@ -56,7 +55,7 @@ def unlike_book(request, slug):
         return HttpResponseForbidden('Login required.')
     book = get_object_or_404(Book, slug=slug)
 
         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})
 
     if is_ajax(request):
         return JsonResponse({"success": True, "msg": "ok", "like": False})
@@ -67,32 +66,29 @@ def unlike_book(request, slug):
 @login_required
 def my_shelf(request):
     template_name = 'social/my_shelf.html'
 @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, {
         
     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 = {}
         '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:
     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({
             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:
             })
     for b in book_ids:
         if b not in data:
@@ -117,12 +113,12 @@ def my_liked(request):
 @login_required
 def my_tags(request):
     term = request.GET.get('term', '')
 @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:
     if term:
-        tags = tags.filter(name__icontains=term)
+        ulists = tags.filter(name__icontains=term)
     return JsonResponse(
         [
     return JsonResponse(
         [
-            t.name for t in tags
+            ul.name for ul in ulists
         ], safe=False
     )
 
         ], safe=False
     )