mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-10 08:01:17 +00:00
@@ -369,7 +369,7 @@ class BaseIngestAPIView(View):
|
||||
# multiple events with the same event_id "don't happen" (i.e. are the result of badly misbehaving clients)
|
||||
raise ValidationError("Event already exists", code="event_already_exists")
|
||||
|
||||
release = create_release_if_needed(project, event.release, event.ingested_at, issue)
|
||||
release, _ = create_release_if_needed(project, event.release, event.ingested_at, issue)
|
||||
|
||||
if issue_created:
|
||||
TurningPoint.objects.create(
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from .models import Release
|
||||
from .serializers import ReleaseSerializer
|
||||
from projects.models import Project
|
||||
from .models import Release, ordered_releases
|
||||
from .serializers import ReleaseListSerializer, ReleaseDetailSerializer, ReleaseCreateSerializer
|
||||
|
||||
|
||||
class ReleaseViewSet(viewsets.ModelViewSet):
|
||||
queryset = Release.objects.all().order_by('sort_epoch')
|
||||
serializer_class = ReleaseSerializer
|
||||
"""
|
||||
LIST requires: ?project=<id>
|
||||
Ordered by sort_epoch.
|
||||
CREATE allowed. DELETE potential TODO.
|
||||
"""
|
||||
queryset = Release.objects.all()
|
||||
serializer_class = ReleaseListSerializer
|
||||
http_method_names = ["get", "post", "head", "options"]
|
||||
|
||||
# TODO: the idea of required filter-fields when listing; in particular: project is required.
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
if self.action != "list":
|
||||
return queryset
|
||||
|
||||
query_params = self.request.query_params
|
||||
project_id = query_params.get("project")
|
||||
if not project_id:
|
||||
raise ValidationError({"project": ["This field is required."]})
|
||||
|
||||
# application-ordering (as opposed to in-db); will have performance implications, but we do "correct first, fast
|
||||
# later":
|
||||
project = Project.objects.get(pk=project_id)
|
||||
return ordered_releases(project=project)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return ReleaseCreateSerializer
|
||||
if self.action == "retrieve":
|
||||
return ReleaseDetailSerializer
|
||||
return ReleaseListSerializer
|
||||
|
||||
@@ -110,7 +110,9 @@ def create_release_if_needed(project, version, timestamp, issue=None):
|
||||
|
||||
version = sanitize_version(version)
|
||||
|
||||
release, release_created = Release.objects.get_or_create(project=project, version=version)
|
||||
release, release_created = Release.objects.get_or_create(project=project, version=version, defaults={
|
||||
"date_released": timestamp,
|
||||
})
|
||||
if release_created and version != "":
|
||||
if not project.has_releases:
|
||||
project.has_releases = True
|
||||
@@ -138,7 +140,7 @@ def create_release_if_needed(project, version, timestamp, issue=None):
|
||||
issue.fixed_at = issue.fixed_at + release.version + "\n"
|
||||
issue.is_resolved_by_next_release = False
|
||||
|
||||
return release
|
||||
return release, release_created
|
||||
|
||||
|
||||
def sanitize_version(version):
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Release
|
||||
from projects.models import Project
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from .models import Release, create_release_if_needed
|
||||
|
||||
|
||||
class ReleaseSerializer(serializers.ModelSerializer):
|
||||
class ReleaseListSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Release
|
||||
fields = ["id", "project", "version", "date_released"]
|
||||
|
||||
# TODO: distinguish read vs write fields
|
||||
fields = [
|
||||
"id",
|
||||
"project",
|
||||
"version",
|
||||
"date_released",
|
||||
"semver",
|
||||
"is_semver",
|
||||
"sort_epoch",
|
||||
]
|
||||
|
||||
class ReleaseDetailSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Release
|
||||
fields = ["id", "project", "version", "date_released", "semver", "is_semver", "sort_epoch"]
|
||||
read_only_fields = ["semver", "is_semver", "sort_epoch"]
|
||||
|
||||
|
||||
class ReleaseCreateSerializer(serializers.Serializer):
|
||||
project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all())
|
||||
version = serializers.CharField(allow_blank=True)
|
||||
timestamp = serializers.DateTimeField(required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
project = validated_data["project"]
|
||||
version = validated_data["version"]
|
||||
timestamp = validated_data.get("timestamp") or timezone.now()
|
||||
|
||||
release, release_created = create_release_if_needed(project=project, version=version, timestamp=timestamp)
|
||||
if not release_created:
|
||||
raise ValidationError({"version": ["Release with this version already exists for the project."]})
|
||||
return release
|
||||
|
||||
def to_representation(self, instance):
|
||||
return ReleaseDetailSerializer(instance).data
|
||||
|
||||
92
releases/test_api.py
Normal file
92
releases/test_api.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from django.test import TestCase as DjangoTestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from bsmain.models import AuthToken
|
||||
from projects.models import Project
|
||||
from releases.models import ordered_releases
|
||||
|
||||
|
||||
class ReleaseApiTests(DjangoTestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
token = AuthToken.objects.create()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.token}")
|
||||
self.project = Project.objects.create(name="RelProj")
|
||||
|
||||
def _create(self, version, **extra):
|
||||
payload = {"project": self.project.id, "version": version}
|
||||
payload.update(extra)
|
||||
response = self.client.post(reverse("api:release-list"), payload, format="json")
|
||||
return response
|
||||
|
||||
def test_list_requires_project(self):
|
||||
response = self.client.get(reverse("api:release-list"))
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual({"project": ["This field is required."]}, response.json())
|
||||
|
||||
def test_list_uses_ordered_releases(self):
|
||||
# Create in arbitrary order
|
||||
self._create("1.0.0")
|
||||
self._create("1.0.0+build")
|
||||
self._create("1.0.1")
|
||||
|
||||
response = self.client.get(reverse("api:release-list"), {"project": str(self.project.id)})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
versions_from_api = [row["version"] for row in response.json()["results"]]
|
||||
versions_expected = [r.version for r in ordered_releases(project=self.project)]
|
||||
self.assertEqual(versions_from_api, versions_expected)
|
||||
|
||||
def test_create_new_returns_201_and_detail_shape(self):
|
||||
response = self._create("1.2.3", timestamp="2024-01-01T00:00:00Z")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
body = response.json()
|
||||
self.assertIn("id", body)
|
||||
|
||||
# model-computed fields are present in response:
|
||||
self.assertIn("semver", body)
|
||||
self.assertIn("is_semver", body)
|
||||
self.assertIn("sort_epoch", body)
|
||||
|
||||
def test_create_duplicate_returns_400(self):
|
||||
result1 = self._create("2.0.0")
|
||||
self.assertEqual(result1.status_code, 201)
|
||||
|
||||
result2 = self._create("2.0.0") # same project+version
|
||||
self.assertEqual(result2.status_code, 400)
|
||||
self.assertIn("version", result2.json())
|
||||
|
||||
def test_create_allows_empty_version(self):
|
||||
response = self._create("")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
def test_create_without_timestamp_is_allowed(self):
|
||||
response = self._create("3.0.0")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
def test_detail_returns_readonly_fields(self):
|
||||
created = self._create("4.5.6")
|
||||
self.assertEqual(created.status_code, 201)
|
||||
release_id = created.json()["id"]
|
||||
|
||||
response = self.client.get(reverse("api:release-detail", args=[release_id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = response.json()
|
||||
self.assertIn("semver", body)
|
||||
self.assertIn("is_semver", body)
|
||||
self.assertIn("sort_epoch", body)
|
||||
|
||||
def test_update_and_delete_methods_not_allowed(self):
|
||||
created = self._create("9.9.9")
|
||||
self.assertEqual(created.status_code, 201)
|
||||
release_id = created.json()["id"]
|
||||
detail_url = reverse("api:release-detail", args=[release_id])
|
||||
|
||||
put_response = self.client.put(detail_url, {"version": "X"}, format="json")
|
||||
patch_response = self.client.patch(detail_url, {"version": "X"}, format="json")
|
||||
delete_response = self.client.delete(detail_url)
|
||||
|
||||
self.assertEqual(put_response.status_code, 405)
|
||||
self.assertEqual(patch_response.status_code, 405)
|
||||
self.assertEqual(delete_response.status_code, 405)
|
||||
Reference in New Issue
Block a user