Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 03/02/2022 in all areas

  1. Hi @Ivan Gretsky - thanks for your monthly donation to Tracy - it's always appreciated.
    3 points
  2. I was about to create a similar topic, but why do it if this one already exists))) Not so long ago I started donating to some ProcessWire modules' authors. And each 1st of the month I am looking at my Patreon receipt in my email with a kind of a pride. I think that this thing is my little contribution to the community I love so much and which gave so much to me for free. I do know that Ryan likes to receive his support with pro modules payment. But many modules really shouldn't be paid/pro as they are so essential to many (and so fun to build). But their authors still would be glad to receive some money to sustain their enthusiasm supporting them and creating new features. I am sure the appreciation itself is equally important, but what is an easier way to show it than ???))) The other thing is that I sometimes notice some great improvements that seem to happen to modules as soon as some support comes in. I am writing this to encourage everyone to support the PW ecosystem by donating to module authors. I will list the modules I know that clearly asked for donations. I am sure that is not an extensive list. But at least something to start with. Tracy Debugger Mystique and other modules by ukyo (please don't miss this one @Jonathan Lahijani))) Rock Migrations and a bunch of rock stuff by bernhard All the great stuff from teppo I really like the Patreon/OpenCollective/Github donations, but only 1 of 3 listed used them. Sooooo..... Start throwing money at these fine gentlemen)) Support yourself by making you favorite modules not go away. I wish we could bring back @tpr to support his AOS and wire shell by @marcus and @justb3a. Would they keep supporting their thing is they had some donations coming in? Who knows. And please feel free to list you other module authors that you know asked for support below. Edit 2022-10-27: added teppo's link
    3 points
  3. Hello, I would really appreciate your comments on this blog post I am preparing for our site. =========== Creating a dynamic search with very little code in ProcessWire is easy. This search cannot compete with engines like Elasticsearch, Solr, and others. However, it is suitable for most "showcase" sites. Here is how we did it on the Spiria site using the small library htmx and its companion hyperscript. The goal The recipe Inclusion of libraries htmx and hyperscript (the latter is optional). A field of type textarea integrated to the page models that we want to index. A code for indexing the existing content in the file `ready.php` A search controller which we name here `api.php`. This controller will also be a page with the `api` template. A form placed in the pages requiring the search. Content indexing Before we can program, we need to index the content on which we want to apply our search. In my proof of concept, I have developed two strategies. This is probably overrated, because I am not sure of the gain in speed. Index for a single term search Indexing for a multiple term search To do this, we need to introduce two fields in each model where we want an indexation. The `search_text` field which will contain only one occurrence of each word on a page. The `search_text_long` field which will preserve all sentences without HTML tags. We place a hook in the `ready.php` page in this way: <?php namespace ProcessWire; pages()->addHookAfter("saveReady", function (HookEvent $event) { $p = $event->arguments[0]; switch ($p->template->name) { case "blog_article": $french = languages()->get('fr'); $english = languages()->get('default'); $txt_en = $p->page_content->getLanguageValue($english) . ' ' . $p->blog_summary->getLanguageValue($english); $txt_fr = $p->page_content->getLanguageValue($french) . ' ' . $p->blog_summary->getLanguageValue($french); $title_en = $p->title->getLanguageValue($english); $title_fr = $p->title->getLanguageValue($french); $resultEn = stripText($txt_en, $title_en); $resultFr = stripText($txt_fr, $title_fr); $p->setLanguageValue($english, "search_text", $resultEn[0]); $p->setLanguageValue($english, "search_text_long", $resultEn[1]); $p->setLanguageValue($french, "search_text", $resultFr[0]); $p->setLanguageValue($french, "search_text_long", $resultFr[1]); break; } }); And function stripText($t, $s) { $resultText = []; $t = strip_tags($t); $t .= " " . $s; $t = str_replace("\n", " ", $t); $t = str_replace(",", "", $t); $t = str_replace("“", "", $t); $t = str_replace("”", "", $t); $t = str_replace("'", "", $t); $t = str_replace("?", "", $t); $t = str_replace("!", "", $t); $t = str_replace(":", "", $t); $t = str_replace("«", "", $t); $t = str_replace("»", "", $t); $t = str_replace(",", "", $t); $t = str_replace(".", "", $t); $t = str_replace("l’", "", $t); $t = str_replace("d’", "", $t); $t = str_replace("&nbsp;", "", $t); $t = preg_replace('/\[\[.*\]\]/', '', $t); //$t = preg_replace('/\?|\[\[.*\]\]|“|”|«|»|\.|!|\&nbsp;|l’|d’|s’/','',$t); $arrayText = explode(" ", $t); foreach ($arrayText as $item) { if (strlen(trim($item)) > 3 && !in_array($item, $resultText)) { $resultText[] = $item; } } return [implode(" ", $resultText), $t]; } If you have the ListerPro module, it becomes easy to save all the pages to be indexed in batch and any new page will then be indexed as it is created. The `stripText()` function aims at cleaning up the text as we want. Note that, in my example, I distinguish between French and English. This little algorithm is entirely perfectible! I have commented a shorter way to clean up the text, but at the expense of comprehension. As mentioned before, it is probably unnecessary to create two search fields. The most important thing would be to optimize the text as much as possible, as many small words are useless. The current code restricts to words longer than three characters, which is tricky in a computing context like our site where names like `C#`, `C++`, `PHP` compete with `the`, `for`, `not`, etc. That said, perhaps this optimization is superfluous in the context of a simple content and not and limited in number. So now see the process and the research code. The structure structure.svg This scheme is classical and needs few explanations. The `htmx` library allows a simple Ajax call. The form code01.svg The form has a `get` method that returns to a conventional search page when the user presses the `enter` key. A hidden field with the secret key generated on the fly enhances security. The third field is the `input` of the dynamic search. It has a `htmx` syntax. The first command, `hx-post`, indicates the method of sending the data to the API. Here, it is a `post`. `htmx` allows to handle events on any DOM element. So we could have several calls on different elements of a form. The second line indicates where the API response will be sent when received, which is `div#searchResult` below the form. The `hx-trigger` command describes the context of sending to the API. Here, when the user releases a key, with a delay of 200 ms between each reading of the event. The `hx-indicator` command is optional. It tells the user that something is happening. In the example, the `#indexsearch` image is displayed. This is automatically handled by htmx. We have the possibility to pass other parameters to the search with the `hx-vals` command. The given example is simplified. We send the search language. The last command comes from the `hyperscript` syntax. It indicates that if you click anywhere outside the search field, you will make the contents of `#found` disappear, which will be described below. It is clear from this example that no javascript is called, except the [htmx] and [hyperscript] libraries. It is worth visiting the web site of these two libraries to understand their philosophy and possibilities. The Search API The API resides in a normal ProcessWire page. Although it is published, it remains "hidden" from CMS searches. This page allows requests to be answered and the correct functions to be called. Several requests to the CMS can be gathered in this type of page. <?php namespace ProcessWire; $secretsearch = session()->get('secretToken'); $request = input()->post(); $lang = sanitizer()->text($request["lang"]); if (isset($request['CSRFTokenBlog'])) { if (hash_equals($secretsearch, $request['CSRFTokenBlog'])) { if (!empty($request["search"])) { echo page()->querySite(sanitizer()->text($request["search"]),$lang); } } else { echo __("A problem occurred. We are sorry of the inconvenience."); } } exit; In this case : We extract the secret token of the session, token that will have been created in the search form page. We then process everything that is in the `post` query. Note that this is a simplified example. We compare the token with the one received in the query. If all goes well, we call the SQL query. Our example uses a class residing in `site/classes/ApiPage.php`; it can therefore be called directly by `page()`. Any other strategy is valid. The following code represents the core of the process. <?php namespace ProcessWire; public function querySite($q, $l) { $this->search = ""; $this->lang = $l == 'en' ? 'default' : 'fr'; user()->setLanguage($this->lang); $whatQuery = explode(" ", $q); $this->count = count($whatQuery); if ($this->count > 1) { $this->search = 'template=blog_article,has_parent!=1099,search_text_long~|*= "' . $q . '",sort=-created'; } elseif (strlen($q) > 1) { $this->search = 'template=blog_article,has_parent!=1099,search_text*=' . $q . ',sort=-created'; } if ($this->search !== "") { $this->result = pages()->find($this->search); return $this->formatResult(); } return ""; } protected function formatResult() { $html = '<ul id="found">'; if (count($this->result) > 0) { foreach ($this->result as $result) { $html .= '<li><a href="' . $result->url . '">' . $result->title . '</a></li>'; } } else { $html .= __('Nothing found'); } $html .= '</ul></div>'; return $html; } The `formatResult()` function is simple to understand and it is here that we see the `ul#found` tag appear which, remember, is deleted by the _hyperscript_ line of the form. _="on click from elsewhere remove #found" Actually, I've commented out this line in the production code so far. Instead of clicking anywhere, it would be better to add a background behind the form when doing a search in order to focus the `click` event. But again, all strategies are good! In the current code, it is not necessary to add CSS to display the result. It is placed in an empty `#searchResult` tag, so it is invisible at first. As soon as it is filled by the search result, everything becomes accessible, the CSS being targeted on the `ul#found` list and not on its parent. Conclusion The purpose of this article was to demonstrate htmx and the possibilities that this library allows. There is already an excellent search module for ProcessWire, SearchEngine, which can coexist very well with the code described here.
    2 points
  4. Thanks for your help and suggestions @elabx I'd need to check with my team regarding the JS options you mentioned as that isn't my strong point. I wasn't aware of the reload method you mentioned - that certainly looks promising too.
    2 points
  5. Something I remember trying too, but don't recall if it worked, is adding some Js to the admin to dynamically add the options after an ajax call literally editing the markup. If you're working on recent PW version I'm going it should be straightforward with the new URL hooks. So: Listen to on change event on the theme fiel, call backend with picked value, and do some js work to generate the options for the field and append them to the highlight colors select. Maybe something like this will work better for the scenario where you wan't to use repeaters, although I guess it will involve more work to handle the creation of new repeaters, but I am almost sure there are js events for that. Another idea that I also don't remember if it worked is, using the reload method on the inputfield.js API. So maybe then you just have to listen to the on change on the first field, and trigger reload? Maybe that'll load the field again with the right options?
    2 points
  6. Very nice! In the context of this post in the PW forums I find myself looking for a link to your site so I can see the search in action. Maybe you could add a link? A tip: str_replace() accepts an array for the $search argument, so your stripText() function could be made more compact where it is removing multiple different characters.
    2 points
  7. Depending on how many locations you have findRaw() might be a useful alternative to find()
    1 point
  8. Yet another follow-up: I have started to play around with more "Headless CMS" out there and... the more I use them and the more I try with them, I imagine ProcessWire not only could but would be such a perfect and way more advanced way of using "Headless CMS" with SSGs. No need to write schemas, templates, fields, conditions in a JSON, yaml, toml, or whatever file - only if you like to do so. (which we recently had a nice discussion about) Try it on your own. Sanity, Forestry, strapi, whatever. They are nice and work really well, yet... using those I miss ProcessWire. Not only is ProcessWire way more capable of different inputfield types and more (like Matrix, Combo), it's even more suited with lots of things in regards to access control, data sets aka "collections". Another thing is... those other CMS claim they are "Developer centric/focussed", yet their API and docs are way less easy to understand than ProcessWire. (at least from my non-developer perspective) For example and without naming names: Imagine a code block that's a default for a module or default setting, but it doesn't work because it's from 2019 and doesn't work anymore since at least 4 main/stable releases of that very "Headless CMS". To be fair... we maybe could find such things here as well, yet... often most of their forums/communities and support are located somewhere on Slack, Discord, StackOverflow, Github or are overall very thin - to say it mildly. (Ignore those stars on Github) Which brings me straight back to my original question: How to export those data in a comfortable way through JSON, Rest API, GraphQL.
    1 point
  9. @ryan I can confirm this on latest PW dev - having "tags" on an images field with extra custom fields, then removing tags from that field, then trying to upload images shows this error. For now I just re-enabled tags on that field.
    1 point
  10. Thanks for continuing to think and work on it, @teppo! I really like the new partials methods. If there would be a way to call a partial from a custom path like layouts/sublayouts that would be almost it. The question I still have is whether the view placeholders will still work in those placeholders, so I can still use them where needed? --- The other way I can think of is defining a layout stack with an array, where 1st layout when rendered is passed to the second and so on. Each of the layouts from the stack receive all view placeholders, so they could be used anywhere. But this should be thought out thoroughly.
    1 point
  11. Good post @Ivan Gretsky, I recently asked something similar about ProcessWire it-self there and forgot to mention modules's authors. Shame on me and I also suggest @adrian (and others) to give the possibility to not only to send a coffee or a simple donation, but a monthly donation. What's is "cool" about it on Github - it could be taken a bit as narcissism but it's not - it's to show that we are sponsoring a project on our page and make us proud of it.
    1 point
  12. Haha yeah I'm a fan of chaining ? eg https://github.com/baumrock/rockseo Thx for clarifying. Just wanted to make sure I'm not missing anything ?
    1 point
  13. Hello That's what I get for not reading all the documentation. Thank you very much for clarifying this point.
    1 point
  14. Nice, thx for sharing the results ? Though I'm still not sure why RockFinder helped here? I think it would be the same to do this: <?php $end = strtotime( "2021-02-27 23:59:59"); // loop $database->beginTransaction(); $ids = $pages->findIDs("template=receipt, numChildren=0, created<$end, limit=5000") foreach($ids as $id) $pages->delete($pages->get($id)); $database->commit();
    1 point
  15. Result, it crashed two times, but managed to delete 2 millions of pages in about ~15 hours ?? RockFinder really helped here - it reduce the code that should be written and the server overhead of getting the millions of record through a loop.
    1 point
  16. Thanks for the purchase :-). Sorry the notes weren't clear. It must be the adjacent (immediate previous) column. So, yes, 'its previous column'. Please see the docs here, especially the introduction and the section on column relationships. This is because both the 2nd and the 3rd columns are related to the first. The duplicate values getting shown in 3 are due to the JavaScript which determines the select to populate based on the trigger. In your case, your target selects 2 and 3 have an identical identifier (built off the field name 'background_colour'), hence both get populated. No, you are not missing anything. What's happening is expected behaviour. The cascade is strictly one-to-one (trigger-dependent selects relationship). Incidentally, I am soon reworking Dynamic Selects to use htmx. That will open new possibilities. I'll have a think around your issue, as a feature request. Hope this helps.
    1 point
  17. Thanks @elabx I know what you mean about the page refresh and being intuitive. I was hoping to avoid a page refresh if possible. I recently purchased the Dynamic Selects module and this certainly does solve the issue if I only have one set of choices based on the theme chosen in the first select eg. Highlight Colour. However I'm finding that if I want to have more than one set of choices mapped to the first select (such as 'Text Colour', 'Icon colour' etc etc) I haven't figured out a way to achieve that with Dynamic Selects (yet). I logged my thoughts earlier here. I was thinking that with the solution in my first post here that I would then be able to add additional options under the theme like below (Highlight Colour, Text Colour, anything else that applies etc) and be able to target them separately. Also, I may have a scenario where I have a block/component that has the main theme set, but inside of that I have a repeater where each element can have one of the allowed highlight colours. In that scenario I need the two fields split up. As Dynamic Select is one field I can't split the theme part for the block up from the highlight colour on the individual repeater items lower down. With the approach in my first post I noticed that the second highlight colour field updates whether it is next to the theme select or inside the repeater so I thought that could have been a bit of a win, like below (but with correct option showing within the repeater). Maybe my only option here is to go with separate page reference fields, using a hook like you mentioned and just accepting that a page refresh is needed after selecting an option from the first dropdown. There's the chance that the user could switch the first select (theme) to something else though after selecting an option from the second select (highlight colour). Tricky stuff eh...
    1 point
  18. Cross-posting here that $partials->get('path/to/partial') is now supported in Wireframe 0.22.0, along with new method $partials->render('path/to/partial'). More details in the docs: https://wireframe-framework.com/docs/view/partials/.
    1 point
  19. Hi @Laegnur and welcome here. Your issue come from the fact that your translated strings are on the same line (I admit it could be tricky to spot on first instance if you are not used with ProcessWire). To understand better, please read this sentence : So to get it working, write the first line on two lines lol : <?php // i18n - copyright echo _x('All rights reserver', 'copyright'); ?> <!-- html markup - separator --> | <?php // i18n - developer echo _x('Developed by', 'developer'); ?> You can use of course the syntax you want, just keep in mind that the strings need to be on two lines. For more information about: https://processwire.com/docs/multi-language-support/code-i18n/
    1 point
  20. The term "hint" is a good here, but it's more than that. The purpose of any site profile (no matter how minimal) is to be a functional starting point. The blank profile is now the only core profile so most people will start with it. The /site/classes/ directory is important and is likely forgotten by all but the most experienced users, if it is not present. I think it belongs in the most minimal starting point. You have to have at least one file in a directory in order for it to remain through git, site profile and installation. Being the blank profile, the class itself is blank, a placeholder, like the directory itself. It exists to explain what the directory is for and the format files must use within it. It's a readme that exemplifies what it describes. Any site profile must be functional and it's worth noting that all site profiles, no matter how minimal, all have a "home" template (and an "admin" template). So a home.php template file and a HomePage.php class file are part of that minimal but functional starting point, even if blank.
    1 point
  21. Hi @xportde. That field is not part of a stock ProcessWire installation, afaik. It is installed by the SystemNotifications module that was introduced in this blog post: https://processwire.com/blog/posts/processwire-2.5.3-master-2.5.4-dev/ The module comes with PW but is not installed by default (maybe it is with some site profiles?). Uninstalling the module automatically removes the field as well.
    1 point
  22. @flydev ?? Thanks, I appreciate that. I will look into this.
    1 point
  23. The trick is to put all the affected images into a new Pageimages object before you start setting the field values, so you can later get each image from there even after it has been removed from a field value. A proof of concept... Image fields in Page Edit: Tempate file markup: <script src="/site/templates/js/jquery-ui.min.js"></script> <script> $(document).ready(function() { var $sortable_images = $('.sortable-images'); $sortable_images.sortable({ // When any image has been moved... update: function(event, ui) { // Add the image basenames to an array in the new order var value = []; $sortable_images.find('img').each(function() { value.push($(this).data('filename')); }); // Join to a comma separate string and set as the form field value $('#images').val(value.join(',')); } }); }); </script> <div class="sortable-images"> <img src="<?= $page->image_1->size(150,150)->url ?>" data-filename="<?= $page->image_1->basename ?>" alt=""> <img src="<?= $page->image_2->size(150,150)->url ?>" data-filename="<?= $page->image_2->basename ?>" alt=""> <img src="<?= $page->image_3->size(150,150)->url ?>" data-filename="<?= $page->image_3->basename ?>" alt=""> </div> <form id="image-order" action="./" method="post"> <textarea name="images" id="images"></textarea> <button>Submit</button> </form> Template file PHP: // Look for the comma separated value in POST $images_value = $input->post('images'); // If the value exists then the form was submitted if($images_value) { // Explode the comma separated value to an array of image basenames $image_names = explode(',', $images_value); // Create a Pageimages object using the individual image basenames $all_images = new Pageimages($page); foreach($image_names as $image_name) { $all_images->add($image_name); } // Turn off output formatting because we are about to set field values $page->of(false); // Loop over the image basenames foreach($image_names as $index => $image_name) { // Get the Pageimage from $all_images $pageimage = $all_images->get($image_name); // Create a new empty Pageimages object $pageimages = new Pageimages($page); // Add the Pageimage $pageimages->add($pageimage); // Determine the field name that will be updated $image_field_name = 'image_' . ($index + 1); // Set the field value $page->$image_field_name = $pageimages; } // Save the page $page->save(); // Redirect back to the page (Post/Redirect/Get pattern) $session->redirect($page->url); }
    1 point
  24. They do it all the time. No matter how many accounts exists... most of the time all people use just one. Some just don't want to be responsible for changes so they take the credentials from another user or "admin" account they somehow can get.
    1 point
×
×
  • Create New...