Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 121 additions & 7 deletions django/library/jinja2/library/codebases/releases/retrieve.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,18 @@
<h2>Release Notes</h2>
<p>{{ release.release_notes|safe }}</p>
<h2>Associated Publications</h2>
<p>{{ markdown(codebase.associated_publication_text) }}</p>
{% if codebase.associated_publications %}
<ul>
Comment on lines 80 to +82
{% for publication in codebase.associated_publications %}
<li>
<a href="{{ publication.doi }}" target="_blank" rel="nofollow">{{ publication.doi }}</a>
{% if publication.include_in_citation %}
<span class="badge bg-info ms-1">cited</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}

Expand Down Expand Up @@ -134,9 +145,19 @@
{{ codebase.description|safe }}
</div>
{% with featured_image=codebase.get_featured_image() %}
{% if featured_image is not none %}
<div id="image-gallery" class="my-4" data-title="{{ codebase.title }}" data-images="{{ codebase.get_image_urls()|tojson|forceescape }}">
{{ image(featured_image, "max-900x600", class="img-fluid") }}
{% if featured_image is not none or codebase.youtube_url %}
<div
id="image-gallery"
class="my-4"
data-title="{{ codebase.title }}"
data-images="{{ codebase.get_image_urls()|tojson|forceescape }}"
{% if codebase.youtube_url %}
data-video-url="{{ codebase.youtube_url }}"
{% endif %}
>
{% if featured_image is not none %}
{{ image(featured_image, "max-900x600", class="img-fluid") }}
{% endif %}
{{ vite_asset("apps/image_gallery.ts") }}
</div>
{% endif %}
Expand All @@ -155,7 +176,23 @@
<h2 class='card-title'><u>Cite this Model</u></h2>
<div class='pb-3'>
<div id='citation-text'>
{{ markdown(release.citation_text) }}
<div class="mb-3">{{ markdown(release.citation_text) }}</div>
{% if codebase.citable_associated_publication_links %}
<p class="mb-1"><strong>Associated publications to cite</strong></p>
<div class="mb-3">
{% for publication in codebase.citable_associated_publication_links %}
<div class="mb-2">
<span
class="doi-citation"
data-doi-link="{{ publication }}"
data-fallback-text="{{ publication }}"
>
<a href="{{ publication }}" target="_blank" rel="nofollow">{{ publication }}</a>
</span>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<button class='btn btn-clipboard btn-outline-info' data-clipboard-target='#citation-text'>
<i class='fas fa-copy'></i> Copy citation text to clipboard
Expand Down Expand Up @@ -186,18 +223,32 @@
<p class='card-text'>{{ markdown(codebase.replication_text) }}</p>
</div>
{% endif %}
{% if codebase.associated_publication_text %}
{% if codebase.associated_publications %}
{% set ns = namespace(has_uncited_publications=false) %}
{% for publication in codebase.associated_publications %}
{% if not publication.include_in_citation %}
{% set ns.has_uncited_publications = true %}
{% endif %}
{% endfor %}
{% if ns.has_uncited_publications %}
<div class='pt-1'>
<h2 class='card-title'><i class='fas fa-book'></i> <u>Associated Publication(s)</u></h2>
<p class='card-text'>
{% if release.live or has_change_perm %}
{{ markdown(codebase.associated_publication_text) }}
{% for publication in codebase.associated_publications %}
{% if not publication.include_in_citation %}
<div class="mb-1">
<a href="{{ publication.doi }}" target="_blank" rel="nofollow">{{ publication.doi }}</a>
</div>
{% endif %}
{% endfor %}
{% else %}
<i class='fas fa-lock'>(private)</i>
{% endif %}
</p>
</div>
{% endif %}
{% endif %}
{% if codebase.references_text %}
<div class='pt-1'>
<h2 class='card-title'><i class='fas fa-book'></i> <u>References</u></h2>
Expand Down Expand Up @@ -357,6 +408,17 @@
</a>
</div>
{% endif %}
{% if release.input_data_url %}
<b class='card-title'>Input Data</b>
<div class="card-text mb-3">
<a href="{{ release.input_data_url }}" class="d-block text-truncate">
<span class='badge bg-info'>
{{ strip_url_scheme(release.input_data_url) }}
</span>
</a>
</div>
{% endif %}

