Skip to content

Commit 21687fa

Browse files
Fix: configuration and export problems
1 parent 28e7182 commit 21687fa

File tree

8 files changed

+74
-86
lines changed

8 files changed

+74
-86
lines changed

api/serializers.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
SavedFeatureSelection,
1010
validate_aoi,
1111
validate_mbtiles,
12+
normalize_not_in,
1213
PartnerExportRegion,
1314
)
1415
from rest_framework import serializers
@@ -73,11 +74,23 @@ class Meta:
7374

7475
class ConfigurationSerializer(serializers.ModelSerializer):
7576
user = UserSerializer(read_only=True, default=serializers.CurrentUserDefault())
77+
# Override to bypass model-level validators; normalization + validation run in validate_yaml.
78+
yaml = serializers.CharField(validators=[])
7679

7780
class Meta:
7881
model = SavedFeatureSelection
7982
fields = ("uid", "name", "description", "yaml", "public", "user", "pinned")
8083

84+
def validate_yaml(self, value):
85+
from django.core.exceptions import ValidationError as DjangoValidationError
86+
from jobs.models import validate_feature_selection
87+
normalized = normalize_not_in(value)
88+
try:
89+
validate_feature_selection(normalized)
90+
except DjangoValidationError as e:
91+
raise serializers.ValidationError(e.messages)
92+
return normalized
93+
8194

8295
class JobGeomSerializer(serializers.ModelSerializer):
8396
"""Since Job Geoms can be large, these are serialized separately,
@@ -89,7 +102,8 @@ class Meta:
89102

90103

91104
class JobSerializer(serializers.ModelSerializer):
92-
user = UserSerializer(default=serializers.CurrentUserDefault())
105+
user = UserSerializer(read_only=True)
106+
feature_selection = serializers.CharField(validators=[])
93107

94108
class Meta:
95109
model = Job
@@ -121,6 +135,9 @@ class Meta:
121135
"simplified_geom": {"read_only": True},
122136
}
123137

138+
def validate_feature_selection(self, value):
139+
return _validate_and_normalize_feature_selection(value)
140+
124141
def validate(self, data):
125142
try:
126143
validate_aoi(data["the_geom"])
@@ -142,6 +159,17 @@ def validate_model(model):
142159
raise serializers.ValidationError(e.message_dict)
143160

144161

162+
def _validate_and_normalize_feature_selection(value):
163+
from django.core.exceptions import ValidationError as DjangoValidationError
164+
from jobs.models import validate_feature_selection as _validate_fs
165+
normalized = normalize_not_in(value)
166+
try:
167+
_validate_fs(normalized)
168+
except DjangoValidationError as e:
169+
raise serializers.ValidationError(e.messages)
170+
return normalized
171+
172+
145173
class PartnerExportRegionListSerializer(serializers.ModelSerializer):
146174
export_formats = serializers.ListField()
147175
feature_selection = serializers.CharField()
@@ -175,6 +203,9 @@ class PartnerExportRegionSerializer(serializers.ModelSerializer): # noqa
175203
event = serializers.CharField()
176204
description = serializers.CharField()
177205

206+
def validate_feature_selection(self, value):
207+
return _validate_and_normalize_feature_selection(value)
208+
178209
class Meta: # noqa
179210
model = PartnerExportRegion
180211
fields = (
@@ -332,6 +363,9 @@ class HDXExportRegionSerializer(serializers.ModelSerializer): # noqa
332363
name = serializers.CharField()
333364
buffer_aoi = serializers.BooleanField()
334365

366+
def validate_feature_selection(self, value):
367+
return _validate_and_normalize_feature_selection(value)
368+
335369
def validate(self, data):
336370
"""
337371
Check for export formats for country exports.

api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def perform_create(self, serializer):
151151
raise ValidationError(
152152
{"the_geom": ["You are rate limited to 5 exports per hour."]}
153153
)
154-
job = serializer.save()
154+
job = serializer.save(user=self.request.user)
155155
task_runner = ExportTaskRunner()
156156
task_runner.run_task(job_uid=str(job.uid))
157157

