David Burke 9 месяцев назад
Родитель
Сommit
8676a17a82

+ 1 - 4
apps/importer/importer.py

@@ -48,10 +48,7 @@ class GlitchTipImporter:
             "organization-projects-list",
             kwargs={"organization_slug": self.organization_slug},
         )
-        self.teams_url = reverse(
-            "organization-teams-list",
-            kwargs={"organization_slug": self.organization_slug},
-        )
+        self.teams_url = reverse("api:list_teams", args=[self.organization_slug])
 
     async def run(self, organization_id=None):
         """Set organization_id to None to import (superuser only)"""

+ 0 - 4
apps/organizations_ext/urls.py

@@ -8,7 +8,6 @@ from apps.performance.views import (
 )
 from apps.projects.views import OrganizationProjectsViewSet
 from apps.releases.views import ReleaseViewSet
-from apps.teams.views import NestedTeamViewSet
 from apps.uptime.views import (
     MonitorCheckViewSet,
     MonitorViewSet,
@@ -29,9 +28,6 @@ router.register(r"organizations", OrganizationViewSet)
 organizations_router = routers.NestedSimpleRouter(
     router, r"organizations", lookup="organization"
 )
-organizations_router.register(
-    r"teams", NestedTeamViewSet, basename="organization-teams"
-)
 organizations_router.register(
     r"members", OrganizationMemberViewSet, basename="organization-members"
 )

+ 79 - 0
apps/projects/api.py

@@ -1,9 +1,88 @@
+from django.db.models import Count, Q
+from django.http import HttpResponse
+from django.shortcuts import aget_object_or_404
 from ninja import Router
 
+from apps.organizations_ext.models import Organization, OrganizationUserRole
+from apps.teams.models import Team
+from glitchtip.api.pagination import paginate
+from glitchtip.api.permissions import AuthHttpRequest, has_permission
+
+from .models import Project
+from .schema import ProjectSchema
+
 router = Router()
 
 
 """
+GET /api/0/projects/
+GET /api/0/teams/{organization_slug}/{team_slug}/projects/
+POST /api/0/teams/{organization_slug}/{team_slug}/projects/
 POST /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams)
 DELETE /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams)
 """
+
+
+def get_projects_queryset(
+    user_id: int, organization_slug: str = None, team_slug: str = None
+):
+    qs = Project.objects.filter(organization__users=user_id).annotate(
+        is_member=Count("team__members", filter=Q(team__members__id=user_id))
+    )
+    if organization_slug:
+        qs = qs.filter(organization__slug=organization_slug)
+    if team_slug:
+        qs = qs.filter(team__slug=team_slug)
+    return qs
+
+
+@router.get("projects/", response=list[ProjectSchema])
+@paginate
+@has_permission(["project:read"])
+async def list_projects(request: AuthHttpRequest, response: HttpResponse):
+    return get_projects_queryset(request.auth.user_id).order_by("name")
+
+
+@router.get(
+    "teams/{slug:organization_slug}/{slug:team_slug}/projects/",
+    response=list[ProjectSchema],
+)
+@paginate
+@has_permission(["project:read"])
+async def list_team_projects(
+    request: AuthHttpRequest,
+    response: HttpResponse,
+    organization_slug: str,
+    team_slug: str,
+):
+    return get_projects_queryset(
+        request.auth.user_id, organization_slug=organization_slug, team_slug=team_slug
+    ).order_by("name")
+
+
+@router.get(
+    "teams/{slug:organization_slug}/{slug:team_slug}/projects/",
+    response=ProjectSchema,
+)
+@paginate
+@has_permission(["project:write", "project:admin"])
+async def create_project(
+    request: AuthHttpRequest, organization_slug: str, team_slug: str, payload
+):
+    user_id = request.auth.user_id
+    team = await aget_object_or_404(
+        Team,
+        slug=team_slug,
+        organization__slug=organization_slug,
+        organization__users=user_id,
+        organization__organization_users__role__gte=OrganizationUserRole.ADMIN,
+    )
+    organization = await aget_object_or_404(
+        Organization,
+        slug=organization_slug,
+        users=user_id,
+        organization_users__role__gte=OrganizationUserRole.ADMIN,
+    )
+    project = await Project.objects.acreate(organization=organization, **payload.dict())
+    await project.team_set.aadd(team)
+    return project

+ 10 - 6
apps/projects/tests/tests.py → apps/projects/tests/test_api.py

@@ -14,7 +14,7 @@ class ProjectsAPITestCase(APITestCase):
     def setUp(self):
         self.user = baker.make("users.user")
         self.client.force_login(self.user)
-        self.url = reverse("project-list")
+        self.url = reverse("api:list_projects")
 
     def test_projects_api_create(self):
         """This endpoint can't be used to create"""
@@ -29,20 +29,24 @@ class ProjectsAPITestCase(APITestCase):
         project = baker.make("projects.Project", organization=organization)
         res = self.client.get(self.url)
         self.assertContains(res, project.name)
+        data_keys = res.json()[0].keys()
+        self.assertNotIn("keys", data_keys, "Project keys shouldn't be in list")
+        self.assertNotIn("teams", data_keys, "Teams shouldn't be in list")
 
     def test_default_ordering(self):
         organization = baker.make("organizations_ext.Organization")
         organization.add_user(self.user, role=OrganizationUserRole.OWNER)
         projectA = baker.make(
-            "projects.Project", organization=organization, name="A Project"
+            "projects.Project", organization=organization, name="A Proj"
         )
         projectZ = baker.make(
-            "projects.Project", organization=organization, name="Z Project"
+            "projects.Project", organization=organization, name="Z Proj"
         )
-        baker.make("projects.Project", organization=organization, name="B Project")
+        baker.make("projects.Project", organization=organization, name="B Proj")
         res = self.client.get(self.url)
-        self.assertEqual(res.data[0]["name"], projectA.name)
-        self.assertEqual(res.data[2]["name"], projectZ.name)
+        data = res.json()
+        self.assertEqual(data[0]["name"], projectA.name)
+        self.assertEqual(data[2]["name"], projectZ.name)
 
     def test_projects_api_retrieve(self):
         organization = baker.make("organizations_ext.Organization")

+ 3 - 3
apps/projects/tests/test_api_permissions.py

@@ -12,7 +12,7 @@ class ProjectAPIPermissionTests(APIPermissionTestCase):
         self.team = baker.make("teams.Team", organization=self.organization)
         self.project = baker.make("projects.Project", organization=self.organization)
         self.project.team_set.add(self.team)
-        self.list_url = reverse("project-list")
+        self.list_url = reverse("api:list_projects")
         self.team_list_url = reverse(
             "team-projects-list",
             kwargs={"team_pk": self.organization.slug + "/" + self.team.slug},
@@ -48,8 +48,8 @@ class ProjectAPIPermissionTests(APIPermissionTestCase):
     def test_create(self):
         self.auth_token.add_permission("project:read")
         data = {"name": "new project"}
-        self.assertPostReqStatusCode(self.list_url, data, 403)
-        self.assertPostReqStatusCode(self.team_list_url, data, 403)
+        self.assertPostReqStatusCode(self.list_url, data, 405)
+        self.assertPostReqStatusCode(self.team_list_url, data, 405)
 
         self.auth_token.add_permission("project:write")
         self.assertPostReqStatusCode(

+ 1 - 1
apps/projects/views.py

@@ -19,7 +19,7 @@ from .serializers.serializers import (
 )
 
 
-class BaseProjectViewSet(viewsets.ReadOnlyModelViewSet):
+class BaseProjectViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
     serializer_class = BaseProjectSerializer
     queryset = Project.undeleted_objects.all()
     lookup_field = "slug"

+ 0 - 13
apps/teams/permissions.py

@@ -1,13 +0,0 @@
-from glitchtip.permissions import ScopedPermission
-
-
-class TeamPermission(ScopedPermission):
-    scope_map = {
-        "GET": ["team:read", "team:write", "team:admin"],
-        "POST": ["team:write", "team:admin"],
-        "PUT": ["team:write", "team:admin"],
-        "DELETE": ["team:admin"],
-    }
-
-    def get_user_scopes(self, obj, user):
-        return obj.organization.get_user_scopes(user)

+ 2 - 2
apps/teams/tests/test_api.py

@@ -75,7 +75,7 @@ class TeamAPITestCase(TestCase):
         """Only admins can create teams for that org"""
         data = {"slug": "team"}
         organization = baker.make("organizations_ext.Organization")
-        url = reverse("organization-teams-list", args=[organization.slug])
+        url = reverse("api:list_teams", args=[organization.slug])
         res = self.client.post(url, data)
         # Not even in this org
         self.assertEqual(res.status_code, 400)
@@ -88,7 +88,7 @@ class TeamAPITestCase(TestCase):
         self.assertEqual(res.status_code, 400)
 
     def test_invalid_create(self):
-        url = reverse("organization-teams-list", args=["haha"])
+        url = reverse("api:list_teams", args=["haha"])
         data = {"slug": "team"}
         res = self.client.post(url, data)
         self.assertEqual(res.status_code, 400)

+ 3 - 51
apps/teams/views.py

@@ -1,61 +1,13 @@
-from django.shortcuts import get_object_or_404
-from rest_framework import exceptions, viewsets
-
-from apps.organizations_ext.models import Organization, OrganizationUserRole
+from rest_framework import viewsets
 
 from .models import Team
-from .permissions import TeamPermission
 
 
-class NestedTeamViewSet(viewsets.ModelViewSet):
+class NestedTeamViewSet(viewsets.ReadOnlyModelViewSet):
     """Teams for an Organization"""
 
-    queryset = Team.objects.all()
-    permission_classes = [TeamPermission]
-
-    def get_queryset(self):
-        if self.request.user.is_authenticated:
-            queryset = self.queryset.filter(organization__users=self.request.user)
-            organization_slug = self.kwargs.get("organization_slug")
-            if organization_slug:
-                queryset = queryset.filter(organization__slug=organization_slug)
-            return queryset.prefetch_related("members", "projects")
-        return self.queryset.none()
-
-    def perform_create(self, serializer):
-        try:
-            organization = Organization.objects.get(
-                slug=self.kwargs.get("organization_slug"),
-                users=self.request.user,
-                organization_users__role__gte=OrganizationUserRole.ADMIN,
-            )
-        except Organization.DoesNotExist as org_no_exist:
-            raise exceptions.ValidationError(
-                "Organization does not exist"
-            ) from org_no_exist
-        if Team.objects.filter(
-            organization=organization, slug=serializer.validated_data.get("slug")
-        ).exists():
-            raise exceptions.ValidationError("Slug must be unique for organization")
-        team = serializer.save(organization=organization)
-        org_user = organization.organization_users.filter(
-            user=self.request.user
-        ).first()
-        team.members.add(org_user)
+    queryset = Team.objects.none()
 
 
 class TeamViewSet(NestedTeamViewSet):
     lookup_value_regex = r"(?P<organization_slug>[^/.]+)/(?P<team_slug>[-\w]+)"
-
-    def get_object(self):
-        queryset = self.filter_queryset(self.get_queryset())
-        obj = get_object_or_404(
-            queryset,
-            slug=self.kwargs["team_slug"],
-            organization__slug=self.kwargs["organization_slug"],
-        )
-
-        # May raise a permission denied
-        self.check_object_permissions(self.request, obj)
-
-        return obj

+ 2 - 0
glitchtip/api/api.py

@@ -24,6 +24,7 @@ from apps.event_ingest.embed_api import router as embed_router
 from apps.files.api import router as files_router
 from apps.importer.api import router as importer_router
 from apps.issue_events.api import router as issue_events_router
+from apps.projects.api import router as projects_router
 from apps.releases.api import router as releases_router
 from apps.teams.api import router as teams_router
 from apps.users.api import router as users_router
@@ -59,6 +60,7 @@ api.add_router("0", environments_router)
 api.add_router("0", files_router)
 api.add_router("0", importer_router)
 api.add_router("0", issue_events_router)
+api.add_router("0", projects_router)
 api.add_router("0", teams_router)
 api.add_router("0", users_router)
 api.add_router("0", wizard_router)