Leaderboard
Popular Content
Showing content with the highest reputation on 09/14/2017 in all areas
-
Just something I was trying out recently that might be useful to someone... With the following hook added to /site/ready.php you can adjust the CKEditor toolbar that a particular role gets for a particular field. Use the name of your CKEditor field in the hook condition. $this->addHookBefore('Field(name=my_ckeditor_field)::getInputfield', function(HookEvent $event) { $field = $event->object; // Define toolbar for a particular role if($this->user->hasRole('editor')) $field->toolbar = 'Format, Bold, Italic, -, NumberedList, BulletedList, Outdent, Indent'; }); Or what I find useful on some sites is adding extra toolbar buttons for superuser only that you don't trust editors to use. $this->addHookBefore('Field(name=my_ckeditor_field)::getInputfield', function(HookEvent $event) { $field = $event->object; // Add extra buttons for superuser only if($this->user->isSuperuser()) $field->toolbar .= ', Table, TextColor'; }); You could use the same technique to selectively define other CKEditor settings such as 'stylesSet', 'customOptions', 'extraPlugins', etc.9 points
-
I've found a cleaner way to modify prepend/append files depending on the current page. wire()->addHookBefore('PageRender::renderPage', function (HookEvent $e) { /** @var Page $page */ /** @var HookEvent $event */ /** @var Template $template */ $event = $e->arguments(0); $options = $event->arguments(0); $page = $event->object; $template = $page->template; $options['prependFiles'] = [ "{$template}.routes.php", "_common.php", ]; $options['appendFiles'] = [ "views/{$template}.php", "_after.php", "layouts/main.php", ]; $event->setArgument(0, $options); }); The older way felt too much like a hack to me. To early exit and skip including remaining files during render, use `return $this->halt();` anywhere in your templates.4 points
-
I really would try upgrading to latest dev first - that wireInstanceOf function adds a lot of extra logic that I assume will take care of things when $type is null.4 points
-
From what it looks like, $user is the currently logged in user - this check is outside your functions, so I don't see that this is actually doing anything. The email check looks fine This should also be ok, but remember the issue I mentioned with the first line. This turns off output formatting which is important when saving fields to a page - consider a datetime - you want to save a unix timestamp, and not a formatted string like 14 September, 2017 7:17:11 am Really your call - there are arguments for both approaches. You might actually find things simpler if you try this approach to user pages: https://processwire.com/blog/posts/processwire-core-updates-2.5.14/#multiple-templates-or-parents-for-users which lets you set up users with URL accessible pages directly. BTW - a great writeup of your approach here - I am sure others will learn lots!3 points
-
you should definitely invest some minutes and start writing process modules. it's incredible how fast you can create your very own admin GUIs and you will learn to understand processwire a lot better define "success" i agree with most of all posts above but i also want to say that i like the fact (or at least it seems to be like this) that we do NOT attract a lot of "i am looking for a click click and it's done" users. i like the way people here behave, trying to learn things, trying to understand, trying to find their way around the code... maybe if processwire looked more fancy that would be different. i don't know. and maybe it's not ryans goal to become more popular. the roadmap states it as a goal for 2017 - but i'm not sure how important it is for ryan. personally i would love to hear more personal and strategic infos regarding the project. who is the team behind processwire? is it only ryan? what happens if ryan can not continue to work on it? he said at several occasions that there ARE people that could continue the project but i would love to have a more reliable solution/statement for that. i don't really care if we have 10.000 or 10.000.000 users - but i do care if i have enough certainty to be able to work with some kind of software on the long run. to be honest i have some concerns about the last point sometimes. but i calm myself knowing that even if processwire would not get updated for a long time it would still run safely and without any issues and i would have enough time to learn something else3 points
-
I'm making progress with all the necessary changes. On my dev server (not git-pushed yet), the following is complete: Languages: Multi-language is sorted. It now renders the sitemap according to Google standards, where a page's <url> is generated for each languages with alternate langs, including current. Method is not the same as above example (page-loop), but expands on the root-first-then-children process - this allows easier flexibility in terms of what we can ignore on a page by page basis. ISOs: Haven't created the separate language ISO field yet, as my my main concern is regarding names vs ISOs. If they are normally identical, then we would only need to declare a proper ISO name for the default language only when its URL matches the default root URL (ie: default URL root = site root). In this case, perhaps it would be better to simply have a module config entry for the default language only? The rationale behind this is that the language name (other than default) should be the same as the iso (for better SEO practice, I'd guess). I haven't yet bumped into a ProcessWire site that doesn't use the ISO for each language name accordingly. If I'm wrong, please point it out, and then I'll use the iso field, per the above example. Or perhaps I should simple allow the flexibility and have the ISO name fall back to the language name if not provided via a field (the default would still need one if it is root)? Image-support: Images are now picked up in all languages, given the process-change. If an image field is not specified in another language, it will fall back to the default language. Support for ImageExtra remains. Priority: not done yet. I'll follow a basic principal with this: if the priority is set on a specific page, then it will be added. Otherwise, the tag will be left out. If this is not the correct methodology, please let me know. (cc @psy) Note: I still need to do some testing on this...2 points
-
Can we set up a Slack channel for Processwire? My key concern is to have ProcessWire stick around and look the part. I want my clients to easily find other devs, see that there is a team and a community behind ProcessWire and have it come across as a modern platform that will solve their business needs.2 points
-
the button somethink like this <a href="./?add=1">add item</a> the code something like this (in your template) <?php if($input->get->add) { $parent = $pages->get( your parent page ); if($parent->addable()) { // create a new page and save it } } // continue with your regular template code foreach($pages->find('yourselector') as $p) { // list your products // will already contain your new item } you could also make different add-buttons that hold some variables (like category or the like) --- delete: <a href="./?delete=123">delete item</a> and the code: <?php if($input->get->delete) { // find page that should be deleted $p = $pages->get($input->get->delete); if($p->id AND $p->trashable()) $pages->trash($p); } this would always need a page reload. with ajax the user experience would be much nicer (page not jumping around) hope that helps - but maybe someone has better suggestions2 points
-
Following on from this post: I got to work in trying to make this a reality. After studying the API for a bit plus reading examples, I got a sign up form going: //views/signup.php <?php namespace ProcessWire; function createUser($user, $email, $password) { // create user $u = new User(); // set name $u->name = $user; // set email $u->email = $email; //set password $u->pass = $password; // assign role of "member" $u->addRole("member"); // save new member $u->save(); // if successful $success = true; return $u; } function createProfile($user) { // create profile page $p = new Page(); // set template (can only be child of profile-index) $p->template = "profile-entry"; // set page name $p->name = $user->name; // set page title $p->title = $user->name; // set page title $p->altTitle = "Profile page for " . $user->name; // set page reference to correpsonding user page $p->userRef = $user->id; // save page $p->save(); } // input field validation wireIncludeFile("./vendor/vlucas/valitron/src/Valitron/Validator.php"); // sanitize input field values $user = $sanitizer->userName($input->post->inputUsername); $email = $sanitizer->email($input->post->inputEmail); $password = $input->post->inputPassword; $v = new \Valitron\Validator(array( "user" => $user, "email" => $email, "password" => $password ) ); $v->rule("required", ["user", "email", "password"]); $v->rule("email", "email"); $v->rule("lengthMin", "password", 6); // if form has been submitted if($input->post->sendMe) { if ($v->validate()) { // return user if exists $u = $users->get($user); // return user if email address exists $e = $users->get("email=$email"); // if both user or email don't exist if (!($u->id) && !($e->id)) { // create user and profile createProfile(createUser($user, $email, $password)); // return result of attempted login $u = $session->login($user, $password); // check if user now exists if (!is_null($u)) { $session->redirect($pages->get(1037)->url . $user); } } else { $session->success = false; // email taken if ($e->id) { $session->flashMessage = 'Email already exists. Please use another email.'; } // user taken else { $session->flashMessage = 'User already exists. Please choose another username.'; } } } else { $session->success = false; $session->flashMessage = 'All fields must be complete.'; } } ?> <div class="container"> <div class="row justify-content-center py-5"> <div class="col-6"> <?php if($session->flashMessage):?> <div class="alert <?= $session->success ? 'alert-success' : 'alert-danger'?>" role="alert"> <?php echo $session->flashMessage;?> </div> <?php endif;?> <form action="./" method="post"> <div class="form-group <?php echo $v->errors('user') ? 'has-danger' : ''?>"> <input type="username" class="form-control" name="inputUsername" placeholder="Username" value="<?= $user; ?>"> </div> <div class="form-group <?php echo $v->errors('email') ? 'has-danger' : ''?>"> <input type="email" class="form-control" name="inputEmail" placeholder="Email" value="<?= $email; ?>"> </div> <div class="form-group <?php echo $v->errors('password') ? 'has-danger' : ''?>"> <input type="password" class="form-control" name="inputPassword" placeholder="Password"> </div> <div class="form-group"> <button type="submit" name="sendMe" value="1" class="btn btn-primary">Create account</button> </div> </form> </div> </div> </div> <?php $session->remove('flashMessage'); ?> I've got templates: profiles-index (just used in the tree to hold the profiles) profile-entry (hold the individual profiles) I put a page ref field on profile-entry, which points to the user. See question (3) below. So, I end up with a URL like 'mysite.com/profiles/beastman' So the questions I have: 1) Is there a better way of doing this: // return user if exists $u = $users->get($user); // return user if email address exists $e = $users->get("email=$email"); // if both user or email don't exist if (!($u->id) && !($e->id)) { Any advice on writing better code, then throw it my way please. I'm trying different techniques like using functions etc. for practice. 2) I haven't used this code, but what does: $u->of(false); ...do? I see this in a lot of the login forms examples. i found it in the API but still not sure why to use it. 3) Have I done this the right way round? Should the page reference be on user template pointing to the profile, or the on the profile template pointing to the user? The whole process goes something like this. Go to /signup/: Validation, fields not filled out: Validation, user already exists: Validation, email already exists: Enter some details, logs you in and redirects: Default login page: Refresh the admin login page: So now I can at last demonstrate my issues with users getting the tree view vs having a custom dashboard. Bear in mind they will only get to this page if they use the admin login form. If they go to the tree view here, I have another problem. If they click profile name (top right), then go to change the password, the rules are different from the ones I set in the initial sign up form. The built in rules are "Minimum requirements: at least 6 characters long, letter, digit." My rule was just 6 characters at least. Anyway, going off topic. The "where does the user go and how do they change their profile details once they've logged in" bit part confuses me. Presuming they could go to a dashboard or something else if they used the 'other' login form block I'm working on and avoid the admin backend entirely: So, the new user is in the system: and: Sweet! Basic, pretty useless, but a great learning experience so far --EDIT-- Added some more images to show how the form works when validation fails. I would recommend recaptcha too though to stop spam users: https://modules.processwire.com/modules/markup-google-recaptcha/ Once you've got a site key from Google, it's as simple as configuring the module in the PW admin then in your template: $captcha = $modules->get("MarkupGoogleRecaptcha"); // render the recaptcha <?= $captcha->render(); ?> For valitron, install composer then (I ran this command from inside /site/templates/): composer require vlucas/valitron ...results in '/site/templates/vendor' folder which has valitron inside. Then just include the file: wireIncludeFile("./vendor/vlucas/valitron/src/Valitron/Validator.php"); Hopefully this will help someone2 points
-
It looks to me like you are running 3.0.62 (or thereabouts) as line 374 of that file looks like: } else if(strpos($type->className(), 'FieldtypeRepeater') !== false) { I would suggest upgrading to the latest dev and see if that fixes it. The new version uses this instead: } else if(wireInstanceOf($type, 'FieldtypeRepeater')) { Otherwise, we'll need to debug $field (type and name) to see what field is causing it to hang up.2 points
-
2 points
-
@kongondo You wont be creating these files manually of course, you extend logic for the signup form to create the files for you. Then in index.config.php, you parse, merge and return the array. Something like this function ProcessWireHostSiteConfig() { $configDir = __DIR__ . '/.config/'; $files = wire()->files; $siteConfigs = $files->find($configDir, [ 'extensions' => ['yaml'] ]); $sites = []; foreach ($siteConfigs as $cfg) { $config = yaml_parse_file($cfg); // needs better sanitization $isValidConfig = true; if (!$isValidConfig) continue; $sites[$config->domain] = $config->siteDir; } $sites['*'] = 'site'; return $sites; }2 points
-
Why not create a simple JSON/YAML config file (one for each domain) in a folder with strict permissions then in your index.config.php parse all files in the folder to build your array? Or from a small sqlite db if that's your thing. http://php.net/manual/en/book.sqlite3.php http://php.net/manual/en/yaml.examples.php2 points
-
Good point To me, in the case of ProcessWire it means that ProcessWire "never" becomes obsolete, at least not in my life. That is why I have the same concerns as you have, see: Becoming a tiny bit more popular could attract more dedicated developers who could form a strong dev team behind ProcessWire in the long run (stronger then we have now, 'cos we sure have one it is just not so obvious as they partially work behind the scenes). Maybe I'm just daydreaming, I dunno2 points
-
Okay, so it turns out I was wrong, but only for the bottom, which kinda makes sense for them, but may not work nicely in a range of scenarios. Edit: landscape mode crops both sides.2 points
-
Superusers may edit individual repeater items inline (not in a modal) but non-superusers may not. There's an open issue regarding it (with a possible workaround you can use at your own risk) but based on Ryan's comment here I think he is not inclined to view this as a bug to be fixed.2 points
-
That # symbol has bugged me too: https://github.com/processwire/processwire-issues/issues/284 When you look into the Repeater module code it seems very much like that symbol only exists to allow a Javascript fix to a problem that could be easily solved in the module PHP.2 points
-
I am not sure if this is the right thread anymore to discuss the redesign of ProcessWire.com, but as an example how a style guide could look like, I have made a little prototype in Sketch (my favorite prototyping app) which you can find here: I think it would be great, if the community could help @ryan to build the new ProcessWire.com. In my opinion this would boost the popularity of ProcessWire a lot.2 points
-
I got a client request to make Word template so she can create an image of the weekly menu of a restaurant to post on Facebook. I usually get sick when I have to deal with M$ Word - it's frustration and it's not meant to be used things like that. So I figured out a better system: PW + html2canvas.js. It took more time to finish but it was worth the trouble. The main issues were html2canvas versus some CSS issues, plus switching to different languages in PW on each line. Also, JavaScript cannot offer named downloads, only "download.png", so I had to use HTML5 attribute "download" on links to be able to add custom download filename. I used ProField modules Table and Matrix Repeater which allow very pleasant editing of menu items, even in multilanguage context. Front-end editing is created using the built-in front-end editor. I was about to achieve inline editing of menu items but it soon turned out that repeaters and tables can't be edited this way. Anyway, it's still better compared to my module FEEL because sections are reloaded using Ajax and the admin lightbox shows only the fields I choose. Here's a quick screencap - when the gray background turns to dark gray, then the menu is not HTML but canvas, that can be downloaded.2 points
-
Found myself needing to filter repeater items based upon start and end date fields. These fields aren't required, so repeater items with no dates should be included and those with dates (or a just one date -- start or end) should be evaluated for inclusion. I slowly figured out that I was dealing with in-memory filtering (of a RepeaterPageArray) and not a database-driven operation. So, that eliminated the possibility of using or-groups in my selector. I still thought I could use the 'filter' and 'not' functions to get the job done. Turns out the way selectors are handled for in-memory filtering is quite different from database-driven selectors. Here is what I want(ed) to do to filter the dates: // Start date not specified or in the past $repeater->filter('start_time<='.$now); // Not when end date is specified and has passed $repeater->not('end_time>0, end_time<'.$now); The first one worked exactly as expected. The second it where I ran into problems. The 'filter' function (which calls the 'filterData' function) takes a $selector argument. From everything I read about selectors, I assumed that the entire selector would have to match. In other words, each comma represented an 'and' scenario. Turns out that each separate comma-separated-selector in the $selector as a whole gets evaluated independently. In the case of the 'filterData' function, the elements are removed if any one of those selectors matches. Essentially, we have an 'or' scenario instead of an 'and' scenario. In my case above, there was no way for me to filter for a non-empty date AND one that occurs before a given date...at least that I could think of. So, my main question is if the filter (and related) function should operate on the entire selector passed to the function instead of its individual parts in some sequence. That is what I would have assumed a selector to do. In my project, altering the segment of the WireArray::filterData function solved my problem for now. ...at least till I forget about it and overwrite it when I upgrade next. Here is the code I changed within the function // now filter the data according to the selectors that remain foreach($this->data as $key => $item) { $filter_item = true; foreach($selectors as $selector) { if(is_array($selector->field)) { $value = array(); foreach($selector->field as $field) $value[] = (string) $this->getItemPropertyValue($item, $field); } else { $value = (string) $this->getItemPropertyValue($item, $selector->field); } if($not === $selector->matches($value)) continue; $filter_item = false; break; } if($filter_item && isset($this->data[$key])) { $this->trackRemove($this->data[$key], $key); unset($this->data[$key]); } } I would love to hear what you all think. If I have misunderstood something, then I would welcome a different solution. Thanks!1 point
-
Yes, that's what it boils down to basically. There are several reasons for this depending on the fieldtype to be edited, but in the case of a repeater PW has no idea what you are doing in terms of displaying your repeater output on the frontend - it gives you total flexibility there. So PW cannot really provide an inline frontend interface for drag-sorting repeater items for instance, because there is no guarantee you are even presenting all of your repeater subfields contiguously inside a single containing div. So I think you'll need to resign yourself to modal editing unless you are up for some serious custom frontend interface work. I'm curious to see the existing interface your client is using - it sounds quite fancy. Maybe you could post a video? (LICEcap is useful) Repeaters scale to a medium level fine so long as you use the AJAX-loading setting. I haven't tried Table with frontend editing but I think any fieldtype becomes editable via a modal.1 point
-
Welcome to the forum! You mean an "exported site profile"? Did you export it form the same version you are trying to install? If not, you might want to try that first. Please note that you do not have to do it by exporting the site profile and installing it along with ProcessWire. You can simply clone the database, copy over all files and change at least Database Configuration and $config->httpHosts in /site/config.php accordingly. Should you have issues afterwards, you can delete the contents of /site/assets/cache EDIT: I forgot to mention that .htaccess might need different rules too, but that a is server specific issue1 point
-
Maybe people should be forking some of these kind of modules and breathing some new life into them.1 point
-
1 point
-
I tend to do this sort of thing where I attach all the user based fields to the current user i.e. $user->fieldname instead fo creating a new page each time, then simply create a profile template shows those fields. which is fine if you have to be logged in, in your case you could enable page url partials in the profile tamplte and look for the partial (your user name input) to get the same effect.1 point
-
as far as i understood this would only be possible for superusers and i would definitely not give your client superuser access... i think you have those options: profield table backend gui, but very clean and easy to manage, 150 items is a lot though repeater backend gui, little more "bloated", not sure how this would work for 150 items... pagetable you could use FE, but you need to implement add/delete/move functionality on your own. add/delete would be very simple, move would be a little more complicated. you could also do the sorting based on the position number (item number, seems to be your first field). that would maybe be not so user friendly. how do you plan to do the item numbering? automatically? why do you plan to have a field for that?1 point
-
I upgraded to the latest dev and the problem is gone, so you were absolutely right @adrian. Thanks! A side note: I still might go for a clean install to get rid of some old, now obsolete, modules and scrappy coding from my side. A new start makes it also easier to use new PW features.1 point
-
you could use the core pagetable field, then you would also have frontend editing. adding and removing items would need some manual work but would not be very hard (send an ajax request, create a page with dummy-data, reload the frontend, double click to edit text)1 point
-
When adding link ProcessPageEditLink module checks for file fields on the page and repeater items. And does a recursion when it encounters a repeater field. Reading the core, it looks like getting the type of a field fails (field->type == null) and causes the error. Which version of PW are you using? Do you have a repeater field on the page? Uninstalled a fieldtype? Removed a field from repeater field or the page's template? Tried repairing the DB? protected function getFilesPage(Page $page, $prefix = '') { $files = array(); foreach($page->template->fieldgroup as $field) { /** @var Fieldtype $type */ $type = $field->type; if($type instanceof FieldtypeFile) { $value = $page->get($field->name); if($value) foreach($page->get($field->name) as $file) { $files[$file->url] = $prefix . $field->getLabel() . ': ' . $file->basename; } } else if(wireInstanceOf($type, 'FieldtypeRepeater')) { $value = $page->get($field->name); if($value) { if($value instanceof Page) $value = array($value); if(WireArray::iterable($value)) { foreach($value as $repeaterPage) { $files = array_merge($this->getFilesPage($repeaterPage, $field->getLabel() . ': '), $files); } } } } } return $files; }1 point
-
awesome, thanks again, got the export working now for them...1 point
-
I haven't looked at your code closely, but if you need to write to a file, you need to write to a file. You have file_put_contents and fwrite. I see no other way around it irrespective of the software you use. ProcessWire itself writes to /site/config.php/ during install. I use this multi-site setup myself. I've been meaning to write some code or a module to automate the process but never got round to it. You could do with some error checking and backing up the original file as part of the updating the file.1 point
-
@Lenz what is so bad about the admin UI? see this post for example using the adminbar module there is also FREDI and FEEL. not as good as real frontend-editing, i admit, but close1 point
-
Yeah, we've been missing you welcome back to the best community support forum on Earth.1 point
-
Yep, the old version is linked to in this post: https://processwire.com/talk/topic/7905-profields-table-csv-importer-exporter/?do=findComment&comment=1509531 point
-
FYI, a while ago a wrote a little tutorial demonstrating how an array_chunk method can be added to WireArray:1 point
-
Hi and welcome @dscONE! Yes, I do think the behaviour you have discovered is unexpected. Just to reiterate in a simplified way what I think you are saying.... The expected behaviour is that for any selector, the combined results of $pagearray->find($some_selector) and $pagearray->not($some_selector) should equal the entire PageArray. But this is not the case, as can be demonstrated with a PageArray containing three pages titled "red car", "blue car", "green truck"... $matches = $pagearray->find("title~=car"); // "red car", "blue car" $not_matches = $pagearray->not("title~=car"); // "green truck" // So far, so good $matches = $pagearray->find("title~=car, !title~=red"); // "blue car" $not_matches = $pagearray->not("title~=car, !title~=red"); // expected "red car", "green truck" but got unexpected empty PageArray Would you please report this issue over at the PW issues GitHub repo? https://github.com/processwire/processwire-issues/issues1 point
-
A post from Ryan where he shows how you can use MarkupPagerNav to paginate an image field: https://processwire.com/talk/topic/390-images-pagination/?do=findComment&comment=29281 point
-
Sorry for the late reply; I have been on holiday for a while. This module acts only on changes to a Page Reference field - this is deliberate for reasons of efficiency. If you already have some Page Reference values in place before you start using the module on those fields then you can get things set up with a one-off API operation: // Get all pages where page_field_a is not empty $pgs = $pages->find("page_field_a.count>0"); // Get derefAsPage property of page_field_b to distinguish multiple/single page fields $deref = $fields->get('page_field_b')->derefAsPage; foreach($pgs as $p) { // For this page, get the pages selected in page_field_a $selected_pages = $p->page_field_a; // Standardise $selected_pages to PageArray in case of "single" field if($selected_pages instanceof Page) $selected_pages = $selected_pages->and(); foreach($selected_pages as $selected_page) { // Add this page to page_field_b on the selected page $selected_page->of(false); if($deref) { $selected_page->page_field_b = $p; } else { $selected_page->page_field_b->add($p); } $selected_page->save(); } } You can do a similar operation for page_field_b -> page_field_a if needed. Once you set up the Page Reference field pair in the module config then the module will take care of all changes from that point onwards.1 point
-
@Zeka, that's a different method. That add() is for adding a new page to a specified parent and assigning it a given template. The add() @celfred is referring to is the WireArray method documented here and as seen here. It adds an item to the end of a WireArray (and its derivatives, e.g. PageArray). @celfred It shouldn't add without a save. There's something funky going on in your code. It's hard to pinpoint where because we can't see the original code. Maybe there's some other save or $real is being overwritten somewhere. I've tested your pseudo function and it works as expected in both PW 2.7 and PW 3.0.71. If I pass $real as false, the $page is not added to the Page Field. Btw, though made up, this variable name will throw an error $new-eq1 point
-
hehe... fair enough to check for a closing tag Next time I'll promise I'll use correct markup which would have solved the problem right away. Thanks for your support1 point
-
Sounds off topic but may not be so. As pointed out earlier and above, documentation is an important part of the success. Everyone knows that (probably...) And building front-end logins, forms, admins, dashboards, modules and stuff like that is not documented, except for module and core code comments and hard to scan blog posts, of course. But those are hidden bits of information and kinda time consuming to dig up. Also, basic frontend design tutorials are outdated. I propose to make 2018 the year of the documentation, so to speak. What if we could stop hunting for new features for a while and concentrate on the following in 2018: new processwire.com new UIkit admin theme documentation: updated basic frontend design tutorials and lots of real world form API tutorials for the masses. Just my two cents here1 point
-
I wish, I wish... ProcessWire had loooooots of @ryans behind it. As it stands, we all depend on him. As long as he is with us, all is good though...1 point
-
I just forked the module and added support for repeaters (only on the multi select inputfield for now); it basically involves 2 steps - changing the module code to use mostly the renderReady method instead of render(), and then some small update to the js file to init the field on the necessary events, which for the sake of brevity also involves moving the init code into its own function var... I submitted a pull request; in the meantime if you need this to work now, you can try the forked version https://github.com/outflux3/InputfieldChosenSelect1 point
-
I'd still keep the Page fields, and use two fields to determine all jobs and current jobs a person has. What you should do is to pull up your sleeves and get down to build a select field, maybe extend @ryan's asmSelect, or more modern alternatives (chosen.js select2.js etc) that have tagging support. In fact, I had some free time and went ahead and created a signup form that does this. I used selectize.js for selecting/creating jobs on the frontend. Options load with AJAX at page load Once you submit the form, you get this On the backend, I get the input, sanitize it and filter values, if a new job name is specified, I create the page, and add it to `jobs` or `currentJobs`. Here's the code in all its glory: <?php namespace ProcessWire; /* @var $input WireInput */ /* @var $sanitizer Sanitizer */ /* @var $pages Pages */ if (input()->requestMethod('GET') && input()->urlSegment1 === 'jobs.json') { // serve json for input fields $jobs = pages('template=profession, sort=title'); echo wireEncodeJSON($jobs->explode(['id', 'title'])); return $this->halt(); } elseif (input()->urlSegment1) { // don't allow other url segments throw new Wire404Exception(); } elseif (input()->requestMethod('POST')) { $jobs = $input->post->jobs; $currentJobs = $input->post->currentJobs; // sanitize inputs $saneJobs = []; foreach ($jobs as $j) { $jobId = $sanitizer->int($j); $jobTitle = $sanitizer->text($j); if ($jobId) $saneJobs[$jobId] = false; // not current job elseif ($jobTitle) $saneJobs[$jobTitle] = false; // not current job } // check if a job is one of current jobs foreach ($currentJobs as $cj) { $currId = $sanitizer->int($cj); $currTitle = $sanitizer->text($cj); // if person has the job, set it to current if (isset($saneJobs[$currId])) $saneJobs[$currId] = true; // set to current elseif (isset($saneJobs[$currTitle])) $saneJobs[$currTitle] = true; // set to current } // save info $person = $pages->get(1228); $person->of(false); $addableJobs = []; $addableCurrents = []; foreach ($saneJobs as $j => $isCurrent) { // $j is job id or new job title $job = null; // does job already exist? if (is_int($j)) { $job = $pages->get($j); } else { // create a new job and add it $job = new Page(); $job->template = 'profession'; $job->parent = 1027; $job->name = $sanitizer->pageName($j); $job->title = ucwords($j); // already sanitized, but capitalize it $job->save(); } if (!$job) continue; $addableJobs[] = $job; if ($isCurrent) $addableCurrents[] = $job; } $person->jobs->add($addableJobs); $person->currentJobs->add($addableCurrents); $person->save(); } ?> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/js/standalone/selectize.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet" href="https://rawgit.com/selectize/selectize.js/master/dist/css/selectize.bootstrap3.css"> <div class="flex"> <form method="POST" class="signup-form"> <div class="form-group"> <label for="name">Name</label> <input name="name" required type="text" class="form-control" id="name" placeholder="Name Lastname"> </div> <div class="form-group"> <label for="jobs">Your Jobs</label> <select name="jobs[]" required id="jobs" multiple class="form-control"> <option>Loading...</option> </select> </div> <button class="btn" type="submit">Save</button> </form> </div> <script> $jobs = $('#jobs'); // fetch all jobs $.getJSON('./jobs.json', function (data) { initSelect(data); }); function initSelect(data) { const makeOption = (it) => ({text: it.title, value: it.id}); let selected = data.splice(0, 5).map(makeOption); window.jobs = data; $jobs.selectize({ create: true, render: { item: function (data, escape) { return `<div class='option'> <span class="option__label">${escape(data.text)}</span> <input class="option__current" type="checkbox" name="currentJobs[]" value="${escape(data.value)}"> </div>`; } }, options: data.map(makeOption), // populate the field with the jobs person already has // items: data.splice(1,4).map(i => i.id) }); } </script> <style> .flex { display: flex; justify-content: center; margin-top: 10rem; } .signup-form { width: 500px; margin: auto; } .option { width: 100%; } .option:after { content: ''; display: table; } .option__label { float: left; } .option__current { float: right; } </style>1 point
-
Here's how I did it. Searching, sorting, filtering, pagination all works <?php namespace ProcessWire; /* @var $config Config */ /* @var $pages Pages */ /* @var $input WireInput */ /* @var $sanitizer Sanitizer */ // fields to return $fields = ['id', 'name', 'publishedStr', 'createdStr']; if ($config->ajax) { // sanitize inputs $search = $sanitizer->selectorValue($input->get->queries['search']); $sorts = []; if ($input->get->sorts) { foreach ($input->get->sorts as $f => $direction) { $key = $sanitizer->fieldName($f); $direction = $sanitizer->int($direction, ['min' => -1, 'max' => 1]); if ((!$key) || (!$direction)) continue; $sorts[$key] = $direction > 0 ? '' : '-'; // sort=field or sort=-field } } $page = $sanitizer->int($input->get->page, ['min' => 0, 'blankValue' => 1]); $perPage = $sanitizer->int($input->get->perPage, ['min' => 10, 'max' => 100, 'blankValue' => 10]); $offset = $sanitizer->int($input->get->offset, ['min' => 0]); // base selector $selector = [ "id>0", "include=all", ]; $selectorFiltered = array_merge($selector, [ "name|title*=$search", // change fields to search ]); $selectorFilteredLimited = array_merge($selectorFiltered, [ "limit=$perPage", "start=$offset" ]); // include sorts as sort=(-)fieldName foreach ($sorts as $f => $d) { $selectorFilteredLimited[] = "sort={$d}{$f}"; } // perform database query $totalCount = $pages->count(join(', ', $selector)); // # of all pages $queryCount = $pages->count(join(', ', $selectorFiltered)); // # of filtered pages $pageData = $pages->find(join(', ', $selectorFilteredLimited))->explode($fields); // data to return // output json header("Content-type: application/json"); $data = [ 'records' => array_values($pageData), 'queryRecordCount' => $queryCount, 'totalRecordCount' => $totalCount ]; echo json_encode($data); // stop return $this->halt(); } ?> <?php if (!$config->ajax): ?> <table class="table"> <thead> <tr> <?php foreach ($fields as $field): ?> <td><?= $field ?></td> <?php endforeach; ?> </tr> </thead> <tbody></tbody> </table> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Dynatable/0.3.1/jquery.dynatable.min.js"></script> <script> $('table').dynatable({ dataset: { ajax: true, ajaxUrl: '.', // current page ajaxOnLoad: true, records: [] } }); </script> <?php endif; ?> Here's a screenshot Although I think the code is clear, feel free to ask me if you have any questions. https://processwire.com/api/ref/wire-array/explode/ https://processwire.com/api/ref/sanitizer/ https://processwire.com/api/ref/pages/count/ https://processwire.com/blog/posts/processwire-2.6.8-brings-new-version-of-reno-admin-theme-and-more/#new-this-gt-halt-method-for-use-in-template-files1 point
-
Just a word of caution. Do not use this in high traffic pages, because it holds a race condition, where people might be assigned the same number.1 point
-
For a little more info on what I found which will hopefully help others: There were lots of JS errors in the browser developer tools Console tab like this: Uncaught SyntaxError: Unexpected token < which is a good indication that there is an error message, or at least some HTML being returned in the AJAX request which is not expected by the JS script. So then I went to the Network tab of the developer tools and found the script that is uploading images (?id=1&InputfieldFileAjax=1), and checked the response - that is where I found this login form: Also. of course, when I saved the page, it asked me to login again, so that was also a hint. @Roberts R mentioned that he didn't get the console errors - turns out he didn't have "Disable Cache" checked on the Network tab and apparently that made a difference. I always have it checked because Chrome caching drives me crazy but this might be a helpful hint for others.1 point
-
This basic tutorial is primarily aimed at those new to PW. It could also serve as a reference to others more seasoned PW users. The question about how to categorise content comes up in the forums now and again. Hopefully with this post we’ll have a reference to guide us right here in the tutorials board. Many times we need to organise our site content into various categories in order to make better sense of the data or to logically and easily access it. So, how do you organise your data when you need to use categories? Here are a few tips gathered from the PW forums on how to go about this. Using these tips will, hopefully, help you avoid repeating yourself in your code and site content and keep things simple. See the links at the end of this post to some useful discussion around the topic of categorisation. Before making decisions about how to organise your site, you need to consider at least three questions: What items on my site are the main items of interest? These could be people or things (cars, plants, etc.). In most cases, these are the most important content on which all the other stuff point to. Where do items need to be grouped into categories? This is about where items need to “live”. It is about the attributes of the items of interest (e.g. responsibilities, job types, colour, etc.). Attributes can have sub-attributes (e.g. a category job type = driver could be further sub-classified as job type role = train driver). Can they live in more than one place? - This is about having multiple attributes. There could be other issues such as the type of content your main items of interest are but that’s for another post. We’ll keep these examples simple. The main principles explained below still apply. There are at least three possible ways in which you can organise your content depending on your answers to the above three questions. These are: Single category Simple multiple categories Complex multiple categories These are illustrated below. Note that this is what I call them; these are not PW terms. 1. Single Category Suppose you need to do a site for a company that’s made up of several Departments each with employees performing unique functions. These could include “Finance”; “Media Communications”; “Administration”; “Technicians”; “Human Resources”; “Logistics”. We ask ourselves the following questions based on our 3 questions above: 1. Q: What items on my site are the main items of interest? A: Employees. 2. Q: What attributes of our items of interests are we interested in? A: Departments. (Single main category) 3. Do the Departments have sub-categories? A: Yes. (Multiple sub-categories) 4.Can Employees belong to multiple sub-categories? A: No. (Single sub-category) We conclude that what we need is a Single Category model. Why? This is because, in Single Categories model, items of interest can only belong to 1 and only 1 main/parent category and within that only 1 sub-category Employees in this company can only belong to one and only one department. Finance guys do their finance and Logistics guys do their stuff. Letting Techies do press conferences is probably not going to work; that we leave to the Media guys . Assuming the company has the following employees - James, John, Mary, Ahmed, Peter, Jason, Barbara etc., arranging our site content to fit this model could look like the following: Items of interest = Employees Categories = Departments Adopting out strategy to keep it simple and logical, let us write down, hierarchically, our employee names against their departments to mimic the PW tree like this: James Finance John Finance Mary Technician Ahmed Logistics Barbara Media Etc. We notice, of course, that departments start repeating. It doesn't look like we are doing this very logically. If we think about this carefully, we will conclude that, naturally, the thing (attribute in this case) that keeps repeating should be the main criteria for our categorisation. This may seem obvious, but it is worth pointing out. Also, remember, that as per the responses to our questions, the categories (Finance, Logistics, etc.) do not have sub-categories. In this aspect, we are OK. Using this principle about repeating attributes, we find that Departments, rather than Employees, need to be the main categories. Hence, we categorise our PW site content by doing the following. Create a template for each Department. Hence, we have a template called Finance, Logistics, etc. Add the fields needed to those templates. This could be a text field for holding Employee phone numbers, email field for email, title field for their names, etc. Create top level pages for each Department and assign to them their respective templates. Give them appropriate titles, e.g., Finance, Media, etc. Create a page for each employee as a child page of the Department which they belong to. Give them appropriate titles, e.g. James, John, etc. We end up with a tree that looks like this: 1. Finance (ex. main category) a. James (ex. item of interest) b. John c. Shah d. Anne 2. Logistics (ex. main category) a. Ahmed b. Matthew c. Robert d. Cynthia 3. Media a. Barbara b. Jason c. Danita 4. Human Resources a. Michael b. Pedro c. Sally 5. Technician a. Mary b. Oswald c. Dmitri d. Osiris Since an employee can only belong to one Department, our work here is done. We can then use PW variables, e.g. $page->find, $pages->find with the appropriate selectors to find employees within a Department. This is a very basic example, of course, but you get the idea. You have the choice of creating one template file for each category template as well. I prefer the method of using one main template file (see this thread). You could do that and have all Departments use different templates but a single template file. In the template file you can include code to pull in, for example, the file “technician.inc” to display the relevant content when pages using the template “Technician” are viewed. Example code to access and show content in Single Categories model $hr = $pages->find("template=human-resources, limit 50"); foreach ($hr as $h) { echo "{$h->title}"; } But sites do not always lend themselves to this model. Many times, items of interest will need to belong to multiple categories. 2. Simple Multiple Categories Let’s say you were building a site for cars - red cars, blue cars, 2-seaters, 5-seaters, etc. Again, we ask ourselves our questions based on our initial three questions: 1. Q: What items on my site are the main items of interest? A: Cars. 2. Q: What attributes of our items of interests are we interested in? A: Colour, Number of seats, Models, Year of manufacture, Types. (Multiple categories) 3. Do these multiple attributes have sub-attributes? A: Yes. e.g., the attribute Colour has several sub-categories - red, white, green, etc. (Multiple sub-categories) 4. Can Cars have multiple sub-attributes? A: No. e.g., a yellow car cannot be a green car. (Single sub-categories) We therefore conclude that what we need is a Simple Multiple Category model. Why? This is because, in Simple Multiple Categories, items of interest can belong to multiple parent categories. However, within those parent categories, they can only belong to one sub-category. Assuming we have the following cars, manufactured between 2005 and 2008, as items of interest: Mercedes, Volvo, Ford, Subaru, Toyota, Nissan, Peugeot, Renault, Mazda, arranging our site content to fit this model could look like the following: Items of interest = Cars Categories = Model, Year, Colour, Number of seats, Type Sub Categories = Model [Prius, etc.]; Year [2005, 2006, 2007, 2008]; Colour [Red, Silver, Black, White, Green]; Number of seats [2, 5, 7]; Types [sports, SUV, MPV]. Adopting out strategy to keep it simple and logical, if we wrote down our cars names against their attributes like this: Mercedes Model-Name: Year: 2005 Colour: Silver Seats: 2-seater Type: Sports Volvo Model-Name: Year: 2007 Colour: Green Seats: 5-seater Type: SUV Ford Model-Name: Year: 2007 Colour: Red Seats: 7-seater Type: MPV Etc We notice, again, that car attributes start repeating. In order not to repeat ourselves, we want to avoid the situation where our child pages “names” keep repeating. For instance, in the above example tree, we want to avoid repeating year, colour, etc. within the tree. Of course in the frontend our output needs to look like the above where we can list our cars and their respective attributes. We just don’t need a tree that looks like this in the backend. Since we have multiple categories and sub-categories, we need to rethink our strategy for categorising our content as illustrated below. The strategy we used in the first category model will not work well here. Hence, these repeating attributes (year, colour, etc.) need to be the main criteria for our categorisation. We need to end up with a tree that looks like this: 1. Cars a. Mercedes (ex. item of interest) b. Volvo c. Ford d. Subaru e. Toyota f. Range Rover g. Peugeot h. Renault i. Mazda 2. Model (ex. main category) a. Fiesta (ex. sub-category) b. Miata c. Impreza d. Matrix e. Prius f. E-Class g. XC-90 h. Scenic i. L322 j. 505 3. Year a. 2005 b. 2006 c. 2007 (ex. sub-category) d. 2008 4. Colour a. Red b. Silver c. Black d. White e. Green 5. Number of Seats a. 2 b. 5 c. 7 6. Type a. MPV b. Sports c. SUV d. Other At the top of the tree, we have our main items of interest, Cars. They do not have to come first on top of the tree like that but it just makes sense to have them like this. Next, we have the Cars’ categories (attributes). The main categories are parent pages. Each main category has children which act as its sub-categories (cars’ sub-attributes). For instance, the main category colour has sub-categories “red”, “green”, etc. Grouping them under their main category like this makes better sense than having them dangling all over the tree as parent pages themselves. Now that we know what we want to achieve, the next question is how do we go about relating our categories and sub-categories to our main items of interest, i.e., cars? Fields come to mind. OK, yes, but what about the sub-categories (2006, red, 5-seater, etc.)? Surely, we can’t keep typing those in text fields! Of course not; this is PW. We like to simplify tasks as much as we can. What we need is a special type of field. Page Reference Fields or Page Fieldtypes add the ability to reference other pages, either single or multiple pages, within a page. For instance, we could have a Page Reference Field in the template that our Car pages use. Let’s call this “car-template”. When viewing Car pages, we would have the ability to select other pages on our site that we wish to reference, for instance, because they are related to the page we are viewing. In other cases, we could also wish to reference other pages that contain attributes/values of the page we are viewing. This is the situation with our Cars example above. Hence, the sub-categories/sub-attributes for our Cars will be pulled into our car pages using Page Reference Fields. There are two types of Page Reference Fields; single page and multiple pages. What each do is obvious from their names. Single Page Reference Fields will only reference one page at a time. Multiple Page Reference Fields will reference multiple pages. OK, let’s go back to the issue at hand. We need to categorise Cars by various attributes. Do we need to reference the main categories (Year, Type, etc.) in our Car pages? In fact, we don’t. What we need to reference are the sub-categories, i.e. 2005, red, SUV, etc. These will provide the actual attributes regarding the parent attribute of the Cars. We have said we do not wish to type these sub-categories/attributes all the time hence we use Page Reference Fields. Which type of Page Reference Field should we use? Remember that our Cars can have only one sub-category/sub-attribute. That’s our cue right there. In order to select one and only one sub-attribute per Car, we need to use the single Page Reference Field. Hence, we categorise our Cars PW site by doing the following (you may follow a different order of tasks if you wish). Create a template to be used by the Car pages. Give it a name such as car-template Create a page for each of your cars and make them use the car-template Create one template to be used by all the main attribute/categories and their children (the sub-categories). We do not need a template for each of the categories/sub-categories. I name my template “car-attributes” Of course you can name yours differently if you wish. Add the fields needed to this template. You don’t need anything other than a title field for each actually. Create top level pages for each main category and assign to them the template car-attributes. As before, give your pages meaningful titles. Do the same respectively for their child pages. E.g., you should have the following child pages under the parent “Year” - 2005, 2006, 2007 and 2008. Create the Page Reference Fields for each of your main categories/parent attributes. Using our example, you should end up with 5 Page Reference Fields (model, year, colour, seats and type). Each of these should be single Page Reference Fields. It’s a good idea, under the BASICS settings while editing the fields, to include some Description text to, include additional info about the field, e.g. instructions. In addition, you don’t want any page that doesn't belong to a particular attribute to be selectable using any of the Page Reference Fields. For instance, when referencing the year a car was manufactured, we want to be able to only select children of the page Year since that is where the year sub-categories are. We do not want to be able to select children of Colour (red, green, etc.) as the year a car was manufactured! How do we go about this? PW makes this very easy. Once you have created your Page Reference Fields, while still in the editing field mode, look under the settings INPUT. The fourth option down that page is “Selectable Pages”. Its first child option is “Parent of selectable page(s)”. Where it says “Select the parent of the pages that are selectable” click on change to change the parent. By now you know where I am going with this. For the Page Reference Field named Year, choose the page “Year” as the parent whose children will be selectable when using that Page Reference Field to select pages. Similarly, do this for the remaining 4 Page Reference Fields. Note that under this field settings INPUT you can change how you want your pages to be selectable. Be careful that you only select the types that match single Page Reference Fields, i.e. the ones WITHOUT *. For single Page Reference Fields, you have the choices:Select - a drop down select Radio buttons PageListSelect Now edit the car-template to add all 5 of your Car Page Reference Fields. We are now ready to roll. Go ahead and edit your Car pages. In each of them you will see your 5 Page Reference Fields. If you followed the instructions correctly, each of them should only have the relevant child pages/sub-attributes as selectable. Do your edits - select year when car was manufactured, its colour, type, number of seats, etc. and hit Save. By the way, note that Page Reference Fields give you access to all the fields and properties of the page being referenced! You have access to the referenced page’s title, name, path, children, template name, page reference fields, etc. This is really useful when creating complex sites. I call it going down the rabbit hole! These properties of the referenced page are available to you on request. It does mean that you will have to specifically echo out the property you want from that page. Page Reference Fields are echoed out like any other field. Example code to access and show content in Simple Multiple Categories model $cars = $pages->find("template=car-template, limit=10, colour=red, year=2006, seats=5"); foreach ($cars as $car) { echo $car->title; echo $car->year; echo $car->colour; } I have made the above verbose so you can easily follow what I'm trying to achieve. The above code will find 10 red 5-seater cars manufactured in 2006. Remember, colour, year and seats are the names of your custom Page Reference Fields that you created earlier. Some sites will have content that belong to multiple categories and multiple sub-categories. We address this below. 3. Complex Multiple Categories Suppose you are developing a site for a school. The school has teachers (duh!) some of whom teach more than one subject. Besides their classroom duties, some teachers are active in various clubs. On the administration side, some teachers are involved in various committees. You know the drill by now. Let’s deal with our basic questions. 1. Q: What items on my site are the main items of interest? A: Teachers. 2. Q: What attributes of our items of interest are we interested in? A: Subjects, Administration, Clubs (Multiple categories) 3. Do these multiple attributes have sub-attributes? A: Yes. e.g., the attribute Subjects has several sub-categories - History, Maths, Chemistry, Physics, Geography, English, etc. (Multiple sub-categories) 4. Can Teachers have multiple sub-attributes? A: Yes. e.g., a Teacher who teaches both maths and chemistry (Multiple sub-categories) Apart from the response to the final question, the other responses are identical to our previous model, i.e. the Simple Multiple Categories. We already know how to deal with multiple categories so we’ll skip some of the steps we followed in the previous example. Since our items of interest (Teachers) can belong to more than one sub-category, we conclude that what we need is a Complex Multiple Category model. In Complex Multiple Categories, items of interest can belong to multiple parent categories and multiple sub-categories both within and without main/parent categories. By now we should know what will be the main criteria for our categorisation. We need to end up with a tree that looks like this: 1. Teachers a. Mr Smith (ex. item of interest) b. Mrs Wesley c. Ms Rodriguez d. Mr Peres e. Mr Jane f. Mrs Potter g. Ms Graham h. Mrs Basket i. Dr Cooper 2. Subjects (ex. main category) a. History (ex. sub-category) b. Maths c. English d. Physics e. Chemistry f. Geography g. Religion h. Biology i. French j. Music 3. Clubs a. Basketball b. Debate c. Football d. Scouts e. Sailing f. Writing 4. Administration a. Discipline b. Counselling c. Exams board d. Public relations e. Education We are ready to build our site. Which type of Page Reference Field should we use? Remember that our Teachers can teach more than one subject and can be involved in various sub-category activities. That’s our cue right there. In order to select multiple attributes/categories, we of course go for the multiple Page Reference Field. Similar to the previous example, create necessary templates and fields for the site. For our multiple Page Reference Fields, remember to select the correct input field types. These should match multiple Page Reference Fields and are marked with *. For multiple Page Reference Fields, the available choices are: Select Multiple* AsmSelect* Checkboxes* PageListSelectMultiple* PageAutoComplete* Remember to add the multiple Page Reference Fields to the Teachers template. Go ahead and test different selectors, e.g. find Teachers that teach Maths and Chemistry and are involved in the Writing club. Whether you get results or not depends on whether there is actually that combination. An important point to remember is that your multiple Page Reference Fields will return an array of pages. You will need to traverse them using foreach (or similar). Example code Complex Multiple Categories model Find the subjects taught by the Teacher whose page we are currently viewing. You can use if statements to only show results if a result is found. In this case, of course we expect a result to be found; if a Teacher doesn't teach any subject, he/she has no business teaching! subjects is the name of one of your custom Multiple Page Reference Fields. echo "<ul>"; foreach ($page->subjects as $x) { echo "<li>{$x->title}</li>"; } echo "</ul>"; There will be situations where you will need to use both Single and Multiple Page Reference Fields (independently, of course). For instance, in our Teachers example, we might be interested in the Gender of the Teacher. That would require a Single Page Reference Field. Summary What we have learnt: Categorising our site content need not be a nightmare if we carefully think it through. Of course not all sites will fit neatly into the 3 models discussed. By providing answers to a few simple key questions, we will be able to quickly arrive at a decision on how to categorise our content. There are at least 3 models we can adopt to categorise our content - single category; simple multiple category; and complex multiple category. In the latter two models, we make full use of PW’s powerful Page Reference Fields to mimic a relational database enabling us to roll out complex sites fast and easy. Useful links: http://processwire.com/talk/topic/3553-handling-categories-on-a-product-catalogue/ http://processwire.com/videos/create-new-page-references/ http://processwire.com/videos/page-fieldtype/ http://processwire.com/talk/topic/1041-raydale-multimedia-a-case-study/ http://processwire.com/talk/topic/683-page-content-within-another-page/ http://processwire.com/talk/topic/2780-displaying-products-category-wise/ http://processwire.com/talk/topic/1916-another-categories-question/ http://processwire.com/talk/topic/2802-how-would-you-build-a-daily-newspaper/ http://processwire.com/talk/topic/2519-nested-categories/ http://processwire.com/talk/topic/71-categorizingtagging-content/ http://processwire.com/talk/topic/2309-best-way-to-organize-categories-in-this-case/ http://processwire.com/talk/topic/2200-related-pages/ http://processwire.com/talk/topic/64-how-do-you-call-data-from-a-page-or-pages-into-another-page/1 point