endoreg-db 0.6.2__py3-none-any.whl → 0.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/data/__init__.py +14 -0
- endoreg_db/data/disease_classification/chronic_kidney_disease.yaml +2 -2
- endoreg_db/data/disease_classification_choice/chronic_kidney_disease.yaml +6 -6
- endoreg_db/data/distribution/numeric/data.yaml +1 -1
- endoreg_db/data/examination/examinations/data.yaml +22 -21
- endoreg_db/data/examination/type/data.yaml +12 -0
- endoreg_db/data/examination_indication/endoscopy.yaml +417 -1
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +157 -5
- endoreg_db/data/finding/data.yaml +18 -11
- endoreg_db/data/finding_intervention/endoscopy.yaml +26 -121
- endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +163 -0
- endoreg_db/data/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/information_source/endoscopy_guidelines.yaml +7 -0
- endoreg_db/data/medication_indication/anticoagulation.yaml +4 -4
- endoreg_db/data/pdf_type/data.yaml +9 -16
- endoreg_db/data/requirement/colonoscopy_indications.yaml +56 -0
- endoreg_db/data/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +38 -0
- endoreg_db/data/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/requirement/disease_renal.yaml +80 -0
- endoreg_db/data/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/requirement/lab_value.yaml +120 -0
- endoreg_db/data/requirement_operator/lab_operators.yaml +128 -0
- endoreg_db/data/requirement_operator/model_operators.yaml +90 -0
- endoreg_db/data/requirement_set/endoscopy_bleeding_risk.yaml +12 -0
- endoreg_db/data/requirement_set_type/data.yaml +20 -0
- endoreg_db/data/requirement_type/requirement_types.yaml +83 -0
- endoreg_db/data/risk/bleeding.yaml +26 -0
- endoreg_db/data/risk/thrombosis.yaml +37 -0
- endoreg_db/data/risk_type/data.yaml +27 -0
- endoreg_db/data/unit/time.yaml +36 -1
- endoreg_db/management/commands/load_base_db_data.py +14 -1
- endoreg_db/management/commands/load_center_data.py +46 -21
- endoreg_db/management/commands/load_examination_indication_data.py +49 -27
- endoreg_db/management/commands/load_requirement_data.py +156 -0
- endoreg_db/management/commands/load_risk_data.py +56 -0
- endoreg_db/mermaid/Overall_flow_patient_finding_intervention.md +10 -0
- endoreg_db/mermaid/anonymized_image_annotation.md +20 -0
- endoreg_db/mermaid/binary_classification_annotation.md +50 -0
- endoreg_db/mermaid/classification.md +8 -0
- endoreg_db/mermaid/examination.md +8 -0
- endoreg_db/mermaid/findings.md +7 -0
- endoreg_db/mermaid/image_classification.md +28 -0
- endoreg_db/mermaid/interventions.md +8 -0
- endoreg_db/mermaid/morphology.md +8 -0
- endoreg_db/mermaid/patient_creation.md +14 -0
- endoreg_db/mermaid/video_segmentation_annotation.md +17 -0
- endoreg_db/migrations/0009_requirementoperator_requirementsettype_and_more.py +154 -0
- endoreg_db/models/__init__.py +20 -0
- endoreg_db/models/ai_model/ai_model.py +0 -13
- endoreg_db/models/ai_model/model_meta.py +2 -12
- endoreg_db/models/data_file/base_classes/abstract_frame.py +0 -2
- endoreg_db/models/data_file/base_classes/abstract_pdf.py +0 -9
- endoreg_db/models/data_file/base_classes/abstract_video.py +7 -8
- endoreg_db/models/data_file/base_classes/utils.py +0 -22
- endoreg_db/models/data_file/frame.py +1 -1
- endoreg_db/models/data_file/import_classes/raw_pdf.py +5 -11
- endoreg_db/models/data_file/import_classes/raw_video.py +6 -4
- endoreg_db/models/data_file/video/video.py +3 -3
- endoreg_db/models/disease.py +88 -19
- endoreg_db/models/event.py +108 -21
- endoreg_db/models/examination/examination_indication.py +108 -29
- endoreg_db/models/examination/examination_type.py +20 -6
- endoreg_db/models/information_source.py +37 -1
- endoreg_db/models/laboratory/lab_value.py +83 -32
- endoreg_db/models/requirement/__init__.py +11 -0
- endoreg_db/models/requirement/requirement.py +325 -0
- endoreg_db/models/requirement/requirement_evaluation/__init__.py +134 -0
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +102 -0
- endoreg_db/models/requirement/requirement_operator.py +58 -0
- endoreg_db/models/requirement/requirement_set.py +127 -0
- endoreg_db/models/risk/__init__.py +7 -0
- endoreg_db/models/risk/risk.py +72 -0
- endoreg_db/models/risk/risk_type.py +55 -0
- endoreg_db/serializers/raw_pdf_anony_text_validation.py +137 -0
- endoreg_db/serializers/raw_pdf_meta_validation.py +223 -0
- endoreg_db/serializers/raw_video_meta_validation.py +163 -1
- endoreg_db/serializers/video_segmentation.py +208 -126
- endoreg_db/urls.py +127 -14
- endoreg_db/utils/__init__.py +43 -0
- endoreg_db/utils/dataloader.py +38 -19
- endoreg_db/utils/hashs.py +1 -0
- endoreg_db/utils/paths.py +86 -0
- endoreg_db/views/raw_pdf_anony_text_validation_views.py +95 -0
- endoreg_db/views/raw_pdf_meta_validation_views.py +111 -0
- endoreg_db/views/raw_video_meta_validation_views.py +128 -18
- endoreg_db/views/video_segmentation_views.py +28 -11
- endoreg_db/views/views.py +107 -0
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/METADATA +1 -1
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/RECORD +96 -46
- endoreg_db/management/commands/load_name_data.py +0 -37
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/WHEEL +0 -0
- {endoreg_db-0.6.2.dist-info → endoreg_db-0.6.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,38 +1,148 @@
|
|
|
1
1
|
from rest_framework.views import APIView
|
|
2
2
|
from rest_framework.response import Response
|
|
3
3
|
from rest_framework import status
|
|
4
|
+
from django.http import FileResponse, Http404
|
|
5
|
+
import mimetypes
|
|
6
|
+
import os
|
|
4
7
|
from ..models import RawVideoFile
|
|
5
|
-
from ..serializers.raw_video_meta_validation import VideoFileForMetaSerializer
|
|
8
|
+
from ..serializers.raw_video_meta_validation import VideoFileForMetaSerializer,SensitiveMetaUpdateSerializer
|
|
9
|
+
from ..models import SensitiveMeta
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
class VideoFileForMetaView(APIView):
|
|
8
13
|
"""
|
|
9
14
|
API endpoint to fetch video metadata step-by-step.
|
|
10
|
-
|
|
11
|
-
If last_id is given Returns the next available video.
|
|
15
|
+
Uses the serializer to get the first or next available video.
|
|
12
16
|
"""
|
|
13
17
|
|
|
14
|
-
##need to change this fucntion , like the previous one
|
|
15
|
-
|
|
16
18
|
def get(self, request):
|
|
17
19
|
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
- Fetches the **first available video** if `last_id` is NOT provided.
|
|
21
|
+
- Fetches the **next available video** where `id > last_id` if provided.
|
|
22
|
+
- If no video is available, returns a structured error response.
|
|
21
23
|
"""
|
|
22
|
-
last_id = request.GET.get("last_id") # Get last_id from query params
|
|
24
|
+
last_id = request.GET.get("last_id") # Get last_id from query params
|
|
25
|
+
|
|
26
|
+
# Get the next video as a model instance
|
|
27
|
+
video_entry = VideoFileForMetaSerializer.get_next_video(last_id)
|
|
28
|
+
|
|
29
|
+
if video_entry is None:
|
|
30
|
+
return Response({"error": "No more videos available."}, status=status.HTTP_404_NOT_FOUND)
|
|
31
|
+
|
|
32
|
+
serialized_video = VideoFileForMetaSerializer(video_entry, context={'request': request})
|
|
33
|
+
|
|
34
|
+
# Check if required fields are missing
|
|
35
|
+
response_data = serialized_video.data
|
|
36
|
+
missing_fields = {}
|
|
37
|
+
|
|
38
|
+
if response_data.get('file') is None:
|
|
39
|
+
missing_fields['file'] = "No file associated with this entry."
|
|
40
|
+
|
|
41
|
+
if response_data.get('video_url') is None:
|
|
42
|
+
missing_fields['video_url'] = "Video file is missing."
|
|
43
|
+
|
|
44
|
+
if response_data.get('full_video_path') is None:
|
|
45
|
+
missing_fields['full_video_path'] = "No file path found on server."
|
|
23
46
|
|
|
47
|
+
if not response_data.get('patient_first_name'):
|
|
48
|
+
missing_fields['patient_first_name'] = "Patient first name is missing."
|
|
49
|
+
|
|
50
|
+
if not response_data.get('patient_last_name'):
|
|
51
|
+
missing_fields['patient_last_name'] = "Patient last name is missing."
|
|
52
|
+
|
|
53
|
+
if not response_data.get('patient_dob'):
|
|
54
|
+
missing_fields['patient_dob'] = "Patient date of birth is missing."
|
|
55
|
+
|
|
56
|
+
if not response_data.get('examination_date'):
|
|
57
|
+
missing_fields['examination_date'] = "Examination date is missing."
|
|
58
|
+
|
|
59
|
+
if response_data.get('duration') is None:
|
|
60
|
+
missing_fields['duration'] = "Unable to determine video duration. The file might be corrupted or unreadable."
|
|
61
|
+
|
|
62
|
+
if missing_fields:
|
|
63
|
+
return Response({"error": "Missing required data.", "details": missing_fields},
|
|
64
|
+
status=status.HTTP_400_BAD_REQUEST)
|
|
65
|
+
|
|
66
|
+
return Response(serialized_video.data, status=status.HTTP_200_OK)
|
|
67
|
+
|
|
68
|
+
def serve_video_file(self, video_entry):
|
|
69
|
+
"""
|
|
70
|
+
Streams the video file dynamically.
|
|
71
|
+
"""
|
|
24
72
|
try:
|
|
25
|
-
|
|
26
|
-
# id__gt is orm syntax which is equal to SELECT * FROM rawvideofile WHERE id > 2 ORDER BY id ASC LIMIT 1;
|
|
73
|
+
full_video_path = video_entry.file.path # Get file path
|
|
27
74
|
|
|
28
|
-
|
|
29
|
-
|
|
75
|
+
if not os.path.exists(full_video_path):
|
|
76
|
+
raise Http404("Video file not found.")
|
|
30
77
|
|
|
31
|
-
|
|
32
|
-
|
|
78
|
+
mime_type, _ = mimetypes.guess_type(full_video_path) # Detects file type
|
|
79
|
+
response = FileResponse(open(full_video_path, "rb"), content_type=mime_type or "video/mp4")
|
|
33
80
|
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
response["Content-Disposition"] = f'inline; filename="{os.path.basename(full_video_path)}"' # Allows direct streaming
|
|
82
|
+
|
|
83
|
+
return response # Sends the video file as a stream
|
|
36
84
|
|
|
37
85
|
except Exception as e:
|
|
38
|
-
return Response({"error": f"Internal
|
|
86
|
+
return Response({"error": f"Internal error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def patch(self, request, *args, **kwargs):
|
|
90
|
+
"""
|
|
91
|
+
Calls the serializer to update `SensitiveMeta` data.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# Ensure all required fields are provided
|
|
95
|
+
required_fields = ["sensitive_meta_id", "patient_first_name", "patient_last_name", "patient_dob", "examination_date"]
|
|
96
|
+
missing_fields = [field for field in required_fields if field not in request.data]
|
|
97
|
+
|
|
98
|
+
if missing_fields:
|
|
99
|
+
return Response({"error": "Missing required fields", "missing_fields": missing_fields}, status=status.HTTP_400_BAD_REQUEST)
|
|
100
|
+
|
|
101
|
+
# Call serializer for validation
|
|
102
|
+
serializer = SensitiveMetaUpdateSerializer(data=request.data, partial=True)
|
|
103
|
+
|
|
104
|
+
if serializer.is_valid():
|
|
105
|
+
# Get the instance and update it
|
|
106
|
+
sensitive_meta = SensitiveMeta.objects.get(id=request.data["sensitive_meta_id"])
|
|
107
|
+
updated_instance = serializer.update(sensitive_meta, serializer.validated_data)
|
|
108
|
+
|
|
109
|
+
return Response({
|
|
110
|
+
"message": "Patient information updated successfully.",
|
|
111
|
+
"updated_data": SensitiveMetaUpdateSerializer(updated_instance).data
|
|
112
|
+
}, status=status.HTTP_200_OK)
|
|
113
|
+
|
|
114
|
+
# Return validation errors
|
|
115
|
+
return Response({"error": "Invalid data.", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
await import('https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js');
|
|
120
|
+
const updatePatientInfo = async () => {
|
|
121
|
+
const updatedData = {
|
|
122
|
+
sensitive_meta_id: 2,
|
|
123
|
+
patient_first_name: "Placeholder",
|
|
124
|
+
patient_last_name: "Placeholder",
|
|
125
|
+
patient_dob: "1994-06-15",
|
|
126
|
+
examination_date: "2024-06-15"
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const response = await axios.patch("http://localhost:8000/api/video/update_sensitivemeta/", updatedData, {
|
|
131
|
+
headers: { "Content-Type": "application/json" }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log("Update Success:", response.data);
|
|
135
|
+
alert("Patient information updated successfully!");
|
|
136
|
+
|
|
137
|
+
return response.data;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("Update Error:", error.response?.data || error);
|
|
140
|
+
alert("Failed to update patient information.");
|
|
141
|
+
return error.response?.data || { error: "Unknown error" };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
updatePatientInfo().then(response => console.log("Final Response:", response));
|
|
147
|
+
|
|
148
|
+
"""
|
|
@@ -31,9 +31,14 @@ class VideoView(APIView):
|
|
|
31
31
|
Returns a list of all available videos along with available labels.
|
|
32
32
|
Used to populate the video selection dropdown in Vue.js.
|
|
33
33
|
"""
|
|
34
|
+
|
|
34
35
|
videos = RawVideoFile.objects.all()
|
|
35
36
|
labels = Label.objects.all() # Fetch all labels
|
|
36
37
|
|
|
38
|
+
if not videos.exists():
|
|
39
|
+
return Response({"error": "No videos found in the database."}, status=status.HTTP_404_NOT_FOUND)
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
video_serializer = VideoListSerializer(videos, many=True)
|
|
38
43
|
label_serializer = LabelSerializer(labels, many=True) # Serialize labels
|
|
39
44
|
|
|
@@ -134,16 +139,28 @@ class UpdateLabelSegmentsView(APIView):
|
|
|
134
139
|
|
|
135
140
|
def put(self, request, video_id, label_id):
|
|
136
141
|
"""
|
|
137
|
-
|
|
142
|
+
Updates segments for a given video & label.
|
|
138
143
|
"""
|
|
139
|
-
serializer = LabelSegmentUpdateSerializer(data=request.data)
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
# Ensure required fields are provided
|
|
146
|
+
required_fields = ["video_id", "label_id", "segments"]
|
|
147
|
+
missing_fields = [field for field in required_fields if field not in request.data]
|
|
148
|
+
|
|
149
|
+
if missing_fields:
|
|
150
|
+
return Response({"error": "Missing required fields", "missing": missing_fields}, status=status.HTTP_400_BAD_REQUEST)
|
|
151
|
+
|
|
152
|
+
# Validate input data
|
|
153
|
+
serializer = LabelSegmentUpdateSerializer(data=request.data, partial=True)
|
|
154
|
+
|
|
155
|
+
if not serializer.is_valid():
|
|
156
|
+
return Response({"error": "Invalid segment data", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
|
157
|
+
|
|
158
|
+
# Process and save segment updates
|
|
159
|
+
result = serializer.save()
|
|
160
|
+
|
|
161
|
+
return Response({
|
|
162
|
+
"message": "Segments updated successfully.",
|
|
163
|
+
"updated_segments": result["updated_segments"],
|
|
164
|
+
"new_segments": result["new_segments"],
|
|
165
|
+
"deleted_segments": result["deleted_segments"]
|
|
166
|
+
}, status=status.HTTP_200_OK)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#for keycloak need to make separate onne,demo
|
|
2
|
+
from rest_framework.permissions import IsAuthenticated
|
|
3
|
+
from rest_framework.views import APIView
|
|
4
|
+
from rest_framework.response import Response
|
|
5
|
+
|
|
6
|
+
from django.shortcuts import redirect, render
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
User hits /api/videos/
|
|
13
|
+
Middleware checks for token; if missing, redirects to /login/
|
|
14
|
+
/login/ redirects to Keycloak
|
|
15
|
+
User logs in → Keycloak sends them back to /login/callback/
|
|
16
|
+
/login/callback/ exchanges code for token, stores it in session
|
|
17
|
+
User is redirected to /api/videos/ again
|
|
18
|
+
Middleware now sees token, verifies it, injects user
|
|
19
|
+
DRF view (VideoView) is allowed to execute and returns data
|
|
20
|
+
"""
|
|
21
|
+
class VideoView(APIView):
|
|
22
|
+
permission_classes = [IsAuthenticated] #This uses DRF permissions to ensure request.user.is_authenticated == True.
|
|
23
|
+
|
|
24
|
+
def get(self, request):
|
|
25
|
+
"""
|
|
26
|
+
We already inject a mock user in the middleware, so this will pass if the middleware succeeded.
|
|
27
|
+
Returns a message including the Keycloak username.
|
|
28
|
+
"""
|
|
29
|
+
username = getattr(request.user, 'preferred_username', 'Unknown')
|
|
30
|
+
return Response({"message": f"🎥 Hello, {username}. You are viewing protected videos!"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def keycloak_login(request):
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
- This gets triggered when middleware redirects to /login/.
|
|
37
|
+
"""
|
|
38
|
+
redirect_uri = request.build_absolute_uri('/login/callback/')
|
|
39
|
+
print("Redirect URI:", redirect_uri)
|
|
40
|
+
auth_url = f"{settings.KEYCLOAK_SERVER_URL}/realms/{settings.KEYCLOAK_REALM}/protocol/openid-connect/auth"
|
|
41
|
+
|
|
42
|
+
#OAuth2 Authorization Code Flow
|
|
43
|
+
params = {
|
|
44
|
+
"client_id": settings.KEYCLOAK_CLIENT_ID,
|
|
45
|
+
"response_type": "code",
|
|
46
|
+
"scope": "openid",
|
|
47
|
+
"redirect_uri": redirect_uri,
|
|
48
|
+
}
|
|
49
|
+
# Redirect user to Keycloak login page.
|
|
50
|
+
return redirect(f"{auth_url}?{urlencode(params)}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
from django.http import HttpResponse
|
|
55
|
+
|
|
56
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
57
|
+
import json
|
|
58
|
+
|
|
59
|
+
def keycloak_callback(request):
|
|
60
|
+
|
|
61
|
+
#User lands here after login (Keycloak redirects here with code).
|
|
62
|
+
code = request.GET.get("code")
|
|
63
|
+
if not code:
|
|
64
|
+
return HttpResponse(" No authorization code provided.", status=400)
|
|
65
|
+
|
|
66
|
+
# Exchanges the code for an access_token.
|
|
67
|
+
token_url = f"{settings.KEYCLOAK_SERVER_URL}/realms/{settings.KEYCLOAK_REALM}/protocol/openid-connect/token"
|
|
68
|
+
redirect_uri = request.build_absolute_uri('/login/callback/')
|
|
69
|
+
|
|
70
|
+
data = {
|
|
71
|
+
"grant_type": "authorization_code",
|
|
72
|
+
"code": code,
|
|
73
|
+
"client_id": settings.KEYCLOAK_CLIENT_ID,
|
|
74
|
+
"client_secret": settings.KEYCLOAK_CLIENT_SECRET,
|
|
75
|
+
"redirect_uri": redirect_uri,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
response = requests.post(token_url, data=data)
|
|
80
|
+
|
|
81
|
+
print("Token Response Status:", response.status_code)
|
|
82
|
+
print(" Token Response Body:", response.text)
|
|
83
|
+
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
return HttpResponse(
|
|
86
|
+
f"<h2> Token exchange failed</h2><pre>{response.text}</pre>",
|
|
87
|
+
status=500
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
token_data = response.json()
|
|
91
|
+
|
|
92
|
+
if "access_token" not in token_data:
|
|
93
|
+
return HttpResponse(" Access token missing in response.", status=500)
|
|
94
|
+
|
|
95
|
+
# Stores the token in Django session. Middleware will use this on the next request.
|
|
96
|
+
request.session["access_token"] = token_data["access_token"]
|
|
97
|
+
request.session["refresh_token"] = token_data["refresh_token"]
|
|
98
|
+
print("Refresh Token:", request.session.get("refresh_token"))
|
|
99
|
+
|
|
100
|
+
return redirect("/api/videos/")
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return HttpResponse(f" Exception during token exchange: {str(e)}", status=500)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def public_home(request):
|
|
107
|
+
return HttpResponse("just for demo - This is a public home page — no login required.")
|