Uploading files to tastypie with Backbone? - ajax

Checked some other questions and I think my tastypie resource should look something like this:
class MultipartResource(object):
def deserialize(self, request, data, format=None):
if not format:
format = request.META.get('CONTENT_TYPE', 'application/json')
if format == 'application/x-www-form-urlencoded':
return request.POST
if format.startswith('multipart'):
data = request.POST.copy()
data.update(request.FILES)
return data
return super(MultipartResource, self).deserialize(request, data, format)
class ImageResource(MultipartResource, ModelResource):
image = fields.FileField(attribute="image")
Please tell me if that's wrong.
What I don't get, assuming the above is correct, is what to pass to the resource. Here is a file input:
<input id="file" type="file" />
If I have a backbone model img what do I set image to?
img.set("image", $("#file").val()); // tastypie doesn't store file, it stores a string
img.set("image", $("#file").files[0]); // get "{"error_message": "'dict' object has no attribute '_committed'" ...
What do I set my backbone "image" attribute to so that I can upload a file to tastypie via ajax?

You may override sync method to serialize with FormData api to be able to submit files as model's attributes.
Please note that it will work only in modern browsers. It worked with Backbone 0.9.2, I advise to check the default Backbone.sync and adopt the idea accordingly.
function getValue (object, prop, args) {
if (!(object && object[prop])) return null;
return _.isFunction(object[prop]) ?
object[prop].apply(object, args) :
object[prop];
}
var MultipartModel = Backbone.Model.extend({
sync: function (method, model, options) {
var data
, methodMap = {
'create': 'POST',
'update': 'PUT',
'delete': 'DELETE',
'read': 'GET'
}
, params = {
type: methodMap[method],
dataType: 'json',
url: getValue(model, 'url') || this.urlError()
};
if (method == 'create' || method == 'update') {
if (!!window.FormData) {
data = new FormData();
$.each(model.toJSON(), function (name, value) {
if ($.isArray(value)) {
if (value.length > 0) {
$.each(value, function(index, item_value) {
data.append(name, item_value);
})
}
} else {
data.append(name, value)
}
});
params.contentType = false;
params.processData = false;
} else {
data = model.toJSON();
params.contentType = "application/x-www-form-urlencoded";
params.processData = true;
}
params.data = data;
}
return $.ajax(_.extend(params, options));
},
urlError: function() {
throw new Error('A "url" property or function must be specified');
}
});
This is excerpt from upload view, I use <input type="file" name="file" multiple> for file uploads so user can select many files. I then listen to the change event and use collection.create to upload each file.
var MultipartCollection = Backbone.Collection.extend({model: MultipartModel});
var UploadView = Backbone.View.extend({
events: {
"change input[name=file]": "changeEvent"
},
changeEvent: function (e) {
this.uploadFiles(e.target.files);
// Empty file input value:
e.target.outerHTML = e.target.outerHTML;
},
uploadFiles: function (files) {
_.each(files, this.uploadFile, this);
return this;
},
uploadFile: function (file) {
this.collection.create({file: file});
return this;
}
})

Related

asp.net mvc not getting value from ajax when converting data into json.stringify

I am using a event handler that checks if the product id or name already exist. But my problem is when I am using JSON.stringify() my C# controller does not receive the data from the ajax call,.
// check if Product name already exist
$('#productName').bind('keyup blur', function () {
// check if input is empty
if ($(this).val().length > 0) {
var data = JSON.stringify({
value: $(this).val(),
fieldName: 'productName'
});
$.ajax({
type: "post",
url: '/Product/ValidateProductDetailsExist',
contenttype: "application/json; charset=utf-8",
datatype: "json",
data: data,
context: this,
success: function (result) {
if (result === true) {
// append error message
// check if error message already exist
if ($('#errorprodcutName').length === 0) {
var errormessage = '<div class="col-md-offset-2"><span id = "errorprodcutName" class="validation-error-message">Product name already exist</span></div >';
$('.form-group:nth-child(2)').append(errormessage);
}
$(this).focus();
//disables the save button
$('#btnSaveProduct').prop('disabled', true);
}
else {
// check if error message already exist
if ($('#errorprodcutName').length > 0) {
$('#errorprodcutName').remove();
}
//enables the save button
$('#btnSaveProduct').prop('disabled', false);
}
},
error: function () {
alert("unable to request from server");
}
});
}
});
When I use debugger to check the value, it is null. I don't see any errors that displays in the console as well. Can anyone please explain to me why it is not working.
public JsonResult ValidateProductDetailsExist(string value, string fieldName)
{
using (POSEntities3 db = new POSEntities3())
{
bool isExist = false;
switch (fieldName)
{
case "productId":
var dataItemProductId = db.Products.Where(product => product.product_id == value).SingleOrDefault();
isExist = (dataItemProductId != null);
break;
case "productName":
var dataItemProductName = db.Products.Where(product => product.name == value).SingleOrDefault();
isExist = (dataItemProductName != null);
break;
}
return Json(isExist, JsonRequestBehavior.AllowGet);
}
}

