Complex forms with Django - django

Consider the following survey question:
Do you like this question? (choose one)
Yes
No, because
I don't like its formatting
I don't like the wording
Other reason: ...................
Other response (please specify) ......................
I'm trying to represent a series of questions, some of which are like that. Some are more simple (just a list of choices). But I'm having some problems trying to square this off with Django and its own way of doing forms. We have the following problems:
We need server side validation. Only one choice can be specified. And in the case of the "other" choices above, those need a follow-up charfield.
We need to squeeze a charfield into the options! I'm fairly sure I can hack these in via the templates but keep that in mind.
Just to complicate things, the questions and their answers need to be editable. I've done this with YAML already and to the point of generating the form, that works fine.
So what's the best way to hack the Django form system to allow me to do this? Should I bother with django.forms at all or just write something that does everything in its own way? How would you do this?

If you want to avoid dealing with forms I suggest using jquery's $.ajax() method. Basically, you simply need to create a blank form model to catch the POST, then you can grab the data and do with it what you want. Here is an example:
#models.py
class BlankForm(forms.Form):
def __unicode__(self):
return "BlankForm"
#views.py
def my_view(request):
if request.method == 'POST':
if 'answer' in request.POST:
form = BlankForm(request.POST)
if form.is_valid():
foo = request.POST.__getitem__('add')
bar = request.POST.__getitem__('bar')
baz = request.POST.__getitem__('baz')
#Do stuff with your data:
return HttpResponse('ok')
Then in your webpage you could some something like this:
<script type="text/javascript">
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
var csrftoken = getCookie('csrftoken');
$.ajaxSetup({
crossDomain: false, // obviates need for sameOrigin test
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
$.ajax({
type: 'POST',
data: {
'answer': true,
'foo': foo,
'bar': bar,
'baz': baz
},
dataType: 'application/json'
});
}
</script>
All the stuff about the cookies and CSRF token have to do with django's CSRF protection system. Basically all you would need to worry about would be editing the Data field in the $.ajax() method

The various options would be a ForeignKey to another table containing the responses. You'd have to have multiple fields in that FK to hold both the main answer and the subanswer, and you'd have to regroup (or reorder, if you need an explicit order) apropriately. You'd also need a CharField in order to hold the "other" response. It might be easiest to encapsulate both fields in a custom field, and use a custom widget to display the controls in the form.

Related

Tabulator PUT via Ajax to Django REST Endpoint - Reduces Table to Last Edited Record

I am using Tabulator with Django to edit a model. After any change to a cell, I use setData to make an Ajax call to a REST endpoint created using Django REST Framework. The database updates ok. The problem is that the response from the server contains only the single record that was updated, and this is making the Tabulator data reduce to only that record.
My question is, how can I get Tabulator to disregard the response, or otherwise have the data be left alone following the edit?
I am pretty new at this stuff (both Django and especially JavaScript) so apologies if I've missed something basic.
My tabulator code is below.
The function getCookie is to generate a CSRF_TOKEN as per the instructions in the Django documentation here. This is then included in the header as 'X-CSRFTOKEN': CSRF_TOKEN.
The variable ajaxConfigPut is used to set the method to PUT and to include the CSRF_TOKEN as noted above. This is then used in the table.setData call later on (table.setData(updateurl, updateData, ajaxConfigPut);).
The function ajaxResponse at the end just checks if the response is an array or not (because Tabulator expects an array which is fine for GET, but the PUT response was only a single {} object. So this function forces the PUT response into an array consisting of one object [{}].
<div id="example-table"></div>
<script type="text/javascript">
// get CSRF token
// https://docs.djangoproject.com/en/dev/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-and-csrf-cookie-httponly-are-false
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var CSRF_TOKEN = getCookie('csrftoken');
// set variable to customise ajaxConfig for use in the setData call
var ajaxConfigPut = {
method:"PUT", //set request type to Position
headers: {
// "Content-type": 'application/json; charset=utf-8', //set specific content type
'X-CSRFTOKEN': CSRF_TOKEN,
},
};
//create Tabulator on DOM element with id "example-table"
var table = new Tabulator("#example-table", {
ajaxURL:"{% url 'cust_listapi' %}", // reverse pick up the url since in a django template (?)
height:205, // set height of table (in CSS or here), this enables the Virtual DOM and improves render speed dramatically (can be any valid css height value)
layout:"fitColumns", //fit columns to width of table (optional)
columns:[ //Define Table Columns
{title:"Name", field:"name", width:150, editor:true},
{title:"Age", field:"age", hozAlign:"center",editor:true},
{title:"Age_Bar", field:"age", hozAlign:"left", formatter:"progress"},
{title:"Customer Status", field:"is_customer", hozAlign:"left"},
// {title:"Favourite Color", field:"col"},
// {title:"Date Of Birth", field:"dob", sorter:"date", hozAlign:"center"},
],
// see http://tabulator.info/docs/4.6/components#component-cell
cellEdited:function(cell){ //trigger an alert message when the row is clicked
console.log("Cell edited in row " + cell.getData().id
+ " and column " + cell.getField()
+ " from " + cell.getOldValue() + " to "
+ cell.getValue()
+ ". The row pk=" + cell.getData().id
);
console.log(cell.getData());
var updateurl = "{% url 'cust_listapi' %}" + cell.getData().id + "/"
console.log('URL is: ' + updateurl)
// Create variable from full row data but drop the id;
console.log('About to create updateData')
var updateData = {};
updateData[cell.getField()] = cell.getValue();
console.log(updateData);
console.log('About to setData');
table.setData(updateurl, updateData, ajaxConfigPut);
console.log('Finished setData');
//cell.restoreOldValue();
},
ajaxResponse:function(url, params, response){
console.log('Beginning ajaxResponse')
console.log('The type is:', typeof(response));
console.log(Array.isArray(response))
console.log(response)
result = response;
if(Array.isArray(response) === false){
result = [response];
};
return result;
}
});
</script>
Here's a screenshot of the table before editing:
Table Before Editing
And here's a screenshot after editing the top row (changing 'Mabel' to 'Jemima'):
Screenshot after editing
And here's the console log:
Console Log
I tried amending the response from the endpoint so that all records from the database are returned, but the problem with that is it doesn't include the edit, so the Tabulator table data is overwritten. Here's the code I used in the Django views.py. Maybe there's a way to return the data that has been changed?
views.py
from rest_framework import generics, mixins
from apps.app_mymodel.models import Customer
from .serializers import CustomerSerializer
class CustomerListAPIView(generics.ListAPIView):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
class CustomerUpdateAPIView(generics.GenericAPIView,
mixins.ListModelMixin,
mixins.UpdateModelMixin):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
# Override the put function here to return all records
def put(self, request, *args, **kwargs):
# return self.update(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
Here's the serializer:
serializers.py
from rest_framework import serializers
from apps.app_mymodel.models import Customer
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
Can someone please point me in the right direction?
None of the Mixins used by your CustomerUpdateAPIView have a method called put. I don't think that function is called. Instead, you can try to override the update method of your viewset. It could look like this:
def update(self, request, *args, **kwargs):
obj = super().update(request, *args, **kwargs) # performs the update operation
return self.list(request, *args, **kwargs)
You can check out this URL to understand the classes you are using: http://www.cdrf.co/
There you will see all the methods of the classes you are using to better understand the flow of your request.

Get single object in Django rest framework / Angular2 buggs after login

Im building Django + Angular2 project.
I have side bar and need to add Settings there, which will route to /users/{{ user.id }}, at the moment it works with bug, that when I login, this:
<li *ngFor="let user of users"><a routerLink="/users/{{ user.id }}" style="cursor: pointer">Settings</a></li>
will not show because for loop will not have any data. If I refresh page it will show in sidebar and will work perfect. Main idea is to open settings for authorized user.
This is how I get users:
getUsers() {
let headers = new Headers();
headers.append('Content-Type', 'application/json');
let authToken = localStorage.getItem('auth_token');
headers.append('Authorization', `JWT ${authToken}`);
return this.http
.get(this.apiURL, { headers })
.toPromise()
.then(res => res.json());
}
Problem is that I need to use for loop because in django framework I filter objects and get list of items. Actually I filter them to get only one, user who is logged in at the moment (code from views.py):
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
def get_queryset(self):
user = self.request.user
queryset = User.objects.filter(id=user.id)
return queryset
queryset = User.objects.filter(id=None)
Is there way to get only one object not list of data to not use for loop in angular (maybe that will fix the bug). Or maybe you can explain me and say how to fix that thing, that Settings disappears and shows only after refresh. Actually I think that for sure I use getUsers function in wrong component or wrong way.
My app.component.ts:
ngOnInit(): void {
this.userService
.getUsers()
.then(users => this.users = users)
}
I don't know for sure where is problem and why after login for loop don't have data, so I don't know what peace of code you need to help me. If need more code just ask.
I found solution, problem was that I used ngOnInit() and it loads on first time when user isn't logged in and gives to html empty variable so it can't loop.
At the moment I use ngDoCheck() which check if is session.
Example:
this.isSession = localStorage.getItem('auth_token');
if ((this.isSession != null) && (this.i == false)){
this.userService
.getUsers()
.then(users => this.users = users);
this.i = true;
}
}

Getting 403 on POST

I am learning to work with Django Rest Framework and following the tutorial. I have create a simple index based on the tutorial, that works for GET, but not for POST:
#api_view(['GET','POST'])
def game_list(request):
if request.method == 'GET':
games = Game.objects.all()
serializer = GameSerializer(games, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = GameSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I set the default settings to AllowAny:
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny'
]
}
but I still get a HTTP 403 when I try to POST anything, using the Firefox RESTClient. I read that I have to add a X-CSRFToken header and cookie for this to work, but I do not have those.
From documentation:
By default, a ‘403 Forbidden’ response is sent to the user if an incoming request fails the checks performed by CsrfViewMiddleware. This should usually only be seen when there is a genuine Cross Site Request Forgery, or when, due to a programming error, the CSRF token has not been included with a POST form.
Also, as stated if the official documentation, CSRF is enabled by default, and you need to add a X-CSRFToken in your AJAX requests.
Here is the code from the documentation:
// using jQuery
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
Take in mind that the documentation suggest to use ajaxSetup method from jquery, which is not a recommended way to do it because it can alter the way that others scripts uses the ajax function, so it's better to add the specific CSRF's code in your custom JS code like this:
$.ajax({
method: 'POST',
url: 'your.url.com/',
beforeSend: function(xhr, settings) {
if (!WU._csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
},
success: function(msg)
{}
});
Reference: https://docs.djangoproject.com/en/1.9/ref/csrf/#ajax

AngularJS/Django Post Response Data

I'm using AngularJS for the front-end and Django for the backend of a web app I'm working on. Right now I'm working on logging in users and I'm having a strange problem. Heres the relevant Angular code:
app.factory('AuthService', ["$http", "$q", "Session", "URL", function($http, $q, Session, URL) {
return {
login: function(credentials) {
var deferred = $q.defer();
$http.post(URL.login, credentials)
.then(function(data, status, headers, config) {
data=data.data; //WHY DOES THIS WORK?
if (data.success == true) {
alert("logged in");
Session.create(credentials.username, data.api_key);
deferred.resolve();
}
else {
deferred.reject("Login failed!");
}
}, function(data, status, headers, config) {
deferred.reject("Login failed!");
});
return deferred.promise
},
And here is the corresponding Django view:
def login_user(request):
'''
Given a username and password, returns the users API key.
'''
if request.method == 'POST':
username = request.POST.get('username',None)
password = request.POST.get('password',None)
user = authenticate(username=username,password=password)
if user is not None:
api_key = ApiKey.objects.get(user=user)
response_data = {}
response_data["api_key"] = str(api_key).split(" ")[0]
response_data["success"] = True
return HttpResponse(json.dumps(response_data), content_type="application/json")
else:
return HttpResponse(json.dumps({"username":username,"success":False}),content_type="application/json")
return HttpResponseBadRequest()
When the user logs in a POST request is sent and handled by the above Django code. The response is then picked up by the AngularJS code above. As you can see the then() method in the Angular code takes the usual four parameters: data, status, config and headers. I expect to see data contain the dictionary output from the Django code, appropriately serialized into a JSON object.
However what happens is that the only parameter of the then() method which is not undefined is data, and this contains EVERYTHING; headers, data, status code,etc.
The line commented 'WHY DOES THIS WORK' fixes the problem, by accessing the data inside. However, I want to know why this is happening and if there is any way to avoid this. My best guess is that it has something to do with the way Django serializes a response but I'm not sure.
I'm using Django 1.6.5.
That is actually how Angular promises work according to the docs. Here is the relevant quote.
Since the returned value of calling the $http function is a promise,
you can also use the then method to register callbacks, and these
callbacks will receive a single argument – an object representing the
response. See the API signature and type info below for more details.
The emphasis was mine.

How do I add a custom button next to a field in Django admin?

I have a Client model, which includes a field for a client API key.
When adding a new client in Django Admin, I'd like to have a button next to the API field to generate a new key (i have the method for this). The field will then be updated with the key once generated.
How can I add this button next to the field? Should I use a custom widget?
In my case I am making an API call with a button I create so I'll throw in how I did that too. Ofcourse your button can do whatever you like.
First, in your model create a function that will output your button. I will use my example, i.e. models.py:
class YourModel(models.Model):
....
def admin_unit_details(self): # Button for admin to get to API
return format_html(u'<a href="#" onclick="return false;" class="button" '
u'id="id_admin_unit_selected">Unit Details</a>')
admin_unit_details.allow_tags = True
admin_unit_details.short_description = "Unit Details"
I then added the field as readonly and added it to the fieldsets, note you can only have either fields or fieldsets defined on the model admin. I aslo added media to overwrite some css and also added the js for where the ajax call will be made, admin.py:
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
list_display = ('id', 'agent', 'project', 'completed_date', 'selected_unit', 'is_accepted',
'get_lock_for_admin', 'status')
fields = ('agent', 'project', 'completed_date', 'selected_unit', 'is_accepted',
'lock', 'status')
readonly_fields = ('admin_unit_details', )
...
class Media:
js = ('admin_custom/js/myjs.js',) # in static
css = {'all': ('admin_custom/css/mycss.css', )}
I also wanted to note that I passed the API address and header through the Form, but you can use the right header/password in the code. I just keep mine all in one place (settings.py), forms.py (optional):
from settings import API_ADDRESS, API_HEADER
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(WorksheetForm, self).__init__(*args, **kwargs)
self.fields['selected_unit'].widget = forms.Select(choices=get_worksheet_unit_choice_list(self.instance.id),
attrs={'api_address': API_ADDRESS, 'api_header': API_HEADER})
....
Lastly here is a look at my js, as referenced by my admin Media class, it is in admin_custom/js/myjs.js:
This is similar to adding an admin image, see here. Also search for allow_tags attribute in this django doc, it shows a good example.
// Make sure jQuery (django admin) is available, use admin jQuery instance
if (typeof jQuery === 'undefined') {
var jQuery = django.jQuery;
}
var unit_information = {};
jQuery( document ).ready(function() {
jQuery('#id_admin_unit_selected').click( function() {
//get the data from id_selected_unit, from the api_header api_address attributes
var unit_select = jQuery('#id_selected_unit');
var header = unit_select.attr('api_header');
var address = unit_select.attr('api_address');
var selected_unit = unit_select.val();
if (header && address && selected_unit){
var unit_address = address + '/units/' + selected_unit
get_unit(header, unit_address)
}
else{
// if can't connect to api, so hide
jQuery('.field-admin_unit_details').hide();
}
});
});
function get_unit(header, address){
jQuery.ajax
({
type: "GET",
url: address,
dataType: 'json',
headers: {
"Authorization": header
},
success: function (result) {
//TODO: output this in a modal & style
unit_information = JSON.stringify(result);
alert(unit_information)
},
error: function(xhr, textStatus, errorThrown) {
alert("Please report this error: "+errorThrown+xhr.status+xhr.responseText);
}
});
}
This outputs it in an alert, you can also log it to the console or define your own modal / style for it.
Hope this helps, Cheers!