From 7157f3b0b05789a84210b4c54605de38f7a62fec Mon Sep 17 00:00:00 2001
From: Your Name
Date: Sat, 15 Jun 2024 18:26:20 -0400
Subject: [PATCH] add progress to note
---
catalog/templates/_item_user_pieces.html | 31 +++--
common/forms.py | 12 ++
common/static/scss/_dialog.scss | 10 ++
common/static/scss/_lightbox.scss | 26 ++++
common/static/scss/_post.scss | 7 ++
common/static/scss/neodb.scss | 1 +
journal/migrations/0001_initial_0_10.py | 132 ++++++++++++++++++--
journal/migrations/0002_note.py | 12 +-
journal/migrations/0003_note_progress.py | 44 +++++++
journal/models/common.py | 4 +-
journal/models/itemlist.py | 10 +-
journal/models/like.py | 6 +-
journal/models/mark.py | 4 +-
journal/models/note.py | 152 +++++++++++++++++++++++
journal/templates/note.html | 55 +++-----
journal/templates/posts.html | 14 ++-
journal/urls.py | 4 +-
journal/views/__init__.py | 3 +-
journal/views/mark.py | 52 +-------
journal/views/note.py | 110 ++++++++++++++++
locale/zh_Hans/LC_MESSAGES/django.po | 141 ++++++++++++++++-----
locale/zh_Hant/LC_MESSAGES/django.po | 141 ++++++++++++++++-----
22 files changed, 779 insertions(+), 192 deletions(-)
create mode 100644 common/static/scss/_lightbox.scss
create mode 100644 journal/migrations/0003_note_progress.py
create mode 100644 journal/views/note.py
diff --git a/catalog/templates/_item_user_pieces.html b/catalog/templates/_item_user_pieces.html
index 4e03832d..7b38541a 100644
--- a/catalog/templates/_item_user_pieces.html
+++ b/catalog/templates/_item_user_pieces.html
@@ -90,18 +90,25 @@
{% include "action_open_post.html" with post=note.latest_post %}
{% endif %}
- {{ note.title|default:'' }}
- {{ note.content|linebreaks }}
-
- {% for attachment in note.attachments %}
- {% if attachment.type == 'image' %}
-

