-
Posts
695 -
Joined
-
Last visited
-
Days Won
20
Jan Romero last won the day on July 24
Jan Romero had the most liked content!
Profile Information
-
Gender
Not Telling
-
Location
Germany
Recent Profile Visitors
Jan Romero's Achievements
-
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.