Laravel save nested model hasManyThrough - laravel-5.3

I have 3 models
Event has many Shifts
Shift belongs to Event
Shift has many ShiftWorkers
ShiftWorker belongs to Shift
when i get to save all models at once through html form with this store method in controller:
public function store(EventsForm $request)
{
//dd($request->all());
// save event
$event = Auth::user()->events()->create($request->all());
$total_shifts = count($request->shift_start);
$shifts = [];
for ($s = 0; $s <= $total_shifts-1; $s++ ) {
array_push($shifts, new Shift(['shift_start' => $request->shift_start[$s], 'shift_end' => $request->shift_end[$s]]));
}
// save shifts
$event->shifts()->saveMany($shifts);
$workers = [];
for ($w = 0; $w <= $total_shifts-1; $w++ ) {
array_push($workers, new ShiftWorker(['quantity' => $request->quantity[$w], 'hour_wage' => $request->hour_wage[$w]]));
}
// save workers
$event->shifts()->shift_workers()->saveMany($workers);
i get this error
Call to undefined method Illuminate\Database\Query\Builder::shift_workers()
How can i save my third Model (ShiftWorker) ?

Each shift has a hasMany, so you can't call shift_workers() on a collection.
You've to loop, like below:
foreach ($event->shifts as $shift) {
$shift->shift_workers()->saveMany($workers);
}

Related

Iterate through all pages and output in a single html file

I need to output a node (ID 2334) including sub-nodes into a single document to print them later.
How can I load all sub-nodes and output them beneath the main node?
Update
I'm using Drupal 8.
I want to combine all subsites for a given node into a single page.
By sub-nodes / sub-sites I'm referring to the navigation tree.
The key goal is to have a printable version of a whole website section.
Like a category where it combines all related articles into one page.
Update 2
I created a custom page controller and managed to get an array of "sub-nodes":
$subnodes = [];
$tree = \Drupal::menuTree()->load('internal', new \Drupal\Core\Menu\MenuTreeParameters());
foreach ($tree as $item) {
if ($item->link->getRouteParameters()['node'] == $node) {
foreach($item->subtree as $subitem) {
$subnodes[] = $subitem->link->getRouteParameters()['node'];
}
}
}
var_dump($subnodes); // array of ids
I'm now wondering how render the given nodes?
I managed to solve it with a custom controller:
public function exportAll($node) {
$node = (int)$node;
if ($node < 1) {
throw new NotFoundHttpException($node . 'not available');
}
$subnodes = [$node];
$tree = \Drupal::menuTree()->load('internal', new \Drupal\Core\Menu\MenuTreeParameters());
foreach ($tree as $item) {
if ($item->link->getRouteParameters()['node'] == $node) {
foreach($item->subtree as $subitem) {
$subnodes[] = $subitem->link->getRouteParameters()['node'];
}
}
}
$nodes = \Drupal\node\Entity\Node::loadMultiple($subnodes);
return $build = \Drupal::entityTypeManager()->getViewBuilder('node')->viewMultiple($nodes);
}

AngularFire, extended $firebaseArray save errors with 'Invalid key $id (cannot contain .$[]#)'

When extending $firebaseArray I get the error 'invalid key $id (cannot contain .$[]#)'
The issue is with the $id property on the book object. When calling $save on the array and passing the modified book in, the factory doesn't recognize $id as the id field like it normally would prior to extending.
Html
<ul>
<li ng-repeat="book in bookList"> {{book.$id}}: {{book.date|date:"medium"}}
<button ng-click="setDate(book)">set date</button>
</li>
</ul>
<pre>{{book|json}}</pre>
Javascript
// reference to our Firebase instance
var fb = new Firebase('YourUrl');
function Book(snap) {
// records in synchronized arrays must have a $id property
this.$id = snap.key();
this.update(snap);
}
Book.prototype.update = function (snap) {
angular.extend(this, snap.val());
// convert json dates to Date objects
this.date = new Date(this.date||0);
this.$priority = snap.getPriority();
};
Book.prototype.toJSON = function() {
return angular.extend({}, this, {
// revert Date objects to json data
date: this.date.getTime()
});
};
var app = angular.module('app', ['firebase']);
app.controller('ctrl', function ($scope, bookFactory) {
// create a synchronized array with a customized version
// of $FirebaseArray
$scope.bookList = new bookFactory(fb);
// loads a single record for display
$scope.book = "Click on a book id";
$scope.loadBook = function (book) {
$scope.book = book;
};
// changes the date on a book record, note that we're working
// with Date objects here
$scope.setDate = function(book) {
book.date = new Date();
$scope.bookList.$save(book);
};
});
app.factory('bookFactory', function ($firebaseArray, $firebaseUtils) {
return $firebaseArray.$extend({
// override $$added to return a Book object
$$added: function (snap, prevChild) {
return new Book(snap);
},
// override $$updated to update the book object
$$updated: function (snap) {
var rec = this.$getRecord(snap.name());
if(angular.isObject(rec) ) {
book.update(snap);
}
}
});
});
Made a fiddle showing the error, just press 'Set Date' on any of the books.
https://jsfiddle.net/jensenben/h98mjsoz/
This can be remedied if make the book object use id instead of $id and add a $$getKey method to the $extend call to tell it what field has the id in it, but I'd prefer to stick with the traditional $id field that I use when not extending a $firebaseArray.
function Book(snap) {
// records in synchronized arrays must have a $id property
this.id = snap.key();
this.update(snap);
}
return $firebaseArray.$extend({
$$getKey: function(rec) {
return rec.id;
},
...
There's a bit much going on, so I'm not sure if this is your problem. But to get rid of the error, convert the $firebaseObject() to a regular JSON object when saving:
$scope.setDate = function(book) {
book.date = new Date();
$scope.bookList.$save(book.toJSON());
};
Note that the CSS and most of the JavaScript code are irrelevant to the problem.

Silverstripe 3.2: How to make a custom action button in the CMS to create a new Dataobject and populate it from another one

I'm searching for a way to create a custom action button which allows me to make a new DataObject with pre-filled content from another DataObject. As a simple example: When I have an email and click the "answer"-button in my email-client, I get a new window with pre-filled content from the email before. I need exactly this functionality for my button. This button should appear next to each DataObject in the GridField.
So I know how to make a button and add it to my GridField (--> https://docs.silverstripe.org/en/3.2/developer_guides/forms/how_tos/create_a_gridfield_actionprovider/) and I know how to go to a new DataObject:
Controller::curr()->redirect($gridField->Link('item/new'));
I also found out that there is a duplicate function for DataObjects:
public function duplicate($doWrite = true) {
$className = $this->class;
$clone = new $className( $this->toMap(), false, $this->model );
$clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
if($doWrite) {
$clone->write();
$this->duplicateManyManyRelations($this, $clone);
}
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
return $clone;
}
Perhaps it's easier than I think but at the moment I just don't get how to rewrite this to get what I need. Can somebody give me a hint?
That's for sure not the cleanest solution but I think it should do the trick.
At first let's create the custom gridfield action. Here we will save all accessible records in a session and add a query string to the url so that we'll know which object we want to "clone"
public function getColumnContent($gridField, $record, $columnName) {
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'clone'.$record->ID,
'Clone',
'clone',
array('RecordID' => $record->ID)
);
$values = Session::get('ClonedData');
$data = $record->data()->toMap();
if($arr = $values) {
$arr[$record->ID] = $data;
} else {
$arr = array(
$record->ID => $data
);
}
Session::set('ClonedData', $arr);
return $field->Field();
}
public function getActions($gridField) {
return array('clone');
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'clone') {
$id = $arguments['RecordID'];
Controller::curr()->redirect($gridField->Link("item/new/?cloneID=$id"));
}
}
after adding this new component to our gridfield,
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
we'll need to bring the data into the new form. To do so, add this code directly above "return $fields" on your getCMSFields function so it will be executed every time we'll open this kind of object.
$values = Session::get('ClonedData');
if($values) {
Session::clear('ClonedData');
$json = json_encode($values);
$fields->push(LiteralField::create('ClonedData', "<div id='cloned-data' style='display:none;'>$json</div>"));
}
At the end we need to bring the content back into the fields. We'll do that with a little bit of javascript so at first you need to create a new script.js file and include it in the ss backend (or just use an existing one).
(function($) {
$('#cloned-data').entwine({
onmatch: function() {
var data = JSON.parse($(this).text()),
id = getParameterByName('cloneID');
if(id && data) {
var obj = data[id];
if(obj) {
$.each(obj, function(i, val) {
$('[name=' + i + ']').val(val);
});
}
}
}
});
// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript#answer-901144
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
})(jQuery);
And that's it ... quite tricky. Hope it will solve your problem.

Laravel 4 Model Events don't work with PHPUnit

I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}

Node.js: waiting for callbacks in a loop before moving on

I have a loop that has an asynchronous call inside it, with a callback. To be able to move on, I need the callback to fire for the entire loop all the way through, to then display the results of the loop.
Every way I've tried to control this doesn't work (have tried Step, Tame.js, async.js, and others) - any suggestions on how to move forward?
array = ['test', 'of', 'file'];
array2 = ['another', 'array'];
for(i in array) {
item = array[i];
document_ids = new Array();
for (i2 in array2) {
item2 = array2[i2];
// look it up
mongodb.find({item_name: item2}).toArray(function(err, documents {
// because of async,
// the code moves on and calls this back later
console.log('got id');
document_ids.push(document_id);
}))
}
// use document_ids
console.log(document_ids); // shows []
console.log('done');
}
// shows:
// []
// done
// got id
// got id
You're logging document_ids before your callbacks fire. You have to keep track of how many callbacks you've run to know when you're done.
An easy method is to use a counter, and to check the count on each callback.
Taking your example
var array = ['test', 'of', 'file'];
var array2 = ['another', 'array'];
var document_ids = [];
var waiting = 0;
for(i in array) {
item = array[i];
for (i2 in array2) {
item2 = array2[i2];
waiting ++;
mongodb.find({item_name: item2}).toArray(
function(err, document_id) {
waiting --;
document_ids.push(document_id);
complete();
})
);
}
}
function complete() {
if (!waiting) {
console.log(document_ids);
console.log('done');
}
}

Resources