restrict editing and creating of sys_categories in TYPO3 to certain category mounts - typo3

i need to restrict the creating and editing of sys_category entries to certain category mounts for user oder usergroups.
More precise: a given backend user should be able to create a new category but not anywhere in the category treee but only under a certain category mount. And he should not be able to delete or edit categories which are not under his category mount.
Please note: i don't mean displaying only branches of category trees in other records like news. I do mean creating and editing sys_category records.
Is there a possibility?
Thanks!

Register 2 new Hooks:
// Check record while creating categories
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
= \JWeiland\SitePackage\Hooks\CheckCategoryRecords::class;
// Check record while moving categories
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][]
= \JWeiland\SitePackage\Hooks\CheckCategoryRecords::class;
Add the file for Hook:
class CheckCategoryRecords
{
public function processDatamap_beforeStart(DataHandler $dataHandler): void
{
$context = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Context\Context::class);
$groups = $context->getPropertyFromAspect('backend.user', 'groupIds');
if (
array_key_exists('sys_category', $dataHandler->datamap)
&& is_array($dataHandler->datamap['sys_category'])
&& !empty($dataHandler->datamap['sys_category'])
&& !$this->getBackendUserAuthentication()->isAdmin()
) {
// Hook is only valid, if a record is saved and editor has no admin rights
foreach ($dataHandler->datamap['sys_category'] as $uid => $categoryRecord) {
if (!isset($categoryRecord['pid'])) {
continue;
}
//...add further conditions, if needed...
if (!in_array($groupUidWithCategoryRights, $groups, true)) {
// Add further condition, if category is allowed for given parent column
// editor is not allowed to add or move categories
unset($dataHandler->datamap['sys_category'][$uid]);
$dataHandler->log(
'sys_category',
$uid,
1,
0,
1,
'You are not allowed to create or move categories'
);
}
}
}
}
}

Related

Salesforce Apex Trigger data copy from one field to other

