Jump to content


  • Posts

  • Joined

  • Last visited

  • Days Won


Everything posted by teppo

  1. As it happens, I'm about to hop in a project where invoicing is a key part; while I don't know if I'll be able to use your site profile directly, at the very least it'll be interesting to dig in for ideas. What we see here already looks awesome 🙂 Would be nice if this was implemented in a way where one can easily swap the payment processor, kind of like what Antti did with PaymentModule (and PaymentStripe, PaymentPaypal, etc.)
  2. Definitely sounds like a job for ProDrafts. Version Control keeps track of changes after they've occurred, but there's no (automatic) way to put them on hold and apply after some action/trigger. In theory one could restore the page to previous revision automatically and step "forward" again once changes are approved, but that would get very complicated very fast, and going back and forth could also result in "conflicts" where updates applied meanwhile would simply get discarded. Anyway, that's not something I would recommend doing 🙂
  3. README lists three supported multilingual fieldtypes: Text (regular and multi-language) Textarea (regular and multi-language) Page Title (regular and multi-language) Technically other multilanguage fieldtypes should be supported as well, but these are the ones I've tested. Version Control has a config setting for enabling fieldtypes that are available on the site, but not listed as compatible by default.
  4. Out of curiosity, which version of PW are you using? PW auto-corrects the rotation, and some issues related to this were fixed in version 3.0.197. Just wondering if updating to the latest stable version (3.0.200), for an example, would resolve this problem. My understanding is that PW doesn't remove EXIF data from uploaded image, just resized images — though I could be wrong here! One exception at least is that if you have configured a max image size for your image field, then it will be automatically resized after being uploaded, which means that PW no longer has any version of it with EXIF data in place. Anyway, I'm not sure which one you're talking about here: original uploaded image, or a PW generated size variation of it? If it's the latter, then currently there's no way to retain EXIF data, but the image should still be automatically rotated according to what was in said EXIF data. (There is an open issue about keeping EXIF data, at least in some cases, but no clear solution yet: https://github.com/processwire/processwire-issues/issues/1055.)
  5. Just wanted to point out that when it comes to building menus, it can get a bit... complicated. This is why modules such as MarkupMenu exist. I'm not saying that you can't build your own menu logic without a module using custom code, just that it's — in my opinion — one of those things that you don't necessarily need to 🙂 Depends a lot on your use case, of course; some sites have a super simple menu logic. And sorry for going off-topic, as well as shameless self-promotion via MarkupMenu 🙂
  6. Pages::___save() method has a forceID parameter: * - `forceID` (integer): Use this ID instead of an auto-assigned one (new page) or current ID (existing page). I've never tried to do anything like this, but it seems like this would be a good place to start, e.g. hooking before this method and forcing the ID in case of a new page using the user template. You would still have to figure out some way to share auto-increment ID values between instances 🙂
  7. A few new additions to this old module, available as of version 1.8.0 (session.txt import) and 1.9.0 (new API methods): There's an option in ProcessLoginHistoryHooks module config to import login history from session.txt. This can be useful when adding this module to a site with existing history, since ProcessWire (by default) collects data about successful/unsuccessful login attempts to said log file. This option is intentionally hidden under collapsed "advanced" fieldset. It's not super well tested yet, could result in a very slow request in case there's a lot of data to import, and in case disk timezone is different than the one in database duplicate records may be created on first run. $this->modules->get('ProcessLoginHistoryHooks')->getUserLoginHistory($user, $start, $limit, $login_was_successful) returns login history data for provided ProcessWire User object. Default values are 0 for "start", 2 for "limit", and 1 for "login_was_successful", which means that by default this method will return latest successful login and the one before that. Return value is always an array; in case there are no results, an empty array is returned. $user->getLoginHistory($start, $limit, $login_was_successful) does the same as previous command for a specific User object. All arguments are optional. This method returns a single timestamp (when limit is 1, or an integer value is provided for the "start" argument and "limit" argument is omitted), an array of login history, or null in case there are no results. By default timestamps for last two successful logins are returned.
  8. If you're not a ProDevTools user yet it might be a bit overkill, but just wanted to mention that ProfilerPro is brilliant for this type of use case 🙂
  9. Just my two cents: used to avoid Repeaters as well, because in the beginning they were quite unstable, but from my point of view they have matured nicely. For years I/we have used them a lot — in fact nearly all sites we've built last couple of years have used a Repeater Matrix based page builder. Can't remember the last time I had an issue directly related to repeaters 🤷‍♂️ That being said, I do try to keep things simple, just in case. Field dependencies is an example of a feature I've not tried with repeaters 🙂
  10. Page Table was developed/intended as a part of the ProFields bundle, but later bundled with the core package, so "ProFields" in the name is kind of a nod towards that history 🙂 There are many ways to handle rendering, but here's one solution that may help you: Create a shared root for your content elements — something like /blocks/ or /content-elements/, perhaps. Configure your Page Table field to save new items below aforementioned root path, and make sure that you only enable templates that you've created for this purpose. You don't want to allow a "regular basic-page" (or anything like that) in your Page Table field as a "content element". For each of your "content element templates", leave template file empty (so that these pages can't be reached with their actual URL). Now even though your content elements will be actual pages in the page tree, they won't be reachable by visitors. You can render them in any way you see fit, but here's one approach using ProcessWire's native field rendering feature that, in my opinion, works pretty well. Here I've assumed that the Page Table field is called "blocks", and it's used on template called "basic-page". /site/templates/basic-page.php <?php namespace ProcessWire; ?> <?= $page->render('blocks') ?> /site/templates/fields/blocks.php <?php namespace ProcessWire; ?> <?php foreach ($value as $item): ?> <div class="block"> <?= $item->render(__DIR__ . '/blocks/' . $item->template . '.php') ?> </div> <?php endforeach; ?> /site/templates/fields/blocks/block_template_name.php <?php namespace ProcessWire; ?> <h2> <?= $page->title ?> </h2> <?= $page->content ?> Anyway, as I mentioned that's just one approach 🙂 I'm not entirely sure what you're referring to here, but if you mean the processwire.com website, then yes — that's technically proprietary. In my experience that's a common thing for open source projects. API reference, on the other hand, is automatically generated from ProcessWire's source code, so that is technically open source 🙂
  11. Thanks @flydev 👊🏻 for identifying the error, and sorry @Roych for the inconvenience. This is now fixed in version 1.7.2 of Login History, display date format should no longer intervene with date difference calculations 🙂
  12. Out of the box (without third party modules) your best option is the Page Table ("ProFields: Page Table") fieldtype. It's included with the core package, but might be uninstalled by default. The user interface for Page Table is a bit different in that you'll edit content in modal windows instead of "in context", but I'd suggest that you give it a try. You can save your content items under current page, or a different location in your page tree. If third party modules are an option, the Repeater Matrix module is worth checking out. It's commercial (paid) module, but if that's not an issue, it's really quite amazing. Other (non-commercial) module options include PageTableNext and PageTableExtended.
  13. wire('pages')->get('template=template_custom_orders')->delete() uses $pages->get(), which means that it will include pages that are either in trash, or unpublished/hidden. Meanwhile this part... $tcos = pages('template=template_custom_order'); foreach ($tcos as $tco) : $tco->delete(); endforeach; ... will only find non-trashed public pages. One thing to check is that it matches every possible page: $tcos = pages('template=template_custom_order, include=all'); foreach ($tcos as $tco) : $tco->delete(); endforeach; BUT that being said, there's potentially an even easier method: wire('pages')->get('template=template_custom_orders')->delete(true); If you pass "true" to the delete method, it will automatically (attempt to) remove child pages.
  14. Hey @Zeka! I assume you're referring to "method aliases" from MethodPropsTrait? If so, this is a feature that, to be honest, I've not had much use for myself. As far as I can remember, the original intention was two-fold: 1) Whether to make code cleaner or to mitigate issues resulting from partial rewrites, one can create an alias or "virtual method" that calls another local method behind the scenes. Say, if a controller class had method called "words", but later it was decided that it should be "terms" instead (or for some reason front-end code requires it to be that) yet changing it now could potentially cause other issues, one could handle this via an alias: class DictonaryController extends \Wireframe\Controller { protected $method_aliases = [ 'terms' => [ 'callable' => ['self', 'words'], 'params' => [], ], ]; public function words(): array { return $this->pages->findRaw('parent=' . $this->page->id . ', template=Word', [ 'name', 'title', 'dictionary_category.title', ], [ 'objects' => true, 'entities' => true, ]); } } Here I'm defining the $method_aliases property directly on class declaration, but in some cases it could make more sense to define individual aliases via the API: $this->setAlias($alias, $callable, $params). 2) Another use case is referring to a method defined somewhere else — another controller, utility class, third party library, etc. Essentially this makes it easy to inject new methods or properties into classes implementing the MethodPropsTrait (such as controllers and components): class DictonaryController extends \Wireframe\Controller { public function init() { $this->setAlias('terms', '\Wireframe\Lib\Utils::getWords', ['page' => $this->page]); } } You can do the same with ProcessWire hooks or just have a method that calls another method, so this is largely a matter of preference 🙂 --- As I mentioned above, I've not had much use for this feature myself. It's been there since the very beginning, and it's a concept that seemed potentially useful (not to mention fun) while I was working on the initial implementation of Wireframe, but real world use cases have been few and far between. If you can think of other use cases, let me know 🙂
  15. What the error you posted says is basically that your code doesn't found a page matching the path you've provided. This is why it returns a NullPage object, which is kind of like saying "sorry, didn't find anything, here's a placeholder object for you instead". And since you can't remove a NullPage, that explains the error you saw. Now, what you're describing here sounds like some kind of a bug, though it's a hard to pinpoint exactly without access to the code, site, and/or database. Couple of suggestions: When this happens, I would first check if logging out and then in again removes that "page" from the menu. Admin menu is cached independently, so this could be the reason. Though I'm not sure why said cache wouldn't reset automatically. If that doesn't help and there is indeed an actual page there, it would be interesting to know what kind of data it has in the "pages" table in the database. Another reason why this might happen would be if a page is somehow corrupted. A typical signal of that would be that it exists in the database, but has a strange value in the "status" column. If you post your current code, I would be happy to take a closer look (at some point) in case there's some sort of issue there 🙂
  16. The problem here is that $pages->get() can return either a Page or a NullPage. NullPage is a null object that (in terms of API usage) behaves like a Page, but doesn't have any content, and doesn't actually match any page on the system. Thus you can't delete a NullPage. NullPage doesn't have an ID, so here's one way you can solve this, checking for an object with an ID: public function ___uninstall() { // ... $dashboard = wire('pages')->get('/admin/kiosk_dashboard/'); if($dashboard->id){ $dashboard->delete(); } // ... } Edit: just for the record, admin pages can be renamed or moved, so keep in mind that at least technically your uninstall routine may leave "orphaned" pages behind. In case your dashboard page uses your Process module as it's process, here's what you could do instead of relying on the path/name always being /admin/kiosk_dashboard/: public function ___uninstall() { // ... $module_id = wire('modules')->getModuleID($this); $dashboard = wire('pages')->get('template=admin, process=' . $module_id); if($dashboard->id){ $dashboard->delete(); } // ... }
  17. HTTP/2 makes things more efficient, but it doesn't completely eliminate the overhead of downloading multiple files. As long as those files are the kind that "must always be loaded" (and "must always be loaded together"), I'd still say that the best practice is to merge and offer only one file to download. Cases where merging makes little sense or might even have negative effect include scripts/styles that are only needed some of the time (e.g. big JS app related to single page/template, which the visitor may never encounter), JS/CSS that is intentionally split so that it gets loaded before other content (e.g. styles for the above the fold content), bundles where some parts are regularly updated while others remain as-is for long periods of time (i.e. having to invalidate cache for entire bundle unnecessarily often), etc. If merging has to happen real-time, server resource usage / resulting delay would further diminish the benefits. So merged scripts/styles should preferably be served directly from disk, as static files. Just my two cents. Not exactly a simple topic 🙂
  18. Looking at your code, and specifically your ___uninstall() method, I can spot at least a few potential issues. $custom_order_pages = wire('pages')->find('template=template_custom_orders|template_custom_order')->count(); // if ($custom_order_pages > 0) // throw new WireException("There are pages using custom-order-templates. Remove those first before uninstall"); foreach($custom_order_pages as $custom_order_page){ $custom_order_page->delete(); } The foreach loop above doesn't make any sense. You're essentially trying to iterate over the count() value, which is an integer. If you remove the "->count()" part, it's more likely to work — unless your pages are hidden/unpublished, in which case you should also add "include=all" to the selector string. if($fields->get('custom_order_id')) : foreach ($fields->get('custom_order_id')->getFieldgroups() as $fieldgroup) { $fieldgroups->delete($fieldgroup); } endif; if($fields->get('custom_order_products')) : foreach ($fields->get('custom_order_products')->getFieldgroups() as $fieldgroup) { $fieldgroups->delete($fieldgroup); } endif; if($fields->get('custom_order_customer_name')) : foreach ($fields->get('custom_order_customer_name')->getFieldgroups() as $fieldgroup) { $fieldgroups->delete($fieldgroup); } endif; if($fields->get('custom_order_customer_address')) : foreach ($fields->get('custom_order_customer_address')->getFieldgroups() as $fieldgroup) { $fieldgroups->delete($fieldgroup); } endif; if($fields->get('custom_order_customer_emailaddress')) : foreach ($fields->get('custom_order_customer_emailaddress')->getFieldgroups() as $fieldgroup) { $fieldgroups->delete($fieldgroup); } endif; The part above looks fine, but keep in mind that none of these fields were added to the "fieldgroup_custom_orders" fieldgroup, so that won't get removed. You should probably add something like this right after the lines above: $fieldgroups->delete($fieldgroups->get('fieldgroup_custom_orders')); Also this is likely unrelated to your bigger issue(s) here, but note that this part doesn't seem quite right: $template_custom_orders->childrenTemplates = array($template_custom_order->id); There's no "childrenTemplates" property on Templates. What you probably meant was childTemplates. Finally, your ___install() method is rather error prone. In case any of the fields already exist, you will run into problems. For an example, look at these lines here: if(!$fields->get('custom_order_id')) : $custom_order_id = new Field(); // int or text $custom_order_id->type = $this->modules->get("FieldtypeInteger"); $custom_order_id->title = 'Custom Order ID'; $custom_order_id->name = wire('sanitizer')->pageName($custom_order_id->title, true); $custom_order_id->label = 'Order ID'; $custom_order_id->tags = 'kiosk'; $custom_order_id->save(); endif; What happens here is that a) if custom_order_id field is not found, it will be created and stored in $custom_order_id, but b) if it doesn't exist, then it won't be created and it also won't be stored in $custom_order_id. Basically if install/uninstall has been only partially completed, even once, then you're likely going to run into issues. Here's one approach that should work a bit more reliably: $custom_order_id = $fields->get('custom_order_id'); if(!$custom_order_id) : $custom_order_id = new Field(); // int or text $custom_order_id->type = $this->modules->get("FieldtypeInteger"); $custom_order_id->title = 'Custom Order ID'; $custom_order_id->name = wire('sanitizer')->pageName($custom_order_id->title, true); $custom_order_id->label = 'Order ID'; $custom_order_id->tags = 'kiosk'; $custom_order_id->save(); endif; It's quite likely that this error is linked to the issue I mentioned above about your install/uninstall method being error prone: if the process has failed even once, this part will not work as expected: if(!$fieldgroups->get('fieldgroup_custom_orders')) : $fieldgroup_custom_orders = new Fieldgroup(); $fieldgroup_custom_orders->name = 'fieldgroup_custom_orders'; $fieldgroup_custom_orders->add($this->fields->get('title')); $fieldgroup_custom_orders->save(); $this->message('creating fieldgroup fieldgroup_custom_orders'); endif; This — and every other code snippet that uses similar approach — should be replaced with something like this instead: $fieldgroup_custom_orders = $fieldgroups->get('fieldgroup_custom_orders'); if(!$fieldgroup_custom_orders) : $fieldgroup_custom_orders = new Fieldgroup(); $fieldgroup_custom_orders->name = 'fieldgroup_custom_orders'; $fieldgroup_custom_orders->add($this->fields->get('title')); $fieldgroup_custom_orders->save(); $this->message('creating fieldgroup fieldgroup_custom_orders'); endif; If your module needs to create templates via the API and add fields to said templates, you do need fieldgroups. There's no way around that.
  19. You've stumbled upon a concept that is, admittedly, a little obscure. The Fieldgroup class comments explain what Fieldgroups are all about, and it boils down to this: Fieldgroups are something that most users don't need to concern themselves with, but technically there's always a fieldgroup behind a template — fields don't belong directly to template, but rather to a fieldgroup, which in turn may be used by one or more templates. Multiple templates using one fieldgroup is, though, pretty rare — I don't think I've ever had that need myself 🙂 All the examples I've seen so far have created a Fieldgroup and then applied it to a Template. If there's a way to create template with fields without creating a fieldgroup to hold said fields, I'm not aware of it (or can't remember it right now — to be honest creating templates via API is not something I do often). No. RockMigrations can make things easier and provide you with many useful tools, but it's not required by any means. If you just need to create a few templates and fields in a module, I personally feel that it's probably overkill to add a dependency to a third party module for that task alone. (Though it could be quite helpful in the future in case you also want to keep those templates/fields in sync with new versions of your module.) I would recommend opening a request for (additional) documentation at https://github.com/processwire/processwire-requests. In my opinion you have a valid point when it comes to fieldgroups — creating templates with fields (and fieldgroups) via API could be more clearly documented. As for "many other things", I don't know what those things would be, but if you can think of some then by all means open a request for those as well. Based on your latest message it seems that you've already solved this — nice! 🙂
  20. Thanks for reporting the issue, @ngrmm. This should be fixed in SE 0.35.1. I wasn't able to reproduce this issue myself, so please let me know if the problem still persists and I'll be happy to take a closer look 🙂
  21. This is not particularly helpful, I'm afraid, but if this is a constant / easily reproducible thing, I would start debugging this by checking what page these errors apply to — i.e. what's the "this page" in that particular case? Is it current page being edited, or perhaps a Repeater page? (Might have to edito ProcessPageEdit.module to add Page ID to that error. Or install Tracy.) There was only one issue mentioning this specific error (https://github.com/processwire/processwire-issues/issues/736), but that seems to apply to a rather specific situation with front-end editing and Repeaters. Not sure if it has anything to do with your issue. Of course it would also be helpful to rule out any third party modules that might be causing this. And/or try to reproduce this on a blank installation 🙂 If that's the real PW version, I would recommend updating. I wouldn't be surprised if there were at least some quirks in this version, not to mention that it might not be 100% compatible with PHP 8.
  22. As a security feature ProcessWire's .htaccess file blocks access to any PHP files in assets directory: https://github.com/processwire/processwire/blob/master/htaccess.txt#L383:L384. And there's also a separate (fallback) .htaccess file in /site/ or /site/assets/, which does the same. If you want to provide direct access to a .php file, you'll have to... poke a hole in these rules on your own site (you could do that by creating a .htaccess file that you put on a specific folder, in which you specifically allow access to the file you know to be safe), or route the request via ProcessWire, or put it into a directory that is not protected in this way (such as root path).
  23. This module is an optional (and still somewhat experimental) add-on for SearchEngine. It adds support for indexing file contents, replacing earlier SearchEngine PDF indexer module. Features SearchEngine by itself will only store the name, description, tags, and custom field values for file/image fields. This module, on the other hand, attempts to extract human-readable text from the file itself. As for file types, at least in theory this module supports any filetype that can be reasonably converted to text. It has built-in support (mostly via third party libraries) for... office documents (.doc, .docx, .rtf, .odf), pdf documents (.pdf), spreadsheets (.xls, .xlsx, .ods, .csv) and plain text (.txt). The module also ships with a FileIndexer base class and exposes the SearchEngineFileIndexer::addFileIndexer() method for introducing indexers for file types that are not yet supported. Links GitHub: https://github.com/teppokoivula/SearchEngineFileIndexer Composer: composer require teppokoivula/search-engine-file-indexer Modules directory: https://processwire.com/modules/search-engine-file-indexer/ Getting started install and configure SearchEngine (version 0.34.0 or later), install SearchEngine File Indexer, install third party dependencies — if you installed SearchEngineFileIndexer via Composer you should already have these available, otherwise you'll need to run "composer install" in the SearchEngineFileIndexer module directory, choose which file indexers you'd like to enable. The rest should happen automagically behind the scenes. Additional notes The important thing to note here is that we're going to rely on third party libraries to handle parsing (most) files, and things can still go wrong, so please consider this a beta release. It did work in my early tests, but there's little guarantee that it will work in real life use cases. Just to be safe it is recommended to back up your site before installing and enabling this module. Another thing to keep in mind is that indexing files can be resource intensive and take plenty of time. As such, this module provides some settings for limiting files by size etc. Regardless, this is something that likely needs further consideration in the future; some future version of this module, or an additional add-on module, may e.g. add support for indexing pages/files "lazily" in the background.
  24. Quick heads-up: As of version 0.35.0 SearchEngine supports indexing file/image custom fields. In order to avoid surprises, I ended up adding these as separately selectable indexed fields. For an example: in order to have custom field "author_email" for image field "photo" indexed, you'll have to select "photo.author_email" from the "indexed fields" list in module config. There's a "file_field.*" setting available as well, in case you want to index all existing custom fields for a specific file field. Note that description and tags also need to be specifically selected. In earlier versions description text was always indexed, but now if you select the file field name and nothing more, only file name(s) (and content, if you have a suitable file indexing module installed and enabled) are indexed. -- In order to index file/image custom fields I had to refactor parts of the Indexer class. I hope I didn't break too many hooks, but there's a possibility that something goes wrong. Please test carefully, and let me know if you run into any issues!
  25. The Twig renderer for Wireframe has been around a while already, it just didn't have a support forum thread of its own, so here we go. This optional add-on module lets one author layouts, views, partials, and component views using Twig instead of PHP. By design you can have view files that are either Twig or PHP — with this module enabled Wireframe will initially look for a .twig file, but if that doesn't exist, it'll fall back to a regular .php file instead. GitHub repository: https://github.com/wireframe-framework/WireframeRendererTwig Composer installation: composer require wireframe-framework/wireframe-twig-renderer Here's a small sample of what Twig looks like: {% if page.numChildren(true) %} <ul class="menu"> {% for child in page.children %} <li> <a href="{{ child.url }}"> {{ child.title }} </a> </li> {% endfor %} </ul> {% endif %} You can read more about Twig from https://twig.symfony.com/doc/. Twig syntax in specific is covered in Twig for Template Designers. For more details about Wireframe renderers and how they are enabled and used, check out https://wireframe-framework.com/docs/view/renderers/.
  • Create New...