- {% endif %}
- {% endfor %}
-
+ {% if note.title %}{{ note.title|default:'' }}
{% endif %}
+
+ {% if note.progress_value %}{{ note.progress_display }}{% endif %}
+ {{ note.content|linebreaksbr }}
+
+ {% for attachment in note.attachments %}
+ {% if attachment.type == 'image' %}
+
+
+
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
{% endfor %}
diff --git a/common/forms.py b/common/forms.py
index 3ae7c39c..a9ef67fd 100644
--- a/common/forms.py
+++ b/common/forms.py
@@ -3,11 +3,23 @@ import json
import django.contrib.postgres.forms as postgres
from django import forms
from django.core.exceptions import ValidationError
+from django.forms import ModelForm
from django.utils import formats
from django.utils.translation import gettext_lazy as _
from markdownx.fields import MarkdownxFormField
+class NeoModelForm(ModelForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # if "uuid" in self.fields:
+ # if self.instance and self.instance.pk:
+ # self.fields["uuid"].initial = self.instance.uuid
+ for visible in self.visible_fields():
+ w = visible.field.widget
+ w.attrs["class"] = "widget " + w.__class__.__name__.lower()
+
+
class PreviewImageInput(forms.FileInput):
template_name = "widgets/image.html"
diff --git a/common/static/scss/_dialog.scss b/common/static/scss/_dialog.scss
index a13f7992..e7274349 100644
--- a/common/static/scss/_dialog.scss
+++ b/common/static/scss/_dialog.scss
@@ -86,6 +86,16 @@ dialog {
article {
padding-bottom: 1em;
+ form {
+ overflow: hidden;
+ input[type="submit"] {
+ margin-top: 1em;
+ }
+ div.widget.radioselect {
+ display: flex;
+ grid-column-gap: 1em;
+ }
+ }
}
input[type="date"] {
diff --git a/common/static/scss/_lightbox.scss b/common/static/scss/_lightbox.scss
new file mode 100644
index 00000000..4f4efd88
--- /dev/null
+++ b/common/static/scss/_lightbox.scss
@@ -0,0 +1,26 @@
+.lightbox {
+ display: none;
+ position: fixed;
+ z-index: 999;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 1em;
+ background: rgba(0, 0, 0, 0.8);
+ -webkit-backdrop-filter: var(--pico-modal-overlay-backdrop-filter);
+}
+
+.lightbox:target {
+ display: block;
+}
+
+.lightbox span {
+ display: block;
+ width: 100%;
+ height: 100%;
+
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+}
diff --git a/common/static/scss/_post.scss b/common/static/scss/_post.scss
index ba45b7d1..5bba7e20 100644
--- a/common/static/scss/_post.scss
+++ b/common/static/scss/_post.scss
@@ -27,3 +27,10 @@ section.replies {
}
}
}
+
+.attachements {
+ img.preview {
+ max-height: 6em;
+ max-width: 50%;
+ }
+}
diff --git a/common/static/scss/neodb.scss b/common/static/scss/neodb.scss
index eb8afbb0..7e031ea4 100644
--- a/common/static/scss/neodb.scss
+++ b/common/static/scss/neodb.scss
@@ -20,3 +20,4 @@
@import '_form.scss';
@import '_post.scss';
@import '_l10n.scss';
+@import '_lightbox.scss';
diff --git a/journal/migrations/0001_initial_0_10.py b/journal/migrations/0001_initial_0_10.py
index aa7e623d..fbbfa121 100644
--- a/journal/migrations/0001_initial_0_10.py
+++ b/journal/migrations/0001_initial_0_10.py
@@ -159,7 +159,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -205,7 +215,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -233,7 +253,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -265,7 +295,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -316,7 +356,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -343,7 +393,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -382,7 +442,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -415,7 +485,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -451,7 +531,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -596,7 +686,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
@@ -656,7 +756,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
diff --git a/journal/migrations/0002_note.py b/journal/migrations/0002_note.py
index da630bf7..131b4bc5 100644
--- a/journal/migrations/0002_note.py
+++ b/journal/migrations/0002_note.py
@@ -27,7 +27,17 @@ class Migration(migrations.Migration):
to="journal.piece",
),
),
- ("visibility", models.PositiveSmallIntegerField(default=0)),
+ (
+ "visibility",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (0, "Public"),
+ (1, "Followers Only"),
+ (2, "Mentioned Only"),
+ ],
+ default=0,
+ ),
+ ),
(
"created_time",
models.DateTimeField(default=django.utils.timezone.now),
diff --git a/journal/migrations/0003_note_progress.py b/journal/migrations/0003_note_progress.py
new file mode 100644
index 00000000..13c1e770
--- /dev/null
+++ b/journal/migrations/0003_note_progress.py
@@ -0,0 +1,44 @@
+# Generated by Django 4.2.13 on 2024-06-14 22:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("journal", "0002_note"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="note",
+ name="progress_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("page", "Page"),
+ ("chapter", "Chapter"),
+ ("part", "Part"),
+ ("episode", "Episode"),
+ ("track", "Track"),
+ ("cycle", "Cycle"),
+ ("timestamp", "Timestamp"),
+ ("percentage", "Percentage"),
+ ],
+ default=None,
+ max_length=50,
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="note",
+ name="progress_value",
+ field=models.CharField(blank=True, default=None, max_length=500, null=True),
+ ),
+ migrations.AddIndex(
+ model_name="note",
+ index=models.Index(
+ fields=["owner", "item", "created_time"],
+ name="journal_not_owner_i_7f9460_idx",
+ ),
+ ),
+ ]
diff --git a/journal/models/common.py b/journal/models/common.py
index 375198c6..da485e7b 100644
--- a/journal/models/common.py
+++ b/journal/models/common.py
@@ -434,8 +434,8 @@ class PieceInteraction(models.Model):
class Content(Piece):
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
visibility = models.PositiveSmallIntegerField(
- default=0
- ) # 0: Public / 1: Follower only / 2: Self only # type:ignore
+ choices=VisibilityType.choices, default=0, null=False
+ ) # type:ignore
created_time = models.DateTimeField(default=timezone.now)
edited_time = models.DateTimeField(auto_now=True)
metadata = models.JSONField(default=dict)
diff --git a/journal/models/itemlist.py b/journal/models/itemlist.py
index 41e6ecae..a29c815e 100644
--- a/journal/models/itemlist.py
+++ b/journal/models/itemlist.py
@@ -8,7 +8,7 @@ from django.utils import timezone
from catalog.models import Item, ItemCategory
from users.models import APIdentity
-from .common import Piece
+from .common import Piece, VisibilityType
list_add = django.dispatch.Signal()
list_remove = django.dispatch.Signal()
@@ -25,8 +25,8 @@ class List(Piece):
items: "models.ManyToManyField[Item, List]"
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
visibility = models.PositiveSmallIntegerField(
- default=0
- ) # 0: Public / 1: Follower only / 2: Self only # type:ignore
+ choices=VisibilityType.choices, default=0, null=False
+ ) # type:ignore
created_time = models.DateTimeField(default=timezone.now)
edited_time = models.DateTimeField(auto_now=True)
metadata = models.JSONField(default=dict)
@@ -151,8 +151,8 @@ class ListMember(Piece):
parent: models.ForeignKey["ListMember", "List"]
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
visibility = models.PositiveSmallIntegerField(
- default=0
- ) # 0: Public / 1: Follower only / 2: Self only # type:ignore[reportAssignmentType]
+ choices=VisibilityType.choices, default=0, null=False
+ ) # type:ignore
created_time = models.DateTimeField(default=timezone.now)
edited_time = models.DateTimeField(auto_now=True)
metadata = models.JSONField(default=dict)
diff --git a/journal/models/like.py b/journal/models/like.py
index de34ffaa..9e669e55 100644
--- a/journal/models/like.py
+++ b/journal/models/like.py
@@ -5,14 +5,14 @@ from django.utils.translation import gettext_lazy as _
from users.models import APIdentity
-from .common import Piece
+from .common import Piece, VisibilityType
class Like(Piece): # TODO remove
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
visibility = models.PositiveSmallIntegerField(
- default=0
- ) # 0: Public / 1: Follower only / 2: Self only # type: ignore
+ choices=VisibilityType.choices, default=0, null=False
+ ) # type:ignore
created_time = models.DateTimeField(default=timezone.now)
edited_time = models.DateTimeField(auto_now=True)
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name="likes")
diff --git a/journal/models/mark.py b/journal/models/mark.py
index 9b7cb88d..5f97d44e 100644
--- a/journal/models/mark.py
+++ b/journal/models/mark.py
@@ -108,7 +108,9 @@ class Mark:
@cached_property
def notes(self):
- return Note.objects.filter(owner=self.owner, item=self.item)
+ return Note.objects.filter(owner=self.owner, item=self.item).order_by(
+ "-created_time"
+ )
# post_ids = PiecePost.objects.filter(
# piece__note__owner_id=self.owner.pk, piece__note__item_id=self.item.pk
# ).values_list("post_id", flat=True)
diff --git a/journal/models/note.py b/journal/models/note.py
index cf84837b..9011ae30 100644
--- a/journal/models/note.py
+++ b/journal/models/note.py
@@ -1,9 +1,12 @@
+import re
from functools import cached_property
from typing import override
from django.db import models
+from django.utils.translation import gettext_lazy as _
from loguru import logger
+from catalog.models import Item
from mastodon.api import delete_toot_later
from takahe.utils import Takahe
@@ -11,17 +14,74 @@ from .common import Content
from .renderers import render_text
from .shelf import ShelfMember
+_progress = re.compile(
+ r"^\s*(?P(p|pg|page|ch|chapter|pt|part|e|ep|episode|trk|track|cycle))?(\s|\.|#)*(?P(\d[\d\:\.\-]*\d|\d))\s*(?P(%))?\s*(\s|\n|\.|:)",
+ re.IGNORECASE,
+)
+
+_number = re.compile(r"^[\s\d\:\.]+$")
+
class Note(Content):
+ class ProgressType(models.TextChoices):
+ PAGE = "page", _("Page")
+ CHAPTER = "chapter", _("Chapter")
+ # SECTION = "section", _("Section")
+ # VOLUME = "volume", _("Volume")
+ PART = "part", _("Part")
+ EPISODE = "episode", _("Episode")
+ TRACK = "track", _("Track")
+ CYCLE = "cycle", _("Cycle")
+ TIMESTAMP = "timestamp", _("Timestamp")
+ PERCENTAGE = "percentage", _("Percentage")
+
title = models.TextField(blank=True, null=True, default=None)
content = models.TextField(blank=False, null=False)
sensitive = models.BooleanField(default=False, null=False)
attachments = models.JSONField(default=list)
+ progress_type = models.CharField(
+ max_length=50,
+ choices=ProgressType.choices,
+ blank=True,
+ null=True,
+ default=None,
+ )
+ progress_value = models.CharField(
+ max_length=500, blank=True, null=True, default=None
+ )
+ _progress_display_template = {
+ ProgressType.PAGE: _("Page {value}"),
+ ProgressType.CHAPTER: _("Chapter {value}"),
+ # ProgressType.SECTION: _("Section {value}"),
+ # ProgressType.VOLUME: _("Volume {value}"),
+ ProgressType.PART: _("Part {value}"),
+ ProgressType.EPISODE: _("Episode {value}"),
+ ProgressType.TRACK: _("Track {value}"),
+ ProgressType.CYCLE: _("Cycle {value}"),
+ ProgressType.PERCENTAGE: "{value}%",
+ ProgressType.TIMESTAMP: "{value}",
+ }
+
+ class Meta:
+ indexes = [models.Index(fields=["owner", "item", "created_time"])]
@property
def html(self):
return render_text(self.content)
+ @property
+ def progress_display(self) -> str:
+ if not self.progress_value:
+ return ""
+ if not self.progress_type:
+ return str(self.progress_value)
+ tpl = Note._progress_display_template.get(self.progress_type, None)
+ if not tpl:
+ return str(self.progress_value)
+ if _number.match(self.progress_value):
+ return tpl.format(value=self.progress_value)
+ return self.progress_type.display + ": " + self.progress_value
+
@property
def ap_object(self):
d = {
@@ -36,6 +96,11 @@ class Note(Content):
"withRegardTo": self.item.absolute_url,
"href": self.absolute_url,
}
+ if self.progress_value:
+ d["progress"] = {
+ "type": self.progress_type,
+ "value": self.progress_value,
+ }
return d
@override
@@ -47,6 +112,17 @@ class Note(Content):
"sensitive": obj.get("sensitive", post.sensitive),
"attachments": [],
}
+ progress = obj.get("progress", {})
+ if progress.get("type"):
+ params["progress_type"] = progress.get("type")
+ if progress.get("value"):
+ params["progress_value"] = progress.get("value")
+ if post.local:
+ progress_type, progress_value = cls.extract_progress(params["content"])
+ print(progress_type, progress_value)
+ if progress_value:
+ params["progress_type"] = progress_type
+ params["progress_value"] = progress_value
if post:
for atta in post.attachments.all():
params["attachments"].append(
@@ -103,3 +179,79 @@ class Note(Content):
),
# not passing "attachments" so it won't change
}
+
+ @classmethod
+ def extract_progress(cls, content):
+ m = _progress.match(content)
+ if m and m["value"]:
+ typ_ = "percentage" if m["postfix"] == "%" else m["prefix"]
+ match typ_:
+ case "p" | "pg" | "page":
+ typ = Note.ProgressType.PAGE
+ case "ch" | "chapter":
+ typ = Note.ProgressType.CHAPTER
+ # case "vol" | "volume":
+ # typ = ProgressType.VOLUME
+ # case "section":
+ # typ = ProgressType.SECTION
+ case "pt" | "part":
+ typ = Note.ProgressType.PART
+ case "e" | "ep" | "episode":
+ typ = Note.ProgressType.EPISODE
+ case "trk" | "track":
+ typ = Note.ProgressType.TRACK
+ case "cycle":
+ typ = Note.ProgressType.CYCLE
+ case "percentage":
+ typ = Note.ProgressType.PERCENTAGE
+ case _:
+ typ = "timestamp" if ":" in m["value"] else None
+ return typ, m["value"]
+ return None, None
+
+ @classmethod
+ def get_progress_types_by_item(cls, item: Item):
+ match item.__class__.__name__:
+ case "Edition":
+ v = [
+ Note.ProgressType.PAGE,
+ Note.ProgressType.CHAPTER,
+ Note.ProgressType.PERCENTAGE,
+ ]
+ case "TVShow" | "TVSeason":
+ v = [
+ Note.ProgressType.PART,
+ Note.ProgressType.EPISODE,
+ Note.ProgressType.PERCENTAGE,
+ ]
+ case "Movie":
+ v = [
+ Note.ProgressType.PART,
+ Note.ProgressType.TIMESTAMP,
+ Note.ProgressType.PERCENTAGE,
+ ]
+ case "Podcast":
+ v = [
+ Note.ProgressType.EPISODE,
+ ]
+ case "TVEpisode" | "PodcastEpisode":
+ v = []
+ case "Album":
+ v = [
+ Note.ProgressType.TRACK,
+ Note.ProgressType.TIMESTAMP,
+ Note.ProgressType.PERCENTAGE,
+ ]
+ case "Game":
+ v = [
+ Note.ProgressType.CYCLE,
+ ]
+ case "Performance" | "PerformanceProduction":
+ v = [
+ Note.ProgressType.PART,
+ Note.ProgressType.TIMESTAMP,
+ Note.ProgressType.PERCENTAGE,
+ ]
+ case _:
+ v = []
+ return v
diff --git a/journal/templates/note.html b/journal/templates/note.html
index 88085cd0..91ab9588 100644
--- a/journal/templates/note.html
+++ b/journal/templates/note.html
@@ -16,50 +16,23 @@
{% trans 'Note' %} - {{ item.display_title }}