wip: rework request error flow
This commit is contained in:
parent
b947134fa2
commit
ab966722f9
7 changed files with 115 additions and 108 deletions
|
@ -242,6 +242,13 @@ class Map(NamedModel):
|
|||
has_anonymous_cookie = False
|
||||
return has_anonymous_cookie
|
||||
|
||||
def can_delete(self, user=None, request=None):
|
||||
if self.owner and user != self.owner:
|
||||
return False
|
||||
if not self.owner and not self.is_anonymous_owner(request):
|
||||
return False
|
||||
return True
|
||||
|
||||
def can_edit(self, user=None, request=None):
|
||||
"""
|
||||
Define if a user can edit or not the instance, according to his account
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import URLs from './urls.js'
|
||||
import { Request, ServerRequest } from './request.js'
|
||||
import { Request, ServerRequest, HTTPError, NokError } from './request.js'
|
||||
// Import modules and export them to the global scope.
|
||||
// For the not yet module-compatible JS out there.
|
||||
|
||||
// Copy the leaflet module, it's expected by leaflet plugins to be writeable.
|
||||
window.L = { ...L }
|
||||
window.umap = { URLs, Request, ServerRequest }
|
||||
window.umap = { URLs, Request, ServerRequest, HTTPError, NokError }
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
// Uses `L._`` from Leaflet.i18n which we cannot import as a module yet
|
||||
import { Evented, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||
|
||||
export class HTTPError extends Error {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
export class NokError extends Error {
|
||||
constructor(response) {
|
||||
super(response.status)
|
||||
this.response = response
|
||||
this.status = response.status
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
const BaseRequest = Evented.extend({
|
||||
_fetch: async function (method, uri, headers, data) {
|
||||
const id = Math.random()
|
||||
|
@ -15,13 +31,13 @@ const BaseRequest = Evented.extend({
|
|||
body: data,
|
||||
})
|
||||
} catch (error) {
|
||||
this._onError(error)
|
||||
console.error(error)
|
||||
this.fire('dataload', { id: id })
|
||||
return null
|
||||
throw new HTTPError(error.message)
|
||||
}
|
||||
if (!response.ok) {
|
||||
this.fire('dataload', { id: id })
|
||||
return this.onNok(response.status, response)
|
||||
throw new NokError(response.status)
|
||||
}
|
||||
// TODO
|
||||
// - error handling
|
||||
|
@ -30,6 +46,32 @@ const BaseRequest = Evented.extend({
|
|||
this.fire('dataload', { id: id })
|
||||
return response
|
||||
},
|
||||
})
|
||||
|
||||
// Basic class to issue request
|
||||
// It returns a response, or null in case of error
|
||||
// In case of error, an alert is sent, but non 20X status are not handled
|
||||
// The consumer must check the response status by hand
|
||||
export const Request = BaseRequest.extend({
|
||||
initialize: function (ui) {
|
||||
this.ui = ui
|
||||
},
|
||||
|
||||
_fetch: async function (method, uri, headers, data) {
|
||||
try {
|
||||
const response = await BaseRequest.prototype._fetch.call(
|
||||
this,
|
||||
method,
|
||||
uri,
|
||||
headers,
|
||||
data
|
||||
)
|
||||
return response
|
||||
} catch (error) {
|
||||
if (error instanceof NokError) return this._onNok(error)
|
||||
return this._onError(error)
|
||||
}
|
||||
},
|
||||
|
||||
get: async function (uri, headers) {
|
||||
return await this._fetch('GET', uri, headers)
|
||||
|
@ -40,31 +82,21 @@ const BaseRequest = Evented.extend({
|
|||
},
|
||||
|
||||
_onError: function (error) {
|
||||
console.error(error)
|
||||
this.onError(error)
|
||||
},
|
||||
onError: function (error) {},
|
||||
onNok: function (status, reponse) {
|
||||
return response
|
||||
},
|
||||
})
|
||||
|
||||
export const Request = BaseRequest.extend({
|
||||
initialize: function (ui) {
|
||||
this.ui = ui
|
||||
},
|
||||
onError: function (error) {
|
||||
console.error(error)
|
||||
this.ui.alert({ content: L._('Problem in the response'), level: 'error' })
|
||||
},
|
||||
onNok: function (status, response) {
|
||||
this.onError(message)
|
||||
return response
|
||||
|
||||
_onNok: function (error) {
|
||||
this._onError(error)
|
||||
return error.response
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
// Adds uMap specifics to requests handling
|
||||
// like logging, CSRF, etc.
|
||||
// It expects only json responses.
|
||||
// Returns an array of three elements: [data, response, error]
|
||||
// The consumer must check the error to proceed or not with using the data or response
|
||||
export const ServerRequest = Request.extend({
|
||||
_fetch: async function (method, uri, headers, data) {
|
||||
// Add a flag so backend can know we are in ajax and adapt the response
|
||||
|
@ -93,77 +125,33 @@ export const ServerRequest = Request.extend({
|
|||
},
|
||||
|
||||
_as_json: async function (response) {
|
||||
if (!response) return [{}, null, new Error("Undefined error")]
|
||||
if (Array.isArray(response)) return response
|
||||
try {
|
||||
const data = await response.json()
|
||||
if (this._handle_server_instructions(data) !== false) {
|
||||
return [{}, null]
|
||||
}
|
||||
return [data, response, null]
|
||||
} catch (error) {
|
||||
this._onError(error)
|
||||
return [{}, null, error]
|
||||
}
|
||||
},
|
||||
|
||||
_handle_server_instructions: function (data) {
|
||||
// Generic cases, let's deal with them once
|
||||
if (data.redirect) {
|
||||
const newPath = data.redirect
|
||||
if (window.location.pathname == newPath) {
|
||||
window.location.reload() // Keep the hash, so the current view
|
||||
} else {
|
||||
window.location = newPath
|
||||
}
|
||||
} else if (data.info) {
|
||||
if (data.info) {
|
||||
this.ui.alert({ content: data.info, level: 'info' })
|
||||
this.ui.closePanel()
|
||||
} else if (data.error) {
|
||||
this.ui.alert({ content: data.error, level: 'error' })
|
||||
} else if (data.login_required) {
|
||||
// TODO: stop flow and run request again when user
|
||||
// is logged in
|
||||
const win = window.open(data.login_required)
|
||||
window.umap_proceed = () => {
|
||||
console.log('logged in')
|
||||
this.fire('login')
|
||||
win.close()
|
||||
return this._onError(new Error(data.error))
|
||||
}
|
||||
} else {
|
||||
// Nothing to do, we can let the response proceed
|
||||
return false
|
||||
return [data, response, null]
|
||||
} catch (error) {
|
||||
return this._onError(error)
|
||||
}
|
||||
},
|
||||
|
||||
onNok: function (status, message) {
|
||||
if (status === 403) {
|
||||
_onError: function (error) {
|
||||
return [{}, null, error]
|
||||
},
|
||||
|
||||
_onNok: function (error) {
|
||||
if (error.status === 403) {
|
||||
this.ui.alert({
|
||||
content: message || L._('Action not allowed :('),
|
||||
level: 'error',
|
||||
})
|
||||
} else if (status === 412) {
|
||||
const msg = L._(
|
||||
'Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.'
|
||||
)
|
||||
const actions = [
|
||||
{
|
||||
label: L._('Save anyway'),
|
||||
callback: function () {
|
||||
// TODO
|
||||
delete settings.headers['If-Match']
|
||||
this._fetch(settings)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: L._('Cancel'),
|
||||
},
|
||||
]
|
||||
this.ui.alert({
|
||||
content: msg,
|
||||
level: 'error',
|
||||
duration: 100000,
|
||||
actions: actions,
|
||||
})
|
||||
}
|
||||
return [{}, error.response, error]
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1797,19 +1797,21 @@ L.U.Map.include({
|
|||
return this.editTools.startPolygon()
|
||||
},
|
||||
|
||||
del: function () {
|
||||
del: async function () {
|
||||
if (confirm(L._('Are you sure you want to delete this map?'))) {
|
||||
const url = this.urls.get('map_delete', { map_id: this.options.umap_id })
|
||||
this.post(url)
|
||||
const [data, response, error] = await this.server.post(url)
|
||||
if (data.redirect) window.location = data.redirect
|
||||
}
|
||||
},
|
||||
|
||||
clone: function () {
|
||||
clone: async function () {
|
||||
if (
|
||||
confirm(L._('Are you sure you want to clone this map and all its datalayers?'))
|
||||
) {
|
||||
const url = this.urls.get('map_clone', { map_id: this.options.umap_id })
|
||||
this.post(url)
|
||||
const [data, response, error] = await this.server.post(url)
|
||||
if (data.redirect) window.location = data.redirect
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -676,7 +676,7 @@ L.U.DataLayer = L.Evented.extend({
|
|||
this._loading = true
|
||||
const [geojson, response, error] = await this.map.server.get(this._dataUrl())
|
||||
if (!error) {
|
||||
this._last_modified = response.headers['Last-Modified']
|
||||
this._last_modified = response.headers.get('last-modified')
|
||||
// FIXME: for now this property is set dynamically from backend
|
||||
// And thus it's not in the geojson file in the server
|
||||
// So do not let all options to be reset
|
||||
|
@ -1557,21 +1557,45 @@ L.U.DataLayer = L.Evented.extend({
|
|||
const headers = this._last_modified
|
||||
? { 'If-Unmodified-Since': this._last_modified }
|
||||
: {}
|
||||
const [data, response, error] = await this.map.server.post(
|
||||
saveUrl,
|
||||
headers,
|
||||
formData
|
||||
)
|
||||
await this._trySave(saveUrl, headers, formData)
|
||||
this._geojson = geojson
|
||||
},
|
||||
|
||||
_trySave: async function (url, headers, formData) {
|
||||
const [data, response, error] = await this.map.server.post(url, headers, formData)
|
||||
if (!error) {
|
||||
if (response.status === 412) {
|
||||
const msg = L._(
|
||||
'Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.'
|
||||
)
|
||||
const actions = [
|
||||
{
|
||||
label: L._('Save anyway'),
|
||||
callback: async () => {
|
||||
// Save again,
|
||||
// but do not pass If-Unmodified-Since this time
|
||||
await this._trySave(url, {}, formData)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: L._('Cancel'),
|
||||
},
|
||||
]
|
||||
this.ui.alert({
|
||||
content: msg,
|
||||
level: 'error',
|
||||
duration: 100000,
|
||||
actions: actions,
|
||||
})
|
||||
}
|
||||
// Response contains geojson only if save has conflicted and conflicts have
|
||||
// been resolved. So we need to reload to get extra data (saved from someone else)
|
||||
// been resolved. So we need to reload to get extra data (added by someone else)
|
||||
if (data.geojson) {
|
||||
this.clear()
|
||||
this.fromGeoJSON(data.geojson)
|
||||
delete data.geojson
|
||||
}
|
||||
this._geojson = geojson
|
||||
this._last_modified = response.headers['Last-Modified']
|
||||
this._last_modified = response.headers.get('last-modified')
|
||||
this.setUmapId(data.id)
|
||||
this.updateOptions(data)
|
||||
this.backupOptions()
|
||||
|
|
|
@ -304,16 +304,6 @@ def test_user_dashboard_display_user_maps_distinct(client, map):
|
|||
assert body.count(anonymap.name) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_logout_should_return_json_in_ajax(client, user, settings):
|
||||
client.login(username=user.username, password="123123")
|
||||
response = client.get(
|
||||
reverse("logout"), headers={"X_REQUESTED_WITH": "XMLHttpRequest"}
|
||||
)
|
||||
data = json.loads(response.content.decode())
|
||||
assert data["redirect"] == "/"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_logout_should_return_redirect(client, user, settings):
|
||||
client.login(username=user.username, password="123123")
|
||||
|
|
|
@ -830,10 +830,8 @@ class MapDelete(DeleteView):
|
|||
|
||||
def form_valid(self, form):
|
||||
self.object = self.get_object()
|
||||
if self.object.owner and self.request.user != self.object.owner:
|
||||
if not self.object.can_delete(self.request.user, self.request):
|
||||
return HttpResponseForbidden(_("Only its owner can delete the map."))
|
||||
if not self.object.owner and not self.object.is_anonymous_owner(self.request):
|
||||
return HttpResponseForbidden()
|
||||
self.object.delete()
|
||||
return simple_json_response(redirect="/")
|
||||
|
||||
|
@ -1178,8 +1176,6 @@ def webmanifest(request):
|
|||
|
||||
def logout(request):
|
||||
do_logout(request)
|
||||
if is_ajax(request):
|
||||
return simple_json_response(redirect="/")
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue