Construct views for data nested several levels deep - marionette

I'm trying to build an order system for a friend using django/tastypie on the server side and backbone/marionette on the client side. The server side poses no bigger problem but since I'm an inexperienced frontend developer I'm kinda stuck;
The simpler case went just fine, e.g. to list, add, edit and remove an Article (just a table in my database with sku, description and so on) using Composite- and ItemViews.The problem is when I'm trying to construct the views for an Order since it consists of several tables with relations on the server side.
Order
LineItem
Article
StoreQuantity
Store
StoreQuantity
Store
LineItem
Article
StoreQuantity
Store
StoreQuantity
Store
...
So an Order consists of several LineItems. A LineItem consists of an Article and several StoreQuantity:s making it possible to model something like "Order Article A; 10 copies to Store X and 4 copies to Store Y, Article B; 4 copies to Store X and 1 copy to Store Y".
I guess my question is; how would I go about to construct my views for something like above?
Would something like below be the wrong way?
Create an OrderCompositeView and pass it the OrderModel from my controller.
When OrderModel is fetched from the server, let OrderCompositeView create a LineItemCompositeView.
When LineItemCompositeView has fetched its' LineItemCollection from the server.. and so on recursively
Should I create a REST-url that returns the entire JSON for an Order and its relations instead of several smaller recursive calls, and then try to parse the JSON client side?
I've found several good resources on how to get going with Marionette but none on how to handle data nested several layers deep.
Thanks /Magnus
Edit:
Showing some code illustrating what I've been testing
(Views)
var LineItemDetailView = Backbone.Marionette.ItemView.extend({
template: "#lineitem-layout-template",
tagName: "div",
initialize: function() {
}
});
var LineItemView = Backbone.Marionette.CompositeView.extend({
template: "#lineitem-wrapper-template",
childView: LineItemDetailView,
childViewContainer: "div",
initialize: function(coll, obj) {
this.collection = new LineItemCollection({url: "api/v1/lineitem/?order__id=" + obj["order_id"]});
this.collection.fetch({
success: function() {
console.log("Successfully fetched lineitems");
}
});
}
});
var OrderDetailView = Backbone.Marionette.CompositeView.extend({
template: "#order-detail-template",
childView: LineItemView,
childViewContainer: "#lineitems",
initialize: function() {
this.model.on("sync", function(mod) {
lineitemView = new LineItemView([],{order_id: mod.get("id")});
});
}
});
Something along those lines. OrderDetailView is created from my controller and passed the OrderModel. I from this I get OrderDetailView:s template to render and the LineItemCollection is fetched from server but nothing more happens.

