Skip to content

Commit

Permalink
Merge pull request #1013 from thunderstore-io/installers-field
Browse files Browse the repository at this point in the history
Add 'installers' field support to package manifest
  • Loading branch information
MythicManiac authored Jan 28, 2024
2 parents 753441b + b25120b commit 1af5e01
Show file tree
Hide file tree
Showing 16 changed files with 491 additions and 37 deletions.
15 changes: 13 additions & 2 deletions django/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
AsyncPackageSubmissionFactory,
NamespaceFactory,
PackageFactory,
PackageInstallerFactory,
PackageVersionFactory,
PackageWikiFactory,
TeamFactory,
Expand All @@ -43,6 +44,7 @@
from thunderstore.repository.models import (
AsyncPackageSubmission,
Package,
PackageInstaller,
PackageVersion,
PackageWiki,
Team,
Expand Down Expand Up @@ -425,11 +427,15 @@ def api_client(community_site) -> APIClient:


@pytest.fixture(scope="session")
def manifest_v1_package_bytes() -> bytes:
def package_icon_bytes() -> bytes:
icon_raw = io.BytesIO()
icon = Image.new("RGB", (256, 256), "#FF0000")
icon.save(icon_raw, format="PNG")
return icon_raw.getvalue()


@pytest.fixture(scope="session")
def manifest_v1_package_bytes(package_icon_bytes: bytes) -> bytes:
readme = "# Test readme".encode("utf-8")
manifest = json.dumps(
{
Expand All @@ -443,7 +449,7 @@ def manifest_v1_package_bytes() -> bytes:

files = [
("README.md", readme),
("icon.png", icon_raw.getvalue()),
("icon.png", package_icon_bytes),
("manifest.json", manifest),
]

Expand Down Expand Up @@ -500,6 +506,11 @@ def async_package_submission(
)


@pytest.fixture
def package_installer() -> PackageInstaller:
return PackageInstallerFactory()


def create_test_service_account_user():
team_owner = UserFactory()
team = TeamFactory()
Expand Down
1 change: 1 addition & 0 deletions django/thunderstore/repository/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .discord_bot import DiscordUserBotPermissionAdmin
from .namespace import NamespaceAdmin
from .package import PackageAdmin
from .package_installer import PackageInstallerAdmin
from .package_rating import PackageRatingAdmin
from .package_version import PackageVersionAdmin
from .submission import AsyncPackageSubmissionAdmin
Expand Down
21 changes: 21 additions & 0 deletions django/thunderstore/repository/admin/package_installer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib import admin
from django.http import HttpRequest

from thunderstore.repository.models import PackageInstaller


@admin.register(PackageInstaller)
class PackageInstallerAdmin(admin.ModelAdmin):
readonly_fields = ("identifier",)
list_display_links = ("pk", "identifier")
list_display = ("pk", "identifier")
search_fields = ("pk", "identifier")

def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
return False

def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
return False

def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
from django.conf import settings
from django.test import Client

from thunderstore.repository.models import PackageInstaller


@pytest.mark.django_db
def test_admin_package_installer_search(admin_client: Client) -> None:
resp = admin_client.get(
path="/djangoadmin/repository/packageinstaller/?q=asd",
HTTP_HOST=settings.PRIMARY_HOST,
)
assert resp.status_code == 200


@pytest.mark.django_db
def test_admin_package_installer_list(
package_installer: PackageInstaller, admin_client: Client
) -> None:
resp = admin_client.get(
path="/djangoadmin/repository/packageinstaller/",
HTTP_HOST=settings.PRIMARY_HOST,
)
assert resp.status_code == 200
assert package_installer.identifier in resp.content.decode()


@pytest.mark.django_db
def test_admin_package_installer_detail(
package_installer: PackageInstaller,
admin_client: Client,
) -> None:
path = f"/djangoadmin/repository/packageinstaller/{package_installer.pk}/change/"
resp = admin_client.get(
path=path,
HTTP_HOST=settings.PRIMARY_HOST,
)
assert resp.status_code == 200
8 changes: 8 additions & 0 deletions django/thunderstore/repository/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
AsyncPackageSubmission,
Namespace,
Package,
PackageInstaller,
PackageRating,
PackageVersion,
PackageWiki,
Expand Down Expand Up @@ -86,3 +87,10 @@ class Meta:
owner = factory.SubFactory(UserFactory)
file = factory.SubFactory(UserMediaFactory, owner=factory.SelfAttribute("..owner"))
form_data = dict()


class PackageInstallerFactory(DjangoModelFactory):
class Meta:
model = PackageInstaller

identifier = factory.Sequence(lambda n: f"package-installer-{n}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Generated by Django 3.1.7 on 2024-01-27 04:37

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("repository", "0048_populate_visibility"),
]

operations = [
migrations.CreateModel(
name="PackageInstaller",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("identifier", models.SlugField(editable=False, unique=True)),
],
),
migrations.CreateModel(
name="PackageInstallerDeclaration",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
],
),
migrations.RemoveConstraint(
model_name="packageversion",
name="valid_package_format",
),
migrations.AlterField(
model_name="packageversion",
name="format_spec",
field=models.TextField(
blank=True,
choices=[
("thunderstore.io:v0.0", "V0 0"),
("thunderstore.io:v0.1", "V0 1"),
("thunderstore.io:v0.2", "V0 2"),
("thunderstore.io:v0.3", "V0 3"),
],
help_text="Used to track the latest package format spec this package is compatible with",
null=True,
),
),
migrations.AddConstraint(
model_name="packageversion",
constraint=models.CheckConstraint(
check=models.Q(
("format_spec", None),
("format_spec", "thunderstore.io:v0.0"),
("format_spec", "thunderstore.io:v0.1"),
("format_spec", "thunderstore.io:v0.2"),
("format_spec", "thunderstore.io:v0.3"),
_connector="OR",
),
name="valid_package_format",
),
),
migrations.AddField(
model_name="packageinstallerdeclaration",
name="package_installer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="installer_declarations",
to="repository.packageinstaller",
),
),
migrations.AddField(
model_name="packageinstallerdeclaration",
name="package_version",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="installer_declarations",
to="repository.packageversion",
),
),
migrations.AddField(
model_name="packageversion",
name="installers",
field=models.ManyToManyField(
blank=True,
related_name="package_versions",
through="repository.PackageInstallerDeclaration",
to="repository.PackageInstaller",
),
),
]
1 change: 1 addition & 0 deletions django/thunderstore/repository/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .namespace import *
from .package import *
from .package_download import *
from .package_installer import *
from .package_rating import *
from .package_version import *
from .submission import *
Expand Down
27 changes: 27 additions & 0 deletions django/thunderstore/repository/models/package_installer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.db import models


