Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 03/26/2021 in all areas

  1. A Fieldtype for dynamic options that are generated at runtime via a hook. Configuration Inputfield type You can choose from a range of inputfield types on the Details tab of a Dynamic Options field. Your field will return a string or an array depending on if the selected input field type is for "single item selection" or "multiple item selection". Maximum number of items You can define a maximum number of items the field is allowed to contain. The core inputfields supported by this module will become disabled once the limit is reached. This option is only applicable if you have selected an inputfield type that is for multiple item selection. Format as Pagefile/Pageimage object(s) If the field will store paths/URLs to Pagefiles/Pageimages then you can enable this option to have the formatted value be a Pagefile/Pageimage object for "single" fields or an array of Pagefile/Pageimage objects for "multiple" fields. There is a related Select Images inputfield module that allows you to visually select image thumbnails. Defining selectable options Selectable options for a Dynamic Options field should be set in a FieldtypeDynamicOptions::getSelectableOptions hook in /site/ready.php. The hook should return an array of options as 'value' => 'label'. An example hook is shown on the Details tab of a Dynamic Options field: $wire->addHookAfter('FieldtypeDynamicOptions::getSelectableOptions', function(HookEvent $event) { // The page being edited $page = $event->arguments(0); // The Dynamic Options field $field = $event->arguments(1); if($field->name === 'your_field_name') { $event->return = [ 'red' => 'Red', 'green' => 'Green', 'blue' => 'Blue', ]; } }); Formatted value If a Dynamic Options field uses a "single" input type then its formatted value is a string, and if it uses a "multiple" input type then its formatted value is an array. The unformatted value of a Dynamic Options field is always an array. Also see the Configuration section above for description of an option to have the formatted value be Pagefile/Pageimage object(s). Examples of possible uses $wire->addHookAfter('FieldtypeDynamicOptions::getSelectableOptions', function(HookEvent $event) { // The page being edited $page = $event->arguments(0); // The Dynamic Options field $field = $event->arguments(1); // Select from the "files" field on the page if($field->name === 'select_files') { $options = []; foreach($page->files as $file) { // Value is basename, label is description if one exists $options[$file->basename] = $file->get('description|basename'); } $event->return = $options; } // Select from files in a folder if($field->name === 'select_folder_files') { $options = []; $path = $event->wire()->config->paths->root . 'my-folder/'; $files = $event->wire()->files->find($path); foreach($files as $file) { // Value is full path, label is basename $options[$file] = str_replace($path, '', $file); } $event->return = $options; } // Select from non-system templates if($field->name === 'select_template') { $options = []; foreach($event->wire()->templates as $template) { if($template->flags & Template::flagSystem) continue; $options[$template->id] = $template->name; } $event->return = $options; } // Select from non-system fields if($field->name === 'select_field') { $options = []; foreach($event->wire()->fields as $field) { if($field->flags & Field::flagSystem) continue; $options[$field->id] = $field->name; } $event->return = $options; } // Select from FormBuilder forms if($field->name === 'select_formbuilder_form') { $form_names = $event->wire()->forms->getFormNames(); // Use form names as both keys and values $event->return = array_combine($form_names, $form_names); } }); https://github.com/Toutouwai/FieldtypeDynamicOptions https://processwire.com/modules/fieldtype-dynamic-options/
    14 points
  2. This week I've been working on something a little different for the core. Specifically, ProcessWire's database class (WireDatabasePDO) has been rewritten to support separate read-only and read-write database connections. Jan, who administers the processwire.com server, asked if I could implement it. I looked into it and thought it had significant benefits for ProcessWire users, so it worked out that now was a good time to implement it. The rewritten database class is actually complete and now running on a live test installation, but I have not yet committed it to the core dev branch because I want to fully document it first. This will happen next week in a blog post. By then I'll have processwire.com using it too. But I can already say that it's a cool thing watching the graphs in AWS show the difference it makes when we start hitting the site with crawlers. You might be wondering what the benefits are in having separate read-only and read-write database connections. I'll get into some of the details next week. But essentially, read-only database connections can scale in a way (and at much lower cost) than read-write connections can. And when using a service like Amazon Aurora, they can scale on-the-fly automatically according to traffic and demand for resources. Not only does it open up the ability for a ProcessWire-powered site to scale much further than before, but it has potential to reduce the costs of doing so by perhaps 50% or more. If you are interested in reading more, we are currently testing the features using Amazon Aurora and RDS Read Replicas (see also Replication with Aurora). However, the ProcessWire core support for this feature is not bound to anything AWS specific and will work with any platform supporting a similar ability. Thanks for reading, I'll have more on this next week, and also have it ready to use should you want to try it out yourself.
    14 points
  3. I've realized that I've been jumping back and forth between the PW API docs and the source code for site modules far too much. The idea to hold all necessary documentation locally in one place has occurred to me before, but getting PHPDocumentor et al set up and running reliably (and producing readable output) as always been too much of a hassle. Today I was asked how I find the right hooks and their arguments, and that inspired me to finally get my backside down on the chair and whip something up, namely the Module Api Doc Viewer ProcessModuleApiDoc It lets you browse the inline documentation and public (optionally also protected) class/method/property information for all modules, core classes and template files in the ProcessWire instance. The documentation is generated on the fly, so you don't have to remember to update your docs whenever you update a module. The module is quite fresh, so expect some bugs there. Behind the scenes it uses PHP-Parser together with a custom class that extracts the information I needed, and the core TextformatterMarkdownExtra module for rendering the description part in the phpdoc style comments. This is not a replacement / competitor to the API Viewer included in the commercial ProDevTools package. There is quite some information included in the inline documentation that my module can't (and won't) parse, but which makes up parts of the official ProcessWire API docs. This, instead, is a kind of Swiss army knife to view PHPDoc style information and get a quick class or function reference. If you feel daring and want to give it a spin, or if you just want to read a bit more, visit the module's GitHub repository. This is the overview page under "Setup" -> "Module API Docs": And this is what the documentation for an individual class looks like: The core module documentation can of course be found online, but it didn't make sense not to include them. Let me know what you think!
    6 points
  4. File Manager for ProcessWire is a module to manager files and folders from the CMS backend. It supports creating, deleting, renaming, packing, unpacking, uploading, downloading and editing of files and folders. The integrated code editor ACE supports highlighting of all common programming languages. https://github.com/techcnet/ProcessFileManager Warning This module is probably the most powerful module. You might destroy your processwire installation if you don't exactly know what you doing. Be careful and use it at your own risk! ACE code editor This module uses ACE code editor available from: https://github.com/ajaxorg/ace Dragscroll This module uses the JavaScript dragscroll available from: http://github.com/asvd/dragscroll. Dragscroll adds the ability to drag the table horizontally with the mouse pointer. PHP File Manager This module uses a modified version of PHP File Manager available from: https://github.com/alexantr/filemanager
    4 points
  5. Such a great module, very useful indeed!
    2 points
  6. @Robin S, you were spot-on. Thanks! I configured AOS AddNewChildFirst for template of parent page. My bad. Now everything seems to work fine. However, name of submodule is kind of misleading IMHO ... @horst, thanks to you, too! And thanks for your work, your module served me well for a long time. P.S. Just realized that Privacy Bagder add-on prevents automatic "@user name" in Firefox ...
    2 points
  7. OK so I was looking for a module for weekly schedules, available time slots and bookings thereof. But all I could find is other people looking for a module like that. So I gave it a try. It's not a module yet but it will be soon, for now it merely is some templates and some fields. I gave ryan's ProcessHello module a try which makes module development much easier but I'm still not sure where to begin. How and where to create templates, with fields and repeater fields. Also, this setup requires FieldTypeTable which is a pro module so I'm not sure everyone will appreciate this requirement. I guess there could be an easier way? The tableish requirements are quite basic. https://foobar.roofaccess.org/userlogin/ if you have time and feel like it, you can just create a fake account and play around with it a bit, I hope it's somewhat self-explanatory. Should the module do more? or less? I welcome any feedback!
    1 point
  8. I'm all in on pageClasses and it has totally changed the way I'm building apps with processwire ? I'm even rewriting old projects step by step to make my code better readable and maintainable. The great thing is that this does not break any existing code - you can move hooks step by step into the new pageClasses as dedicated methods of that pagetype. I have showcased my way of doing what you are asking for in 2 posts starting here: https://processwire.com/talk/topic/21212-rockmigrations-easy-migrations-from-devstaging-to-live-server/?do=findComment&comment=212491 I'd recommend triggering a method called "ready" only on the PW "ready" event, not on "loaded" - that's something different and if you create a method called "ready" that is actually triggered on "loaded" you'll likely produce code that might at some day produce hard to find bugs... Same goes for "init". I'd be interested what Ryan says to this topic. The reason why ready() and init() of custom page classes are not triggered is obviously because custom page classes are not autoload modules. They are requested on demand and you can therefore not tell when or if they get loaded. I guess that's to minimize the footprint of every request as much as possible. In my modules I simply use RockMigrations class loader to load all my custom pageClasses: https://github.com/BernhardBaumrock/RockMigrations/blob/2d85e460ce5aa394480906ad5b41cd1a0e86d0fe/RockMigrations.module.php#L1991-L2003 That might produce a little overhead, but I don't think that this get's noticable any time soon so the benefits outweigh by far.
    1 point
  9. Hi! I just tried out your solution and it worked perfectly! Thanks a lot for the quick reply and the amazing explanation! Since I am creating temporary files on demand for download I think I don't have a big security issue nor I need to track the files, right? Basically there is a link to a download.php which triggers the download of the file. There is no visible link to that file. I include the download.php code just to close the thread properly. It is probably not the best code so if there something I should change please let me know. // create temp dir $temp_dir = $files->tempDir('downloads'); $temp_dir->setRemove(false); $temp_dir->removeExpiredDirs(dirname($temp_dir), $config->erase_tmpfiles); // remove dirs older than $config->erase_tmpfiles seconds // create zip $zip_file = $temp_dir . "test.zip"; $result_zip = $files->zip($zip_file, $data); // download pop-up if (headers_sent()) { echo 'HTTP header already sent'; } else { if (!is_file($zip_file)) { header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found'); echo 'File not found'; } else if (!is_readable($zip_file)) { header($_SERVER['SERVER_PROTOCOL'].' 403 Forbidden'); echo 'File not readable'; } else { header($_SERVER['SERVER_PROTOCOL'].' 200 OK'); header("Content-Type: application/zip"); header("Content-Transfer-Encoding: Binary"); header("Content-Length: ".filesize($zip_file)); header("Content-Disposition: attachment; filename=\"".basename($zip_file)."\""); readfile($zip_file); exit; } }
    1 point
  10. And here comes the first talking point I'd like to open... Ryan in his introductory article did not mention whether there is a significant difference between "DefaultPage as a direct subclass" of Page class and "other subclasses of DefaultPage". He also did not mention anything about the recommended way of implementing our own custom "initialization" method for the object, in case we need to perform such a thing. EDIT on 2021-03-28 starts ------------------------------ Taking a look at wire/core/Templates.php it is the getPageClass() method which determines the class of a page. Here, $corePageClass = __NAMESPACE__ . "\\Page"; is used to set ProcessWire\\Page as expected, except when there is a custom page class being used. Then, PW checks if a page class has been set "in the admin" and – if it is valid to use it – then uses that one instead. If no valid custom page class has been found so far, then PW looks for a valid custom page class supported by the new technique of PW 3.0.152+ (that is: extending it in our own client code) and this is where the class name ProcessWire\\DefaultPage is hardcoded into PW like this: $defaultPageClass = __NAMESPACE__ . "\\DefaultPage";. If this class is used/exits as a custom page class, then it is set to take over the pace of the core Page class, like this: $this->pageClassNames[0] = $defaultPageClass; and afterwards: $pageClass = $this->pageClassNames[0]; So my understanding is that by using the name DefaultPage to extend Page, one can "ask" ProcessWire to use it as the bases of almost all page objects, except for users, roles and permissions which are treated differently. Taking advantage of the possibility of using DefaultPage is optional of course, one can just simply extend Page by using any other arbitrary names, but the string literal DefaultPage named subclass is treated as the new core class in case one needs to add some custom behavior to all "non-user related" pages. Correct me if I am wrong but this is what I gathered sot far.... EDIT on 2021-03-28 ends ------------------------------ What I mean is the following: //this direct subclass of Page is treated differently class DefaultPage extends Page {} //subclass of DefaultPage class ArticlesPage extends DefaultPage {} Ryan in his article suggests that using DefaultPage as a subclasses can be used. However, I found a shortcoming when relying solely on DefaultPage. Let's consider this: //direct subclass class DefaultPage extends Page { public function ___loaded() { /* *___loaded() is called when the page is "prepared" by PW, having all its properties loaded from the database. * Because of this, it can perhaps be used as an init() method, so that * we might further initialize the object before actually using it in our own "client code". */ } } The issue with using loaded() to do our own additional initializations is that in the case of DefaultPage this method gets called for ALL the pages that are instantiated during a normal HTTP request. What I mean is that any code placed into loaded() will be executed for all sorts of pages, not just for our humble DefaultPage object we wanted to instantiate in the first place. Even worse – for some reason unknown to me –, the same DefaultPage object's loaded() method will be executed twice during a single HTTP request of the frontend. All this behavior renders loaded() unable to fulfill the role of an init() method I am looking for. However, I found that the subclasses of DefaultPage work differently, let's say "normally". While DefaultPage takes the place of Page as far as ProcessWire is concerned, any subclass of it does not. This is a very important difference, because the following one behaves differently: //subclass of DefaultPage class ArticlesPage extends DefaultPage { public function ___loaded() { /* * Code placed here always runs just once during a normal HTTP request. */ } } In this case loaded() only gets called by ProcessWire when our own client code triggers the instantiation of the custom page object in any way (for example when the related page is rendered by requesting it in the browser). And in this case no other custom page objects's loaded() method gets called. Moreover, trying to use Tracy (eg. a bd() call) in the loaded() method of DefaultPage is not possible, however, Tracy is readily useable in the loaded() method of a subclass of DefaultPage. To summarize: I found that in the case of a DefaultPage I cannot use loaded() as an "init() method". However, in the case of any further subclasses of DefaultPage, it looks like it is fine to rely on loaded() because it is called only once (after the object has been prepared for us by PW). I'm interested in hearing how others implement a custom initialization method for their custom page objects. Maybe there are better ways to do it than relying on loaded()? Is using loaded() for such a thing is a good idea in the first place? //de
    1 point
  11. <div style="background: #999 url(<?php echo $page->myimage->httpUrl; ?>) no-repeat center"></div> That should work
    1 point
  12. This is great idea indeed. Thanks for sharing!
    1 point
  13. Hi @kaz This one work for me $image = $image->size(1600, 500)->httpUrl(); //// <div class="masthead masthead--<?= $template_name; ?>" style='background-image:url(<?= $image; ?>)'">
    1 point
  14. You may have a look to the sort ids. With my old module they initially get setup to 9999999 for the first child. Maybe this is what interferes now. Simply uninstalling the module does not change this. Maybe you temporarily can change the sort order to something different then manually drag/drop, so the tree gets reordered and the sort ids also gets rebuild, (from 0 upcounting instead of the old downcountig from 99999999). And after that, set the sort order back to manually drag/drop.
    1 point
  15. @CalleRosa40 I use the AddNewChildFirst feature regularly and so can confirm that it works. Check that you have enabled it for the correct template: the template of the pages that will be added at the top, and not the template of the parent page. The configuration is different from Pagetree Add New Childs Reverse in this regard. And I assume you have uninstalled Pagetree Add New Childs Reverse - you wouldn't want to have this module and the AOS feature running at the same time. Edit: I see now you already said that you uninstalled Pagetree Add New Childs Reverse.
    1 point
  16. RF3 got a very nice little update today ? Aggregations Often we need to calculate sums or averages of table data quickly and efficiently. RockFinder3 makes that easy as well: $avg = $rockfinder->find("template=cat") ->addColumn('weight') ->getObject("SELECT AVG(weight)"); db($avg); $cats = $rockfinder ->find("template=cat") ->addColumns(['title', 'weight']); $cats->dump(); // dump finder data to tracy $obj = $cats->getObject("SELECT SUM(`weight`) AS `total`, COUNT(`id`) AS `cats`, SUM(`weight`)/COUNT(`id`) AS `avg`"); db($obj); // dump result of aggregation What happens behind the scenes is that RockFinder3 gets the current SQL query of the finder and adds that as FROM (...sql...) AS tmp to the query that you provide for aggregation. This is the resulting SQL query of the example above: SELECT SUM(`weight`) AS `total`, COUNT(`id`) AS `cats`, SUM(`weight`)/COUNT(`id`) AS `avg` FROM ( SELECT `pages`.`id` AS `id`, `_field_title_605cab16f38ce`.`data` AS `title`, `_field_weight_605cab16f3993`.`data` AS `weight` FROM `pages` LEFT JOIN `field_title` AS `_field_title_605cab16f38ce` ON `_field_title_605cab16f38ce`.`pages_id` = `pages`.`id` LEFT JOIN `field_weight` AS `_field_weight_605cab16f3993` ON `_field_weight_605cab16f3993`.`pages_id` = `pages`.`id` WHERE (pages.templates_id=44) AND (pages.status<1024) GROUP BY pages.id ) AS tmp You can even provide a suffix for your query to do things like GROUP BY etc: $rf = $rockfinder->find("template=cat|dog"); $rf->addColumns(['created']); $rf->dump(); db($rf->getObjects( "SELECT COUNT(id) AS `count`, DATE_FORMAT(created, '%Y-%m-%d') AS `date`", "GROUP BY DATE_FORMAT(created, '%Y-%m-%d')" ));
    1 point
  17. You can set this under Homepage > edit like shown here: Does this solve your issue?
    1 point
  18. To be honest I've never really understood the maxage setting ? I tried to understand how it works again... but didn't. But I've found this working solution: $td = $files->tempDir('hello-world'); $td->setRemove(false); $td->removeExpiredDirs(dirname($td), 10); // remove dirs older than 10 seconds file_put_contents($td.'some-file.txt', 'Hello world'); Fire that from the tracy console several times and you'll see that on each request a new dir is created and also old dirs are removed. That's because you can't access files in a hidden folder! You'd need to write your own custom logic to include() the content of this file and show that on a custom url (maybe using the new url hooks). But the problem is that you have to keep track of the exact file/folder name of your temporary file. And another problem is, that you can end up with security problems, because the file content of a file can change over time whereas the link to that file could still be the one from an earlier version of another file. eg create temp file --> .../0/yourfile.txt create another --> .../1/yourfile.txt create another --> .../0/yourfile.txt (because the old file in folder 0 does not exist any more because it was too old and was removed) Now you got 2 links: yoursite.com/tempfile0 and yoursite.com/tempfile1 and tempfile0 will show different files depending on WHEN the link is clicked. I guess that's not a good idea ? I'll open an issue regarding the tempDir though. https://github.com/processwire/processwire-issues/issues/1356
    1 point
  19. Thank you both! @horst your solution works great for me. Configuring content types is a part of PW I was not yet familiar with. It's so cool to discover new ways in which PW is such a powerfull and customizable product. This solves my problem, but I'm still curious why my altered RewriteCond didn't work, if anyone knows the answer to that I'd love to know!
    1 point
  20. Hi. Is it possible customize the registration form for different pages ? I have to add a specific roles for the user make the registration from a specific page. And also add one more input in this registration form. I try to add the role following your idea in the other post but this of course doesn't work. wire()->addHookBefore('LoginRegister::buildLoginForm', function ($event) { $form = $event->arguments[0]; $string = $page->path(); $field = 'invoice_IVA_VAT'; // hide a field for all forms except the one on the page. if ($string !== 'business-landing') { $f = $form->get($field); $f->collapsed = Inputfield::collapsedHidden; } // add them new role if ($string == 'business-landing') { $u = $event->arguments[0]; $u->roles->add(wire('roles')->get("bs-user")); $u->save(); } $event->return = $form; }); I see you have use addHookBefore LoginRegister::createdUser in that post. But if I want choose the page where this it's apply I have to use buildLoginForm, correct? How I can add the roles after the registration and a new field? UPDATE: For add a custom roles need to use an hook by this: wire()->addHookBefore('LoginRegister::createdUser', function($event) { $u = $event->arguments[0]; // get user object $u->addRole('bs-user'); $u->save(); }); Be sure to render the login form (and not only the registration form).
    1 point
×
×
  • Create New...