So I ran into this when creating a survey portion of an app the other day. It had a structure like this:
Survey:
Question:
Answer
Answer
Question:
Answer
Answer
So pretty similar to what you're doing. I used the backbone-relational gem - http://backbonerelational.org/ to relate the models together and it worked great. My API sends back all of the JSON in a single call. So surveys/1.json brings back all of the above pieces/their data. Then I parse/break them up with Backbone relational. Here's what they look like:
Survey:
class Entities.Survey extends App.Entities.Model
urlRoot: "surveys"
defaults:
status: "Draft"
number_taken: 0
survey_limit: 500
relations: [
type: Backbone.HasMany
key: "questions"
relatedModel: Entities.Question
reverseRelation:
key: 'survey'
includeInJSON: 'id'
]
Question:
class Entities.Question extends App.Entities.Model
urlRoot: "questions"
defaults:
single_response: true
terminate: false
free_text: false
relations: [
type: Backbone.HasMany
key: "answers"
relatedModel: Entities.Answer
reverseRelation:
key: 'question'
includeInJSON: 'id'
]
Answer:
class Entities.Answer extends App.Entities.Model
urlRoot: "answers"
defaults:
branching: false
next_question_id: null
Then when you go to display them, in my survey display view I have a layout view that has a question region which uses a composite view of the survey questions like this:
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ survey, id } = options
survey or= App.request "survey:entity", id
App.execute "when:fetched", survey, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion survey
#questionRegion survey
#bannerRegion survey
#show #layout
questionRegion: (survey) ->
App.request "show:survey:questions", survey, #layout.questionRegion
Then I come in and get the questions:
questionRegion: (survey) ->
questions = survey.get('questions')
questionView = #getQuestionView questions, survey
The childview of the Questions CompositeView is itself a CompositeView with a childview of answers.
So Survey has a Questions CompositeView of Questions, each of which is a CompositeView of Answers.
You should be able to follow a similar structure with your app. Let me know if you get stuck anywhere!
Edit: Adding View/Controllers.
So here's what I do, when the user navigates to a certain route - say localhost:3000/#surveys/1/edit it hits my surveysrouter (note some code like the list piece I stripped out):
#TheoremReach.module "SurveysApp", (SurveysApp, App, Backbone, Marionette, $, _) ->
class SurveysApp.Router extends Marionette.AppRouter
appRoutes:
"surveys" : "list"
"surveys/:id" : "show"
"surveys/:id/take": "take"
API =
show: (id, survey) ->
new SurveysApp.Show.Controller
id: id
survey: survey
take: (id, survey) ->
new SurveysApp.Take.Controller
id: id
survey: survey
App.vent.on "survey:clicked", (survey) ->
App.navigate "surveys/" + survey.id
API.show survey.id, survey
App.vent.on "take:survey:button:clicked", (survey) ->
App.navigate "surveys/" + survey.id + "/take"
API.take survey.id, survey
App.addInitializer ->
new SurveysApp.Router
controller: API
So I can get here when navigating or by triggering the "survey:clicked" event. This then creates my show controller:
#TheoremReach.module "SurveysApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ survey, id } = options
survey or= App.request "survey:entity", id
App.execute "when:fetched", survey, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion survey
#questionRegion survey
#bannerRegion survey
#show #layout
questionRegion: (survey) ->
App.request "show:survey:questions", survey, #layout.questionRegion
panelRegion: (survey) ->
panelView = #getPanelView survey
#listenTo panelView, "new:question:clicked", (args) ->
question = App.request "new:question:entity"
model = args.model
model.get('questions').add(question)
question.set(survey_id: model.get('id'))
App.request "new:question:added"
#show panelView, region: #layout.panelRegion
bannerRegion: (survey) ->
bannerView = #getBannerView survey
#listenTo bannerView, "take:survey:button:clicked", (args) ->
App.vent.trigger "take:survey:button:clicked", args.model
#show bannerView, region: #layout.bannerRegion
getLayoutView: ->
new Show.Layout
getBannerView: (survey) ->
new Show.Banner
model: survey
getPanelView: (survey) ->
new Show.Panel
model: survey
This makes a new Questions Show Controller (same router case as above that handles "show:survey:questions" request and instigates a new controller so I'll skip that code).
#TheoremReach.module "QuestionsApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ survey } = options
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#questionRegion survey
#show #layout
questionRegion: (survey) ->
questions = survey.get('questions')
questionView = #getQuestionView questions, survey
App.reqres.setHandler "new:question:added", ->
questionView.render()
#show questionView, region: #layout.questionRegion
getLayoutView: ->
new Show.Layout
getQuestionView: (questions, survey) ->
new Show.Questions
collection: questions
model: survey
Standard composite view for the questions:
class Show.Questions extends App.Views.CompositeView
template: "questions/show/_questions"
className: "questions"
itemViewContainer: ".editor"
itemView: Show.Question
Then each question is a composite view:
class Show.Question extends App.Views.CompositeView
template: "questions/show/_question"
id: "1000"
className: "step"
initialize: ->
#collection = #model.get("answers")
#model.set(question_number: #model.collection.indexOf(#model) + 1)
if #model.get('free_text') and #model.get('answers').length < 1
answer = App.request "new:answer:entity"
answer.set(free_text: true, question: #model, title: #model.get('title'))
#collection.reset(answer, {silent: true})
#on "childview:answer:delete:clicked", (child, args) =>
args.collection = #model.get('answers')
#trigger "answer:delete:clicked", args
itemView: Show.Answer
itemViewContainer: ".answer-container"
It gets its collection from the answers group from backbone relational. I would note though that this probably should just be a layout and in the initialize function I should send a request to the answers app to get a list of answers and add those to the answer region. I just haven't gotten around to that yet :).

Related

Set strapi permission to specific collection data

I am planning in developing a large structure for a client and I am looking into using Strapi to manage the content and the APIs.
Before even digging deeper I would like to ask if anyone know if there is an existing plugin to set limitations to the collections data.
For example, I create a collection called restaurant where I am going to have 1 field: name. Then I create 2 restaurants named "The Optimist" & "The Negative"
After, I create 2 more user for my back end: Maria & Julia.
Is there any existing way to set Maria to only be able to edit "The optimist" & Julia to only edit "The Negative"?
Explaination
Well there is a way to limit users from performing specific actions on the entire collection directly out of the box, but limiting to specific entries needs customization in controller.
I would recommend you to go through Users, Roles & Permissions guide from the Strapiv4 documentation for better understanding.
Attaching a snapshot below you give you a brief idea. As you can see, generic actions like create, update, delete etc. can be permitted only to specific roles, which in turn can be assigned to the users of your choice.
# Image showing permissions being assigned to a role
# Image showing role being assigned to a user
Solution
Coming to your question on limiting users to specific entries, you can easily achieve this by writing custom code that checks for the entry id and the role that's trying to update the restaurant. Check the snippet below:
// src/api/resto/controllers/resto.js
"use strict";
/**
* resto controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::resto.resto", ({ strapi }) => ({
async update(ctx) {
const { id } = ctx.params;
const { role } = ctx.state.user;
// if you don't want to hard code the ids, you can do a findOne for the id and do a check on the resto name.
// Assuming id 4 corresponds to entry "The optimist"
// Assuming id 5 corresponds to entry "The Negative"
if ((id === 4 && role.name !== "Role Maria") || (id === 5 && role.name !== "Role Julia")) {
return ctx.badRequest("You are not allowed to update this entry", {
id: id,
role: role.name,
});
}
const entity = await strapi
.service("api::resto.resto")
.update(id, { data: ctx.request.body });
const response = await super.findOne(ctx);
// const sanitizedEntity = await this.sanitizeOutput(response, ctx);
// return this.transformResponse(sanitizedEntity);
return response;
},
}));

Why session.getSaveBatch() is undefined when child record was added - Ext 5.1.1

Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.

Relational Query in Parse

I have 4 classes in Parse. I am using JavaScript.
1) Rangoli - Main item objects
2) Categories - List of all categories
3) Users - List of all users
4) ItemCategories - List of categories that each items are assigned
Below is the structure of all classes.
Now I am trying to get the following details in a single query.
1) User - Name, email, avatar
2) Rangoli - filename, createdAt
3) Categories - name
This is the JavaScript code that I tried.
var Rangoli = Parse.Object.extend("Rangoli");
var ItemCategories = Parse.Object.extend("ItemCategories");
var query = new Parse.Query(ItemCategories);
var innerQuery = new Parse.Query(Rangoli);
innerQuery.matchesQuery("category", query);
query.find({
success : function (data) {
console.log(JSON.stringify(data));
},
error : function (e) {
}
});
With the above JavaScript code, I am getting the record of ItemCategories record and not all the columns of all pointer class like Categories, User, Rangoli.
You can include related objects in the results of a query by using the include method. e.g.
var ItemCategories = Parse.Object.extend("ItemCategories");
var query = new Parse.Query(ItemCategories);
query.include("item");
query.include("category");
You can also use this to get objects nested at multiple levels, e.g., query.include(["category.rangoli"]); but from your data model screenshot I can't see a way to include the Rangoli data with the relationships you have set up.

Backbone, rest, populate collection

I'm trying to do my first webapp with backbone/mvc3 and i would like to have some advices to populate a collection.
Here is a part of my collection
window.TaskList = Backbone.Collection.extend({
model: Task,
url: "../../api/Tasks";
},.......
I can use the crud methods to get/update the models but i've the following problem :
When i open the page, my collection is populated (calling the get method serverside) But i would like to have this kind of behavior :
Page 1 : put/delete/get methods => as usual but the collection has to be populated calling the getTasksByWorkshopId serverside method
Page 2 : put/delete/get methods => as usual but the collection has to be populated calling another serverside method to filter the list
...
(ie : i cant filter the collection client side because of the amount of data)
So, my question is : how to keep a generic collection url (as api/Tasks) and populate the collection with another method (do i have to override smth ?)
(sorry for this newbie question)
Thanks in advance
In a comment to the other answer you said that "When the collection is loaded, the url called is /api/Tasks/Workshop/1 (the good one) but, when i want to update a task, the url called is /api/Tasks/Workshop/1/141 instead of /api/Tasks/141."
In order to "update a task" (a task model, I assume) to a different URL, then your Collection & Model should have different URLs. If you define a collection without specifying the model property, the URL used when saving/fetching/deleting a model will be based off of the collection's URL. The same is also true if the collection's model has no defined url property. See below.
Also, JSFiddle example here.
var WorkshopModel = Backbone.Model.extend({
urlRoot: "api/tasks/"
});
var WorkshopCollection = Backbone.Collection.extend({
model: WorkshopModel,
urlRoot: "api/tasks/workshop",
url: function() { return this.urlRoot + '/' + this.id; },
initialize: function(models, options) {
this.id = options.id;
}
});
var c = new WorkshopCollection(null, { id: 1 });
c.fetch(); // GET => api/tasks/workshop/1
var m = c.add({ id: 300, color: 'red' });
m.save(); // PUT => api/tasks/300
m.destroy(); // DELETE => api/tasks/300
m.fetch(); // GET => api/tasks/300
If you remove the urlRoot property from the WorkshopModel, then the URL that the models use will be the collection.url() + '/' + model.id ( api/tasks/workshop/1/300 )
You can do like this :
window.TaskList = Backbone.Collection.extend({
model: Task,
urlRoot: "../../api/Tasks",
url: function() {
if (/*page 1*/) { // you can access this.options where you can pass parameters to distinct the 2 services, when calling the fetch function
return this.urlRoot + // getTasksByWorkshopId URL ;
} else {
return this.urlRoot + // the other service URL ;
}
} ...
}

