import logging from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework.request import Request from rest_framework import status from django.utils.translation import gettext as _ from labs.api.bases.lab import LabDetailBaseEndpoint, LabTiDBCloudGrantBaseEndpoint from labs.models import LabsOAuthTiDBCloudGrant from labs.lab_manager_v2.consts import REGION_METADATA_FOR_FRONTEND, LANGUAGE_METADATA from labs.lab_manager_v2.lab_spec import get_all_labs from labs.tidb_serverless.oauth import ( tidbcloud_oauth_start, tidbcloud_oauth_complete, TiDBCloudOAuthClient, ) from labs.tidb_serverless.exceptions import ( TiDBCloudException, TiDBCloudUnauthorizedException, ) from labs.billing.voucher import redeem_voucher, InvalidVoucherCodeError from labs.models import LabUsage, LabFeedback from labs.api.serializers.lab import ( RedeemEventSerializer, SubmitLabFeedbackSerializer, LabFeedbackSerializer, TiDBCloudCreateLabServerlessClusterSerializer, ) logger = logging.getLogger(__name__) class LabListEndpoint(APIView): permission_classes = (AllowAny,) def get(self, request: Request) -> Response: labs = get_all_labs().values() if request.user and request.user.is_authenticated: all_usage = { usage.lab_code: usage for usage in LabUsage.objects.filter(email=request.user.email).all() } for lab in labs: usage = all_usage.get(lab["lab_code"]) lab.update( { "usage": { "start_count": usage.start_count if usage else 0, "terminate_count": usage.terminate_count if usage else 0, } } ) return Response(labs) class LabCategoryListEndpoint(APIView): permission_classes = (AllowAny,) def get(self, request: Request) -> Response: language_code = request.query_params.get("language_code", "en") return Response( list( set(lab["category_" + language_code] for lab in get_all_labs().values()) ) ) class LabPinnedListEndpoint(APIView): permission_classes = (AllowAny,) def get(self, request: Request) -> Response: try: limit = int(request.query_params.get("limit", 8)) except ValueError: return Response( {"error": "Invalid limit"}, status=status.HTTP_400_BAD_REQUEST ) labs = list(get_all_labs().values()) cr_labs = [l for l in labs if l["credit_redeemable"]] pinned_labs = cr_labs[:limit] if limit and limit > 0 else cr_labs return Response(pinned_labs) class LabRegionListEndpoint(APIView): permission_classes = (AllowAny,) def get(self, request: Request) -> Response: return Response(REGION_METADATA_FOR_FRONTEND) class LabLanguageListEndpoint(APIView): permission_classes = (AllowAny,) def get(self, request: Request) -> Response: return Response(LANGUAGE_METADATA) class LabFeedbackEndpoint(LabDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, lab_code: str, *args, **kwargs) -> Response: """ GET /api/labs/demo_001/feedback """ try: feedback = LabFeedback.objects.get( email=request.user.email, lab_code=lab_code ) except LabFeedback.DoesNotExist: return Response( {"message": _("The requested feedback does not exist.")}, status=status.HTTP_404_NOT_FOUND, ) return Response(LabFeedbackSerializer(feedback).data) def post(self, request: Request, lab_code: str, *args, **kwargs) -> Response: """ POST /api/labs/demo_001/feedback { "rating_score": 5, "comment": "This lab is awesome!" } """ serializer = SubmitLabFeedbackSerializer( data=request.data, context={"request": request, "view": self} ) serializer.is_valid(raise_exception=True) serializer.save() return Response({"message": _("Feedback submitted successfully.")}) class TiDBCloudOAuthStartEndpoint(APIView): permission_classes = (IsAuthenticated,) def get(self, request: Request, *args, **kwargs) -> Response: return tidbcloud_oauth_start(request) class TiDBCloudOAuthCompleteEndpoint(APIView): permission_classes = (IsAuthenticated,) def get(self, request: Request, *args, **kwargs) -> Response: return tidbcloud_oauth_complete(request) class TiDBCloudServerlessClusterListEndpoint(LabTiDBCloudGrantBaseEndpoint): permission_classes = (IsAuthenticated,) def get( self, request: Request, grant: LabsOAuthTiDBCloudGrant, *args, **kwargs ) -> Response: try: tidbcloud_client = TiDBCloudOAuthClient(grant) lab_region = request.query_params.get("region") cloud_provider = request.query_params.get("cloud_provider") return Response( tidbcloud_client.list_serverless_clusters(lab_region, cloud_provider) ) except TiDBCloudException as e: return Response( { "message": str(e), }, status=status.HTTP_403_FORBIDDEN, ) class TiDBCloudCreateLabServerlessClusterEndpoint(LabTiDBCloudGrantBaseEndpoint): permission_classes = (IsAuthenticated,) def post( self, request: Request, grant: LabsOAuthTiDBCloudGrant, *args, **kwargs ) -> Response: serializer = TiDBCloudCreateLabServerlessClusterSerializer(data=request.data) serializer.is_valid(raise_exception=True) tidbcloud_client = TiDBCloudOAuthClient(grant) try: lab_cluster = tidbcloud_client.create_lab_cluster( cluster_id=serializer.validated_data.get("cluster_id"), region=serializer.validated_data.get("region"), cloud_provider=serializer.validated_data.get("cloud_provider"), ) except TiDBCloudUnauthorizedException as e: return Response( {"message": str(e)}, status=status.HTTP_403_FORBIDDEN, ) except TiDBCloudException as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_400_BAD_REQUEST, ) return Response({"cluster_id": str(lab_cluster.id)}) class RedeemEventEndpoint(APIView): permission_classes = (IsAuthenticated,) def post(self, request: Request) -> Response: """ POST /api/labs/redeem_event { "event_voucher_code": "tidbbootcampbalabala2014|C95P5-0PM60-4K6Z5-5P4M6-OK0B8" } """ serializer = RedeemEventSerializer(data=request.data) serializer.is_valid(raise_exception=True) event_voucher_code = serializer.validated_data["event_voucher_code"] try: response = redeem_voucher(request.user, event_voucher_code) return Response( {"message": response.message}, status=status.HTTP_200_OK, ) except InvalidVoucherCodeError as e: return Response( {"message": str(e)}, status=status.HTTP_400_BAD_REQUEST, )