-
Posts
695 -
Joined
-
Last visited
-
Days Won
20
Everything posted by Jan Romero
-
I can’t believe ProcessWire Weekly is still going strong! Its 10 year anniversary was actually in May last year.
-
Mirroring users data on another site section
Jan Romero replied to Manaus's topic in General Support
If you were doing this you could just make user pages available whereever you want: https://processwire.com/blog/posts/processwire-core-updates-2.5.14/#multiple-templates-or-parents-for-users Otherwise you could use UrlSegments or Path Hooks to load the requested user, such as: //requesting /teachers/paul $teacher = users()->get('roles=teacher, name=' . input()->urlSegment1); //urlsegments are already sanitized if (!$teacher->id) throw new Wire404Exception(); echo $teacher->firstname . ' ' . $teacher->lastname;- 1 reply
-
- 2
-
-
Just add an extension here: $files->setTargetFilename($user->name."-avatar"); Although I might agree that PW should perhaps be able to handle it regardless. But also, why not use the field? user()->of(false); user()->avatar->deleteAll(); $field_avatar = user()->getInputfield('avatar'); $field_avatar->setMaxFilesize(500_000); $field_avatar->processInput(input()->post); if (!user()->save(['quiet'=>true])) die('oh no'); user()->of(true);
-
Is it possible to use null coalescing in a field selector?
Jan Romero replied to artfulrobot's topic in General Support
I’ve also longed for this feature and took this occasion to spend the day figuring out what it might take to support it in the core… Unfortunately, the when|published syntax is already supported as a shorthand for sort=when, sort=published, so we can’t use it without causing major breakage. Putting parens around it throws an exception somewhere deep in the codebase because it’s used for OR-groups. Since that doesn’t make sense in a sort selector, the syntax could probably be used, but I didn’t dare venture that deep, so I invented my own delimiter just for the POC. It’s /, i.e. you’d go pages()->find('template=person, sort=sortname/lastname|givenname'); and the full query would end up as: SELECT pages.id,pages.parent_id,pages.templates_id FROM `pages` LEFT JOIN field_givenname AS _sort_givenname ON _sort_givenname.pages_id=pages.id LEFT JOIN field_sortname AS _sort_sortname ON _sort_sortname.pages_id=pages.id LEFT JOIN field_lastname AS _sort_lastname ON _sort_lastname.pages_id=pages.id WHERE pages.templates_id = 69 AND pages.status < 1024 GROUP BY pages.id ORDER BY coalesce(_sort_sortname.data, _sort_lastname.data),_sort_givenname.data Which I think is a somewhat relatable real-world example (think: surname = “van Aaken”, sortname = “Aaken, van”). Unfortunately, the mod requires refactoring many lines into a method, so it’s a bit long to post here. But here it is regardless. Go to /wire/core/PageFinder.php and replace the entire function getQuerySortSelector() with this to check it out: protected function getQuerySortSelector(DatabaseQuerySelect $query, Selector $selector) { // $field = is_array($selector->field) ? reset($selector->field) : $selector->field; $values = is_array($selector->value) ? $selector->value : array($selector->value); $fields = $this->fields; $pages = $this->pages; $database = $this->database; $user = $this->wire()->user; $language = $this->languages && $user && $user->language ? $user->language : null; // support `sort=a|b|c` in correct order (because orderby prepend used below) if(count($values) > 1) $values = array_reverse($values); foreach($values as $value) { $fc = substr($value, 0, 1); $lc = substr($value, -1); $descending = $fc == '-' || $lc == '-'; $value = trim($value, "-+"); // $terValue = ''; // not currently used, here for future use if($this->lastOptions['reverseSort']) $descending = !$descending; if(!strpos($value, '/')) { $value = $this->processSortValue($query, $pages, $database, $fields, $descending, $language, $value); } else { $coalesce = []; foreach (explode('/', $value) as $value) { $value = $this->processSortValue($query, $pages, $database, $fields, $descending, $language, $value); if(is_string($value) && strlen($value)) $coalesce[] = $value; } if (!count($coalesce)) continue; $value = 'coalesce(' . implode(', ', $coalesce) . ')'; } if(is_string($value) && strlen($value)) { if($descending) { $query->orderby("$value DESC", true); } else { $query->orderby("$value", true); } } } } private function processSortValue($query, $pages, $database, $fields, $descending, $language, $value) { $subValue = ''; if(strpos($value, ".")) { list($value, $subValue) = explode(".", $value, 2); // i.e. some_field.title if(strpos($subValue, ".")) { list($subValue, $terValue) = explode(".", $subValue, 2); $terValue = $this->sanitizer->fieldName($terValue); if(strpos($terValue, ".")) $this->syntaxError("$value.$subValue.$terValue not supported"); } $subValue = $this->sanitizer->fieldName($subValue); } $value = $this->sanitizer->fieldName($value); if($value == 'parent' && $subValue == 'path') $subValue = 'name'; // path not supported, substitute name if($value == 'random') { $value = 'RAND()'; } else if($value == 'num_children' || $value == 'numChildren' || ($value == 'children' && $subValue == 'count')) { // sort by quantity of children $value = $this->getQueryNumChildren($query, $this->wire(new SelectorGreaterThan('num_children', "-1"))); } else if($value == 'parent' && ($subValue == 'num_children' || $subValue == 'numChildren' || $subValue == 'children')) { throw new WireException("Sort by parent.num_children is not currently supported"); } else if($value == 'parent' && (empty($subValue) || $pages->loader()->isNativeColumn($subValue))) { // sort by parent native field only if(empty($subValue)) $subValue = 'name'; $subValue = $database->escapeCol($subValue); $tableAlias = "_sort_parent_$subValue"; $query->join("pages AS $tableAlias ON $tableAlias.id=pages.parent_id"); $value = "$tableAlias.$subValue"; } else if($value == 'template') { // sort by template $tableAlias = $database->escapeTable("_sort_templates" . ($subValue ? "_$subValue" : '')); $query->join("templates AS $tableAlias ON $tableAlias.id=pages.templates_id"); $value = "$tableAlias." . ($subValue ? $database->escapeCol($subValue) : "name"); } else if($fields->isNative($value) && !$subValue && $pages->loader()->isNativeColumn($value)) { // sort by a native field (with no subfield) if($value == 'name' && $language && !$language->isDefault() && $this->supportsLanguagePageNames()) { // substitute language-specific name field when LanguageSupportPageNames is active and language is not default $value = "if(pages.name$language!='', pages.name$language, pages.name)"; } else { $value = "pages." . $database->escapeCol($value); } } else { // sort by custom field, or parent w/custom field if($value == 'parent') { $useParent = true; $value = $subValue ? $subValue : 'title'; // needs a custom field, not "name" $subValue = 'data'; $idColumn = 'parent_id'; } else { $useParent = false; $idColumn = 'id'; } $field = $fields->get($value); if(!$field) { // unknown field return null; } $fieldName = $database->escapeCol($field->name); $subValue = $database->escapeCol($subValue); $tableAlias = $useParent ? "_sort_parent_$fieldName" : "_sort_$fieldName"; if($subValue) $tableAlias .= "_$subValue"; $table = $database->escapeTable($field->table); if($field->type instanceof FieldtypePage) { $blankValue = new PageArray(); } else { $blankValue = $field->type->getBlankValue($this->pages->newNullPage(), $field); } $query->leftjoin("$table AS $tableAlias ON $tableAlias.pages_id=pages.$idColumn"); $customValue = $field->type->getMatchQuerySort($field, $query, $tableAlias, $subValue, $descending); if(!empty($customValue)) { // Fieldtype handled it: boolean true (handled by Fieldtype) or string to add to orderby if(is_string($customValue)) $query->orderby($customValue, true); $value = false; } else if($subValue === 'count') { if($this->isRepeaterFieldtype($field->type)) { // repeaters have a native count column that can be used for sorting $value = "$tableAlias.count"; } else { // sort by quantity of items $value = "COUNT($tableAlias.data)"; } } else if(is_object($blankValue) && ($blankValue instanceof PageArray || $blankValue instanceof Page)) { // If it's a FieldtypePage, then data isn't worth sorting on because it just contains an ID to the page // so we also join the page and sort on it's name instead of the field's "data" field. if(!$subValue) $subValue = 'name'; $tableAlias2 = "_sort_" . ($useParent ? 'parent' : 'page') . "_$fieldName" . ($subValue ? "_$subValue" : ''); if($this->fields->isNative($subValue) && $pages->loader()->isNativeColumn($subValue)) { $query->leftjoin("pages AS $tableAlias2 ON $tableAlias.data=$tableAlias2.$idColumn"); $value = "$tableAlias2.$subValue"; if($subValue == 'name' && $language && !$language->isDefault() && $this->supportsLanguagePageNames()) { // append language ID to 'name' when performing sorts within another language and LanguageSupportPageNames in place $value = "if($value$language!='', $value$language, $value)"; } } else if($subValue == 'parent') { $query->leftjoin("pages AS $tableAlias2 ON $tableAlias.data=$tableAlias2.$idColumn"); $value = "$tableAlias2.name"; } else { $subValueField = $this->fields->get($subValue); if($subValueField) { $subValueTable = $database->escapeTable($subValueField->getTable()); $query->leftjoin("$subValueTable AS $tableAlias2 ON $tableAlias.data=$tableAlias2.pages_id"); $value = "$tableAlias2.data"; if($language && !$language->isDefault() && $subValueField->type instanceof FieldtypeLanguageInterface) { // append language id to data, i.e. "data1234" $value .= $language; } } else { // error: unknown field } } } else if(!$subValue && $language && !$language->isDefault() && $field->type instanceof FieldtypeLanguageInterface) { // multi-language field, sort by the language version $value = "if($tableAlias.data$language != '', $tableAlias.data$language, $tableAlias.data)"; } else { // regular field, just sort by data column $value = "$tableAlias." . ($subValue ? $subValue : "data"); ; } } return $value; } Almost all of this is original PW code. The only changes are: The method processSortValue() was inside getQuerySortSelector() before. I think I only added the return at the end and changed a continue to a return in the middle. Of course, the bit where it goes if(!strpos($value, '/')) is new. The method had to be factored out because it assumes a single “value”, but since we’re injecting multiple values into a single value by our fabricated ”/“ delimiter, we want to call it in our own loop. It figures out what joins to add to the query and transforms the original value from the selector into the corresponding SQL expression (at one point it transforms “random” into “RAND()”, to name a simple example that doesn’t mutate anything outside the method). I have probably missed all kinds of things and introducing yet another arcane operator to selectors may not be a great idea, but my site seems to be running fine with this hack, and it seems to work. -
What exactly is your question? There are actually multiple ecommerce modules available these days: https://www.baumrock.com/en/processwire/modules/rockcommerce/ https://processwire.com/talk/topic/31317-introducing-processwire-commerce/
-
A module for managing login and register pages?
Jan Romero replied to Manaus's topic in Module/Plugin Development
There's LoginRegisterPro: https://processwire.com/store/login-register-pro/ And the older, free LoginRegister: https://github.com/ryancramerdesign/LoginRegister You can also roll your own, a simple login handler might look like this: <?php namespace ProcessWire; http_response_code(400); header('content-type: text/plain'); $username = input()->post->selectorValue('username'); $potentialUsers = users()->find("roles=my-user, name|email={$username}"); if ($potentialUsers->count != 1) die('Uh-oh.'); try { if (!session()->login($potentialUsers->first(), input()->post->pass)) die("Username or password invalid."); } catch (SessionLoginThrottleException $e) { http_response_code(429); die($e->getMessage()); } http_response_code(200); header('HX-Redirect: '. pages()->request()->getRequestPath()); die('Hello.');- 1 reply
-
- 1
-
-
Users as article authors in a dropdown field?
Jan Romero replied to Manaus's topic in General Support
In your page reference field’s settings under Input -> Selectable Pages -> Parent, you can select /admin/access/users. If you don’t specify anything else, that should make all users available for selection. You can of course narrow it down more by using the other settings. I’m sure you’re aware that ProcessWire keeps track of which User created an Article page anyway. It may be a viable option for you to just use that (if the default behaviour doesn’t work for you, override $page->createdUser in a hook or enable editing the user through the advanced template options). Using the built-in user field should improve performance since it’s part of a Page’s basic data, not a regular field, and it may be more idiomatic, depending on what exactly you’re doing.- 1 reply
-
- 2
-
-
I see the problem now. It seems weird that ProcessController::isAjax() even exists, tbh. It may be vestigial and can be replaced by $config->ajax, which you could then override yourself in ready.php. But maybe it’s got some vital purpose. I’d say, open an issue about it on Github. Some other options you can implement right now without hacking the core: Just die() public function ___executeCount() { $count = (int)$this->input->post('count'); die($this->renderCountButton(++$count)); } Instead of returning markup from your execute() method, just terminate the request with your desired output. This will skip things like finished.php, so that’s something to be aware of, but it’s probably fine. Just add X-Requested-With public function ___execute() { $this->config->scripts->add('https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js'); return <<<HTML <div hx-headers='{"X-Requested-With": "XMLHttpRequest"}'> <h2>Hello.</h2> {$this->renderCountButton(0)} </div> HTML; } public function ___executeCount() { $count = (int)$this->input->post('count'); return $this->renderCountButton(++$count); } Make HTMX send the X-Requested-With header. Now you're an ajax request according to ProcessWire. The attribute is inherited, so you can just set it once and forget about it, as in the above example. I have attached a complete module that minimally demonstrates the issue. ProcessHtmxTest.module
-
where or how can we define the modules load order priorities?
Jan Romero replied to horst's topic in General Support
You can apparently set autoload to any integer, the higher the earlier: https://github.com/processwire/processwire/blob/1b0d51e2751aec462b431fc736c3e16e6502c9aa/wire/core/Module.php#L218 Numbers >= 10000 (plus some other conditions) make it a “preload” module and move it up even before core modules. Not sure if there’s a way to change the order of modules without modifying them. -
ProcessWire Commerce: Help Needed
Jan Romero replied to kongondo's topic in ProcessWire Commerce (Padloper) Support
Doesn’t GitHub have a built-in wiki feature? I don’t come across GitHub wikis much, but apparently you can open them up to anyone with an account, and mix and match text formats: https://docs.github.com/en/communities/documenting-your-project-with-wikis/adding-or-editing-wiki-pages -
Using $files->render() in a loop is expensive, cache it!
Jan Romero replied to elabx's topic in Getting Started
Interesting! Unfortunately it doesn’t seem like this can be used with $page->render(), because it’s built outside of TemplateFile::render(), but I’ve only taken a quick look at the code on my phone, so I may be wrong… I have a lot of places where I wrap stuff like this in cache()->get(). -
form->processInput doesn't catch missing InputfieldCheckbox
Jan Romero replied to thausmann's topic in General Support
Interesting. I think I’ve figured out why this happens and I opened an issue here: https://github.com/processwire/processwire-issues/issues/2074 -
form->processInput doesn't catch missing InputfieldCheckbox
Jan Romero replied to thausmann's topic in General Support
Idk, it works as expected for me. The only thing I changed from your code was removing $field->attr('required', true), so the browser would let me send without checking the box. And making everything into plain functions so I could just put them in a template. The required-check happens in InputfieldWrapper and there is no special logic for checkboxes (InputfieldCheckbox::processInput() will find no errors, leaving it to InputfieldWrapper). You can check it out here: https://github.com/processwire/processwire/blob/44fcf13ea2d7f14a04eed54c29afcc79eb46ec45/wire/core/InputfieldWrapper.php#L1304 // check if a value is required and field is empty, trigger an error if so if($child->attr('name') && $child->getSetting('required') && $child->isEmpty()) { $requiredLabel = $child->getSetting('requiredLabel'); if(empty($requiredLabel)) $requiredLabel = $this->requiredLabel; $child->error($requiredLabel); } The error value will be “%consent_label% - Missing required value”. As we can see, this depends on InputfieldCheckbox::isEmpty(), which simply negates InputfieldCheckbox::checked(). The rest is a little weird, but suffice it to say that the posted form data is examined using PHP’s empty(), so if any value at all has been sent for the checkbox (“newsletter_consent”), it’ll pass the required check. By default the value would be the string '1', and if unchecked, as we know, no mention of the checkbox would be sent at all. -
+1. This may not sound very shiny and impressive, but you see a lot of systems where URLs are somehow divorced from the data structure and it’s a big turn-off for me. PW’s more manual routing features (urlsegments, path hooks) are also awesome.
-
Neat. FWIW Tracy also shows hooks that were triggered during the request (in the debug panel). If you, like me, usually define your hook methods as closures, it’ll just say “{closure}”, but it does link to the source, so that’s nice! On the other hand it seems to show all hooks that were defined and doesn’t say whether they actually ran.
-
I feel like it should be supported, since PW itself uses two modules directories, core and site, and they’re not hardcoded. When ProcessWire instantiates itself, it calls addPath() for /site/modules/ here (the core modules path is added in Modules::__construct): // from ProcessWire::load(Config $config) $modules = $this->wire('modules', new Modules($config->paths->modules), true); $modules->addPath($config->paths->siteModules); $modules->setSubstitutes($config->substituteModules); $modules->init(); https://github.com/processwire/processwire/blob/3cc76cc886a49313b4bfb9a1a904bd88d11b7cb7/wire/core/ProcessWire.php#L557 The problem is that addPath() needs to be called between the instantiation of Modules and the init() call and there’s no way for us to get in there. Unfortunately there don’t appear to be any hookable methods during this process (I don’t think hooks can be attached before this point anyway?). But I think you can get away with just adding your path after the fact and calling Modules::refresh(): // seems to work in init.php wire('modules')->addPath(__DIR__ . '/modules-2/'); wire('modules')->refresh(); Of course this adds a lot of unnecessary cost to every request, because it basically loads all modules again. And I don’t think it’ll work for preload modules. If this is important to you maybe ask Ryan to make this a config setting? ProcessWire already uses an array for module directories, only the assumption that the first two entries are core/modules and site/modules seems to be baked in.
-
Chill 😅 You could post little travel logs instead next time 😉
- 4 replies
-
- 12
-
-
saving ..sometimes, and nothing in errors.txt
Jan Romero replied to joe_g's topic in General Support
May want to pass ['noHooks' => true] to the save() call to prevent the hook from being called recursively: https://processwire.com/api/ref/pages/save/ (I also like the 'quiet' option). -
[Solved] How you work with pw on external server
Jan Romero replied to olivetree's topic in Getting Started
I have a local copy that uploads changes via FTP on every save because it’s easy. If I’m just trying stuff out in production I slap “if (user()->isSuperuser())” on it 🤣 -
This seems to be because the XHR request that gets the live results as JSON requests the same URL and headers as a normal visit to the search page, so the browser gives you its cached version. You can also observe it the other way around: If you head to https://processwire.com/search/?q=hello and then use the search box for the same keyword, the browser console will show a JSON parse error because the live search got the full page instead of JSON. I imagine this could be fixed by sending the ol’ X-Requested-With: XMLHttpRequest header. Or by using the appropriate content negotiation header (I think “Accept: application/json”?).
-
RockMollie - Integrates Mollie Payments into ProcessWire.
Jan Romero replied to bernhard's topic in Modules/Plugins
Interesting, I hadn’t heard of Mollie. You seem to have something misconfigured, might want to adjust the page name: It’s currently ”rockmollie-1“, but your short url forwards to “rockmollie” 🙂 -
Adding multiple pages to page reference field within a foreach php
Jan Romero replied to Liam88's topic in Getting Started
Adding all pages and then saving is the way to go. ProcessWire’s arrays have change tracking so they’ll know which items were added or removed. You can dump the field before saving and you should see a property called "added" or some such that lists them. Try TracyDebugger’s bd() method (recommended!) or just var_dump(). -
Adding multiple pages to page reference field within a foreach php
Jan Romero replied to Liam88's topic in Getting Started
Any particular reason for this curly brackets syntax? I’m far from a PHP pro but I’ve never seen that and $assetPage->content_tags->add($tagPage) should suffice. I might look at your code again later when I’m on desktop. Also, might be obvious, but make sure you save the tagPages before adding them 😅