Sending data to controller using Ajax

I have a form that contains different fields for user to fill in and also allow user to attach image. Below is my code to send data to the controller, I can see the image in my controller but I am not sure how to access rest of form data in controller:
$(function () {
var ajaxFormSubmit = function () {
var $form = $(this);
var data = new FormData();
var files = $("#File").get(0).files;
if (files.length > 0) { data.append("File", files[0]); }
else {
common.showNotification('warning', 'Please select file to upload.', 'top', 'right');
return false;
}
var options = {
url: $form.attr("action"),
type: $form.attr("method"),
data: data,
processData: false,
dataType: "html",
contentType: false
};
$.ajax(options).done(function (data) {
$('#ProdList').html(data);
});
return false;
};
$("form[data-ajax='true']").submit(ajaxFormSubmit);
});
My controller :
public ActionResult Create(PostViewProduct postProduct)
{
//code......
}
PostViewProduct model shows only image fields with data rest showing null:
Do I have to add each field using formdata.append() or is there better way to send all the data to controller and then access the data in controller.
Thanks
Try this:
var data = new FormData($(this)[0]);
instead of var data = new FormData();
Try following, this will put the data in right format if all input is within the form.
data = $form.serialize();
You basically need to send the files in FormData along with other form element data.
$(function () {
var ajaxFormSubmit = function () {
var fdata = new FormData();
$('input[name="Image"]').each(function (a, b) {
var fileInput = $('input[name="Image"]')[a];
if (fileInput.files.length > 0) {
var file = fileInput.files[0];
fdata.append("Image", file);
}
});
// You can update the jquery selector to use a css class if you want
$("input[type='text'").each(function (x, y) {
fdata.append($(y).attr("name"), $(y).val());
});
var frmUrl = $(this).attr('action');
$.ajax({
type: 'post',
url: frmUrl,
data: fdata,
processData: false,
contentType: false,
success: function (e) {
console.log(e);
}
});
return false;
};
$("form[data-ajax='true']").submit(ajaxFormSubmit);
});
Assuming your view model has a property called Image of type HttpPostedFileBase for accepting the posted file and your form has an input for that
public class YourViewModel
{
public HttpPostedFileBase Image { set;get;}
//Your other existing properties
}
and your form has an file input tag with name "Image".
<input type="file" name="Image" />

Update knockout viewmodel when uploading documents via ajax