jobs/models.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,24 @@ def validate_export_formats(value):
116116
)
117117

118118

119+
def normalize_not_in(yaml_text):
120+
"""Rewrite NOT IN (...) clauses to chained != conditions.
121+
122+
The osm-export-tool-python SQL parser does not support NOT IN.
123+
Example: highway NOT IN ('footway', 'path') -> highway != 'footway' AND highway != 'path'
124+
"""
125+
def expand(match):
126+
col = match.group(1)
127+
values = [v.strip().strip("'\"") for v in match.group(2).split(",")]
128+
return " AND ".join(f"{col} != '{v}'" for v in values)
129+
130+
return re.sub(r"([\w:]+)\s+NOT\s+IN\s*\(([^)]+)\)", expand, yaml_text, flags=re.IGNORECASE)
131+
132+
119133
def validate_feature_selection(value):
120134
from osm_export_tool.mapping import Mapping
121135

122-
m, errors = Mapping.validate(value)
136+
m, errors = Mapping.validate(normalize_not_in(value))
123137
if not m:
124138
raise ValidationError(errors)
125139

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ dramatiq-abort==1.0
4040
python3-openid
4141

4242
# Hanko SSO Authentication
43-
hotosm-auth[django]==0.2.9
43+
hotosm-auth[django]==0.2.10
4444

4545
##testing
4646
pytest==7.4.3