{% if release.output_data_url %}
<b class='card-title'>Output Data</b>
<div class="card-text mb-3">
Expand Down Expand Up @@ -620,6 +682,58 @@
<script>
document.addEventListener("DOMContentLoaded", function(event) {
new ClipboardJS('.btn-clipboard');

const doiCitationElements = document.querySelectorAll(".doi-citation[data-doi-link]");

const toCanonicalDoiLink = (rawDoiLink) => {
if (!rawDoiLink) {
return null;
}
const trimmed = rawDoiLink.trim();
if (!trimmed) {
return null;
}
return trimmed.replace(/^http:\/\//i, "https://");
};

const toDoiValue = (doiLink) => {
if (!doiLink) {
return null;
}
return doiLink.replace(/^https?:\/\/(?:dx\.)?doi\.org\//i, "").trim();
};

const renderFallbackLink = (element, doiLink, fallbackText) => {
const href = doiLink || fallbackText;
if (!href) {
return;
}
element.innerHTML = `<a href="${href}" target="_blank" rel="nofollow">${fallbackText || href}</a>`;
};

doiCitationElements.forEach(async (element) => {
const doiLink = toCanonicalDoiLink(element.dataset.doiLink);
const doiValue = toDoiValue(doiLink);
const fallbackText = element.dataset.fallbackText || doiLink;

if (!doiLink || !doiValue) {
renderFallbackLink(element, doiLink, fallbackText);
return;
}

try {
const citationUrl = `https://citation.doi.org/format?doi=${encodeURIComponent(doiValue)}&style=apa&lang=en-US`;
const response = await fetch(citationUrl);
const citationText = response.ok ? (await response.text()).trim() : "";
if (citationText) {
element.textContent = citationText;
} else {
renderFallbackLink(element, doiLink, fallbackText);
}
} catch (error) {
renderFallbackLink(element, doiLink, fallbackText);
}
});
});
</script>
{% endscript %}
Expand Down
27 changes: 21 additions & 6 deletions django/library/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ def to_textual_creative_work(cls, text: str) -> dict:
"text": text,
}

@classmethod
def to_url_creative_work(cls, url: str) -> dict:
return {
"@type": "CreativeWork",
"url": url,
}

@classmethod
def license_to_creative_work(cls, license) -> dict:
creative_work_license = {
Expand All @@ -137,6 +144,7 @@ def url_to_datafeed(cls, url: str) -> dict:

@classmethod
def _common_codebase_fields(cls, codebase) -> dict:
associated_publication_links = codebase.associated_publication_links
return dict(
type_="SoftwareSourceCode",
name=codebase.title,
Expand All @@ -150,12 +158,16 @@ def _common_codebase_fields(cls, codebase) -> dict:
]
if text
]
+ [
cls.to_url_creative_work(doi)
for doi in codebase.citable_associated_publication_links
]
or None,
# tags are sorted so that comparisons are deterministic
keywords=[tag.name for tag in codebase.tags.all().order_by("name")] or None,
publisher=cls.COMSES_ORGANIZATION,
description=codebase.description.raw,
referencePublication=codebase.associated_publication_text or None,
referencePublication=associated_publication_links or None,
)

@classmethod
Expand Down Expand Up @@ -196,6 +208,13 @@ def _convert_release_language(cls, release_language) -> dict:
@classmethod
def _convert_release(cls, release) -> CodeMeta:
codebase = release.codebase
supporting = []
# prefer listing input data first, then output data
if getattr(release, "input_data_url", None):
supporting.append(cls.url_to_datafeed(release.input_data_url))
if release.output_data_url:
supporting.append(cls.url_to_datafeed(release.output_data_url))

