I'm trying to submit a form in a Rails 5.1 app that uses Vue.js and Dropzone. During the sendingEvent I am using JSON.stringify on the object before sending it over to the controller. However, I don't feel this is the correct way to do it, as I am then having problems with using strong params in the controller.
JS:
import Vue from 'vue/dist/vue.esm'
import VueResource from 'vue-resource'
import vue2Dropzone from 'vue2-dropzone'
Vue.use(VueResource)
document.addEventListener('DOMContentLoaded', () => {
if(document.getElementById('listing-multistep') !== null) {
Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('input[name="authenticity_token"]').getAttribute('value');
var listingForm = document.getElementById('listing_form');
var listing = JSON.parse(listingForm.dataset.listing);
var locale = document.getElementsByTagName('html')[0].getAttribute('lang');
const myForm = new Vue({
el: '#listing-multistep',
components: {
vueDropzone: vue2Dropzone
},
data: function () {
return {
id: listing.id,
locale: locale,
slug: listing.slug,
activeStep: 0,
// More data
dropzoneOptions: {
url: `/${locale}/listings`,
method: 'post',
acceptedFiles: 'image/*',
uploadMultiple: true,
autoProcessQueue: false,
parallelUploads: 15,
maxFiles: 15,
addRemoveLinks: true,
thumbnailWidth: 150,
maxFilesize: 5,
dictDefaultMessage: "<i class='fa fa-cloud-upload'></i> Drop files here to upload (max. 15 files)",
headers: { 'X-CSRF-Token': Vue.http.headers.common['X-CSRF-Token'] }
}
}
},
methods: {
sendingEvent: function(file, xhr, formData) {
// This function gets called by Dropzone upon form submission.
var listingObj = this.setupListingObj()
formData.append('listing', JSON.stringify(listingObj))
},
listingRedirect: function(files, response) {
window.location = `/${this.locale}/listings/${response.slug}`
},
submitListing: function() {
var numFiles = this.$refs.listingDropzone.getAcceptedFiles().length
// If there are images to upload, use Dropzone
// Else submit the form normally.
if(numFiles > 0) {
this.$refs.listingDropzone.processQueue()
} else {
var listingObj = this.setupListingObj()
if(this.id === null) {
// POST if it's a new listing
this.$http.post(`/${this.locale}/listings`, {listing: listingObj}).then(
response => {
window.location = `/${this.locale}/listings/${response.body.slug}`
}, response => {
console.log(response)
})
} else {
// PUT if it's an existing listing
this.$http.put(`/${this.locale}/listings/${this.slug}`, {listing: listingObj}).then(
response => {
window.location = `/${this.locale}/listings/${response.body.slug}`
}, response => {
console.log(response)
})
}
}
},
setupListingObj: function() {
// do some processing...
var listingObj = {
id: this.id,
name: this.name,
// set more attributes
}
return listingObj
},
}
}
});
as you can see I am using formData.append('listing', JSON.stringify(listingObj)) on the sendingEvent.
My controller:
class ListingsController < ApplicationController
def create
#listing = Listing.new JSON.parse(params[:listing])
#listing.owner = current_user
respond_to do |format|
if #listing.save
format.html { redirect_to listing_path(#listing), notice: 'Listing was created successfully!' }
format.json { render :show, status: :created, location: #listing }
else
format.html { render :new }
format.json { render json: #listing.errors, status: :unprocessable_entity }
end
end
end
private
def listing_params
params.require(:listing).permit(
:name,
:bedrooms,
:beds,
:bathrooms,
:price_cents,
:price_currency,
:property_type,
:city,
:state,
:address,
:lat,
:lng,
:description,
:amenities => []
)
end
end
It seems to work in development, but when I run a test with this code in RSpec I get errors like:
Internal Server Error no implicit conversion of ActionController::Parameters into String
When I try to swap #listing = Listing.new JSON.parse(listing_params) it fails to work in development.
I have a feeling I'm not sending the form data across properly. What is the correct way to send the data over via Javascript to my Rails controller? Does it need to be strigified and then posted? How can I access it via strong params instead?
Thanks in advance!
UPDATE
This is what my spec looks like:
RSpec.feature 'Listing owners can create new listings' do
let(:owner) { create(:user, :owner) }
before do
login_as owner
visit new_listing_path
end
scenario 'successfully', js: true do
fill_in 'Property name', with: 'Example property'
select 'Apartment', from: 'Property type'
fill_in 'Address', with: 'Somewhere'
click_button 'Next'
fill_in 'Property description', with: Faker::Lorem.paragraph(2)
click_button 'Create Listing'
expect(page).to have_content 'Listing was created successfully!'
end
end
I am using Chrome headless for these tests in order to parse the Vue.js stuff on the form. In my rails_helper.rb I have:
require 'spec_helper'
require 'rspec/rails'
require 'capybara/rails'
require 'capybara/rspec'
require 'pundit/rspec'
require 'selenium/webdriver'
RSpec.configure do |config|
# ...
Capybara.javascript_driver = :headless_chrome
end
and I have a support/chrome_driver.rb file with the following:
Capybara.register_driver(:headless_chrome) do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
chromeOptions: { args: %w[headless disable-gpu] }
)
Capybara::Selenium::Driver.new(
app,
browser: :chrome,
desired_capabilities: capabilities
)
end
Related
In odoo 15, how can I add new action menu in user menu? In other odoo versions (13 and 14), it was possible by inheriting from UserMenu.Actions.
In odoo 15, I tried the following code but it is not working.
Thanks for any suggestion
/** #odoo-module **/
import { registry } from "#web/core/registry";
import { preferencesItem } from "#web/webclient/user_menu/user_menu_items";
export function UserLog(env) {
return Object.assign(
{},
preferencesItem(env),
{
type: "item",
id: "log",
description: env._t("UserRecent Log"),
callback: async function () {
const actionDescription = await env.services.orm.call("user.recent.log", "action_get");
actionDescription.res_id = env.services.user.userId;
env.services.action.doAction(actionDescription);
},
sequence: 70,
}
);
}
registry.category("user_menuitems").add('profile', UserLog, { force: true })
This is my model code.
class UserRecentLog(models.Model):
_name = 'user.recent.log'
_order = "last_visited_on desc"
#api.model
def action_get(self):
return self.env['ir.actions.act_window']._for_xml_id('user_recent_log.action_user_activity')
This is my xml view.
<!-- actions opening views on models -->
<record model="ir.actions.act_window" id="action_user_activity">
<field name="name">User Recent Log(s)</field>
<field name="res_model">user.recent.log</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="user_activity_view_tree"/>
</record>
You don't need to change anything, your code should work. You can check the user preferences menu item in web module (similar to your menu item).
export function preferencesItem(env) {
return {
type: "item",
id: "settings",
description: env._t("Preferences"),
callback: async function () {
const actionDescription = await env.services.orm.call("res.users", "action_get");
actionDescription.res_id = env.services.user.userId;
env.services.action.doAction(actionDescription);
},
sequence: 50,
};
}
registry
.category("user_menuitems")
.add("profile", preferencesItem)
There is another implementation in hr module:
import { registry } from "#web/core/registry";
import { preferencesItem } from "#web/webclient/user_menu/user_menu_items";
export function hrPreferencesItem(env) {
return Object.assign(
{},
preferencesItem(env),
{
description: env._t('My Profile'),
}
);
}
registry.category("user_menuitems").add('profile', hrPreferencesItem, { force: true })
So you can rewrite your code above as following:
import { registry } from "#web/core/registry";
import { preferencesItem } from "#web/webclient/user_menu/user_menu_items";
export function UserLog(env) {
return Object.assign(
{},
preferencesItem(env),
{
type: "item",
id: "log",
description: env._t("Log"),
callback: async function () {
const actionDescription = await env.services.orm.call("res.users.log", "action_user_activity");
env.services.action.doAction(actionDescription);
},
sequence: 70,
}
);
}
registry.category("user_menuitems").add('profile', UserLog, { force: true })
Edit:
The tree view mode is ignored when executing the window action.
The _executeActWindowAction will check for the tree view type in the views registry to construct the views object and unfortunately, the tree view mode was not added to that registry.
To show the tree view, you can add [false, 'list'] to the views list and specify the view type (list) in the doAction options:
actionDescription.views.push([actionDescription.view_id[0], 'list'])
env.services.action.doAction(actionDescription, {viewType: 'list'});
Or update the views list and change tree to list:
actionDescription.views[0][1] = 'list';
Of course , you can do the same in the action_get method:
action = self.env['ir.actions.act_window']._for_xml_id('user_recent_log.action_user_activity')
action['views'][0] = action['view_id'][0], 'list'
return action
I want to build an infinite scrolling functionality in my Ruby on Rails application. In order to achieve it, I use Stimulus JS (more information: here), vanilla-lazyload js (more information: here) and pagy gem (more information: here) (including webpacker). Everything works great, however, it seems like vanilla-lazyload js stops working on infinite scrolling.
Here is my setup. index view (I use HAML):
%section(class="section section--tight" data-controller="infinite-scroll")
%div(class="section-body")
%div(class="content content--shot")
%div(class="shots-grid" data-target="infinite-scroll.entries")
= render partial: "shots/shot", collection: #shots, cache: true
%div(class="section-pagination")
%div(class="content")
%div(data-target="infinite-scroll.pagination")
!= pagy_nav #pagy
JS controller - infinite_scroll_controller.js:
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["entries", "pagination"]
initialize() {
let options = {
rootMargin: '500px',
}
this.intersectionObserver = new IntersectionObserver(entries => this.processIntersectionEntries(entries), options)
}
connect() {
this.intersectionObserver.observe(this.paginationTarget)
}
disconnect() {
this.intersectionObserver.unobserve(this.paginationTarget)
}
processIntersectionEntries(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadMore()
}
})
}
loadMore() {
let next_page = this.paginationTarget.querySelector("a[rel='next']")
if (next_page == null) { return }
let url = next_page.href
Rails.ajax({
type: 'GET',
url: url,
dataType: 'json',
success: (data) => {
this.entriesTarget.insertAdjacentHTML('beforeend', data.entries)
this.paginationTarget.innerHTML = data.pagination
}
})
}
}
Lazy loading vanilla-lazyload:
// https://github.com/verlok/lazyload
import LazyLoad from "vanilla-lazyload";
document.addEventListener('turbolinks:load', () => {
var lazyLoadInstance = new LazyLoad({
elements_selector: ".lazy",
cancel_on_exit: true,
use_native: false
});
})
Controller view:
def index
#pagy, #shots = pagy Shot.is_recent.includes(:user).all
respond_to do |format|
format.html
format.json {
render json: { entries: render_to_string(partial: "shots/shot", collection: #shots, formats: [:html]), pagination: view_context.pagy_nav(#pagy) }
}
end
end
The pagy gem website says that the gem requires a setup for webpacker, [here is the link][4]. However, it seems like the problem in turbolinks. In other words, vanillay-lazyload works with the very first 10 records, however, with newly added (records from paginated pages) records, it refuses to work. Perhaps I am missing something? Thank you for your help and time.
JS controller - infinite_scroll_controller.js:
Update loadmore function, after Rails.ajax calling success, run LazyLoad again
Add new code below
new LazyLoad({
elements_selector: ".lazy",
cancel_on_exit: true,
use_native: false
});
loadMore() {
let next_page = this.paginationTarget.querySelector("a[rel='next']")
if (next_page == null) { return }
let url = next_page.href
Rails.ajax({
type: 'GET',
url: url,
dataType: 'json',
success: (data) => {
this.entriesTarget.insertAdjacentHTML('beforeend', data.entries);
this.paginationTarget.innerHTML = data.pagination;
new LazyLoad({
elements_selector: ".lazy",
cancel_on_exit: true,
use_native: false
});
}
})
}
Currently, I created a keystone model as following format:
var keystone = require('keystone');
var SiteSettings = new keystone.List('SiteSetting', {
map: { name: 'siteName' },
nocreate: true,
noedit: false,
nodelete: true,
singular:'SiteSetting',
plural: 'SiteSetting',
});
SiteSettings.add({
siteName: { type: String, required: true },
contactNumber: { type: String, required: false },
facebookGroupUrl: { type: String, required: false },
googlePlusUrl: { type: String, required: false }
});
SiteSettings.register();
Then I went to keystone back-end, created a new Site Setting object.
And on my default template, i am using a partial view like this:
<body>
<main>
{{>header}}
{{>footer}}
</main>
</body>
And this is my footer partial:
<div class="row">//I want to print my site name here</div>
But I have no idea how can retrieve model data without a route. Because it's a partial view.
Any idea ? what should I do ? Is there anything I can do in middleware.js
Thank you everyone,.
Yes, you can load the global site settings in a middleware.
// routes/middleware.js
exports.initLocals = (req, res, next) => {
const { locals } = res;
// Load site settings from db
keystone.list('SiteSettings').model.findOne().exec((err, result) => {
locals.siteSetting = result;
next(err);
});
};
// routes/index.js
const { initLocals } = require('./middleware');
keystone.pre('routes', initLocals);
Then you can use the siteName in the footer.
<div class="row">{{ siteSetting.siteName }}</div>
I'm trying to use more vanilla javascript instead if jquery. I have no problem making ajax calls with $.post but I can't seem to get it to work with vanilla javascript. This is my ajax call:
$(document).ready(function () {
$('#addAssignment').click(function () {
$('#addAssignmentModal').modal('toggle');
});
$('#submitAssignment').click(function () {
if(checkModal()){
var assignment = {
title: $('#newAssignmentTitle').val(),
type: $('#assignmentSelection').val(),
date: $('#newAssignmentDate').val(),
details: $('#newAssignmentDetails').val()
}
console.log(assignment);
submitAssignment(assignment);
}
});
});
function submitAssignment(assignment) {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
//Do stuff
}
};
request.open("POST", '/data/api/create-assignment/', true);
request.setRequestHeader('X-CSRFToken', cookies['csrftoken']);
//assignment is equal to: {title: "Title", type: "Homework", date: "02/18/2017", details: "Detais"}
request.send(JSON.stringify(assignment));
}
When I try and print out the data in my Djano view, it prints out an empty queryset every time.
def createAssignment(request):
if request.method == 'POST':
print(request.POST) # this prints an empty queryset
# assignment = Assignments()
# assignment = request.POST.get('assignment')
# assignment.save()
data = {}
data['status'] = "Success"
return HttpResponse(json.dumps(data), content_type="application/json")
else:
data = {}
data['status'] = "Data must be sent via POST"
return HttpResponse(json.dumps(data), content_type="application/json")
How do I prepare my data and receive it properly?
UPDATE:
I was able to get this working. The ajax call stays the same. In order to print the data in my django view, I used the following code:
body_unicode = request.body.decode('utf-8')
body = json.loads(body_unicode)
print(body)
Printing body gives the following:
{'details': 'Here are some details', 'title': 'Title', 'date': '02/18/2017', 'type': 'Homework'}
body_unicode = request.body.decode('utf-8')
body = json.loads(body_unicode)
print(body)
I'm trying to pass my uploaded photo to javascript on a page, but I get this error. How can I fix it?
werkzeug.routing.BuildError
BuildError: ('uploaded_file', {'filename': u'user/user-1/scan.jpeg'}, None)
class AdminController(BaseController):
route_base = ''
route_prefix = '/admin'
trailing_slash = True
decorators = [login_required]
def __init__(self):
self.theme = "admin"
g.theme = self.theme
g.currentUser = g.auth.getUser()
self.viewData = {
"layout" : self.theme + "/" + "layouts/main.html"
}
class BaseMethodView(MethodView):
pass
class UserJsonDataController(AdminController, BaseMethodView):
def __init__(self):
super(UserJsonDataController, self).__init__()
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
def get(self):
json = {}
users = User.select()
a = []
for user in users:
obj = {
"user_avatar":url_for("uploaded_file", filename = user.user_avatar)
}
a.append(obj)
json["rows"] = a
return flask.jsonify(json)
module.add_url_rule('/index/show', view_func=UserJsonDataController.as_view('show'))
$(document).ready(function () {
$("#grid").kendoGrid({
dataSource: {
transport: {
read: {
url: '{{ url_for("user_admin.show") }}',
dataType: "json"
}
},
schema: {
data: "rows"
}
},
columns: [{
field: "user_avatar",
title: "Profil",
template: "<img src='/#=user_avatar #' /> "
}
});
});
I know this is old but why not. #huseyin, you mentioned that your image is located in: app/upload/user/user-1/scan.jpeg. Your url_for() first argument should match that path and in your code it's 'uploaded_file'. Try changing it to 'upload'