user can track featured collections
This commit is contained in:
parent
0cf37ef0fd
commit
6a745b6dfa
10 changed files with 280 additions and 15 deletions
|
@ -114,17 +114,17 @@ div.jsoneditor-menu {
|
|||
@keyframes fadeIn {
|
||||
0% {opacity: 0;}
|
||||
100% {opacity: 1;}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {opacity: 1;}
|
||||
100% {opacity: 0;}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
0% {transform: scale(0.9);}
|
||||
100% {transform: scale(1);}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomOut {
|
||||
0% {transform: scale(1);}
|
||||
|
@ -159,3 +159,25 @@ div.jsoneditor-menu {
|
|||
#modal li, #modal ul, #modal label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.donut {
|
||||
margin-left: 20%;
|
||||
width: 60%;
|
||||
aspect-ratio : 1 / 1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.donut .hole {
|
||||
width: 80%;
|
||||
aspect-ratio : 1 / 1;
|
||||
border-radius: 50%;
|
||||
background: #f7f7f7;
|
||||
display: table;
|
||||
}
|
||||
.donut .text {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load thumb %}
|
||||
{% load collection %}
|
||||
|
||||
<div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
|
||||
<div class="aside-section-wrapper aside-section-wrapper--no-margin">
|
||||
|
@ -43,11 +44,47 @@
|
|||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{% if user.featured_collections %}
|
||||
<div class="relation-dropdown__body">
|
||||
<div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
|
||||
<style type="text/css">
|
||||
progress {
|
||||
border-radius: 7px;
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
box-shadow: 1px 1px 4px rgba( 0, 0, 0, 0.2 );
|
||||
}
|
||||
::-webkit-progress-bar {
|
||||
background-color: #ccc;
|
||||
}
|
||||
::-webkit-progress-value {
|
||||
background-color: #00a1cc;
|
||||
}
|
||||
</style>
|
||||
<div class="user-relation">
|
||||
<h5 class="user-relation__label">
|
||||
{% trans '当前目标' %}
|
||||
</h5>
|
||||
{% for featured_collection in user.featured_collections.all %}
|
||||
{% user_visibility_of featured_collection as visible %}
|
||||
{% if visible %}
|
||||
{% user_progress_of collection=featured_collection user=user as progress %}
|
||||
<div class="tag-collection" style="margin-left: 10%;">
|
||||
<a href="{{ featured_collection.collection.url }}">{{ featured_collection.collection.title }}</a><br>
|
||||
已完成{{ progress }}% <br>
|
||||
<progress value="{{ progress }}" max="100">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user == request.user %}
|
||||
<div class="relation-dropdown__body">
|
||||
<div
|
||||
class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
|
||||
|
||||
<div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
|
||||
<div class="user-relation" id="followings">
|
||||
<h5 class="user-relation__label">
|
||||
{% trans '关注的人' %}
|
||||
|
@ -100,7 +137,7 @@
|
|||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
|
||||
{% if request.user.is_staff and request.user == user%}
|
||||
|
|
58
journal/migrations/0005_auto_20230114_1134.py
Normal file
58
journal/migrations/0005_auto_20230114_1134.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Generated by Django 3.2.16 on 2023-01-14 03:34
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("journal", "0004_alter_shelflogentry_timestamp"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FeaturedCollection",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_time", models.DateTimeField(auto_now_add=True)),
|
||||
("edited_time", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="journal.collection",
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("owner", "collection")},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="collection",
|
||||
name="featured_by_users",
|
||||
field=models.ManyToManyField(
|
||||
related_name="featured_collections",
|
||||
through="journal.FeaturedCollection",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -679,6 +679,9 @@ class Collection(List):
|
|||
collaborative = models.PositiveSmallIntegerField(
|
||||
default=0
|
||||
) # 0: Editable by owner only / 1: Editable by bi-direction followers
|
||||
featured_by_users = models.ManyToManyField(
|
||||
to=User, related_name="featured_collections", through="FeaturedCollection"
|
||||
)
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
|
@ -690,6 +693,26 @@ class Collection(List):
|
|||
html = markdown(self.brief)
|
||||
return _RE_HTML_TAG.sub(" ", html)
|
||||
|
||||
def is_featured_by_user(self, user):
|
||||
return self.featured_by_users.all().filter(id=user.id).exists()
|
||||
|
||||
def get_stats_for_user(self, user):
|
||||
items = list(self.members.all().values_list("item_id", flat=True))
|
||||
stats = {"total": len(items)}
|
||||
for st, shelf in user.shelf_manager.shelf_list.items():
|
||||
stats[st] = shelf.members.all().filter(item_id__in=items).count()
|
||||
stats["percentage"] = (
|
||||
round(stats["complete"] * 100 / stats["total"]) if stats["total"] else 0
|
||||
)
|
||||
return stats
|
||||
|
||||
def get_progress_for_user(self, user):
|
||||
items = list(self.members.all().values_list("item_id", flat=True))
|
||||
if len(items) == 0:
|
||||
return 0
|
||||
shelf = user.shelf_manager.shelf_list["complete"]
|
||||
return shelf.members.all().filter(item_id__in=items).count() * 100 / len(items)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if getattr(self, "catalog_item", None) is None:
|
||||
self.catalog_item = CatalogCollection()
|
||||
|
@ -704,6 +727,16 @@ class Collection(List):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class FeaturedCollection(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [["owner", "collection"]]
|
||||
|
||||
|
||||
"""
|
||||
Tag
|
||||
"""
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="dividing-line"></div> --> <!-- <div class="entity-card__img-wrapper" style="text-align: center;"> <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img"> </div> -->
|
||||
<!-- <div class="dividing-line"></div> --> <!-- <div class="entity-card__img-wrapper" style="text-align: center;"> <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img"> </div> -->
|
||||
{{ collection.html | safe }}
|
||||
|
||||
</div>
|
||||
|
@ -91,7 +91,46 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.user.is_authenticated and request.user != collection.owner %}
|
||||
{% if is_featured %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="donut" style="background: conic-gradient(#7CBDFE 0deg {{ stats.complete_deg }}deg, #B4D2A5 {{ stats.complete_deg }}deg {{ stats.complete_deg|add:stats.progress_deg }}deg, #ccc {{ stats.complete_deg|add:stats.progress_deg }}deg 1deg );"><div class="hole"><div class="text">
|
||||
{% if stats.progress %}
|
||||
{{ stats.progress }} 进行中<br>
|
||||
{% endif %}
|
||||
{% if stats.complete %}
|
||||
{{ stats.complete }} 已完成
|
||||
{% elif not stats.progress %}
|
||||
尚未开始
|
||||
{% endif %}
|
||||
</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="action-panel" style="margin-bottom: 0;">
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
<form action="{% url 'journal:collection_remove_featured' collection.uuid %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="action-panel__button" title="点击取消目标设置">{% trans '当前进度' %} {{ stats.percentage }}%</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if available_as_featured %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
<form action="{% url 'journal:collection_add_featured' collection.uuid %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="action-panel__button">{% trans '设为目标' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not is_featured and request.user.is_authenticated and request.user != collection.owner %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
|
@ -105,7 +144,7 @@
|
|||
{% csrf_token %}
|
||||
<button class="action-panel__button">{% trans '关注' %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
18
journal/templatetags/collection.py
Normal file
18
journal/templatetags/collection.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django import template
|
||||
from journal.models import Collection, Like
|
||||
from django.shortcuts import reverse
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def user_visibility_of(context, piece):
|
||||
user = context["request"].user
|
||||
return piece.is_visible_to(user)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def user_progress_of(collection, user):
|
||||
return (
|
||||
collection.get_progress_for_user(user) if user and user.is_authenticated else 0
|
||||
)
|
|
@ -70,6 +70,16 @@ urlpatterns = [
|
|||
collection_update_item_note,
|
||||
name="collection_update_item_note",
|
||||
),
|
||||
path(
|
||||
"collection/<str:collection_uuid>/add_featured",
|
||||
collection_add_featured,
|
||||
name="collection_add_featured",
|
||||
),
|
||||
path(
|
||||
"collection/<str:collection_uuid>/remove_featured",
|
||||
collection_remove_featured,
|
||||
name="collection_remove_featured",
|
||||
),
|
||||
re_path(
|
||||
r"^users/(?P<user_name>[A-Za-z0-9_\-.@]+)/(?P<shelf_type>"
|
||||
+ _get_all_shelf_types()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from os import stat
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
|
@ -179,6 +180,27 @@ def collection_retrieve(request, collection_uuid):
|
|||
if request.user.is_authenticated
|
||||
else False
|
||||
)
|
||||
is_featured = request.user.is_authenticated and collection.is_featured_by_user(
|
||||
request.user
|
||||
)
|
||||
available_as_featured = (
|
||||
request.user.is_authenticated
|
||||
and (following or request.user == collection.owner)
|
||||
and not is_featured
|
||||
and collection.members.all().exists()
|
||||
)
|
||||
stats = {}
|
||||
if is_featured:
|
||||
stats = collection.get_stats_for_user(request.user)
|
||||
stats["wishlist_deg"] = (
|
||||
stats["wishlist"] / stats["total"] * 360 if stats["total"] else 0
|
||||
)
|
||||
stats["progress_deg"] = (
|
||||
stats["progress"] / stats["total"] * 360 if stats["total"] else 0
|
||||
)
|
||||
stats["complete_deg"] = (
|
||||
stats["complete"] / stats["total"] * 360 if stats["total"] else 0
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"collection.html",
|
||||
|
@ -186,10 +208,33 @@ def collection_retrieve(request, collection_uuid):
|
|||
"collection": collection,
|
||||
"follower_count": follower_count,
|
||||
"following": following,
|
||||
"stats": stats,
|
||||
"available_as_featured": available_as_featured,
|
||||
"is_featured": is_featured,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def collection_add_featured(request, collection_uuid):
|
||||
if request.method != "POST":
|
||||
return HttpResponseBadRequest()
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
request.user.featured_collections.add(collection)
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
|
||||
|
||||
def collection_remove_featured(request, collection_uuid):
|
||||
if request.method != "POST":
|
||||
return HttpResponseBadRequest()
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
request.user.featured_collections.remove(collection)
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
|
||||
|
||||
def collection_share(request, collection_uuid):
|
||||
pass
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ from django.contrib import messages
|
|||
|
||||
from journal.importers.douban import DoubanImporter
|
||||
from journal.importers.goodreads import GoodreadsImporter
|
||||
from journal.models import reset_visibility_for_user
|
||||
from journal.models import reset_visibility_for_user, Collection
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
|
@ -54,7 +54,11 @@ def preferences(request):
|
|||
"show_last_edit",
|
||||
]
|
||||
)
|
||||
return render(request, "users/preferences.html")
|
||||
return render(
|
||||
request,
|
||||
"users/preferences.html",
|
||||
{"collections": Collection.objects.filter(owner=request.user)},
|
||||
)
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
|
|
|
@ -27,11 +27,11 @@
|
|||
<div class="grid__main grid__main--reverse-order">
|
||||
<div class="main-section-wrapper">
|
||||
<form action="{% url 'users:preferences' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="tools-section-wrapper">
|
||||
<div class="import-panel">
|
||||
<h5 class="import-panel__label">{% trans '使用偏好设置' %}</h5>
|
||||
<div class="import-panel__body">
|
||||
{% csrf_token %}
|
||||
<span>{% trans '新标记默认可见性:' %}</span>
|
||||
<div class="import-panel__checkbox import-panel__checkbox--last">
|
||||
<label for="id_visibility_0"><input type="radio" name="default_visibility" value="0" required="" id="id_visibility_0" {%if request.user.preference.default_visibility == 0 %}checked{% endif %}>
|
||||
|
@ -67,7 +67,6 @@
|
|||
<div class="import-panel">
|
||||
<h5 class="import-panel__label">{% trans '社交网络分享相关设置' %}</h5>
|
||||
<div class="import-panel__body">
|
||||
{% csrf_token %}
|
||||
<span>{% trans '在联邦网络上以公开方式分享的帖文是否发布到公共时间轴上:' %}</span>
|
||||
<div class="import-panel__checkbox import-panel__checkbox--last">
|
||||
<input type="checkbox" name="mastodon_publish_public" id="visibility" {%if request.user.preference.mastodon_publish_public %}checked{% endif %}>
|
||||
|
@ -102,4 +101,4 @@
|
|||
</body>
|
||||
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
Loading…
Add table
Reference in a new issue