Skip to content

Commit

Permalink
Turn reference URLs into hyperlinks (#2234)
Browse files Browse the repository at this point in the history
References in the References section should include a link, if a URL is
provided. Fixes issue #658.

I changed my mind many times about how this should be implemented. At
first, I thought that we should separately provide "description" and
"URL" fields for authors to edit.

But now I think, actually, the URL is part of the formal reference, and
should be displayed as text for transparency's sake. So I think it's
better for the URL to be extracted automatically from the description.

On the other hand, I don't want to have ad-hoc parsing rules applied at
runtime to published content. (It's possible those rules might change in
the future, which I think is okay for active but not for published
projects.) So I ended up with this approach:
- For active projects, the URL is extracted by a regular expression
whenever it's needed.
- When a project is published, the automatically-extracted URL is saved
as a model field.
- For published projects, it's possible to edit the `description` and
`url` separately via Django admin, but it will only appear as a link if
the `url` appears verbatim within the `description`.
  • Loading branch information
tompollard committed May 13, 2024
2 parents a3fc11a + 7b6be98 commit c9beed2
Show file tree
Hide file tree
Showing 13 changed files with 90 additions and 49 deletions.
4 changes: 4 additions & 0 deletions physionet-django/project/fixtures/demo-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@
"pk": 1,
"fields": {
"description": "https://eicu-crd.mit.edu/eicutables/admissiondrug/",
"url": "https://eicu-crd.mit.edu/eicutables/admissiondrug/",
"project": 1
}
},
Expand All @@ -680,6 +681,7 @@
"pk": 2,
"fields": {
"description": "https://eicu-crd.mit.edu/eicutables/apachepatientresult/",
"url": "https://eicu-crd.mit.edu/eicutables/apachepatientresult/",
"project": 1
}
},
Expand All @@ -688,6 +690,7 @@
"pk": 3,
"fields": {
"description": "https://physionet.org/physiotools/wag/",
"url": "https://physionet.org/physiotools/wag/",
"project": 2
}
},
Expand All @@ -696,6 +699,7 @@
"pk": 4,
"fields": {
"description": "https://physionet.org/physiotools/wpg/",
"url": "https://physionet.org/physiotools/wpg/",
"project": 2
}
},
Expand Down
36 changes: 36 additions & 0 deletions physionet-django/project/migrations/0075_publishedreference_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.11 on 2024-05-13 20:18

import re

from django.db import migrations, models


def migrate_forward(apps, schema_editor):
pattern = re.compile(r'\bhttps?://[^\s<>"\']+', re.IGNORECASE)

PublishedReference = apps.get_model('project', 'PublishedReference')
for reference in PublishedReference.objects.all():
m = pattern.search(reference.description)
if m:
reference.url = m.group()
reference.save(update_fields=['url'])


def migrate_backward(apps, schema_editor):
pass


class Migration(migrations.Migration):

dependencies = [
("project", "0074_migrate_archived_to_active"),
]

operations = [
migrations.AddField(
model_name="publishedreference",
name="url",
field=models.URLField(blank=True, null=True),
),
migrations.RunPython(migrate_forward, migrate_backward),
]
1 change: 1 addition & 0 deletions physionet-django/project/modelcomponents/activeproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ def publish(self, slug=None, make_zip=True):
for reference in self.references.all().order_by('order'):
published_reference = PublishedReference.objects.create(
description=reference.description,
url=reference.url,
order=reference.order,
project=published_project)

Expand Down
45 changes: 36 additions & 9 deletions physionet-django/project/modelcomponents/metadata.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import os
import re
import uuid

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone
from django.utils.html import format_html
from django.utils.html import escape, format_html, mark_safe
from html2text import html2text
from project.modelcomponents.access import AccessPolicy, AnonymousAccess
from project.modelcomponents.fields import SafeHTMLField
Expand Down Expand Up @@ -574,23 +575,50 @@ def __str__(self):
return self.description


class Reference(models.Model):
class BaseReference(models.Model):
"""
Abstract base class for a bibliographic reference.
"""
class Meta:
abstract = True

order = models.PositiveIntegerField(null=True)
description = models.CharField(max_length=1000)

def __str__(self):
return self.description

@property
def html_description(self):
description = escape(self.description)
url = self.url
if url:
escaped_url = escape(url)
link = format_html('<a href="{0}">{0}</a>', url)
description = description.replace(escaped_url, link)
description = mark_safe(description)
return description


class Reference(BaseReference):
"""
Reference field for ActiveProject
"""
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
project = GenericForeignKey('content_type', 'object_id')
order = models.PositiveIntegerField(null=True)

description = models.CharField(max_length=1000)
URL_PATTERN = re.compile(r'\bhttps?://[^\s<>"\']+', re.IGNORECASE)

class Meta:
default_permissions = ()
unique_together = (('description', 'object_id', 'order'),)

def __str__(self):
return self.description
@property
def url(self):
m = self.URL_PATTERN.search(self.description)
if m:
return m.group()

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
Expand All @@ -601,14 +629,13 @@ def delete(self, *args, **kwargs):
self.project.content_modified()


class PublishedReference(models.Model):
class PublishedReference(BaseReference):
"""
Reference field for PublishedProject
"""
description = models.CharField(max_length=1000)
project = models.ForeignKey('project.PublishedProject',
related_name='references', on_delete=models.CASCADE)
order = models.PositiveIntegerField(null=True)
url = models.URLField(blank=True, null=True)

class Meta:
default_permissions = ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ol>
{% for reference in references %}
<li>{{ reference.html_description }}</li>
{% endfor %}
</ol>
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ <h2 id="conflicts-of-interest">Conflicts of Interest</h2>

{% if references %}
<h2 id="references">References</h2>
<ol>
{% for reference in references %}
<li>{{ reference.description }}</li>
{% endfor %}
</ol>
{% include "project/reference_list.html" %}
<hr>
{% endif %}

0 comments on commit c9beed2

Please sign in to comment.