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 {