This is my Apex Trigger in which I am taking data from Custom Field "Picklist Multiple" named(Opportunity_Picklist__c) in Opportunity and automatically paste it to a custom field "text" in Account. Now every selected option from Custom Field "Picklist Multiple" is coming to the custom field "text". I want to copy only Custom Field "Picklist Multiple" values with API Name "Premium" and "Basic". Other selected values can be avoided. Please look into the code below and suggest me relatable changes.
Custom Field "Picklist Multiple" have these data:-
Premium
Advanced
Basic
Free
If someone selected all four values in Custom Field "Picklist Multiple" they can remain as it is but I don't want all of them to be copied in custom field "text". I want the values with API Names "Premium" and "Basic" only in the custom field "text".
trigger OppTrigger on Opportunity (After insert,After update,After delete) {
if((Trigger.isInsert|| Trigger.isupdate|| Trigger.isDelete) && Trigger.isAfter)
{
Set<Id> acctSet = new Set<Id>();
List<Account> accList = new List<Account>();
for(Opportunity opp : Trigger.new)
{
acctSet.add(opp.AccountId);
}
Map<Id, Account> mapAccounts = new Map<Id, Account>([SELECT Id, Plan__c FROM Account where Id IN :acctSet]);
for(Opportunity opp : Trigger.new)
{
Account acc = mapAccounts.get(opp.AccountId);
If(Trigger.IsDelete){
acc.Plan__c = null;
}
if(Trigger.isInsert|| Trigger.isupdate){
acc.Plan__c = opp.Opportunity_Picklist__c;
}
accList.add(acc);
}
Update accList;
}
}
You'll be better off cutting it up a bit. Use a helper class as trigger handler. Will let you create some reusable functions rather than just a giant block of code executing top-down. There's this for general reading, I like that one but there are lots of examples, what you'll be comfortable with.
For example imagine a method called static void rollupToAccounts(Set<Id> ids). It'd accept account ids and you could call it from different scenarios (because you'll have different entry points. Insert is easy, base it on trigger.new, cool. Delete is funnier because there's no trigger.new, you need to get Account Ids out of trigger.old And after update if I move opportunity from Account A to B - your code should recalculate both! Good luck writing straight in the trigger's body.
As for actual "meat" of the method... many ways to do it, something like this?
static void rollupToAccounts(Set<Id> ids){
final Set<String> valuesToRollup = new Set<String>{'Premium', 'Basic'};
List<Account> toUpdate = new List<Account>();
List<Account> accounts = [SELECT Id, Plan__c,
(SELECT Opportunity_Picklist__c
FROM Opportunities
WHERE Opportunity_Picklist__c INCLUDES :valuesToRollup)
FROM Account WHERE Id IN :ids];
for(Account a : accounts){
Set<String> currentValues new Set<String>();
if(String.isNotBlank(a.Plan__c)){
currentValues.addAll(a.Plan__c.split(';'));
}
Set<String> newValues = new Set<String>();
if(!a.Opportunities.isEmpty()){
for(Opportunity o : a.Opportunities){
newValues.addAll(o.Opportunity_Picklist__c.split(';')); // this will include Advanced & Free too, it's ok, we'll fix it
}
newValues.retainAll(valuesToRollup); // filter out all the ones we don't want
}
System.debug('old: ' + currentValues + '; new: ' + newValues);
if(currentValues != newValues){
// we'll need to update this account with current data
List<String> temp = new List<String>(currentValues);
temp.sort();
a.Plan__c = String.join(temp, ';');
toUpdate.add(a);
}
}
System.debug(toUpdate);
update toUpdate;
}
It's not perfect. If you have 100 opportunities for same account with "Basic" it'll take them all and possibly waste query rows. You could do it as 2 (sub)queries, WHERE Opportunity_Picklist__c INCLUDES ('Basic') LIMIT 1 and then another query for Premium if you think it'd be a problem..
Why not just check the value of the opportunity picklist before saving it into the account value? You could write this code a little cleaner and more efficient. However, a simple solution is just to check if the values you want exist.
if(Trigger.isInsert|| Trigger.isupdate){
String strAccountPlan;
if(opp.Opportunity__PickList__c.Contains('Premium'){
strAccountPlan = 'Premium';
}
if(opp.Opportunity__PickList__c.Containt('Basic'){
if(String.isBlank(strAccountPlan){
strAccountPlan = 'Basic');
}
else{
strAccountPlan += ';Basic';
}
acc.Plan__c = strAccountPlan;
}

Catalog Price Rule Magento 2

I have created a custom module in Magento 2. In which i want to show some banners, Countdown timer and extra details on product page, also label or text on category page. for this i extended the catalog sales rule module. Added some extra field according to my requirements. Now i want to show get all those products ids on which sale rule has applied with all other details like Top Banner, Countdown timer according to sale etc.
my Block Code is
<?php
namespace Custom\Sales\Block;
class Flash extends \Magento\Framework\View\Element\Template
{
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory $ruleFactory,
array $data = []
) {
$this->_ruleFactory = $ruleFactory;
parent::__construct($context, $data);
}
public function getCatalogeRuleId()
{
$catalogRuleCollection = $this->_ruleFactory->create()
->addFieldToFilter('is_active',1);
// return $catalogRuleCollection;
$resultProductIds = [];
foreach ($catalogRuleCollection as $catalogRule) {
$productIdsAccToRule = $catalogRule->getMatchingProductIds();
// echo json_encode($productIdsAccToRule); exit;
$websiteId = $this->_storeManager->getStore()->getWebsiteId();
foreach ($productIdsAccToRule as $productId => $ruleProductArray) {
if (!empty($ruleProductArray[$websiteId])) {
$resultProductIds[$productId] = $catalogRule->getData();
}
}
return $resultProductIds;
}
}
}
Now when i print my array i get only one sale rule data , however i have created 3 different sales

How to delete file (image) from filesystem when it is detached from DataObject in SilverStripe admin?

For Example I have this code
class MyDataObject extends DataObject {
private static $has_one = [
"MyImage" => Image::class,
];
public function getCMSFields(){
$fields = parent::getCMSFields();
$fields->addFieldsToTab('Root.Main', [
UploadField::create('MyImage');
]);
return $fields;
}
}
When user removes file from MyDataObject in admin
this file still remains in 'files' part of cms, in database and on filesystem, so user need to go to 'files' and remove in manually.
On practice he often forgets to remove file after detaching it from some dataobject and all these files holds a lot of place.
How can SilverStripe automatically remove file from filesystem when users clicks on cross on screenshot?
You need a onAfterDelete() on your Data Object. There you can delete the file.
(don't forget to call Parent::onAfterDelete() in your method)
** UPDATE **
OR if it's only when the user is editing not deleting your Object, then onAfterWrite() is your friend. There you can compare old and new ID of the image, and if is different, delete the Image with the old ID.
To solve the problem I've created this extension
<?php
use SilverStripe\ORM\DataExtension;
use SilverStripe\Assets\File;
class DataObjectRemoveImagesExt extends DataExtension {
private function killFile($fileId) {
$fileToRemove = File::get()->byId($fileId);
if ($fileToRemove) $fileToRemove->delete();
}
public function onAfterWrite() {
$changedFieldsArr = $this->owner->getChangedFields();
if (!$this->owner->config()->get('kill_on_detach')) return;
$detachList = $this->owner->config()->get('kill_on_detach');
foreach ($detachList as $fileFieldName) {
if (!isset($changedFieldsArr["{$fileFieldName}ID"])) continue;
$changedFieldValues = $changedFieldsArr["{$fileFieldName}ID"];
if (
(
$changedFieldValues['before'] != $changedFieldValues['after']
&&
$changedFieldValues['before'] != 0
&&
$changedFieldValues['after'] != 0
)
||
(
$changedFieldValues['after'] == 0
&&
$changedFieldValues['before'] != 0
)
){
$this->killFile($changedFieldValues['before']);
}
}
}
}
Usage:
1. Attach to DataObject
SilverStripe\ORM\DataObject:
extensions:
- DataObjectRemoveImagesExt
In your custom DataObject child use this property to set files/images to be deleted
private static $kill_on_detach = [
'Image',
'Thumb',
];
Solution is not perfect:
it kills file even if it attached to some another DataObject instance
it not kills file if user detached file from admin-panel and forgot
to save DataObject
If somebody wants to propose better decision - welcome.

Symfony: question about the form filters

In the frontend I have a page with a list and a form filter next to it
that shows all the users of a social network.
I would like to hide the user of the session in that list. How can I
do it?
My first thought is creating a function, addXXXXColumnQuery(), for each
field of the form, and in each one add a line like this:
->andWhere("u.id <> ?", $id)
$id being the ID of the user of the current session. But in that way I
find I'm repeating myself.
What should I do?
First, you need to get the user into the filter. You have two options:
Pass the user_id in as an option when you instantiate the form, inside the action:
public function executeList(sfWebRequest $request)
{
$user_id = $this->getUser()->getUserId();
$filter = new ModelFormFilter(array(), array('user_id' => $user_id));
...
Get the user id from the context inside of the form:
sfContext::getInstance()->getUser()->getUserId();
I prefer the former method because it's cleaner and less WTFy.
Once you have the user id, override doBuildQuery to exclude the current user id inside of your FormFilter:
protected function doBuildQuery(array $values)
{
$query = parent::doBuildQuery($values);
$user_id = $this->getOption('user_id'); //or off the context here
if ($user_id)
{
$query->addWhere('r.user_id != ?', $user_id);
}
return $query;
}

Drupal 6: Modifying uid of a submitted node

I have a situation where I want a set of users (employees) to be able to create a node, but to replace the uid (user ID) with that of the users profile currently displayed.
In other words, I have a block that that calls a form for a content type. If an employee (uid = 20) goes to a clients page (uid =105), and fills out the form, I want the uid associated with the form to be the client's(105), not the employee's.
I'm using arg(1) to grab the Client's uid - here is what I have..
<?php
function addSR_form_service_request_node_form_alter(&$form, $form_state) {
if (arg(0) == 'user' && is_numeric(arg(1))) {
$form['#submit'][] = 'addSR_submit_function';
}
}
function addSR_submit_function($form, $form_state) {
$account = user_load(arg(1));
$form_state['values']['uid'] = $account->uid;
$form_state['values']['name'] = $account->name;
}
?>
The form is loading in the block, but when submitted, is still showing the employee uid. I don't want to use hook_form_alter because I don't want to modify the actual form, because clients can fill out the form directly, in this case, I don't want to modify the form at all.
I'm also ashamed that I'm putting this in a block, but I couldn't think of a way to put this in a module, so any suggestions on that would also be appreciated...
To create your form in a block, you could use the formblock module. Especially if you are not used to use the Drupal API. Then all that's left if to add your own submit handler to the form. This is a piece of code that is run, when the form is submitted. You only want to do this on clients pages so you would do that using the hook_form_alter function.
/**
* Hooks are placed in your module and are named modulename_hookname().
* So if a made a module that I called pony (the folder would then be called
* pony and it would need a pony.info and pony.module file I would create this function
*/
function pony_form_service_request_node_form_alter(&$form, $form_state) {
// Only affect the form, if it is submitted on the client/id url
if (arg(0) == 'client' && is_numeric(arg(1))) {
$form['#submit'][] = 'pony_my_own_submit_function';
}
}
function pony_my_own_submit_function($form, &$form_state) {
$account = user_load(arg(1));
$form_state['values']['uid'] = $account->uid;
$form_state['values']['name'] = $account->name;
}
The idea behind this code, is to only alter the form when the condition is met - that it is submitted on a client page. I guessed that the arg(0) would be client so if it's something else you would need to change that of cause. We only need to add a submit function, since what we want is to change the values if the form has passed validation.
Then if that is the case our 2nd function is run, which does that actual alteration of the values.
PHP blocks are bad. You can put them in a module.
function hook_block($op, $delta = 0) {
// Fill in $op = 'list';
if ($op == 'view' && $delta = 'whatever') {
$account = user_load(arg(1));
$node = array('uid' => $account->uid, 'name' => $account->name, 'type' => 'service_request', 'language' => '', '_service_request_client' => $account->uid);
$output = drupal_get_form('service_request_node_form', $node);
// Return properly formatted array.
}
}
Additionally, you want a form_alter just to enforce the values. It's ugly but it works.
function hook_form_service_request_node_form_alter(&$form, $form_state) {
if (isset($form_state['node']['_service_request_client'])) {
$form['buttons']['submit']['#submit'] = array('yourmodule_node_form_submit', 'node_form_submit');
}
}
function yourmodule_node_form_submit($form, &$form_state) {
$account = user_load($form_state['node']['_service_request_cilent'])l
$form_state['values']['uid'] = $account->uid;
$form_state['values']['name'] = $account->name;
}