class PackageInstaller(models.Model):
identifier = models.SlugField(unique=True, db_index=True, editable=False)

def __str__(self):
return self.identifier


class PackageInstallerDeclaration(models.Model):
"""
Used for the m2m relation between PackageVersion and PackageInstaller.
A custom model is used in order to support potential extra data in the
installer declarations in the future.
"""

package_installer = models.ForeignKey(
"repository.PackageInstaller",
on_delete=models.PROTECT,
related_name="installer_declarations",
)
package_version = models.ForeignKey(
"repository.PackageVersion",
on_delete=models.CASCADE,
related_name="installer_declarations",
)
16 changes: 15 additions & 1 deletion django/thunderstore/repository/models/package_version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
import uuid
from typing import Iterator, Optional
from typing import TYPE_CHECKING, Iterator, Optional

from django.conf import settings
from django.core.cache import cache
Expand All @@ -19,6 +19,12 @@
from thunderstore.utils.decorators import run_after_commit
from thunderstore.webhooks.models.release import Webhook

if TYPE_CHECKING:
from thunderstore.repository.models.package_installer import (
PackageInstaller,
PackageInstallerDeclaration,
)


def get_version_zip_filepath(instance, filename):
return f"repository/packages/{instance}.zip"
Expand Down Expand Up @@ -61,6 +67,8 @@ def listed_in(self, community_identifier: str):


class PackageVersion(VisibilityMixin):
installers: "Manager[PackageInstaller]"
installer_declarations: "Manager[PackageInstallerDeclaration]"
objects: "Manager[PackageVersion]" = PackageVersionQuerySet.as_manager()
id: int

Expand Down Expand Up @@ -102,6 +110,12 @@ class PackageVersion(VisibilityMixin):
symmetrical=False,
blank=True,
)
installers = models.ManyToManyField(
"repository.PackageInstaller",
through="repository.PackageInstallerDeclaration",
related_name="package_versions",
blank=True,
)
readme = models.TextField()
changelog = models.TextField(blank=True, null=True)

Expand Down
15 changes: 13 additions & 2 deletions django/thunderstore/repository/package_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ class PackageFormats(models.TextChoices):
PackageVersion!
"""

# Initial version
v0_0 = "thunderstore.io:v0.0"

# Added `dependencies` field to manifest
v0_1 = "thunderstore.io:v0.1"

# Added support for `CHANGELOG.md`
v0_2 = "thunderstore.io:v0.2"

# Added `installers` field to manifest
# - if populated, must have at least a single valid installer
# - if populated, has a maximum of 100 installers defined
# - if not populated, must be emitted entirely
v0_3 = "thunderstore.io:v0.3"

@classmethod
def as_query_filter(cls, field_name: str, allow_none: bool) -> Q:
result = Q()
Expand All @@ -26,8 +37,8 @@ def as_query_filter(cls, field_name: str, allow_none: bool) -> Q:

@classmethod
def get_supported_formats(cls) -> Tuple["PackageFormats"]:
return (cls.v0_1, cls.v0_2)
return (cls.v0_1, cls.v0_2, cls.v0_3)

@classmethod
def get_active_format(cls) -> "PackageFormats":
return PackageFormats.v0_2
return PackageFormats.v0_3
Loading

0 comments on commit 1af5e01

Please sign in to comment.