I'm trying to use knockout for a view where I'm uploading documents and showing a list. For this I'm using jquery.form.js in order to upload them using ajax. I've changed that to use knockout and my viewmodel looks like this
var ViewModel = function (groups) {
var self = this;
self.groups = ko.observableArray(ko.utils.arrayMap(groups, function (group) {
return {
planName: ko.observable(group.Key),
documentList: ko.observableArray(ko.utils.arrayMap(group.Values, function (value) {
return {
document: ko.observable(new Document(value))
};
}))
};
}));
var options = {
dataType: 'json',
success: submissionSuccess
};
self.add = function () {
$('#addForm').ajaxSubmit(options);
return false;
};
function submissionSuccess(result) {
alert('success');
}
};
Having one Document function for doing the mapping. I'm stuck when receiving the Json data from the controller. The result is correct, a list of objects in the same format I'm receiving on first load but I don't know how to "refresh" the viewmodel to use this new list.
Don't know if using the ko mapping plugin would make it easier as I have never used it and don't even know if it's applicable for this.
The controller method, in case is relevant, is this (if something else neede let me know althoug won't have access to the code in the next hours)
[HttpPost]
public ActionResult AddDocument(AddDocumentViewModel viewModel)
{
var partyId = Utils.GetSessionPartyId();
if (viewModel.File.ContentLength > Utils.GetKBMaxFileSize * 1024)
ModelState.AddModelError("File", String.Format("The file exceeds the limit of {0} KB", Utils.GetKBMaxFileSize));
if (ModelState.IsValid)
{
_documentsManager.AddDocument(viewModel, partyId);
if (Request.IsAjaxRequest())
{
var vm = _displayBuilder.Build(partyId);
return Json(vm.Documents);
}
return RedirectToAction("Index");
}
var newViewModel = _createBuilder.Rebuild(viewModel, partyId);
return PartialView("_AddDocument", newViewModel);
}
Thanks
EDIT: I came up with this code which seems to work (this function is inside the ViewModel one
function submissionSuccess(result) {
self.groups(ko.utils.arrayMap(result, function (group) {
return {
planName: ko.observable(group.Key),
documentList: ko.utils.arrayMap(group.Values, function (value) {
return {
document: new Document(value)
};
})
};
}));
};
Are you sure the documentList and document need to be observables themselves ?
To update the list you can push to it as you'd do on a regular array.
You could try something like this:
function submissionSuccess(result) {
self.groups.removeAll();
$.each(result, function(index, value) {
var documentList = [];
$.each(value.Values, function(index, value) {
documentList.push(new Document(value));
});
var group = {
planName:value.Key,
documentList: documentList
};
self.groups.push(group);
});
};

How to keep session in multipage angular application?

I am having single page application with user authentication and there is no problem sharing session information there.
However I have part of site where are static pages where I would like just to include session information (logged in user, or login form). How I can share session information between two apps?
I would recommend creating a service that wraps localStorage or other apis to store persistent data. Here is an example using a localStorage implementation.
This implementation is synchronous but if I would use websql like or even server db then I would refactor it to use promises to return the storage object.
Controller
var demo = angular.module('demo', [ 'appStorage' ]);
demo.controller('AppStorageController', [ '$scope', 'appStorage',
function($scope, appStorage) {
appStorage('MyAppStorage', 'myAppStorage', $scope);
} ]);
HTML
<div ng-controller="AppStorageController">
<p>Local Storage: {{myAppStorage}}</p>
<p>
Username: <input type="text" ng-model="myAppStorage.username"></input>
</p>
<p>
Remember me: <input type="checkbox"
ng-model="myAppStorage.rememberMe"></input>
</p>
</div>
JS
angular.module('appStorage', []).factory('appStorage',
[ '$window', function($window) {
var appStorages = {};
var api = undefined;
if ($window.localStorage) {
api = {
set : function(name, value) {
$window.localStorage.setItem(name, JSON.stringify(value));
},
get : function(name) {
var str = $window.localStorage.getItem(name);
var val = {};
try {
val = str ? JSON.parse(str) : {};
}
catch (e) {
console.log('Parse error for localStorage ' + name);
}
return val;
},
clear : function() {
$window.localStorage.clear();
}
};
}
// possibly support other
if (!api) {
throw new Error('Could not find suitable storage');
}
return function(appName, property, scope) {
if (appName === undefined) {
throw new Error('appName is required');
}
var appStorage = appStorages[appName];
var update = function() {
api.set(appName, appStorage);
};
var clear = function() {
api.clear(appName);
};
if (!appStorage) {
appStorage = api.get(appName);
appStorages[appName] = appStorage;
update();
}
var bind = function(property, scope) {
scope[property] = appStorage;
scope.$watch(property, function() {
update();
}, true);
};
if (property !== undefined && scope !== undefined) {
bind(property, scope);
}
return {
get : function(name) {
return appStorage[name];
},
set : function(name, value) {
appStorage[name] = value;
update();
},
clear : clear
};
};
} ]);

How to include the #Html.AntiForgeryToken() when deleting an object using a Delete link

i have the following ajax.actionlink which calls a Delete action method for deleting an object:-
#if (!item.IsAlreadyAssigned(item.LabTestID))
{
string i = "Are You sure You want to delete (" + #item.Description.ToString() + ") ?";
#Ajax.ActionLink("Delete",
"Delete", "LabTest",
new { id = item.LabTestID },
new AjaxOptions
{ Confirm = i,
HttpMethod = "Post",
OnSuccess = "deletionconfirmation",
OnFailure = "deletionerror"
})
}
but is there a way to include #Html.AntiForgeryToken() with the Ajax.actionlink deletion call to make sure that no attacker can send a false deletion request?
BR
You need to use the Html.AntiForgeryToken helper which sets a cookie and emits a hidden field with the same value. When sending the AJAX request you need to add this value to the POST data as well.
So I would use a normal link instead of an Ajax link:
#Html.ActionLink(
"Delete",
"Delete",
"LabTest",
new {
id = item.LabTestID
},
new {
#class = "delete",
data_confirm = "Are You sure You want to delete (" + item.Description.ToString() + ") ?"
}
)
and then put the hidden field somewhere in the DOM (for example before the closing body tag):
#Html.AntiForgeryToken()
and finally unobtrusively AJAXify the delete anchor:
$(function () {
$('.delete').click(function () {
if (!confirm($(this).data('confirm'))) {
return false;
}
var token = $(':input:hidden[name*="RequestVerificationToken"]');
var data = { };
data[token.attr('name')] = token.val();
$.ajax({
url: this.href,
type: 'POST',
data: data,
success: function (result) {
},
error: function () {
}
});
return false;
});
});
Now you could decorate your Delete action with the ValidateAntiForgeryToken attribute:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
...
}
Modifying the answer by Bronx:
$.ajaxPrefilter(function (options, localOptions, jqXHR) {
var token, tokenQuery;
if (options.type.toLowerCase() !== 'get') {
token = GetAntiForgeryToken();
if (options.data.indexOf(token.name)===-1) {
tokenQuery = token.name + '=' + token.value;
options.data = options.data ? (options.data + '&' + tokenQuery)
: tokenQuery;
}
}
});
combined with this answer by Jon White
function GetAntiForgeryToken() {
var tokenField = $("input[type='hidden'][name$='RequestVerificationToken']");
if (tokenField.length == 0) { return null;
} else {
return {
name: tokenField[0].name,
value: tokenField[0].value
};
}
Edit
sorry - realised I am re-inventing the wheel here SO asp-net-mvc-antiforgerytoken-over-ajax/16495855#16495855

Resources