diff --git a/django/curator/models.py b/django/curator/models.py
index ff2f55903..6f65784b5 100644
--- a/django/curator/models.py
+++ b/django/curator/models.py
@@ -15,7 +15,7 @@
from nltk.tokenize import word_tokenize
from taggit.models import Tag
-from library.models import ProgrammingLanguage, CodebaseReleasePlatformTag
+from library.models import ProgrammingLanguageTag, CodebaseReleasePlatformTag
logger = logging.getLogger(__name__)
@@ -42,10 +42,10 @@ def get_through_tables():
class TagCuratorProxyQuerySet(models.QuerySet):
def with_comma(self):
return self.filter(name__icontains=",")
-
- def programming_languages(self):
+
+ def programming_language_tags(self):
return self.filter(
- id__in=ProgrammingLanguage.objects.values_list("tag_id", flat=True)
+ id__in=ProgrammingLanguageTag.objects.values_list("tag_id", flat=True)
)
def platforms(self):
diff --git a/django/home/metrics.py b/django/home/metrics.py
index 75c9d8ffc..3b8a953e5 100644
--- a/django/home/metrics.py
+++ b/django/home/metrics.py
@@ -352,7 +352,7 @@ def get_release_programming_language_timeseries(self, start_year):
programming_language_metrics = list(
CodebaseRelease.objects.public()
.values(
- programming_language_names=F("programming_languages__name"),
+ programming_language_names=F("release_languages__programming_language__name"),
year=F("first_published_at__year"),
)
.annotate(count=Count("year"))
diff --git a/django/library/jinja2/library/codebases/releases/retrieve.jinja b/django/library/jinja2/library/codebases/releases/retrieve.jinja
index 7a7339402..12bf401a9 100644
--- a/django/library/jinja2/library/codebases/releases/retrieve.jinja
+++ b/django/library/jinja2/library/codebases/releases/retrieve.jinja
@@ -362,8 +362,10 @@
Programming Language
- {% for pl in release.programming_languages.all() %}
- {{ search_tag_href(pl, category='codebases') }}
+ {% for pl in release.release_languages.all() %}
+
+ {{ pl.programming_language.name }}
+
{% endfor %}
Software Framework
diff --git a/django/library/management/commands/convert_language_tags.py b/django/library/management/commands/convert_language_tags.py
new file mode 100644
index 000000000..48fa9c1d1
--- /dev/null
+++ b/django/library/management/commands/convert_language_tags.py
@@ -0,0 +1,131 @@
+import logging
+
+from django.core.management.base import BaseCommand
+from django.core.exceptions import ObjectDoesNotExist
+
+from library.models import (
+ ProgrammingLanguageTag,
+ ProgrammingLanguage,
+ ReleaseLanguage,
+ CodebaseRelease,
+ Codebase,
+)
+
+
+logger = logging.getLogger(__name__)
+
+programming_languages = [
+ {"name": "ABS", "url": "https://www.abs-lang.org", "is_pinned": False},
+ {
+ "name": "Assembly",
+ "url": "https://en.wikipedia.org/wiki/Assembly_language",
+ "is_pinned": False,
+ },
+ {"name": "C", "url": "https://www.c-language.org", "is_pinned": False},
+ {
+ "name": "C#",
+ "url": "https://dotnet.microsoft.com/en-us/languages/csharp",
+ "is_pinned": False,
+ },
+ {"name": "C++", "url": "https://isocpp.org", "is_pinned": False},
+ {"name": "Common Lisp", "url": "https://common-lisp.net", "is_pinned": False},
+ {"name": "Dart", "url": "https://dart.dev", "is_pinned": False},
+ {"name": "Fortran", "url": "https://fortran-lang.org", "is_pinned": False},
+ {"name": "Go", "url": "https://golang.org", "is_pinned": False},
+ {"name": "Groovy", "url": "https://groovy-lang.org", "is_pinned": False},
+ {"name": "Haskell", "url": "https://www.haskell.org", "is_pinned": False},
+ {"name": "Java", "url": "https://www.oracle.com/java/", "is_pinned": True},
+ {
+ "name": "JavaScript",
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
+ "is_pinned": False,
+ },
+ {"name": "Julia", "url": "https://julialang.org", "is_pinned": False},
+ {"name": "Kotlin", "url": "https://kotlinlang.org", "is_pinned": False},
+ {
+ "name": "Logo",
+ "url": "http://el.media.mit.edu/logo-foundation/logo/",
+ "is_pinned": True,
+ },
+ {"name": "Lisp", "url": "https://lisp-lang.org", "is_pinned": False},
+ {
+ "name": "NetLogo",
+ "url": "https://ccl.northwestern.edu/netlogo/",
+ "is_pinned": True,
+ },
+ {
+ "name": "Objective-C",
+ "url": "https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html",
+ "is_pinned": False,
+ },
+ {"name": "PHP", "url": "https://www.php.net", "is_pinned": False},
+ {"name": "Perl", "url": "https://www.perl.org", "is_pinned": False},
+ {"name": "Python", "url": "https://www.python.org", "is_pinned": True},
+ {"name": "R", "url": "https://www.r-project.org", "is_pinned": True},
+ {"name": "Ruby", "url": "https://www.ruby-lang.org/en", "is_pinned": False},
+ {"name": "Rust", "url": "https://www.rust-lang.org", "is_pinned": False},
+ {"name": "Scala", "url": "https://www.scala-lang.org", "is_pinned": False},
+ {"name": "Shell", "url": "https://www.gnu.org/software/bash/", "is_pinned": False},
+ {"name": "Smalltalk", "url": "https://st.cs.uni-saarland.de", "is_pinned": False},
+ {
+ "name": "SQL",
+ "url": "https://www.iso.org/standard/63555.html",
+ "is_pinned": False,
+ },
+ {"name": "Swift", "url": "https://swift.org", "is_pinned": False},
+ {"name": "TypeScript", "url": "https://www.typescriptlang.org", "is_pinned": False},
+ {
+ "name": "Visual Basic",
+ "url": "https://docs.microsoft.com/en-us/dotnet/visual-basic/",
+ "is_pinned": False,
+ },
+ {
+ "name": "Wolfram Language",
+ "url": "https://www.wolfram.com/language/",
+ "is_pinned": False,
+ },
+]
+
+
+class Command(BaseCommand):
+ help = "Convert programming language tags to use the ReleaseLanguage model."
+
+ def handle(self, *args, **options):
+ ProgrammingLanguage.objects.all().delete()
+ for lang in programming_languages:
+ ProgrammingLanguage.objects.create(**lang)
+
+ ReleaseLanguage.objects.all().delete()
+ tags = ProgrammingLanguageTag.objects.all()
+ aliases = {}
+ for tag in tags:
+ version = ""
+ if tag.tag.name.lower().startswith("netlogo"):
+ version = tag.tag.name[7:].strip()
+
+ programming_language_name = tag.tag.name
+ if tag.tag.name in aliases:
+ programming_language_name = aliases[tag.tag.name]
+
+ try:
+ programming_language = ProgrammingLanguage.objects.get(name__iexact=programming_language_name)
+ except ObjectDoesNotExist:
+ programming_language = None
+
+ if programming_language is None:
+ programming_language_name = input("Enter the programming language name for tag '{}' (leave blank to skip): ".format(tag.tag.name))
+ if programming_language_name == "":
+ logger.info("Skipping tag '{}'".format(tag.tag.name))
+ continue
+ aliases[tag.tag.name] = programming_language_name
+
+ programming_language = ProgrammingLanguage.objects.get_or_create(
+ name__iexact=programming_language_name,
+ defaults={"name": programming_language_name, "is_user_defined": True},
+ )[0]
+
+ ReleaseLanguage.objects.create(
+ programming_language=programming_language,
+ release=tag.content_object,
+ version=version,
+ )
diff --git a/django/library/metadata.py b/django/library/metadata.py
index c1c9d2225..55267ddeb 100644
--- a/django/library/metadata.py
+++ b/django/library/metadata.py
@@ -196,8 +196,10 @@ def _convert_release(cls, release) -> CodeMeta:
),
programmingLanguage=[
# FIXME: this can include "version" when langs are refactored
- {"@type": "ComputerLanguage", "name": pl.name}
- for pl in release.programming_languages.all().order_by("name")
+ {"@type": "ComputerLanguage", "name": rl.programming_language.name}
+ for rl in release.release_languages.all().order_by(
+ "programming_language__name"
+ )
],
runtimePlatform=[
tag.name for tag in release.platform_tags.all().order_by("name")
diff --git a/django/library/migrations/0034_alter_codebaserelease_programming_languages.py b/django/library/migrations/0034_alter_codebaserelease_programming_languages.py
new file mode 100644
index 000000000..7156a95b1
--- /dev/null
+++ b/django/library/migrations/0034_alter_codebaserelease_programming_languages.py
@@ -0,0 +1,72 @@
+# Generated by Django 4.2.22 on 2025-09-29 23:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("library", "0033_codebaseimage_description"),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name="ProgrammingLanguage",
+ new_name="ProgrammingLanguageTag",
+ ),
+ migrations.RenameField(
+ model_name="codebaserelease",
+ old_name="programming_languages",
+ new_name="programming_language_tags",
+ ),
+ migrations.CreateModel(
+ name="ProgrammingLanguage",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ ("url", models.URLField(blank=True)),
+ ("is_pinned", models.BooleanField(default=False)),
+ ("is_user_defined", models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="ReleaseLanguage",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("version", models.CharField(max_length=20)),
+ (
+ "programming_language",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="release_languages",
+ to="library.programminglanguage",
+ ),
+ ),
+ (
+ "release",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="release_languages",
+ to="library.codebaserelease",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/django/library/models.py b/django/library/models.py
index ec532f005..a392fa805 100644
--- a/django/library/models.py
+++ b/django/library/models.py
@@ -89,12 +89,38 @@ class CodebaseTag(TaggedItemBase):
content_object = ParentalKey("library.Codebase", related_name="tagged_codebases")
-class ProgrammingLanguage(TaggedItemBase):
+class ProgrammingLanguageTag(TaggedItemBase):
content_object = ParentalKey(
"library.CodebaseRelease", related_name="tagged_release_languages"
)
+@register_snippet
+class ProgrammingLanguage(models.Model):
+ name = models.CharField(max_length=100, unique=True)
+ url = models.URLField(blank=True)
+ is_pinned = models.BooleanField(default=False)
+ is_user_defined = models.BooleanField(default=False)
+
+
+class ReleaseLanguage(models.Model):
+ programming_language = models.ForeignKey(
+ "library.ProgrammingLanguage",
+ related_name="release_languages",
+ on_delete=models.CASCADE,
+ )
+ release = models.ForeignKey(
+ "library.CodebaseRelease",
+ related_name="release_languages",
+ on_delete=models.CASCADE,
+ )
+ version = models.CharField(max_length=20)
+
+ @property
+ def name(self):
+ return self.programming_language.name
+
+
class CodebaseReleasePlatformTag(TaggedItemBase):
content_object = ParentalKey(
"library.CodebaseRelease", related_name="tagged_release_platforms"
@@ -818,9 +844,9 @@ def all_release_frameworks(self):
@property
def all_release_programming_languages(self):
return list(
- self.releases.exclude(programming_languages__isnull=True).values_list(
- "programming_languages__name", flat=True
- )
+ self.releases.exclude(release_languages__isnull=True)
+ .values_list("release_languages__programming_language__name", flat=True)
+ .distinct()
)
def download_count(self):
@@ -986,7 +1012,7 @@ def create_release_from_source(self, source_release, release_metadata):
# cache these before removing source release id to copy it over
contributors = ReleaseContributor.objects.filter(release_id=source_release.id)
platform_tags = source_release.platform_tags.all()
- programming_languages = source_release.programming_languages.all()
+ release_languages = source_release.release_languages.all()
# set source_release.id to None to create a new release
# see https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances
source_release.id = None
@@ -994,7 +1020,7 @@ def create_release_from_source(self, source_release, release_metadata):
source_release.__dict__.update(**release_metadata)
source_release.save()
source_release.platform_tags.add(*platform_tags)
- source_release.programming_languages.add(*programming_languages)
+ source_release.release_languages.add(*release_languages)
contributors.copy_to(source_release)
return source_release
@@ -1143,7 +1169,12 @@ def with_platforms(self):
return self.prefetch_related("tagged_release_platforms__tag")
def with_programming_languages(self):
- return self.prefetch_related("tagged_release_languages__tag")
+ return self.prefetch_related(
+ Prefetch(
+ "release_languages",
+ ReleaseLanguage.objects.prefetch_related("programming_language"),
+ )
+ )
def with_codebase(self):
return self.prefetch_related(
@@ -1285,8 +1316,8 @@ class Status(models.TextChoices):
through=CodebaseReleasePlatformTag, related_name="platform_codebase_releases"
)
platforms = models.ManyToManyField(Platform)
- programming_languages = ClusterTaggableManager(
- through=ProgrammingLanguage, related_name="pl_codebase_releases"
+ programming_language_tags = ClusterTaggableManager(
+ through=ProgrammingLanguageTag, related_name="pl_codebase_releases"
)
codebase = models.ForeignKey(
Codebase, related_name="releases", on_delete=models.PROTECT
@@ -1341,7 +1372,7 @@ class Status(models.TextChoices):
],
),
index.RelatedFields(
- "programming_languages",
+ "release_languages",
[
index.SearchField("name"),
],
@@ -1463,7 +1494,7 @@ def validate_metadata(self):
# naive check for metadata being present (i.e., None or false-y values)
if not self.license:
errors.append(ValidationError(_("Please specify a software license.")))
- if not self.programming_languages.exists():
+ if not self.release_languages.exists():
errors.append(
ValidationError(
_(
@@ -2733,7 +2764,7 @@ def __init__(self, release: CodebaseRelease):
self.description = codebase.description.raw
self.release_notes = release.release_notes.raw if release.release_notes else ""
self.version = release.version_number
- self.programming_languages = release.programming_languages.all()
+ self.release_languages = release.release_languages.all()
self.os = release.os
self.identifier = release.permanent_url
self.url = release.permanent_url
diff --git a/django/library/serializers.py b/django/library/serializers.py
index 3edbf8263..bbfd2ea3f 100644
--- a/django/library/serializers.py
+++ b/django/library/serializers.py
@@ -37,6 +37,8 @@
CodebaseReleaseDownload,
Contributor,
License,
+ ProgrammingLanguage,
+ ReleaseLanguage,
CodebaseImage,
PeerReviewerFeedback,
PeerReviewInvitation,
@@ -63,6 +65,40 @@ class Meta:
)
+class ProgrammingLanguageSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ProgrammingLanguage
+ fields = (
+ "id",
+ "name",
+ "url",
+ "is_pinned",
+ "is_user_defined",
+ )
+
+
+class ReleaseLanguageSerializer(serializers.ModelSerializer):
+ programming_language = ProgrammingLanguageSerializer(read_only=True)
+
+ # def create(self, validated_data):
+ # programming_language_data = self.initial_data.pop("programming_language")
+ # programming_language, created = ProgrammingLanguage.objects.get_or_create(
+ # name=programming_language_data['name']
+ # )
+ # validated_data["programming_language"] = programming_language
+ # instance = ReleaseLanguage(**validated_data)
+ # instance.save()
+ # return instance
+
+ class Meta:
+ model = ReleaseLanguage
+ fields = (
+ "programming_language",
+ "release",
+ "version",
+ )
+
+
class ContributorSerializer(serializers.ModelSerializer):
# Need an ID for Vue-Multiselect
id = serializers.IntegerField(read_only=True)
@@ -563,7 +599,8 @@ class CodebaseReleaseSerializer(serializers.ModelSerializer):
can_edit_originals = serializers.ReadOnlyField()
os_display = serializers.ReadOnlyField(source="get_os_display")
platforms = TagSerializer(many=True, source="platform_tags")
- programming_languages = TagSerializer(many=True)
+ programming_language_tags = TagSerializer(many=True)
+ release_languages = ReleaseLanguageSerializer(read_only=True, many=True)
submitter = RelatedUserSerializer(read_only=True, label="Submitter")
version_number = serializers.ReadOnlyField()
release_notes = MarkdownField(max_length=2048)
@@ -609,7 +646,7 @@ class Meta:
"os_display",
"peer_reviewed",
"platforms",
- "programming_languages",
+ "release_languages",
"submitted_package",
"submitter",
"codebase",
@@ -634,17 +671,41 @@ def get_possible_licenses(self, instance):
)
return serialized.data
+ def resolve_language(self, language_name):
+ programming_language = ProgrammingLanguage.objects.filter(
+ name__iexact=language_name
+ ).first()
+ if not programming_language:
+ programming_language = ProgrammingLanguage.objects.create(
+ name=language_name, is_user_defined=True
+ )
+ return programming_language
+
def update(self, instance, validated_data):
- programming_languages = TagSerializer(
- many=True, data=validated_data.pop("programming_languages")
- )
platform_tags = TagSerializer(
many=True, data=validated_data.pop("platform_tags")
)
- set_tags(instance, programming_languages, "programming_languages")
set_tags(instance, platform_tags, "platform_tags")
+ # Handle programming languages
+ release_languages_data = self.initial_data.pop("release_languages")
+ if release_languages_data:
+ # Clear existing programming languages
+ instance.release_languages.all().delete()
+
+ # Create new release languages
+ for release_language_data in release_languages_data:
+ language_data = release_language_data.get("programming_language")
+ if not language_data or "name" not in language_data:
+ raise ValidationError("Malformed programming language data")
+ programming_language = self.resolve_language(language_data["name"])
+ ReleaseLanguage.objects.create(
+ programming_language=programming_language,
+ release=instance,
+ version=release_language_data.get("version", ""),
+ )
+
raw_license = validated_data.pop("license")
existing_license = License.objects.get(name=raw_license["name"])
instance.license = existing_license
diff --git a/django/library/tests/base.py b/django/library/tests/base.py
index bc68fc7f0..1ada4354a 100644
--- a/django/library/tests/base.py
+++ b/django/library/tests/base.py
@@ -10,6 +10,8 @@
CodebaseRelease,
License,
Role,
+ ProgrammingLanguage,
+ ReleaseLanguage,
Contributor,
PeerReviewInvitation,
PeerReview,
@@ -183,7 +185,12 @@ def setUpPublishableDraftRelease(cls, codebase):
)
draft_release.license, created = License.objects.get_or_create(name="MIT")
draft_release.os = "Any"
- draft_release.programming_languages.add("Python")
+ ReleaseLanguage.objects.create(
+ programming_language=ProgrammingLanguage.objects.get_or_create(
+ name="Python"
+ ),
+ release=draft_release,
+ )
contributor_factory = ContributorFactory(user=draft_release.submitter)
release_contributor_factory = ReleaseContributorFactory(draft_release)
contributor = contributor_factory.create()
diff --git a/django/library/tests/test_models.py b/django/library/tests/test_models.py
index 60a37a1e3..ddcd3868f 100644
--- a/django/library/tests/test_models.py
+++ b/django/library/tests/test_models.py
@@ -14,7 +14,13 @@
ReleaseContributorFactory,
ReleaseSetup,
)
-from ..models import Codebase, CodebaseRelease, License
+from ..models import (
+ ProgrammingLanguage,
+ ReleaseLanguage,
+ Codebase,
+ CodebaseRelease,
+ License,
+)
logger = logging.getLogger(__name__)
@@ -248,7 +254,11 @@ def test_metadata_completeness(self):
ValidationError, lambda: self.codebase_release.validate_publishable()
)
- self.codebase_release.programming_languages.add("Java")
+ ReleaseLanguage.objects.create(
+ programming_language=ProgrammingLanguage.objects.get_or_create(name="Java"),
+ release=self.codebase_release,
+ version="8",
+ )
self.assertRaises(
ValidationError, lambda: self.codebase_release.validate_publishable()
)
diff --git a/django/library/tests/test_views.py b/django/library/tests/test_views.py
index 07aced00e..0be35d1d2 100644
--- a/django/library/tests/test_views.py
+++ b/django/library/tests/test_views.py
@@ -19,7 +19,14 @@
)
from library.forms import PeerReviewerFeedbackReviewerForm
from library.fs import FileCategoryDirectories
-from library.models import Codebase, CodebaseRelease, License, PeerReview
+from library.models import (
+ ProgrammingLanguage,
+ ReleaseLanguage,
+ Codebase,
+ CodebaseRelease,
+ License,
+ PeerReview,
+)
from library.tests.base import ReviewSetup
from .base import (
CodebaseFactory,
@@ -561,7 +568,11 @@ def test_publish_codebaserelease(self):
)
self.assertRaises(ValidationError, lambda: self.codebase_release.publish())
- self.codebase_release.programming_languages.add("Java")
+ ReleaseLanguage.objects.create(
+ programming_language=ProgrammingLanguage.objects.get_or_create(name="Java"),
+ release=self.codebase_release,
+ version="8",
+ )
self.codebase_release.publish()
download_response = self.client.get(
diff --git a/django/library/urls.py b/django/library/urls.py
index ef3524210..b4e72a705 100644
--- a/django/library/urls.py
+++ b/django/library/urls.py
@@ -15,6 +15,7 @@
r"codebases/(?P[\w\-.]+)/releases", views.CodebaseReleaseViewSet
)
router.register(r"reviewers", views.PeerReviewerViewSet),
+router.register(r"programming-languages", views.ProgrammingLanguageViewSet),
router.register(
r"reviews/(?P[\da-f\-]+)/editor/invitations",
views.PeerReviewInvitationViewSet,
diff --git a/django/library/views.py b/django/library/views.py
index 09abf9fa0..aa51bde6a 100644
--- a/django/library/views.py
+++ b/django/library/views.py
@@ -60,6 +60,7 @@
Contributor,
CodebaseImage,
License,
+ ProgrammingLanguage,
PeerReview,
PeerReviewer,
PeerReviewerFeedback,
@@ -73,6 +74,7 @@
ContributorSerializer,
DownloadRequestSerializer,
PeerReviewerSerializer,
+ ProgrammingLanguageSerializer,
ReleaseContributorSerializer,
CodebaseReleaseEditSerializer,
CodebaseImageSerializer,
@@ -426,9 +428,9 @@ def filter_queryset(self, request, queryset, view):
if programming_languages:
# FIXME: this does not work for the same reason tags__name__in does not work, e.g.,
# https://docs.wagtail.org/en/stable/topics/search/indexing.html#filtering-on-index-relatedfields
- # criteria.update(releases__programming_languages__name__in=programming_languages)
+ # criteria.update(releases__release_languages__programming_language__name__in=programming_languages)
codebases = Codebase.objects.public(
- releases__programming_languages__name__in=programming_languages
+ releases__release_languages__programming_language__name__in=programming_languages
)
criteria.update(id__in=codebases.values_list("id", flat=True))
@@ -1227,3 +1229,16 @@ def form_valid(self, form):
def get_success_url(self):
return reverse("core:profile-detail", kwargs={"pk": self.request.user.id})
+
+
+class ProgrammingLanguageViewSet(CommonViewSetMixin, NoDeleteViewSet):
+ """
+ ViewSet for ProgrammingLanguage model
+ """
+
+ queryset = ProgrammingLanguage.objects.all()
+ pagination_class = None
+ serializer_class = ProgrammingLanguageSerializer
+ permission_classes = (IsAuthenticatedOrReadOnly,)
+ filter_backends = (OrderingFilter,)
+ ordering = ["-is_pinned", "name"]
diff --git a/e2e/cypress/tests/codebase.spec.ts b/e2e/cypress/tests/codebase.spec.ts
index e9c5ec3fc..322fec3a5 100644
--- a/e2e/cypress/tests/codebase.spec.ts
+++ b/e2e/cypress/tests/codebase.spec.ts
@@ -80,7 +80,8 @@ describe("Visit codebases page", () => {
getDataCy("operating-system").find("select").select("Operating System Independent");
getDataCy("software-frameworks").type("NetLogo {enter}");
cy.get("body").click(0, 0);
- getDataCy("programming-languages").type("Net Logo {enter}");
+ getDataCy("programming-languages").find("input[type=\"checkbox\"]").check();
+ getDataCy("programming-languages").type("Netlogo {enter}");
cy.get("body").click(0, 0);
getDataCy("license").click();
getDataCy("license").within(() => {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a16692442..a5d394b6a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -33,7 +33,7 @@
"sortablejs": "^1.15.2",
"sortablejs-vue3": "^1.2.11",
"vue": "^3.2.47",
- "vue-multiselect": "^3.0.0-beta.1",
+ "vue-multiselect": "^3.3.1",
"vue-router": "^4.3.0",
"yup": "^1.3.3"
},
@@ -6359,9 +6359,9 @@
}
},
"node_modules/vue-multiselect": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.1.0.tgz",
- "integrity": "sha512-+i/fjTqFBpaay9NP+lU7obBeNaw2DdFDFs4mqhsM0aEtKRdvIf7CfREAx2o2B4XDmPrBt1r7x1YCM3BOMLaUgQ==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.3.1.tgz",
+ "integrity": "sha512-QZPxG60HK4HCeBNq4rkpzHSzh3ow8blipZbKmYdRvN65If/aFWO/Bzz6eUCED4LQNYlvXG7UJuiFlbqFkAeKXg==",
"license": "MIT",
"engines": {
"node": ">= 14.18.1",
diff --git a/frontend/package.json b/frontend/package.json
index 7c2f62614..ec12c2f33 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -42,7 +42,7 @@
"sortablejs": "^1.15.2",
"sortablejs-vue3": "^1.2.11",
"vue": "^3.2.47",
- "vue-multiselect": "^3.0.0-beta.1",
+ "vue-multiselect": "^3.3.1",
"vue-router": "^4.3.0",
"yup": "^1.3.3"
},
diff --git a/frontend/src/components/form/MultiSelectField.vue b/frontend/src/components/form/MultiSelectField.vue
index 71e26fe79..e5af572ad 100644
--- a/frontend/src/components/form/MultiSelectField.vue
+++ b/frontend/src/components/form/MultiSelectField.vue
@@ -63,7 +63,7 @@ export interface MultiSelectFieldProps {
multiple?: boolean;
trackBy?: string;
labelWith?: string;
- customLabel?: (option: any) => void;
+ customLabel?: (option: any, label: any) => string;
options: any[];
}
diff --git a/frontend/src/components/form/ProgrammingLanguageListField.vue b/frontend/src/components/form/ProgrammingLanguageListField.vue
new file mode 100644
index 000000000..d3750bd9b
--- /dev/null
+++ b/frontend/src/components/form/ProgrammingLanguageListField.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/releaseEditor/MetadataFormPage.vue b/frontend/src/components/releaseEditor/MetadataFormPage.vue
index ac83e218a..680527fd8 100644
--- a/frontend/src/components/releaseEditor/MetadataFormPage.vue
+++ b/frontend/src/components/releaseEditor/MetadataFormPage.vue
@@ -39,12 +39,12 @@
help=" Modeling software frameworks (e.g., NetLogo, RePast, Mason, CORMAS, Mesa, etc.) used by this model"
required
/>
-
{
const metadataProgress = computed(() => {
const metadata = store.metadata;
+ console.log(metadata);
return {
notes: !!metadata.releaseNotes,
os: !!metadata.os.length,
platforms: metadata.platforms.length > 0,
- languages: metadata.programmingLanguages.length > 0,
+ languages: metadata.releaseLanguages.length > 0,
license: !!metadata.license,
};
});
diff --git a/frontend/src/composables/api/programmingLanguage.ts b/frontend/src/composables/api/programmingLanguage.ts
new file mode 100644
index 000000000..ee3947394
--- /dev/null
+++ b/frontend/src/composables/api/programmingLanguage.ts
@@ -0,0 +1,40 @@
+import { toRefs } from "vue";
+import { useAxios } from "@/composables/api";
+import type { RequestOptions } from "@/composables/api";
+
+export function useProgrammingLanguageAPI() {
+ /**
+ * Composable function for making requests to the programming languages API
+ *
+ * @returns - An object containing reactive state of the request and helper functions for API requests
+ */
+
+ const baseUrl = "/programming-languages/";
+ const { state, get, post, put, detailUrl, searchUrl } = useAxios(baseUrl);
+
+ async function list() {
+ return get(baseUrl);
+ }
+
+ async function retrieve(id: string | number) {
+ return get(detailUrl(id));
+ }
+
+ async function create(data: any, options?: RequestOptions) {
+ return post(baseUrl, data, options);
+ }
+
+ async function update(id: string | number, data: any, options?: RequestOptions) {
+ return put(detailUrl(id), data, options);
+ }
+
+ return {
+ ...toRefs(state),
+ list,
+ retrieve,
+ create,
+ update,
+ detailUrl,
+ searchUrl,
+ };
+}
diff --git a/frontend/src/stores/releaseEditor.ts b/frontend/src/stores/releaseEditor.ts
index f8d554afe..b74852011 100644
--- a/frontend/src/stores/releaseEditor.ts
+++ b/frontend/src/stores/releaseEditor.ts
@@ -35,7 +35,7 @@ export const useReleaseEditorStore = defineStore("releaseEditor", () => {
embargoEndDate: release.value.embargoEndDate,
os: release.value.os,
platforms: release.value.platforms,
- programmingLanguages: release.value.programmingLanguages,
+ releaseLanguages: release.value.releaseLanguages,
outputDataUrl: release.value.outputDataUrl,
live: release.value.live,
canEditOriginals: release.value.canEditOriginals,
@@ -192,7 +192,7 @@ const INITIAL_STATE: CodebaseReleaseEditorState = {
peerReviewed: false,
platforms: [],
possibleLicenses: [],
- programmingLanguages: [],
+ releaseLanguages: [],
releaseContributors: [],
releaseNotes: "",
reviewStatus: null,
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index deda95fd1..1850ee07e 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -225,6 +225,18 @@ export interface License {
url?: string;
}
+export interface ProgrammingLanguage {
+ id?: number;
+ name: string;
+ pinned?: boolean;
+ is_user_defined?: boolean;
+}
+
+export interface ReleaseLanguage {
+ programmingLanguage: ProgrammingLanguage;
+ version?: string;
+}
+
export interface ReleaseContributor {
contributor: Contributor;
includeInCitation?: boolean;
@@ -291,7 +303,7 @@ export interface CodebaseRelease {
peerReviewed: boolean;
platforms: Tag[];
possibleLicenses: License[];
- programmingLanguages: Tag[];
+ releaseLanguages: ReleaseLanguage[];
releaseContributors: ReleaseContributor[];
releaseNotes: string;
reviewStatus: string | null;
@@ -333,13 +345,7 @@ interface Codebase {
export type CodebaseReleaseMetadata = Pick<
CodebaseRelease,
- | "releaseNotes"
- | "embargoEndDate"
- | "os"
- | "platforms"
- | "programmingLanguages"
- | "live"
- | "license"
+ "releaseNotes" | "embargoEndDate" | "os" | "platforms" | "releaseLanguages" | "live" | "license"
>;
export interface UploadSuccess {