Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

14 error display #22

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,16 @@ The component will start sending If-Unmodified-Since headers if your endpoint re
'inputAttributes': { # input attributes
'type': 'text', # only text and number supported
},
'errors': { # errors to display keyed by HTTP status code
412: 'The data has been updated on the server, refresh your page to check the current value',
401: 'Unauthenticated: Please login',
403: 'Unauthorized: You may not have permission to change this data',
'unknown': 'Sorry there has been an unforeseen error updating this data',
},
}
```

example of overriding to set number input type and an Accept header
example of overriding to set number input type, an Accept header, and a custom error
```
# in your view
def get_context_data(self, **kwargs):
Expand All @@ -58,6 +64,7 @@ example of overriding to set number input type and an Accept header
context['updater_options'] = dict(
inputAttributes=dict(type='number'),
headers=dict(yourHeader='yourHeaderValue'),
errors=dict(401='Please login!'),
)
return context
# in your template
Expand Down
9 changes: 5 additions & 4 deletions static/js/ajaxUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,15 @@ export default class AjaxUpdater {
).then(function(response) {
// update the internal options
self.updateFetchOptions(response.headers);

if(response.ok) {
resolve(response.status, response);
resolve(response.status);
} else {
reject(response.statusText, response.status, response)
reject(response.status)
}
}).catch(function(err) {
reject(err.message, err)
// 598 (Informal convention) Network read timeout error
// used here to indicate generic failure
reject(598)
Copy link
Member Author

@PeteCoward PeteCoward Aug 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really decided what to send back here, code -1, string 'fetch error'?

});
});
}
Expand Down
19 changes: 13 additions & 6 deletions static/js/fieldUpdater.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,23 @@ export default async function initialise(config) {

// handler to encapsulate an async submission action ( update or delete )
function submit(action) {
formElement.hidden = true;
deleteElement.hidden = true;
submitElement.hidden = true;
loaderElement.hidden = false;

action().then(() => {
updateDisplay();
}).catch((err) => {
errorElement.innerHTML = err;
errorElement.hidden = false;
}).finally(() => {
formElement.hidden = true;
displayElement.hidden = false;
formElement.hidden = true;
}).catch((errorCode) => {
if (config.options.errors.hasOwnProperty(errorCode)) {
inputElement.setCustomValidity(config.options.errors[errorCode]);
} else {
inputElement.setCustomValidity(config.options.errors['unknown']);
}
formElement.reportValidity();
}).finally(() => {
submitElement.hidden = false;
loaderElement.hidden = true;
});
}
Expand All @@ -83,6 +89,7 @@ export default async function initialise(config) {

// what happens when a key is hit in the input
inputElement.onkeyup = function(e) {
inputElement.setCustomValidity('');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main reason this PR is still draft. It clears the validation on input, but causes an ugly flicker on submission with enter :/

if (e.key === 'Enter') updateOrCreate(inputElement.value);
}

Expand Down
49 changes: 39 additions & 10 deletions templates/field_updater/example.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ <h2>simple use</h2>
</p>
<div class="code">
{% verbatim %}
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key='value' %}
{% endverbatim %}
</div>
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
{% field_updater submit_url=submit_url key='value' %}

<h2>input types</h2>
Expand All @@ -36,12 +36,12 @@ <h2>input types</h2>
</p>
<div class="code">
{% verbatim %}
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key=42 options=updater_options_number %}
{% endverbatim %}
</div>
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
{% field_updater submit_url=submit_url key=42 options=updater_options_number %}

<h2>allow deletion</h2>
Expand All @@ -51,12 +51,12 @@ <h2>allow deletion</h2>
</p>
<div class="code">
{% verbatim %}
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key='deleteable' options=updater_options_delete %}
{% endverbatim %}
</div>
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
{% field_updater submit_url=submit_url key='deleteable' options=updater_options_delete %}

<h2>sending custom headers</h2>
Expand All @@ -66,12 +66,12 @@ <h2>sending custom headers</h2>
</p>
<div class="code">
{% verbatim %}
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key='headers' options=updater_options_headers %}
{% endverbatim %}
</div>
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
{% field_updater submit_url=submit_url key='headers' options=updater_options_headers %}

<h2>Regex validation</h2>
Expand All @@ -81,13 +81,42 @@ <h2>Regex validation</h2>
</p>
<div class="code">
{% verbatim %}
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key='headers' options=updater_options_regex %}
{% endverbatim %}
</div>
{% url 'example_submit' as submit_url %}
{% url 'cache_submit' as submit_url %}
{% field_updater submit_url=submit_url key='headers' options=updater_options_regex %}