ui/app/actions/meta.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ export const checkHankoAuth = () => (dispatch) => {
129129
withCredentials: true
130130
})
131131
.then(rsp => {
132-
if (rsp.data && rsp.data.auth_provider === "hanko") {
133-
// Set isLoggedIn = true by dispatching LOGIN_SUCCESS with a placeholder token
132+
if (rsp.data && rsp.data.user_id) {
134133
dispatch({
135134
type: LOGIN_SUCCESS,
136135
token: "hanko-cookie-auth",

ui/hanko_helpers.py

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
from typing import Optional
3-
from django.conf import settings
43
from django.contrib.auth.models import User
54
from rest_framework.authentication import BaseAuthentication
65

@@ -11,9 +10,6 @@
1110

1211
class HankoAuthentication(BaseAuthentication):
1312
def authenticate(self, request):
14-
if getattr(settings, 'AUTH_PROVIDER', 'legacy') != 'hanko':
15-
return None
16-
1713
if not hasattr(request, 'hotosm') or not request._request.hotosm.user:
1814
return None
1915

@@ -57,10 +53,7 @@ def find_legacy_user_by_osm_id(osm_id: int) -> Optional[User]:
5753
def find_legacy_user_by_email(email: str) -> Optional[User]:
5854
if not email:
5955
return None
60-
try:
61-
return User.objects.get(email=email)
62-
except User.DoesNotExist:
63-
return None
56+
return User.objects.filter(email=email).order_by('id').first()
6457

6558

6659
def create_export_tool_user(
@@ -91,28 +84,4 @@ def get_mapped_django_user(request) -> Optional[User]:
9184

9285

9386
def is_hanko_authenticated(request):
94-
if getattr(settings, 'AUTH_PROVIDER', 'legacy') != 'hanko':
95-
return False
9687
return hasattr(request, 'hotosm') and request.hotosm.user is not None
97-
98-
99-
def require_hanko_auth(view_func):
100-
from functools import wraps
101-
from django.http import JsonResponse
102-
103-
@wraps(view_func)
104-
def wrapper(request, *args, **kwargs):
105-
if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko':
106-
if not is_hanko_authenticated(request):
107-
return JsonResponse(
108-
{"error": "Not authenticated"},
109-
status=401
110-
)
111-
else:
112-
if not request.user.is_authenticated:
113-
return JsonResponse(
114-
{"error": "Not authenticated"},
115-
status=401
116-
)
117-
return view_func(request, *args, **kwargs)
118-
return wrapper

ui/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"dependencies": {
55
"@blueprintjs/core": "^1.24.0",
66
"@blueprintjs/datetime": "^1.19.0",
7-
"@hotosm/hanko-auth": "0.5.1",
87
"@hotosm/iso-countries-languages": "^1.0.2",
98
"@turf/area": "^4.6.0",
109
"@turf/bbox": "^4.6.0",

ui/views.py

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,17 @@ class ApplicationAdmin(admin.ModelAdmin):
114114

115115
@require_http_methods(["GET"])
116116
def auth_me(request):
117-
if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko':
118-
if not is_hanko_authenticated(request):
119-
return JsonResponse(
120-
{"error": "Not authenticated"},
121-
status=401
122-
)
123-
124-
hanko_user = request.hotosm.user
117+
if is_hanko_authenticated(request):
125118
django_user = get_mapped_django_user(request)
119+
if not django_user:
120+
return JsonResponse({"error": "Not authenticated"}, status=401)
126121

127122
response_data = {
128-
"hanko_user_id": hanko_user.id,
129-
"email": hanko_user.email,
130-
"auth_provider": "hanko",
123+
"user_id": django_user.id,
124+
"username": django_user.username,
125+
"email": django_user.email,
131126
}
132127

133-
if django_user:
134-
response_data.update({
135-
"user_id": django_user.id,
136-
"username": django_user.username,
137-
})
138-
139128
if hasattr(request.hotosm, 'osm') and request.hotosm.osm:
140129
osm = request.hotosm.osm
141130
response_data.update({
@@ -144,71 +133,55 @@ def auth_me(request):
144133
})
145134

146135
return JsonResponse(response_data)
147-
else:
148-
if not request.user.is_authenticated:
149-
return JsonResponse(
150-
{"error": "Not authenticated"},
151-
status=401
152-
)
153136

154-
return JsonResponse({
155-
"user_id": request.user.id,
156-
"username": request.user.username,
157-
"email": request.user.email,
158-
"auth_provider": "legacy",
159-
})
137+
if not request.user.is_authenticated:
138+
return JsonResponse({"error": "Not authenticated"}, status=401)
139+
140+
return JsonResponse({
141+
"user_id": request.user.id,
142+
"username": request.user.username,
143+
"email": request.user.email,
144+
})
160145

161146

162147
@require_http_methods(["GET"])
163148
def auth_status(request):
164149
from hotosm_auth_django import get_mapped_user_id
165150

166-
if getattr(settings, 'AUTH_PROVIDER', 'legacy') != 'hanko':
167-
return JsonResponse({
168-
"auth_provider": "legacy",
169-
"authenticated": request.user.is_authenticated if hasattr(request, 'user') else False,
170-
})
171-
172151
if not is_hanko_authenticated(request):
173152
return JsonResponse({
174-
"auth_provider": "hanko",
175-
"authenticated": False,
153+
"authenticated": request.user.is_authenticated if hasattr(request, 'user') else False,
176154
"hanko_authenticated": False,
155+
"needs_onboarding": False,
177156
})
178157

179158
hanko_user = request.hotosm.user
180159
mapped_user_id = get_mapped_user_id(hanko_user, app_name=APP_NAME)
181160

182161
if mapped_user_id is not None:
183162
try:
184-
django_user_id = int(mapped_user_id)
185-
user = User.objects.get(id=django_user_id)
163+
user = User.objects.get(id=int(mapped_user_id))
186164
return JsonResponse({
187-
"auth_provider": "hanko",
188165
"authenticated": True,
166+
"hanko_authenticated": True,
189167
"needs_onboarding": False,
190168
"user": {
191169
"id": user.id,
192170
"username": user.username,
193171
"email": user.email,
194172
},
195-
"hanko_user": {
196-
"id": hanko_user.id,
197-
"email": hanko_user.email,
198-
}
199173
})
200174
except User.DoesNotExist:
201175
pass
202176

203177
return JsonResponse({
204-
"auth_provider": "hanko",
205178
"authenticated": False,
206-
"needs_onboarding": True,
207179
"hanko_authenticated": True,
180+
"needs_onboarding": True,
208181
"hanko_user": {
209182
"id": hanko_user.id,
210183
"email": hanko_user.email,
211-
}
184+
},
212185
})
213186

214187

0 commit comments

Comments
 (0)