Backbone.js: How to call methods on the collection within an object literal

I have the following backbone.js code. I'm using an object literal for organizing my code, which has left me with a question regarding the best way to proceed. The application (in its simplified form below) has a control panel (which can be shown or hidden) which is used to add new categories to a collection. (Question follows)
(function($){
// ============================= NAMESPACE ========================================
var categoryManager = categoryManager || {};
// ============================= APPLICATION =================================================
categoryManager.app = categoryManager.app || {
/* Used to Initialise application*/
init: function(){
//this.addView = new this.addCategoryView({el: $("#add-new-category")})
//this.collection = new this.categoryCollection();
new this.addCategoryView({el: $("#add-new-category")})
new this.categoryCollection();
},
categoryModel: Backbone.Model.extend({
name: null
}),
addCategoryView: Backbone.View.extend({
events: {
"click #add-new-category-button.add" : "showPanel",
"click #add-new-category-button.cancel" : "hidePanel",
"click #new-category-save-category" : "addCategory"
},
showPanel: function() {
$('#add-new-category-button').toggleClass('add').toggleClass('cancel');
$('#add-new-category-panel').slideDown('fast');
},
hidePanel: function() {
$('#add-new-category-button').toggleClass('add').toggleClass('cancel');
$('#add-new-category-panel').stop().slideUp('fast');
},
addCategory: function() {
//categoryManager.app.collection.create({
categoryManager.app.categoryCollection.create({ // My Problem is with this line
name: $('#name').val()
});
}
}),
categoryCollection: Backbone.Collection.extend({
model: this.categoryModel,
initialize: function () {
}
})
}
// ============================= END APPLICATION =============================================
/* init Backbone */
categoryManager.app.init();
})(jQuery);
Now obviously the problem with the above, is that calling the addCategory function tries to call a function on an object which is uninitialized. I've worked round the problem (see commented out code) by calling the function instead on a object which is instantiated within the init function. My question is - is this the right thing to do? I detect a code smell. I feel that the contents of the object literal shouldn't rely on the object being created in order to be valid. the function addCategory in this instance wouldn't work unless the init function had been called on the parent first. Is there another pattern here that I should be using?
How else would I pass the contents of the 'create new category form' to the collection in order to be added (I'm using create because I want to automatically validate/create/persist the model and It seems like the easiest thing to do). I'm a rock bottom novice with backbone (this is my 'hello world')
Thanks
I think the main issue is you are treating categoryCollection as if it's an object. It's not really an object, but a constructor function. So first you need to create an instance, as you have discovered.
Then the addCategoryView needs some way of referencing the instance. It looks like you don't have a model associated with the view. I would suggest creating a model and storing the categoryCollection instance as a property of the model. Something like this (warning, untested code):
var model = new BackBone.Model({
categories: new categoryManager.app.CategoryCollection()
});
var view = new categoryManager.app.AddCategoryView({
el: $("#add-new-category"),
model: model
});
Then you can just use this.model.categories from inside addCategoryView.
As an aside, a common Javascript convention is to capitalize the names of constructors. Calling the constructor CategoryCollection might make the code a little bit clearer.
You need to initialize collection before create a new instance of a model
addCategory: function() {
var collection = categoryManager.app.categoryCollection;
!collection.create && (collection = new collection);
collection.create({
name: $('#name').val()
});
}

Resources