refactor note footer strip and add test

This commit is contained in:
Your Name 2024-06-17 15:10:34 -04:00 committed by Henri Dickson
parent 43a7bf5174
commit f6229c7b31
9 changed files with 136 additions and 31 deletions

View file

@ -14,6 +14,7 @@ try:
except Exception:
NEODB_VERSION = __version__ + "-unknown"
TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
# Parse configuration from:
# - environment variables

View file

@ -519,6 +519,14 @@ class Item(PolymorphicModel):
item = None
return item
@classmethod
def get_by_remote_url(cls, url: str) -> "Self | None":
url_ = url.replace("/~neodb~/", "/")
if url_.startswith(settings.SITE_INFO["site_url"]):
return cls.get_by_url(url_, True)
er = ExternalResource.objects.filter(url=url_).first()
return er.item if er else None
# def get_lookup_id(self, id_type: str) -> str:
# prefix = id_type.strip().lower() + ':'
# return next((x[len(prefix):] for x in self.lookup_ids if x.startswith(prefix)), None)

View file

@ -12,6 +12,8 @@ class CommonConfig(AppConfig):
def setup(self, **kwargs):
from .setup import Setup
if kwargs.get("using", "") == "default":
# only run setup on the default database, not on takahe
Setup().run()

View file

@ -121,7 +121,15 @@ class Setup:
)
def run(self):
if settings.TESTING:
# Only do necessary initialization when testing
logger.info("Running minimal post-migration setup for testing...")
self.sync_site_config()
Indexer.init()
return
logger.info("Running post-migration setup...")
# Update site name if changed
self.sync_site_config()

View file

@ -252,19 +252,23 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
@classmethod
@abstractmethod
def params_from_ap_object(cls, post, obj, piece):
def params_from_ap_object(
cls, post: "Post", obj: dict[str, Any], piece: Self | None
) -> dict[str, Any]:
return {}
@abstractmethod
def to_post_params(self):
def to_post_params(self) -> dict[str, Any]:
return {}
@abstractmethod
def to_mastodon_params(self):
def to_mastodon_params(self) -> dict[str, Any]:
return {}
@classmethod
def update_by_ap_object(cls, owner: APIdentity, item: Item, obj, post: "Post"):
def update_by_ap_object(
cls, owner: APIdentity, item: Item, obj, post: "Post"
) -> Self | None:
"""
Create or update a content piece with related AP message
"""

View file

