Skip to content

Commit 39b7a17

Browse files
committed
refactor: Use new model instead of tags for Release Programming Languages
1 parent a3c294f commit 39b7a17

File tree

15 files changed

+448
-35
lines changed

15 files changed

+448
-35
lines changed

django/curator/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from nltk.tokenize import word_tokenize
1616
from taggit.models import Tag
1717

18-
from library.models import ProgrammingLanguage, CodebaseReleasePlatformTag
18+
from library.models import ProgrammingLanguageTag, CodebaseReleasePlatformTag
1919

2020
logger = logging.getLogger(__name__)
2121

@@ -42,10 +42,10 @@ def get_through_tables():
4242
class TagCuratorProxyQuerySet(models.QuerySet):
4343
def with_comma(self):
4444
return self.filter(name__icontains=",")
45-
46-
def programming_languages(self):
45+
46+
def programming_language_tags(self):
4747
return self.filter(
48-
id__in=ProgrammingLanguage.objects.values_list("tag_id", flat=True)
48+
id__in=ProgrammingLanguageTag.objects.values_list("tag_id", flat=True)
4949
)
5050

5151
def platforms(self):

django/library/metadata.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ def _convert_release(cls, release) -> CodeMeta:
196196
),
197197
programmingLanguage=[
198198
# FIXME: this can include "version" when langs are refactored
199-
{"@type": "ComputerLanguage", "name": pl.name}
200-
for pl in release.programming_languages.all().order_by("name")
199+
{"@type": "ComputerLanguage", "name": rl.programming_language.name}
200+
for rl in release.release_languages.all().order_by("programming_language__name")
201201
],
202202
runtimePlatform=[
203203
tag.name for tag in release.platform_tags.all().order_by("name")
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Generated by Django 4.2.22 on 2025-09-29 23:02
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import modelcluster.contrib.taggit
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("taggit", "0005_auto_20220424_2025"),
12+
("library", "0032_license_text_codemeta_snapshot"),
13+
]
14+
15+
operations = [
16+
migrations.RenameModel(
17+
old_name="ProgrammingLanguage",
18+
new_name="ProgrammingLanguageTag",
19+
),
20+
migrations.RenameField(
21+
model_name="codebaserelease",
22+
old_name="programming_languages",
23+
new_name="programming_language_tags",
24+
),
25+
migrations.CreateModel(
26+
name="ProgrammingLanguage",
27+
fields=[
28+
(
29+
"id",
30+
models.AutoField(
31+
auto_created=True,
32+
primary_key=True,
33+
serialize=False,
34+
verbose_name="ID",
35+
),
36+
),
37+
("name", models.CharField(max_length=100, unique=True)),
38+
("url", models.URLField(blank=True)),
39+
("is_pinned", models.BooleanField(default=False)),
40+
("is_user_defined", models.BooleanField(default=False)),
41+
],
42+
),
43+
migrations.CreateModel(
44+
name="ReleaseLanguage",
45+
fields=[
46+
(
47+
"id",
48+
models.AutoField(
49+
auto_created=True,
50+
primary_key=True,
51+
serialize=False,
52+
verbose_name="ID",
53+
),
54+
),
55+
("version", models.CharField(max_length=20)),
56+
(
57+
"programming_language",
58+
models.ForeignKey(
59+
on_delete=django.db.models.deletion.CASCADE,
60+
related_name="release_languages",
61+
to="library.programminglanguage",
62+
),
63+
),
64+
(
65+
"release",
66+
models.ForeignKey(
67+
on_delete=django.db.models.deletion.CASCADE,
68+
related_name="release_languages",
69+
to="library.codebaserelease",
70+
),
71+
),
72+
],
73+
),
74+
]

django/library/models.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,34 @@ class CodebaseTag(TaggedItemBase):
9292
content_object = ParentalKey("library.Codebase", related_name="tagged_codebases")
9393

9494

95-
class ProgrammingLanguage(TaggedItemBase):
95+
class ProgrammingLanguageTag(TaggedItemBase):
9696
content_object = ParentalKey(
9797
"library.CodebaseRelease", related_name="tagged_release_languages"
9898
)
9999

100100

