I'm trying to show updated results for a CCK Computed Field.
The computation is based on fields in another node, so are not being automatically updated.
So: I'm calling node_save($node) in hook_view, which does make the adjustment but the results don't show until I refresh the page.
Is there a way to refresh the page automatically, or should I be approaching this from a different angle?
Edit: In response to Henrik's questions, here's more detail:
The hook_view and its node_save are below, the rest of the code is in a Computed Field in the 'project' content type, summing up values from another node. Without the node_save, I have to edit and save the 'project' node to get the result. With it, I just need to refresh the page.
Adding drupal_goto(drupal_get_destination()) in the hook_view gives a 'page not found', rather than the vicious loop I was expecting. Is there another place I could put it?
function mymodule_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
switch ($op) {
case 'view':
if($node->type == 'project') {
project_view($node);
break;
}
}
}
function project_view($node) {
node_save($node);
return $node;
}
Edit 1: Given the newly posted code and additional explanations, I have three suggestions that might solve the problem without redirecting:
As project_view() does not take the node argument by reference, you might want to actually grab its (potentially updated) result in mymodule_nodeapi by writing
$node = project_view($node);
instead of just
project_view($node);
If that works, it should also work without the indirection via project_view() by just calling node_save($node) directly in mymodule_nodeapi. (node_save() takes the node argument by reference).
AFAIK, computed fields basically provide two working modes that you can switch via checkbox on the field configuration form:
Computing the field once on node_save(), storing the result in the database, updating only on new save operations.
Not storing the field at all, instead recomputing it every time the node is viewed.
Have you tried the 'always recompute' option already?
Edit 2: My original answer was flawed in two ways at once, as it used a completely wrong function to retrieve the current request URI and did not check for recursion (as lazysoundsystem pointed out very courteously ;)
So the following has been updated to an actually tested version of doing the redirection:
Is there a way to refresh the page
automatically
You could try:
if (!$_REQUEST['stop_redirect']) {
drupal_goto(request_uri(), array('stop_redirect' => true));
}
This will cause Drupal to send a redirect header to the client, causing a new request of the current page, making sure not to redirect again immediately.
If the value is only ever going to be computed, you could just add something to your node at load time.
function mymodule_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
switch ($op) {
case 'load':
if($node->type == 'project') {
$node->content['myfield'] = array('#value' => mymodule_calculate_value(), '#weight' => 4, '#theme' => 'my_theme');
}
break;
}
}
}
Related
A user create a Post (Entity) and define a UrlKey field (input type url-path)
I want prevent a duplicate value (if another entity of this type already had a urlkey with same content)
Is there a way to accomplish that ?
* Edited *
Looking to the 2sxc code I didnt found a simple way to do that.
One thing that I have in mind is to create a ApiController/Endpoint that I can call and make the validation that I want, but for this I need to change the view from the Edit Content (for the user, not the admin one).
I found to the save in /dist/ng-edit/main.js that is minified, there I could change and call my controller/endpoint, but to change to show some feedback messages to user and after call the original endpoint is difficult with minified file.
Is a possibility to have the code that generate the main.js ? (Maybe is already there, and I couldnĀ“t found)
I am not aware of a built in (easy) way to accomplish this. But doing it in the View with a warning is probably a good fallback option. Assuming you have a View (ListContent) that shows a group of Posts, you could add something like this, though it is catching it AFTER the fact...
I just did something like this a few weeks ago because a client kept repeating Titles in the Blueimp Gallery app. So you can just drop this in to _AlbumList Bootstrap.cshtml at line 4, create a few duplicates (Titles which will generate duplicate Paths) and you should see an option to fix them... So, after the fact, if there are duplicates and the current user is Super/Admin/ContentManager, then let them know the situation and make it easy to fix:
#using DotNetNuke.Entities.Portals #* used by IsAdminOrHost() *#
#using DotNetNuke.Entities.Users #* used by IsAdminOrHost(), IsContentManager() *#
#{
if(IsContentManager(Dnn.User)){
var query = AsDynamic(Data["Default"])
.GroupBy(x => x.Path)
.Where(g => g.Count() > 1)
.Select(y => y.Key)
.ToList();
if(query != null) {
<pre>
You have duplicates (#query.Count()), please fix them:
#foreach(var dupe in AsDynamic(Data["Default"])
.Where(p => query.Any(x => x == p.Path)) ) {
<span #Edit.TagToolbar(dupe)>-- #dupe.EntityId, #dupe.Path</span>
}
</pre>
}
}
}
#functions {
// is the current user an Admin or Host?
public bool IsAdminOrHost(UserInfo userInfo) {
return userInfo.IsSuperUser || userInfo.IsInRole(PortalSettings.Current.AdministratorRoleName);
}
// is the current user a Content or Site Manager (or Admin+)?
public bool IsContentManager(UserInfo userInfo) {
return userInfo.IsInRole("Content Managers")
|| userInfo.IsInRole("Site Managers")
|| IsAdminOrHost(userInfo);
}
}
Results should look something like this:
I have a big entity and a big form. When updating my entity, I only render parts of my form, through ajax calls. On client side, I'm using Jquery and html5 FormData, so I can also send files within my form. To make sure the fields that are not rendered won't be set to null in the process, I'm using PATCH method.
So when a field is not present in the request, it's left as is by Symfony.
But when the field I update is a boolean (rendered a a checkbox) that was set to true and I want to set it to false, it's not passed in the request, so my update is ignored.
Is there an easy way to force unchecked checkboxes to appear in the request?
EDIT
I found a way to force unchecked checkboxes to appear in the request, thanks to Felix Kling's comment on this question :
$("input:checkbox:not(:checked)").each(function() {
formData.set($(this).attr('name'), formData.has($(this).attr('id')) ? '1' : '0');
});
Unfortunately, this didn't solve my problem, because of Symfony's behaviour:
- When using PUT, if the boolean field appears in the request, it's set to true, regardless of its value (even if it's "0" or "false").
- When using PATCH method, the fields not appearing in the request are ignored.
Could that be solved with DataTransformer? (I've never used it)
You are absolutely right, Symfony will ignore it if method is PATCH because of this line in Request Handler:
$form->submit($data, 'PATCH' !== $method);
Now, I would generally suggest that you use a PUT request if that is an option, but if it isn't then second argument to FormInterface::submit($submittedData, $clearMissing = true) is what you're after.
The "proper" way would probably be to make your own implementation of Symfony\Component\Form\RequestHandlerInterface which would force $clearMissing to be true.
Other, way is a lot easier but might not work for all use-cases: use $form->submit() directly.
If you have the following code:
$form->handleRequest($request);
You can do:
$form->submit($request->get($form->getName()), true);
You can also omit second parameter since true is the default value
Here goes a working solution, that could be improved.
To force unchecked checkboxes to appear in the request, thanks to Felix Kling's comment on this question, I've added this js before my ajax request :
$("input:checkbox:not(:checked)").each(function() {
formData.set($(this).attr('name'), formData.has($(this).attr('id')) ? '1' : '0');
});
Then, on the Symfony side, I had to override the BooleanToStringTransformer behaviour, that returns true for whatever string and false only for null value. Making a change in the last line, we now return false if the value doesn't match the value defined for true ("1" by default). So if the value returned by the form is "0", we get false, as expected.
public function reverseTransform($value)
{
if (null === $value) {
return false;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return ($this->trueValue === $value); // initially: "return true;"
}
Following the docs, I made my own DataTransformer, as well as a custom AjaxCheckboxType
Unfortunately, it seems that Symfony uses both DataTransformers (mine and the original one), one after the other, so it didn't work. In the docs they extend TextType not CheckboxType, that must explain the problems I encountered.
I ended up copying and pasting the whole CheckboxType class in my own AjaxCheckboxType, only changing the DataTransformer's call in order to use mine.
A much nicer solution would be to totally override the DataTransformer, but I don't know how.
Symfony handles this out of the box, just prepare your PATCH-payload properly :)
The Symfony CheckboxType, at least in the current version 3.3 (seems like since 2.3, see update below), accepts an input value of null, interpreted as "not checked" (as you can see in lines 3-5 of the snippet in Roubi's really helpful answer).
So in your client-side AJAX-PATCH-controller you set the value of of your (dirty) unchecked checkbox-field in your application/merge-patch+json payload to null and everything is fine. No form extensions overwriting CheckboxType's behavior needed at all.
Problem is: I think, you cannot set values of HTTP-POST-payload to null, so this only works with JSON (or other compatible) payload within the request body.
A simple demo
To demonstrate this, you can use this simplified test controller:
/**
* #Route("/test/patch.json", name="test_patch")
* #Method({"PATCH"})
*/
public function patchAction(\Symfony\Component\HttpFoundation\Request $request)
{
$form = $this->createFormBuilder(['checkbox' => true, 'dummyfield' => 'presetvalue'], ['csrf_protection' => false])
->setAction($this->generateUrl($request->get('_route')))
->setMethod('PATCH')
->add('checkbox', \Symfony\Component\Form\Extension\Core\Type\CheckboxType::class)
->add('dummyfield', \Symfony\Component\Form\Extension\Core\Type\TextType::class)
->getForm()
;
$form->submit(json_decode($request->getContent(), true), false);
return new \Symfony\Component\HttpFoundation\JsonResponse($form->getData());
}
For PATCH-requests with Content-Type: application/merge-patch+json or in this case also any valid JSON-payload, the following will happen:
Submitting the checkbox with null value
{"checkbox": null}
will overwrite the checkbox to false:
{"checkbox": false, "dummyfield": "presetvalue"}
and submitting the checkbox with its original value
{"checkbox": "1"}
will set the checkbox to true (was also true before)
{"checkbox": true, "dummyfield": "presetvalue"}
and no submitted value for the checkbox
{"dummyfield": "requestvalue"}
will leave the checkbox in its initial true-state and only overwrites the dummyfield:
{"checkbox": true, "dummyfield": "requestvalue"}
This is how a PATCH request should work, no extra hidden inputs needed. Just prepare your JSON-payload on the client-side properly and you are fine.
OK, but what about the expanded ChoiceType/EntityType?
For expanded ChoiceType (or child types of it like EntityType), which renders checkboxes or radiobuttons and expects a simple list of the checked checkboxes/radiobuttons values within the submitted payload, this simple solution doesn't work. I implemented a form extension, adding an event listener for PRE_SUBMIT on those fields, setting the non submitted checkboxes/radiobuttons to null. This event listener must be called after the closure-listener of CheckboxType, transferring the simple list ["1", "3"] to a hash with checkbox-values as keys and values. A priority of -1 workes for me. So ["1" => "1", "3" => "3"] coming out of the closure gets ["1" => "1", "2" => null, "3" => "3"] after my listener. The listener of my PatchableChoiceTypeExtension looks basically like this:
$builder->addEventListener(
\Symfony\Component\Form\FormEvents::PRE_SUBMIT,
function (\Symfony\Component\Form\FormEvent $event) {
if ('PATCH' === $event->getForm()->getRoot()->getConfig()->getMethod()
&& $event->getForm()->getConfig()->getOption('expanded', false)
) {
$data = $event->getData();
foreach ($event->getForm()->all() as $type) {
if (!array_key_exists($type->getName(), $data)) {
$data[$type->getName()] = null;
}
}
ksort($data);
$event->setData($data);
}
}, -1
);
Update: have a look at this comment within the submit-method in /Symfony/Component/Form/Form.php (it is there since Symfony 2.3):
// Treat false as NULL to support binding false to checkboxes.
// Don't convert NULL to a string here in order to determine later
// whether an empty value has been submitted or whether no value has
// been submitted at all. This is important for processing checkboxes
// and radio buttons with empty values.
Update 2017-09-12: Radiogroups must be handled the same way as Checkboxgroups, so my listener handles both. Selects and multi selects work correctly out of the box.
i am trying to save form state in database and want to view in a listing page with its error validation.
i.e, i want to validate a previously saved form state from my database.
this is a node type form .
i had already tried node_validate its not working because i fetch the data before submitting the node . so there is no nid and for that it is not working
and also tried drupal_validate_form but it is showing
[form_token] => The form has become outdated. Copy any unsaved work in the form below and then reload this page
EDIT
Any one with any help , "How to save a form inputs in data base and retrive it from database with out form submision.
Thank You In advance
Any help is most Appreciable.
If you look in Drupal Core, you see this in includes/form.inc at the drupal_validate_form function:
if (isset($form['#token'])) {
if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) {
$path = current_path();
$query = drupal_get_query_parameters();
$url = url($path, array('query' => $query));
// Setting this error will cause the form to fail validation.
form_set_error('form_token', t('The form has become outdated. Copy any unsaved work in the form below and then reload this page.', array('#link' => $url)));
// Stop here and don't run any further validation handlers, because they
// could invoke non-safe operations which opens the door for CSRF
// vulnerabilities.
$validated_forms[$form_id] = TRUE;
return;
}
}`
This shows that the "form has become outdated" message is being set here. So, you can make the isset($form[#token']) condition false by unsetting #token to prevent this message from appearing.
All you have to do is load the form state you're going to validate, and call
unset($form[#token']); before you call drupal_validate_form.
I need to add or remove fields to a doc before insert or update in the allow or deny methods. I had presumed that the transform function would provide the needed functionality.
The meteor docs state
"An optional transformation function. Documents will be passed through
this function before being returned from fetch or findOne, and before
being passed to callbacks of observe, allow, and deny."
Whenever I tried to transform and return the doc from the function either from allow or deny the transformed version of the document was not what was inserted into the mongodb. I tried transforming via 2 strategies.
Strategy 1
var ts = new Date();
return _.extend(_.pick(doc, 'name', 'discounts', 'locations', 'url_map', 'client_updated_td', '_id'), { created_td:
ts, updated_td: ts, });
Strategy 2
// Discountsroutings.fields is in /lib/Discountroutings.js
Discountsroutings.fields = ['_id', 'created_td', 'updated_td', 'client_updated_td', 'name', 'discounts', 'locations', 'url_map'];
// this is in /server/discountsroutings.js var ts = new Date();
doc.created_td = ts; doc.updated_td = ts; return _.each(doc,function(value, key, list){
if(Discountsroutings.fields.indexOf(key) == -1 ){
delete doc[key];
}
});
Neither worked. In both cases fields were not removed though fields were added.
Interestingly, I tried the same two strategies from inside an insert allow and an insert deny and only Strategy #2 worked. So, for now I am just using Strategy #2 inside Deny insert/update methods. Works fine and isn't that difficult to wire up.
Am I doing this correctly? I want to add or remove fields from a collection server side the correct way.
Steeve have you tried my collection-hooks package? Sounds like what you need
you seem to know the list of fields you want to remove. So why don't you use $set and $unset to add and remove fields?
Recently needed to do the same thing and found no example here... thought I'd share how I did it:
Using
https://atmospherejs.com/matb33/collection-hooks
http://arasatasaygin.github.io/is.js/
Prospects.before.update(function (userId, doc, fieldNames, modifier, options) {
//check existence of other segment property and make sure to delete it if segment is updated from 'Other...' to something else
if (is.existy(doc.other_segment)) {
var segment = Segments.findOne({_id: modifier.$set.segment});
if (is.not.undefined(segment) && is.not.empty(segment)) {
if (is.not.equal(segment.name, 'Other...')) {
Prospects.update( {_id: doc._id} , {$unset: { other_segment : '' } } );
}
}
}});
Hope this helps! :)
Is it possible to define a new operation for a node access?
As I know, the operations for a node that are used in hook_access() are:
create
delete
update
view
I have a custom content type for which I need another operation, such as "suggest."
short answer is NO as node_access() who is responsible to call hook_access() does a check
on the $op parameter
if (!$node || !in_array($op,
array('view', 'update', 'delete',
'create'), TRUE)) {
return FALSE; }
you can attach some extra info to the node object in your suggest() function - hopefully called before node_access() - then check these extra informations in your hook_access() and return TRUE/FALSE according.
another option consists in hardcode permission checks into the suggest() action itself without messing around with hook_access.