@ -16,12 +16,12 @@ from .renderers import render_text
from .shelf import ShelfMember
_progress = re.compile(
r"(.*\s)?(?P<prefix>(p|pg|page|ch|chapter|pt|part|e|ep|episode|trk|track|cycle))(\s|\.|#)*(?P<value>(\d[\d\:\.\-]*\d|\d))\s*(?P<postfix>(%))?(\s|\n|\.|。)?$",
r"(.*\s)?(?P<prefix>(p|pg|page|ch|chapter|pt|part|e|ep|episode|trk|track|cycle))(\s|\.|#)*(?P<value>([\d\:\.\-]+))\s*(?P<postfix>(%))?(\s|\n|\.|。)?$",
re.IGNORECASE,
)
_progress2 = re.compile(
r"(.*\s)?(?P<value>(\d[\d\:\.\-]*\d|\d))\s*(?P<postfix>(%))?(\s|\n|\.|。)?$",
r"(.*\s)?(?P<value>([\d\:\.\-]+))\s*(?P<postfix>(%))?(\s|\n|\.|。)?$",
re.IGNORECASE,
)
@ -106,7 +106,7 @@ class Note(Content):
}
if self.progress_value:
d["progress"] = {
"type": self.progress_type,
"type": self.progress_type or "",
"value": self.progress_value,
}
return d
@ -114,33 +114,32 @@ class Note(Content):
@override
@classmethod
def params_from_ap_object(cls, post, obj, piece):
content = obj.get("content", "").strip()
footer = []
if post.local:
# strip footer from local post if detected
lines = content.splitlines()
if len(lines) > 2 and lines[-2].strip() in _separaters:
content = "\n".join(lines[:-2])
footer = lines[-2:]
params = {
"title": obj.get("title", post.summary),
"content": content,
"content": obj.get("content", "").strip(),
"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 and len(footer) == 2:
progress_type, progress_value = cls.extract_progress(footer[1])
if progress_value:
if post.local:
# for local post, strip footer and detect progress from content
# if not detected, keep default/original value by not including it in return val
params["content"], progress_type, progress_value = cls.strip_footer(
params["content"]
)
if progress_value is not None:
params["progress_type"] = progress_type
params["progress_value"] = progress_value
elif not footer[1].startswith("https://"):
# add footer back if unable to regconize correct patterns
params["content"] += "\n" + "\n".join(footer)
else:
# for remote post, progress is always in "progress" field
progress = obj.get("progress", {})
params["progress_value"] = progress.get("value", None)
params["progress_type"] = None
if params["progress_value"]:
t = progress.get("type", None)
try:
params["progress_type"] = Note.ProgressType(t)
except ValueError:
pass
if post:
for atta in post.attachments.all():
params["attachments"].append(
@ -205,11 +204,24 @@ class Note(Content):
}
@classmethod
def extract_progress(cls, content):
def strip_footer(cls, content: str) -> tuple[str, str | None, str | None]:
"""strip footer if 2nd last line is "-" or similar characters"""
lines = content.splitlines()
if len(lines) < 3 or lines[-2].strip() not in _separaters:
return content, None, None
progress_type, progress_value = cls.extract_progress(lines[-1])
# if progress_value is None and not lines[-2].startswith("https://"):
# return content, None, None
return "\n".join(lines[:-2]), progress_type, progress_value
@classmethod
def extract_progress(cls, content) -> tuple[str | None, str | None]:
m = _progress.match(content)
if not m:
m = _progress2.match(content)
if m and m["value"]:
if m["value"] == "-":
return None, ""
m = m.groupdict()
typ_ = "percentage" if m["postfix"] == "%" else m.get("prefix", "")
match typ_:

View file

@ -236,3 +236,60 @@ class DebrisTest(TestCase):
update_journal_for_merged_item(self.book3.uuid, delete_duplicated=True)
cnt = Debris.objects.all().count()
self.assertEqual(cnt, 4) # Rating, Shelf, 2x TagMember
class NoteTest(TestCase):
databases = "__all__"
# def setUp(self):
# self.book1 = Edition.objects.create(title="Hyperion")
# self.user1 = User.register(email="test@test", username="test")
def test_parse(self):
c0 = "test \n - \n"
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, c0)
self.assertEqual(t, None)
self.assertEqual(v, None)
c0 = "test\n \n - \nhttps://xyz"
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test\n ")
self.assertEqual(t, None)
self.assertEqual(v, None)
c0 = "test \n - \np1"
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test ")
self.assertEqual(t, Note.ProgressType.PAGE)
self.assertEqual(v, "1")
c0 = "test \n - \n pt 1 "
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test ")
self.assertEqual(t, Note.ProgressType.PART)
self.assertEqual(v, "1")
c0 = "test \n - \nx chapter 1.1 \n"
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test ")
self.assertEqual(t, Note.ProgressType.CHAPTER)
self.assertEqual(v, "1.1")
c0 = "test \n - \n book pg 1.1% "
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test ")
self.assertEqual(t, Note.ProgressType.PERCENTAGE)
self.assertEqual(v, "1.1")
c0 = "test \n - \n show e 1. "
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test ")
self.assertEqual(t, Note.ProgressType.EPISODE)
self.assertEqual(v, "1.")
c0 = "test \n - \nch 2"
c, t, v = Note.strip_footer(c0)
self.assertEqual(c, "test ")
self.assertEqual(t, Note.ProgressType.CHAPTER)
self.assertEqual(v, "2")

View file

@ -57,8 +57,12 @@ class NoteForm(NeoModelForm):
self.fields["content"].required = False
# get the corresponding progress types for the item
types = Note.get_progress_types_by_item(item)
if self.instance.progress_type and self.instance.progress_type not in types:
types.append(self.instance.progress_type)
pt = self.instance.progress_type
if pt and pt not in types:
try:
types.append(Note.ProgressType(pt))
except ValueError:
pass
choices = [("", _("Progress Type (optional)"))] + [(x, x.label) for x in types]
self.fields["progress_type"].choices = choices # type: ignore

View file

@ -1080,6 +1080,15 @@ class Post(models.Model):
},
)[0]
@cached_property
def piece(self):
from journal.models import Piece, ShelfMember
pcs = Piece.objects.filter(post_id=self.pk)
if len(pcs) == 1:
return pcs[0]
return next((p for p in pcs if p.__class__ == ShelfMember), None)
@classmethod
def create_local(
cls,