101+
@register_snippet
102+
class ProgrammingLanguage(models.Model):
103+
name = models.CharField(max_length=100, unique=True)
104+
url = models.URLField(blank=True)
105+
is_pinned = models.BooleanField(default=False)
106+
is_user_defined = models.BooleanField(default=False)
107+
108+
109+
class ReleaseLanguageQuerySet(models.QuerySet):
110+
def for_release(self, release):
111+
return self.select_related('programming_language').filter(release=release)
112+
113+
class ReleaseLanguage(models.Model):
114+
programming_language = models.ForeignKey(
115+
"library.ProgrammingLanguage", related_name="release_languages", on_delete=models.CASCADE
116+
)
117+
release = models.ForeignKey(
118+
"library.CodebaseRelease", related_name="release_languages", on_delete=models.CASCADE
119+
)
120+
version = models.CharField(max_length=20)
121+
122+
101123
class CodebaseReleasePlatformTag(TaggedItemBase):
102124
content_object = ParentalKey(
103125
"library.CodebaseRelease", related_name="tagged_release_platforms"
@@ -989,15 +1011,15 @@ def create_release_from_source(self, source_release, release_metadata):
9891011
# cache these before removing source release id to copy it over
9901012
contributors = ReleaseContributor.objects.filter(release_id=source_release.id)
9911013
platform_tags = source_release.platform_tags.all()
992-
programming_languages = source_release.programming_languages.all()
1014+
release_languages = source_release.release_languages.all()
9931015
# set source_release.id to None to create a new release
9941016
# see https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances
9951017
source_release.id = None
9961018
source_release._state.adding = True
9971019
source_release.__dict__.update(**release_metadata)
9981020
source_release.save()
9991021
source_release.platform_tags.add(*platform_tags)
1000-
source_release.programming_languages.add(*programming_languages)
1022+
source_release.release_languages.add(*release_languages)
10011023
contributors.copy_to(source_release)
10021024
return source_release
10031025

@@ -1146,7 +1168,7 @@ def with_platforms(self):
11461168
return self.prefetch_related("tagged_release_platforms__tag")
11471169

11481170
def with_programming_languages(self):
1149-
return self.prefetch_related("tagged_release_languages__tag")
1171+
return self.prefetch_related("release_languages__programming_language")
11501172

11511173
def with_codebase(self):
11521174
return self.prefetch_related(
@@ -1287,8 +1309,8 @@ class Status(models.TextChoices):
12871309
through=CodebaseReleasePlatformTag, related_name="platform_codebase_releases"
12881310
)
12891311
platforms = models.ManyToManyField(Platform)
1290-
programming_languages = ClusterTaggableManager(
1291-
through=ProgrammingLanguage, related_name="pl_codebase_releases"
1312+
programming_language_tags = ClusterTaggableManager(
1313+
through=ProgrammingLanguageTag, related_name="pl_codebase_releases"
12921314
)
12931315
codebase = models.ForeignKey(
12941316
Codebase, related_name="releases", on_delete=models.PROTECT
@@ -1462,7 +1484,7 @@ def validate_metadata(self):
14621484
# naive check for metadata being present (i.e., None or false-y values)
14631485
if not self.license:
14641486
errors.append(ValidationError(_("Please specify a software license.")))
1465-
if not self.programming_languages.exists():
1487+
if not self.release_languages.exists():
14661488
errors.append(
14671489
ValidationError(
14681490
_(
@@ -2725,7 +2747,7 @@ def __init__(self, release: CodebaseRelease):
27252747
self.description = codebase.description.raw
27262748
self.release_notes = release.release_notes.raw if release.release_notes else ""
27272749
self.version = release.version_number
2728-
self.programming_languages = release.programming_languages.all()
2750+
self.release_languages = release.release_languages.all()
27292751
self.os = release.os
27302752
self.identifier = release.permanent_url
27312753
self.url = release.permanent_url

django/library/serializers.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
CodebaseReleaseDownload,
3838
Contributor,
3939
License,
40+
ProgrammingLanguage,
41+
ReleaseLanguage,
4042
CodebaseImage,
4143
PeerReviewerFeedback,
4244
PeerReviewInvitation,
@@ -63,6 +65,39 @@ class Meta:
6365
)
6466

6567

68+
class ProgrammingLanguageSerializer(serializers.ModelSerializer):
69+
class Meta:
70+
model = ProgrammingLanguage
71+
fields = (
72+
"id",
73+
"name",
74+
"url",
75+
"is_pinned",
76+
"is_user_defined",
77+
)
78+
79+
class ReleaseLanguageSerializer(serializers.ModelSerializer):
80+
programming_language = ProgrammingLanguageSerializer(read_only=True)
81+
82+
# def create(self, validated_data):
83+
# programming_language_data = self.initial_data.pop("programming_language")
84+
# programming_language, created = ProgrammingLanguage.objects.get_or_create(
85+
# name=programming_language_data['name']
86+
# )
87+
# validated_data["programming_language"] = programming_language
88+
# instance = ReleaseLanguage(**validated_data)
89+
# instance.save()
90+
# return instance
91+
92+
class Meta:
93+
model = ReleaseLanguage
94+
fields = (
95+
"programming_language",
96+
"release",
97+
"version",
98+
)
99+
100+
66101
class ContributorSerializer(serializers.ModelSerializer):
67102
# Need an ID for Vue-Multiselect
68103
id = serializers.IntegerField(read_only=True)
@@ -563,7 +598,8 @@ class CodebaseReleaseSerializer(serializers.ModelSerializer):
563598
can_edit_originals = serializers.ReadOnlyField()
564599
os_display = serializers.ReadOnlyField(source="get_os_display")
565600
platforms = TagSerializer(many=True, source="platform_tags")
566-
programming_languages = TagSerializer(many=True)
601+
programming_language_tags = TagSerializer(many=True)
602+
release_languages = ReleaseLanguageSerializer(read_only=True, many=True)
567603
submitter = RelatedUserSerializer(read_only=True, label="Submitter")
568604
version_number = serializers.ReadOnlyField()
569605
release_notes = MarkdownField(max_length=2048)
@@ -609,7 +645,7 @@ class Meta:
609645
"os_display",
610646
"peer_reviewed",
611647
"platforms",
612-
"programming_languages",
648+
"release_languages",
613649
"submitted_package",
614650
"submitter",
615651
"codebase",
@@ -634,17 +670,41 @@ def get_possible_licenses(self, instance):
634670
)
635671
return serialized.data
636672

673+
def resolve_language(self, language_name):
674+
programming_language = ProgrammingLanguage.objects.filter(name__iexact=language_name).first()
675+
if not programming_language:
676+
programming_language = ProgrammingLanguage.objects.create(
677+
name=language_name,
678+
is_user_defined=True
679+
)
680+
return programming_language
681+
637682
def update(self, instance, validated_data):
638-
programming_languages = TagSerializer(
639-
many=True, data=validated_data.pop("programming_languages")
640-
)
641683
platform_tags = TagSerializer(
642684
many=True, data=validated_data.pop("platform_tags")
643685
)
644686

645-
set_tags(instance, programming_languages, "programming_languages")
646687
set_tags(instance, platform_tags, "platform_tags")
647688

689+
# Handle programming languages
690+
release_languages_data = self.initial_data.pop("release_languages")
691+
if release_languages_data:
692+
# Clear existing programming languages
693+
instance.release_languages.all().delete()
694+
695+
696+
# Create new release languages
697+
for release_language_data in release_languages_data:
698+
language_data = release_language_data.get("programming_language")
699+
if not language_data or "name" not in language_data:
700+
raise ValidationError("Malformed programming language data")
701+
programming_language = self.resolve_language(language_data["name"])
702+
ReleaseLanguage.objects.create(
703+
programming_language=programming_language,
704+
release=instance,
705+
version=release_language_data.get("version", ""),
706+
)
707+
648708
raw_license = validated_data.pop("license")
649709
existing_license = License.objects.get(name=raw_license["name"])
650710
instance.license = existing_license

django/library/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
r"codebases/(?P<identifier>[\w\-.]+)/releases", views.CodebaseReleaseViewSet
1616
)
1717
router.register(r"reviewers", views.PeerReviewerViewSet),
18+
router.register(r"programming-languages", views.ProgrammingLanguageViewSet),
1819
router.register(
1920
r"reviews/(?P<slug>[\da-f\-]+)/editor/invitations",
2021
views.PeerReviewInvitationViewSet,

django/library/views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
Contributor,
6161
CodebaseImage,
6262
License,
63+
ProgrammingLanguage,
6364
PeerReview,
6465
PeerReviewer,
6566
PeerReviewerFeedback,
@@ -73,6 +74,7 @@
7374
ContributorSerializer,
7475
DownloadRequestSerializer,
7576
PeerReviewerSerializer,
77+
ProgrammingLanguageSerializer,
7678
ReleaseContributorSerializer,
7779
CodebaseReleaseEditSerializer,
7880
CodebaseImageSerializer,
@@ -1227,3 +1229,14 @@ def form_valid(self, form):
12271229

12281230
def get_success_url(self):
12291231
return reverse("core:profile-detail", kwargs={"pk": self.request.user.id})
1232+
1233+
1234+
class ProgrammingLanguageViewSet(CommonViewSetMixin, NoDeleteViewSet):
1235+
"""
1236+
ViewSet for ProgrammingLanguage model
1237+
"""
1238+
queryset = ProgrammingLanguage.objects.all()
1239+
serializer_class = ProgrammingLanguageSerializer
1240+
pagination_class = SmallResultSetPagination
1241+
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
1242+
ordering = ["-is_pinned", "name"]

frontend/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"sortablejs": "^1.15.2",
4343
"sortablejs-vue3": "^1.2.11",
4444
"vue": "^3.2.47",
45-
"vue-multiselect": "^3.0.0-beta.1",
45+
"vue-multiselect": "^3.3.1",
4646
"vue-router": "^4.3.0",
4747
"yup": "^1.3.3"
4848
},

0 commit comments

Comments
 (0)