return CodeMeta(
**cls._common_codebase_fields(codebase),
id_=release.permanent_url,
Expand All @@ -218,11 +237,7 @@ def _convert_release(cls, release) -> CodeMeta:
downloadUrl=f"{settings.BASE_URL}{release.get_download_url()}",
operatingSystem=release.os,
releaseNotes=release.release_notes.raw,
supportingData=(
cls.url_to_datafeed(release.output_data_url)
if release.output_data_url
else None
),
supportingData=supporting or None,
# FIXME: need better guidance on author vs contributor fields in CodeMeta
author=cls.convert_contributors(
release.author_release_contributors, "author"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 5.2.11 on 2026-05-26 16:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("library", "0037_initial_github_integration_overview_content"),
]

operations = [
migrations.AddField(
model_name="codebase",
name="associated_publications",
field=models.JSONField(
blank=True,
Comment on lines +13 to +17
default=list,
help_text="List of DOI links associated with this codebase. Each item should include a DOI link and whether it should appear in the citation area.",
),
),
migrations.AddField(
model_name="codebase",
name="preferred_citation",
field=models.TextField(
blank=True,
help_text="Legacy preferred citation text. Use associated_publications to control what appears in the citation area.",
),
),
migrations.AddField(
model_name="codebase",
name="youtube_url",
field=models.URLField(
blank=True,
help_text="Optional YouTube URL showcasing this codebase (e.g., a demo or tutorial video).",
),
),
migrations.AddField(
model_name="codebaserelease",
name="input_data_url",
field=models.URLField(
blank=True, help_text="Permanent URL to input data used by this model."
),
),
]
49 changes: 45 additions & 4 deletions django/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,12 @@ class Codebase(index.Indexed, ModeratedContent, ClusterableModel):
"URL to code repository, e.g., https://github.com/comses/wolf-sheep"
),
)
youtube_url = models.URLField(
blank=True,
help_text=_(
"Optional YouTube URL showcasing this codebase (e.g., a demo or tutorial video)."
),
)
replication_text = models.TextField(
blank=True,
help_text=_("URL / DOI / citation for the original model being replicated"),
Expand All @@ -1106,6 +1112,19 @@ class Codebase(index.Indexed, ModeratedContent, ClusterableModel):
"DOI, Permanent URL, or citation to a publication associated with this codebase."
),
)
preferred_citation = models.TextField(
blank=True,
help_text=_(
"Legacy preferred citation text. Use associated_publications to control what appears in the citation area."
),
)
associated_publications = models.JSONField(
default=list,
blank=True,
help_text=_(
"List of DOI links associated with this codebase. Each item should include a DOI link and whether it should appear in the citation area."
),
)
tags = ClusterTaggableManager(through=CodebaseTag)
# evaluate this JSONField as an add-anything way to record relationships between this Codebase and other entities
# with URLs / resolvable identifiers
Expand Down Expand Up @@ -1317,6 +1336,27 @@ def publication_year(self):
self.last_published_on.year if self.last_published_on else date.today().year
)

@staticmethod
def _publication_doi(publication: dict) -> str:
return (publication.get("doi") or "").strip()

@property
def associated_publication_links(self):
return [
doi
for publication in self.associated_publications
if (doi := self._publication_doi(publication))
]

@property
def citable_associated_publication_links(self):
return [
doi
for publication in self.associated_publications
if publication.get("include_in_citation")
and (doi := self._publication_doi(publication))
]

@property
def all_contributors(self):
"""
Expand Down Expand Up @@ -1529,7 +1569,6 @@ def get_or_create_draft(self, initial_version: str | None = None):
# reset fields that should not be copied over to a new draft
draft_release.doi = None
draft_release.release_notes = ""
draft_release.output_data_url = ""
draft_release.save()
return draft_release

Expand Down Expand Up @@ -1856,6 +1895,9 @@ class Status(models.TextChoices):
output_data_url = models.URLField(
blank=True, help_text=_("Permanent URL to output data from this model.")
)
input_data_url = models.URLField(
blank=True, help_text=_("Permanent URL to input data used by this model.")
)
version_number = models.CharField(
max_length=32, help_text=_("semver string, e.g., 1.0.5, see semver.org")
)
Expand Down Expand Up @@ -3378,10 +3420,9 @@ def __init__(self, release: CodebaseRelease):
release.citation_text,
codebase.references_text,
codebase.replication_text,
codebase.associated_publication_text,
]
if text
]
] + codebase.citable_associated_publication_links

if release.live:
self.first_published = release.first_published_at.date()
Expand Down Expand Up @@ -3737,7 +3778,7 @@ def convert(cls, codebase: Codebase):
```
"""
# FIXME: establish CommonMetadata for Codebases as well and change signature to operate on CommonMetadata
# add references_text and associated_publication_text fields when better structured metadata for those fields are available
# add references_text and associated_publications fields when better structured metadata for those fields are available
metadata = {
"creators": cls.to_citable_authors(codebase.all_author_contributors),
"titles": [{"title": codebase.title}],
Expand Down
Loading
Loading