diff --git a/characters/migrations/0017_character_linked_objects_and_more.py b/characters/migrations/0017_character_linked_objects_and_more.py new file mode 100644 index 0000000..c6d372e --- /dev/null +++ b/characters/migrations/0017_character_linked_objects_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("characters", "0016_remove_character_nickname_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="character", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + migrations.AddField( + model_name="historicalcharacter", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + ] diff --git a/common/management/commands/refresh_html.py b/common/management/commands/refresh_html.py index c9c9a54..b27a6b1 100644 --- a/common/management/commands/refresh_html.py +++ b/common/management/commands/refresh_html.py @@ -36,9 +36,10 @@ class Command(BaseCommand): objects.extend(list(Note.objects.all())) objects.extend(list(IngameDay.objects.all())) for object in objects: - fresh_html = md_to_html(object.description_md, replacements=replacements) + fresh_html, linked_objects = md_to_html(object.description_md, replacements=replacements) if object.description_html != fresh_html: print_diff_call(object.description_html, fresh_html, str(object)) if store: object.description_html = fresh_html + object.linked_objects = ",".join(linked_objects) object.save() diff --git a/common/models/descriptionmodel.py b/common/models/descriptionmodel.py index 2dd5abe..56933a6 100644 --- a/common/models/descriptionmodel.py +++ b/common/models/descriptionmodel.py @@ -7,11 +7,13 @@ from utils.markdown import md_to_html class DescriptionModel(models.Model): description_md = models.TextField(_("Description"), blank=True) description_html = models.TextField(_("Description (HTML)"), blank=True, editable=False) + linked_objects = models.TextField(max_length=1000, blank=True, default="") class Meta: abstract = True def save(self, *args, **kwargs): - self.description_html = md_to_html(self.description_md) + self.description_html, linked_objects = md_to_html(self.description_md) + self.linked_objects = ",".join(linked_objects) super(DescriptionModel, self).save(*args, **kwargs) diff --git a/common/tests.py b/common/tests.py index 3313997..aac50a0 100644 --- a/common/tests.py +++ b/common/tests.py @@ -28,20 +28,20 @@ class ColorsTests(SimpleTestCase): class MarkdownTests(SimpleTestCase): def test_basic_markdown(self): self.assertHTMLEqual( - md_to_html("**test** *it*", replacements={}), + md_to_html("**test** *it*", replacements={})[0], "

test it

" ) def test_nb_md(self): self.assertHTMLEqual( - md_to_html("This\nis\nTest", replacements={}), + md_to_html("This\nis\nTest", replacements={})[0], "

This
is
Test

" ) def test_bleach(self): self.assertEqual( md_to_html( - " Hi", replacements={}), + " Hi", replacements={})[0], "<script>console.log()</script>\n

Hi</button>

" ) diff --git a/days/migrations/0007_historicalingameday_linked_objects_and_more.py b/days/migrations/0007_historicalingameday_linked_objects_and_more.py new file mode 100644 index 0000000..1e8cd1e --- /dev/null +++ b/days/migrations/0007_historicalingameday_linked_objects_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("days", "0006_alter_historicalsession_date_alter_session_date"), + ] + + operations = [ + migrations.AddField( + model_name="historicalingameday", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + migrations.AddField( + model_name="ingameday", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + ] diff --git a/factions/migrations/0005_faction_linked_objects_and_more.py b/factions/migrations/0005_faction_linked_objects_and_more.py new file mode 100644 index 0000000..f370251 --- /dev/null +++ b/factions/migrations/0005_faction_linked_objects_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("factions", "0004_faction_aliases_historicalfaction_aliases"), + ] + + operations = [ + migrations.AddField( + model_name="faction", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + migrations.AddField( + model_name="historicalfaction", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + ] diff --git a/graph/views.py b/graph/views.py index 174e6ee..fc4797a 100644 --- a/graph/views.py +++ b/graph/views.py @@ -37,6 +37,12 @@ class Graph: "target": target.graphkey }) + def add_edge_str(self, source: str, target: str): + self.edges.append({ + "source": source, + "target": target + }) + def prune(self) -> None: connected_nodes = set() for e in self.edges: @@ -62,14 +68,22 @@ class GraphView(TemplateView): template_name = "graph/graph.jinja" +def get_description_links(el: GraphModelEl, g: Graph): + if el.linked_objects: + for lo in el.linked_objects.split(","): + g.add_edge_str(el.graphkey, lo) + + def get_graph(request: HttpRequest) -> HttpResponse: g = Graph() for loc in list(Location.objects.all()) + list(Note.objects.all()): g.add_node(loc) if loc.parent: g.add_edge(loc, loc.parent) + get_description_links(loc, g) for faction in Faction.objects.all(): g.add_node(faction, faction.name) + get_description_links(faction, g) for user in TenantUser.objects \ .filter(tenants=connection.get_tenant()) \ .exclude(pk__in=[1, 2]): @@ -82,6 +96,7 @@ def get_graph(request: HttpRequest) -> HttpResponse: g.add_edge(char, char.faction) if char.player: g.add_edge(char, char.player) + get_description_links(char, g) for loottype in LootType.objects.all(): g.add_node(loottype) @@ -94,6 +109,8 @@ def get_graph(request: HttpRequest) -> HttpResponse: g.add_edge(loot, loot.owner) if loot.type: g.add_edge(loot, loot.type) + get_description_links(loot, g) + g.prune() return JsonResponse(g.export()) diff --git a/locations/migrations/0011_historicallocation_linked_objects_and_more.py b/locations/migrations/0011_historicallocation_linked_objects_and_more.py new file mode 100644 index 0000000..dedef7a --- /dev/null +++ b/locations/migrations/0011_historicallocation_linked_objects_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("locations", "0010_historicallocation_aliases_location_aliases"), + ] + + operations = [ + migrations.AddField( + model_name="historicallocation", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + migrations.AddField( + model_name="location", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + ] diff --git a/loot/migrations/0016_historicalloot_linked_objects_loot_linked_objects.py b/loot/migrations/0016_historicalloot_linked_objects_loot_linked_objects.py new file mode 100644 index 0000000..20d88ae --- /dev/null +++ b/loot/migrations/0016_historicalloot_linked_objects_loot_linked_objects.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("loot", "0015_loottype_slug"), + ] + + operations = [ + migrations.AddField( + model_name="historicalloot", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + migrations.AddField( + model_name="loot", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + ] diff --git a/notes/migrations/0006_historicalnote_linked_objects_note_linked_objects.py b/notes/migrations/0006_historicalnote_linked_objects_note_linked_objects.py new file mode 100644 index 0000000..ff5e718 --- /dev/null +++ b/notes/migrations/0006_historicalnote_linked_objects_note_linked_objects.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("notes", "0005_historicalnote_aliases_note_aliases"), + ] + + operations = [ + migrations.AddField( + model_name="historicalnote", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + migrations.AddField( + model_name="note", + name="linked_objects", + field=models.TextField(blank=True, default="", max_length=1000), + ), + ] diff --git a/utils/markdown.py b/utils/markdown.py index 46b2f85..5046960 100644 --- a/utils/markdown.py +++ b/utils/markdown.py @@ -1,5 +1,6 @@ import re from html.parser import HTMLParser +from typing import Tuple, Set import bleach import markdown @@ -8,8 +9,8 @@ from bleach_allowlist import markdown_tags, markdown_attrs custom_allowed_tags = ["del", "ins"] -def md_to_html(md: str, replacements=None) -> str: - md = autolink(md, replacements=replacements) +def md_to_html(md: str, replacements=None) -> Tuple[str, Set[str]]: + md, linked_objects = autolink(md, replacements=replacements) html = markdown.markdown( md, output_format="html", @@ -22,25 +23,28 @@ def md_to_html(md: str, replacements=None) -> str: tags=markdown_tags + custom_allowed_tags, attributes=markdown_attrs ) - return html + return html, linked_objects -def autolink(md: str, replacements=None) -> str: +def autolink(md: str, replacements=None) -> Tuple[str, Set[str]]: if replacements is None: from utils.urls import name2url replacements = name2url() links = {} + linked_objects = set() i = 0 - for name, url in replacements.items(): + for name, (url, obj) in replacements.items(): regex = r"\bWORD\b".replace("WORD", name) placeholder = f"SOME{i}LINK" - md = re.sub(regex, placeholder, md) + md, n_replacements = re.subn(regex, placeholder, md) + if n_replacements > 0: + linked_objects.add(obj.graphkey) links[placeholder] = f"[{name}]({url})" i += 1 for placeholder, value in links.items(): md = md.replace(placeholder, value) - return md + return md, linked_objects class HTMLFilter(HTMLParser): diff --git a/utils/urls.py b/utils/urls.py index 914e7e7..d64996f 100644 --- a/utils/urls.py +++ b/utils/urls.py @@ -9,17 +9,17 @@ from notes.models import Note def name2url() -> Dict[str, str]: data = {} - objects=[] + objects = [] objects.extend(Character.objects.all()) objects.extend(Location.objects.all()) objects.extend(Loot.objects.all()) objects.extend(Faction.objects.all()) objects.extend(Note.objects.all()) for object in objects: - data[object.name] = object.get_absolute_url() + data[object.name] = (object.get_absolute_url(), object) if object.aliases: for alias in object.aliases: - data[alias] = object.get_absolute_url() + data[alias] = (object.get_absolute_url(), object) # longer replacements first data = {k: v for k, v in sorted(data.items(), key=lambda item: -len(item[0]))}