<h2>If-Match</h2>
<p>
Provide an if_match value to send an If-Match ETag value <br />
</p>
<div class="code">
{% verbatim %}
{% url 'cache_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key='if-match' if_match='sdasdasdasd' %}
{% endverbatim %}
</div>
{% url 'cache_submit' as submit_url %}
{% field_updater submit_url=submit_url key='if-match' if_match='sdasdasdasd' %}

<h2>Error messages</h2>
<p>
Provide an options dict containing errors<br />
context['updater_options_errors'] = {{ updater_options_errors|to_str}}
</p>
<div class="code">
{% verbatim %}
{% url 'faking_submit' as submit_url %}
<br />
{% field_updater submit_url=submit_url key='will error' options=updater_options_errors %}
{% endverbatim %}
</div>
{% url 'faking_submit' as submit_url %}
{% field_updater submit_url=submit_url key='will error' options=updater_options_errors %}
</section>
</body>
</html>
17 changes: 16 additions & 1 deletion templatetags/field_updater_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,23 @@ def field_updater(
'inputAttributes': { # input attributes
'type': 'text',
},
'errors': { # errors to display keyed by HTTP status code
412: 'The data has been updated on the server, refresh your page to check the current value',
401: 'Unauthenticated: Please login',
403: 'Unauthorized: You may not have permission to change this data',
'unknown': 'Sorry there has been an unforeseen error updating this data',
},
}
updater_options = dict(default_options, **options) if options else default_options

# build field updater options
updater_options = dict(default_options)
if options:
for key, value in options.items():
if key != 'errors':
updater_options[key] = value
errors = options.get('errors', {});
for key, value in errors.items():
updater_options['errors'][key] = value

validate_options(updater_options)

Expand Down
16 changes: 10 additions & 6 deletions urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from django.urls import path
from .views import LoggingSubmitView, ExampleView
from .views import FakingSubmitView, CacheSubmitView, ExampleView

urlpatterns = [
# an example endpoint to demonstrate component use
# example endpoint to demonstrate component use
path('', ExampleView.as_view()),
# a simple example submit endpoint that returns 200 ok
path('example_submit', LoggingSubmitView.as_view(), name='example_submit'),
# an example submit endpoint with a parameter that returns 200 ok
path('example_submit/<name>/', LoggingSubmitView.as_view(), name='example_submit'),

# submit endpoint that uses the fieldupdater cache
path('cache_submit', CacheSubmitView.as_view(), name='cache_submit'),
path('cache_submit/<name>/', CacheSubmitView.as_view(), name='cache_submit'),

# submit endpoint that will do nothing but
# return any status you put in Catalpa-FieldUpdater-Fake header
path('fake_submit', FakingSubmitView.as_view(), name='faking_submit'),
]
82 changes: 71 additions & 11 deletions views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import time
import hashlib
import logging

from django.core.cache import caches
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.http import http_date, quote_etag
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import condition

from django.views.generic import View, TemplateView

logger = logging.getLogger('django.field_updater')
logger = logging.getLogger('catalpa.field_updater')


@method_decorator(ensure_csrf_cookie, name='dispatch')
Expand All @@ -31,26 +38,79 @@ def get_context_data(self, **kwargs):
pattern="[\w]{3}",
title='3 word characters'),
)
context['updater_options_errors'] = dict(
headers={'Catalpa-FieldUpdater-Fake': 401},
errors={
401: 'Your custom Error',
}
)
return context


class LoggingSubmitView(View):
''' An example submit view that does nothing just logs usefully '''
log_headers = ['Content-Type', 'Accept', 'IfMatch', 'IfUnmodifiedSince']
log_headers = ['Content-Type', 'Accept', 'If-Match', 'If-Unmodified-Since']

def post(self, request, *args, **kwargs):
''' update or create your stored value '''
logger.info(request)
def dispatch(self, request, *args, **kwargs):
for key in self.log_headers:
if key in request.headers:
logger.info("{}: {}".format(key, request.headers[key]))
return super().dispatch(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
''' delete your stored value '''
logger.info(request.POST)
return HttpResponse()

def etag_from_value(value):
etag = hashlib.md5(','.join(value).encode()).hexdigest()
return etag

def cache_view_etag(request, *args, **kwargs):
cache = caches['field_updater']
store = cache.get(request.path)
if store is None:
return None
return etag_from_value(store['value'])

@method_decorator(condition(etag_func=cache_view_etag, last_modified_func=None), name='dispatch')
class CacheSubmitView(LoggingSubmitView):
''' An example submit view that works against the cconfigured cache '''
def dispatch(self, request, *args, **kwargs):
self.cache = caches['field_updater']
return super().dispatch(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
''' update or create your stored value '''
super().post(request, *args, **kwargs)
key = list(request.POST.keys())[0]
value = request.POST.getlist(key)
store = {
'value': value,
'modified': time.time()
}
self.cache.set(request.path, store)
response = HttpResponse()
response["ETag"] = quote_etag(etag_from_value(value))
response["Last-Modified"] = http_date(store['modified'])
return response

@condition(etag_func=cache_view_etag, last_modified_func=None)
def delete(self, request, *args, **kwargs):
''' delete your stored value '''
logger.info(request)
for key in self.log_headers:
if key in request.headers:
logger.info("{}: {}".format(key, request.headers[key]))
return HttpResponse()
store = self.cache.delete(request.path)
response = HttpResponse()
return response

def get(self, request, *args, **kwargs):
''' get your stored value '''
store = self.cache.get(request.path)
response = HttpResponse(store.value)
response["ETag"] = quote_etag(store['etag'])
response["Last-Modified"] = http_date(store['modified'])
return response


class FakingSubmitView(LoggingSubmitView):
''' will return 200 to all requests unless header 'Catalpa-FieldUpdater-Fake' is set with a desired status code '''
def dispatch(self, request, *args, **kwargs):
return HttpResponse(status=request.META.get('HTTP_CATALPA_FIELDUPDATER_FAKE', 200))