Leaderboard
Popular Content
Showing content with the highest reputation on 03/21/2018 in all areas
-
Not sure where the best place for this is, but I felt like sharing a little snippet for site/ready.php I wrote that creates a PageArray::groupBy method: <?php /** * * Adds a groupBy method to all PageArray instances. * * Returns a nested array, with the values of the page properties whose names * were passed as arguments as the keys. * * Usage: * ====== * * $grouped = $mypagearray->groupBy(field1 [, field2 ...] [, mutator_function]); * or * $grouped = $mypagearray->groupBy(mutator_function); * * Example: * ======== * * $mypagearray = $pages->find("template=blog-post, sort=year, sort=month"); * $grouped = $mypagearray->groupBy("year", "month"); * * foreach($grouped as $year => $monthgroup) { * echo "<div class='year'><h2>$year</h2>" . PHP_EOL; * echo "\t<ul class='month'>" . PHP_EOL; * * foreach($monthgroup as $month => $mypages) { * echo "\t\t<li><h3>$month</h3>" . PHP_EOL; * * echo "\t\t\t<ul class='post'>" . PHP_EOL; * * foreach($mypages as $post) { * echo "\t\t\t\t<li><a href='{$post->url}'>{$post->title}</a></li>" . PHP_EOL; * } * * echo "\t\t\t</ul>\n" . PHP_EOL; * echo "\t\t</li>\n" . PHP_EOL; * } * * echo "\t</li>" . PHP_EOL; * echo "</ul>" . PHP_EOL; * } * * Example Output: * =============== * * <div class='year'><h2>2016</h2> * <ul class='month'> * <li><h3>1</h3> * <ul class='post'> * <li><a href='/grouptest/grouptest-10/grouptest-10-10/'>Group Test 10 10</a></li> * <li><a href='/grouptest/grouptest-6/grouptest-6-10/'>Group Test 6 10</a></li> * <li><a href='/grouptest/grouptest-10/grouptest-10-12/'>Group Test 10 12</a></li> * <li><a href='/grouptest/grouptest-1/grouptest-1-12/'>Group Test 1 12</a></li> * <li><a href='/grouptest/grouptest-5/grouptest-5-3/'>Group Test 5 3</a></li> * <li><a href='/grouptest/grouptest-10/grouptest-10-4/'>Group Test 10 4</a></li> * <li><a href='/grouptest/grouptest-3/'>Group Test 3</a></li> * <li><a href='/grouptest/grouptest-6/grouptest-6-4/'>Group Test 6 4</a></li> * <li><a href='/grouptest/grouptest-10/grouptest-10-7/'>Group Test 10 7</a></li> * </ul> * * </li> * * <li><h3>2</h3> * <ul class='post'> * <li><a href='/grouptest/grouptest-5/grouptest-5-10/'>Group Test 5 10</a></li> * <li><a href='/grouptest/grouptest-3/grouptest-3-7/'>Group Test 3 7</a></li> * <li><a href='/grouptest/grouptest-9/grouptest-9-5/'>Group Test 9 5</a></li> * <li><a href='/grouptest/grouptest-7/grouptest-7-12/'>Group Test 7 12</a></li> * <li><a href='/grouptest/grouptest-3/grouptest-3-11/'>Group Test 3 11</a></li> * <li><a href='/grouptest/grouptest-9/grouptest-9-11/'>Group Test 9 11</a></li> * </ul> * * </li> * * <li><h3>3</h3> * <ul class='post'> * <li><a href='/grouptest/grouptest-7/grouptest-7-10/'>Group Test 7 10</a></li> * <li><a href='/grouptest/grouptest-12/grouptest-12-12/'>Group Test 12 12</a></li> * <li><a href='/grouptest/grouptest-11/grouptest-11-5/'>Group Test 11 5</a></li> * </ul> * * </li> * * <li><h3>4</h3> * <ul class='post'> * <li><a href='/grouptest/grouptest-8/grouptest-8-3/'>Group Test 8 3</a></li> * <li><a href='/grouptest/grouptest-12/grouptest-12-6/'>Group Test 12 6</a></li> * </ul> * * </li> * * </li> * </ul> * * IMPORTANT! * ========== * * Your PageArray needs to be sorted by the fields you group by, or your return array will be an * unorderly mess (the grouping is still correct, but the order of keys is arbitrary). * * Mutator Function: * ================= * * Instead of just reading properties from the page, you can also pass a mutator function * to groupBy. It gets passed the page object as its first arguments and any optional property * names after that. * * This might come in handy if you want to group by year and month but only have a single * DateTime field. You can then use a grouping function like this: * * $blogposts = $pages->find("template=blog-post, sort=created"); * $grouped = $blogposts->groupBy(function($pg) { * return array(strftime('%Y', $pg->created), strftime('%m', $pg->created)); * }); * */ wire()->addHook("PageArray::groupBy", function(HookEvent $event) { $out = array(); $args = $event->arguments(); if(count($args) == 0) throw new InvalidArgumentException("Missing arguments for function PageArray::groupBy, at least 1 required!"); $last = count($args) - 1; $fnc = is_string($args[$last]) ? FALSE : array_pop($args); foreach($event->object as $pg) { if($fnc) { $props = call_user_func_array($fnc, array_merge(array($pg), $args)); } else { $props = array_map(function($propname) use($pg) { return $pg->{$propname}; }, $args); } $cur = &$out; foreach($props as $prop) { if(!isset($cur[$prop])) $cur[$prop] = array(); $cur = &$cur[$prop]; } $cur[] = $pg; } $event->return = $out; });6 points
-
I am doing something very similar to this and am just using pages. I use two templates: 'newsletters_parent' and 'newsletters_child'. The 'newsletters_parent' is the container and onyl has a title, but its settings on the "Family" tab are: Can have children: YES Can be used for new pages: ONE Allowed template(s) for children: newsletter_child Name format for children: Y/m/d H:i:s (the page creation date/time -- this will get overwritten at a later stage) Sort settings for children: pub_date (a field that will be added to the newsletter_child page) Reverse sort direction: CHECKED (yes) The 'newsletters_child' template has the following fields: title (default and required by all templates) pub_date (a datepicker field set to YYYY-MM-DD -- the publication date might not necessarily be the same as the date the file was uploaded!) pdf_file (file-upload field, single file only, and also set to only allow PDF files) The settings ("Family" tab) for the child template are: Can have children: NO Can be used for new pages: YES Allowed template(s) for parents: newsletter_parent Show in the add-page shortcut menu: YES To make the process of adding a new newsletter as easy as possible for users -- all they have to do is select the file to upload and the pick the pub date -- I use a couple of hooks hook in the site's ready.php file: The first hook adds dummy text to the page title field on page creation so that the user doesn't have to enter anything (if it is left blank, it will throw an error) -- the proper title, along with the page name will be created from the pub_date field when the page is saved. The second hook also creates the actual page title and the sets page name based on the supplied 'pub_date'. [NOTE: the newsletters only get published once a month, so we only use the Year/Month in page names/titles.] /** * 1. Set a dummy title on page save at creation, to prevent the required field showing an error if it is blank. * (Requires the parent template option 'Name format for children' to be set) * 2. Modify newsletter title and page name from pub_date before saving. */ $pages->addHookAfter('saveReady', function(HookEvent $event) { $page = $event->arguments(0); // Only on newsletter_child templates where the title is blank (on page creation) if ($page->template->name == 'newsletter_child' && $page->title == '') { $page->title = 'Newsletter'; // a dummy title, will be replaced at a later stage } // Only on newsletter_child templates with a pub_date if ($page->template->name == 'newsletter_child' && $page->pub_date) { $newName = wire('sanitizer')->pageName('newsletter-' . date('Y-M', $page->pub_date), true); $newTitle = date('F Y', $page->pub_date); if ($page->title != $newTitle) { $page->title = $newTitle; } if ($page->name != $newName) { $page->name = $newName; } } }); Thiis then makes it easy to search the system for whatever you want: // All newsletter files (no limit applied) $allNews = $pages->find('template=newsletter_child, sort=pub_date'); // ascending order (earliest first) $allNews = $pages->find('template=newsletter_child, sort=-pub_date'); // descending order (most recent first) // All newsletters from a particular year (e.g., 2017): $yearStart = strtotime('2017-01-01 00:00:00'); $yearEnd = strtotime('2018-01-01 00:00:00'); $yearNews = $pages->find("template=newsletter_child, pub_date>={$yearStart}, pub_date<{$yearEnd}, sort=-pub_date"); // The most recent newsletter: $mostRecent = $pages->findOne('template=newsletter_child, sort=-pub_date'); // Accessing the file itself (using $mostRecent) if ($mostRecent->pdf_file) { $file = $mostRecent->pdf_file; $downloadLink = "<a href='{$file->url}'>Download the newsletter: {$mostRecent->title}</a> ({$file->filesizeStr})"; } That's all there is, really. There is lots you can do with the system.5 points
-
Wow o_O @dadish I am humbled by your excellent work here. I stumbled on this thread researching PWAs and then was transfixed for the full 37 minutes of your excellent video intro. Thank you very much indeed for all of this, I am yet again thankful for the ProcessWire community's generous and excellent work. I've had Vue.js noted down as "something I want to use", along with using ProcessWire as/via an API, and most recently, PWAs. I am hoping, if I've not misunderstood, now I may be able to use all these together for a project. Brilliant! \o/5 points
-
@bernhard, you are of course free to put whatever you like into your modules. However, having your modules "phone home" like this discloses data that users may not want shared. At the minimum the website URL is disclosed. Not every site that exists on the internet is intended for public access. There are things like staging servers, and probably a whole range of other sensitive sites. My personal opinion is: 1. I think there should be a very clear and prominent statement so that users know that the module will send data to a third party, and what data will be sent. And I'm not sure that only a notice in the module readme would be satisfactory because users might download via the PW admin without seeing the readme. And if you start adding this to existing modules that didn't have it before it could pass by unnoticed when the module is updated. Basically there needs to be informed consent before data is sent, and probably also a privacy policy. 2. I think this crosses a line that shouldn't be crossed, and sets a dangerous precedent that if followed by others could be harmful to the PW project/ecosystem. There is a huge amount of trust placed in third-party modules by users. Modules can potentially access any website data. The very notion that a module phones home erodes trust. We don't want to create a sense of unease where every time a user contemplates installing a module they feel like they ought to comb through the source code to see what the module might be disclosing to a third party. In the bigger picture, I'd like to see PW get a reputation as being a system suitable for large institutional and government clients. Here in New Zealand, SilverStripe is in the enviable position of being seen as the go-to system for government websites. I think PW should be competing in this space. But government and institutional clients won't touch PW with a pole if there is a perception that there are privacy issues with modules in the PW ecosystem. I think the right way to track software popularity is by counting downloads, not phoning home. Sadly GitHub and GitLab don't provide useful tools for this, but that is another discussion.4 points
-
Thanks @Doug G for your offer. I see myself a bit in the same situation. Maybe it is helpful to summarise here the documentation as it is available on the PW website and then ask for contributions to this list as to get a full picture of what already exists and what is actually still missing. PW Documentation API Reference : Gives an overview of the API variables and detailed information about the class functionality. According to @horst 's post this is directly generated from the code based on a special markup code included in the PHP comments. Tutorials : As the name suggests, giving insights into a number of topics. How to contribute to the tutorial? API Cheatsheet : A comprehensive reference of ProcessWire API variables, methods and properties – all on one page. Is this also directly generated from the code? API Documentation : Learn more about the API including the concepts behind it, how to work with template files, and how to develop modules. Also labeled as: DEVELOPING A SITE IN PROCESSWIRE. Covers additionally topics like Concept, Template Files, Selectors, Include&Bootstrap, Syntax, Hooks, Plugin Modules, Users&Access, Arrays, Fieldtypes, Multi-Language Support and Coding Style Guide. How to contribute to this documentation? Directory Structure : This section outlines ProcessWire’s default directory structure, highlighting the locations where your site’s files go. Template Files : Every time a page is loaded on your site, ProcessWire looks at what template is assigned to the page, loads it, gives it several API variables, and then runs it as a PHP script. This section covers the use of template files and serves as an introduction to using ProcessWire’s API with examples. This can be accessed also by way of "API Documentation" Selectors : A selector allows you to specify fields for finding a page or group of pages that match your criteria. Selectors in ProcessWire are loosely based around the idea and syntax of attribute selectors in jQuery. This can be accessed also by way of "API Documentation" Multi-Language Support : ProcessWire makes multi-language support easy with multi-language fields, URLs (page names) and code internationalization. Security : Security is our number one priority with ProcessWire. Make it your number one priority too. In this section we attempt to cover some of the more important aspects in maintaining a secure installation. Install & Upgrade : Covers installation of ProcessWire, troubleshooting and upgrades. Is there more documentation hidden on this site? Or on different sites?4 points
-
I totally agree that we need a new processwire.com. Our existing clients that use PW think it is brilliant but it's always quite hard to sell it in initially to new clients using pages from processwire.com. Admin look and feel I've found that most of our clients aren't concerned with the admin skin. It's simple and does the job really well. That said it could be modernised and it would be more visually appealing. A good example would be Umbraco, who's CMS used to look terrible, until it was given a fresh new face. It's really stripped back now and almost has a kind of Apple, utilitarian feel which lends itself to the task at hand. I think their users increased rapidly following this. Who is ProcessWire for? Everyone... but for different reasons! One thing I've learned from working with ProcessWire, is that it means different things to different people in terms of what is important and how PW tackles the job. A facelift may make it look nicer but I think the site needs to really target a few different audiences with some quick summary pages, highlighting why it's perfect for them. Designers Back end developers Front end developers Project Managers Content Editors Clients Marketing Directors If you can target these groups, under a general "Why ProcessWire is great" kind of entry point, then I think we would see a lot more people hitting that download button. I know the current PW site kind of tackles this, but I would say that it should be more obvious. After Installing PW... what next? When I downloaded Processwire initially I thought "okay what do I do now?". I came from an umbraco background so knew that I would essentially be installing a blank CMS (which I like as there's nothing that I don't need)... but if you're coming from Wordpress or are a first time CMS user, maybe you're expecting more... maybe you're expecting a landing page with links to useful resources and videos... Maybe this landing page could be tailored based on the type of user you are? Video - Tell me stuff I searched on google for demo videos and stumbled across a great video that Ryan had recorded. I think video is the way forward here. If people are time starved, then they just want to be introduced to the admin and all the great things it does. The thing is though, that if an admin reskin is in the pipeline, you can't record your video until that's done. I'm just rambling now... but hopefully some of this is useful. Cheers!4 points
-
@cmscritic, you should fix that 3.2 mb stock photo. maybe with that one $ rm -rf $ git clone https://github.com/processwire/processwire4 points
-
PW does not really use the concept of folders. I agree with @flydev - you should use pages in place of folders. I've used a repeater for doing something quite similar to what I think you are describing. Create a page that will be the central place where all files are uploaded. Add a repeater to that page's template, containing an integer field for year and a files field to hold the uploads. Users will upload their files to the relevant year item. Using API code you can get the most recent year (i.e. the highest integer) and the most recently added file in that year (the last file in the field). For automatic upload renaming there is @adrian's Custom Upload Names module.3 points
-
As the topic title says I want to share an idea I came up today: Why not track my module installations via Google Analytics? Very simple one, but huge potential and maybe somebody else has not thought of this possibility yet. It can also be used to track sent emails, submitted forms, log errors, whatsoever... $http = new WireHttp(); $http->post('http://www.google-analytics.com/collect', [ // Make sure to use HTTP, not HTTPS! 'v' => 1, // Version 'tid' => 'your-tracking-id', // Tracking ID / Property ID. 'cid' => 555, // Anonymous Client ID. 't' => 'event', // hit type 'ec' => 'myEventCategory', // category 'ea' => 'myEventAction', // action 'el' => 'myEventLabel', // label ]); You can test your setup here: https://ga-dev-tools.appspot.com/hit-builder/ Happy tracking2 points
-
Perhaps a dashboard with all kinds of infos, links etc. would be useful, when you enter the PW-backend for the first time. Such a dashboard should of course be able to be de-activated, or easily modified. Or a dedicated help page instead (that you could also be free to modify, or be able to show or hide, depending on user-permissions/-roles).2 points
-
2 points
-
Hi, check the doc : https://processwire.com/api/ref/pagefile/ https://processwire.com/api/ref/pagefile/url/ Your link markup should be something like that : <a href="{$page->myfilesfield->first->url}" download>download</a> A tutorial :2 points
-
@MSP01 You can create endpoint (template and page) where you can catch get parameters like ?unsubscribe=140530045 or ?disable=140530045 then do something like if($input->get->unsubscribe || $input->get->disable) { $subscription = $pages->get($sanitizer->pageName($input->get->unsubscribe)); if($subscription->id && $input->get->unsubscribe) { $pages->trash($subscription); } elseif ($subscription->id && $input->get->disable) { ... logic for disabling user subscription } }2 points
-
Hi, As far as your webserver has write permission on a given folder outside the web directory, you can write your logic in a module which handle your upload thing - everything is possible inside ProcessWire. I personnaly would go with pages. Make a parent page "newsletters-upload" in your page tree then create a child page for each "year", then create a child page inside "year" for each uploaded newsletter. Each newsletter page have a fieldtype-secure-file field which give you the possibility to store your uploaded files outside the webfolder. In the process module, the author could create/select a "year" page (folder) and upload file by drag&drop in the admin. You can also pull files from an FTP server where authors create their folder tree, then do the logic in the module. This is just an idea from what I understand, more details needed ?2 points
-
Good and funny ! For example, on the site I deployed the module, it is a custom dashboard with sensible informations, I had to take care of hand crafted request which could retrieve data from other users. When this behavior is detected, the user is logged out, the role login-disabled is assigned and then the user is redirected into the blackhole to be banned. public function SecureParks() { if($this->input->post->park) { $ids = explode('-', $this->sanitizer->pageName($this->input->post->park)); $userroles = $this->getParkRoles(); $userhaveright = $this->searchForParkId($ids[2], $userroles); if ($userhaveright === null) { $this->user->addRole('login-disabled'); $this->user->save(); $this->session->logout(); $this->session->redirect($this->pages->get('/blackhole/')->url); // :) } } }2 points
-
Its not so important, because only bad bots will see it and probably no humans (I hope so). By the way 2 bots from China were caught in the trap - works!!!2 points
-
In this tutorial I will cover how to use clsource's REST Helper classes to create a RESTful API endpoint within a PW-powered site and how to connect to it from the outside world with a REST client. This is a quite lengthy tutorial. If you follow all along and make it through to the end, you should get both, a working REST API with ProcessWire and hopefully some more basic understanding how these APIs work. As always with PW, there are many ways you could do this. My way of implementing it and the code examples are loosely based on a real world project that I am working on. Also, this is the first tutorial I am writing, so please bear with me if my instructions and examples are not that clear to understand. And please let me know if something is missing or could be made more clear. The steps covered: create templates and pages in the PW backend to get an API endpoint (an URL where the API can be accessed at) copy and save the REST Helper classes to your site create a template file and write some logic to receive and process data through our endpoint and send data back to the REST client test the whole setup with a Browser REST Client Addon I will not go into fundamentals and technical details on how RESTful APis are supposed to work. I assume that you have already read up on that and have a basic understanding of the principles behind that technology. Some helpful resources to brush up your knowledge: https://en.wikipedia.org/wiki/Representational_state_transfer http://www.restapitutorial.com/lessons/whatisrest.html The complete pages.php template is attached to this post for copy/paste. Lets get started. 1. create templates and pages in the PW backend to get an API endpoint (an URL where the API can be accessed) First we need to create some templates and pages in the PW backend to make our REST API accessible from the outside world through an URL (API endpoint). In my example this URL will be: https://mysite.dev/api/pages/ Note the "https" part. While this is not mandatory, I strongly recommend having your API endpoint use the https protocol, for security reasons. Further down in step 3 we will use this URL to create new pages / update and get data of existing pages. Go to your PW test site admin and: create 2 new templates: one is called "api", the other one "pages". For a start, they both have only a title field assigned. Just create the templates. We will create the corresponding files later, when we need them. enable "allow URL segments" for the "pages" template. We will need this later to access data sent by the requests from the client. in the Files tab of the "pages" template check "Disable automatic append of file: _main.php" create a new page under the home page with title, name and template "api" and set it to hidden create a child page for the "api" page with title, name and template "pages" The pagetree should look somewhat like this: Ok, now we're all set up for creating our API endpoint. If you browse to https://mysite.dev/api/pages/ you will most likely get a 404 error or will be redirected to your home page as we do not have a template file yet for the "pages" template. We will add that later in step 3. 2. copy and save the REST Helper classes to your site I have the REST Helper class sitting in site/templates/inc/Rest.php and include it from there. You could save it in any other location within your site/templates folder. I forked clsource's original code to add basic HTTP authentication support. Click here to open my raw gist, copy the contents and save them to /site/templates/inc/Rest.php In the next step we will include this file to make the classes "Rest" and "Request" available to our template file. 3. create a template file and write some logic to receive and process data through our endpoint and send data back to the client This will be the longest and most complex part of the tutorial. But I will try to present it in small, easy to follow chunks. Since we access our API at https://mysite.dev/api/pages/, we need to create a template file called "pages.php" for our "pages" template and save it to /site/templates/pages.php. Go ahead and create this file (if you're lazy, copy the attached file). Now right at the top of pages.php, we start with <?php require_once "./inc/Rest.php"; to include the REST Helper classes. Next, we initialize some variables that we will keep using later on // set vars with the default output $statuscode = 200; $response = []; $header = Rest\Header::mimeType('json'); 3.1 retrieve data with a GET request Now that we have the basics set up, we will next create the code for handling the easiest request type, a GET request. With the GET request we will ask the API to return data for an existing page. To let the API know which page it should return data for, we need to send the page id along with our request. I am attaching the page id as an url segment to the API endpoint. So the URL that the client will use to retrieve data for a page will look like: https://mysite.dev/api/pages/1234 where 1234 is the unique page id. Add following code to pages.php // if we have an urlsegment and it is a numeric string we get data from or update an existing page: handle GET and PUT requests if($input->urlSegment1 && is_numeric($input->urlSegment1)) { $pageId = $input->urlSegment1; // GET request: get data from existing page if(Rest\Request::is('get')) { // get the page for given Id $p = $pages->get($pageId); if($p->id) { $pdata = ["id" => $pageId]; // array for storing page data with added page id $p->of(false); // set output formatting to false before retrieving page data // loop through the page fields and add their names and values to $pdata array foreach($p->template->fieldgroup as $field) { if($field->type instanceof FieldtypeFieldsetOpen) continue; $value = $p->get($field->name); $pdata[$field->name] = $field->type->sleepValue($p, $field, $value); } $response = $pdata; } else { //page does not exist $response["error"] = "The page does not exist"; $statuscode = 404; // Not Found (see /site/templates/inc/Rest.php) } } } else { // no url segment: handle POST requests } // render the response and body http_response_code($statuscode); header($header); echo json_encode($response); Lets brake this down: First we check for a numeric url segment which is our $pageId. Then the Rest Request class comes into play and checks what type of request is coming in from the client. For the GET request, we want to return all data that is stored for a page plus the page id. This is all good old PW API code. I am using the $pdata array to store all page data. Then I am handing this array over to the $response variable. This will be used further down to render the JSON response body. If the page does not exist, I am setting an error message for the $response and a status code 404 so the client will know what went wrong. The else statement will later hold our POST request handling. The last 3 lines of code are setting the header and status code for the response and print out the response body that is sent back to the client. You can now browse to https://mysite.dev/api/pages/1 where you should see a JSON string with field names and values of your home page. If you enter a page id which does not exist you should see a JSON string with the error message. Lets move on to updating pages through a PUT request 3.2 update pages with a PUT request Since our API needs to know the id of the page we want to update, we again need to append an id to our endpoint url. In this example we will update the title and name of our homepage. So the request url will be: https://mysite.dev/api/pages/1. For the GET request above, anyone can connect to our API to retrieve page data. For the PUT request this is not a good idea. Thus we will add basic authentication so that only authorized clients can make updates. I use basic HTTP authentication with username and password. In combination with the https protocol this should be fairly safe. To set this up, we need an API key for the password and a username of our choosing. We add the API key in the PW backend: add a new text field "key" and assign it to the "api" template. edit the "api" page, enter your key and save. (I am using 123456 as key for this tutorial) Now add following code right after the if(Rest\Request::is('get')) {...} statement: // PUT request: update data of existing page if(Rest\Request::is('put')) { // get data that was sent from the client in the request body + username and pass for authentication $params = Rest\Request::params(); // verify that this is an authorized request (kept very basic) $apiKey = $pages->get("template=api")->key; $apiUser = "myapiuser"; if($params["uname"] != $apiUser || $params["upass"] != $apiKey) { // unauthorized request $response["error"] = "Authorization failed"; $statuscode = 401; // Unauthorized (see /site/templates/inc/Rest.php) } else { // authorized request // get the page for given Id $p = $pages->get($pageId); if($p->id) { $p->of(false); $p->title = $sanitizer->text($params["title"]); $p->name = $sanitizer->pageName($params["name"]); $p->save(); $response["success"] = "Page updated successfully"; } else { // page does not exist $response["error"] = "The page does not exist"; $statuscode = 404; // Not Found (see /site/templates/inc/Rest.php) } } } Breakdown: We check if the request from the client is a put request. All data that was sent by the client is available through the $params array. The $params array also includes $params["uname"] and $params["upass"] which hold our API user and key. We set API key and user and check if they match with the values that were sent by the client. If they don't match we store an error message to the response body and set the appropriate status code. If authentication went through ok, we get the page via PW API and update the values that were sent in the request body by the client. Then we put out a success message in the response body. If the page does not exist, we send the appropriate response message and status code, just like in the GET request example above. Now you might wonder how the client sends API user/key and new data for updating title and name. This is covered further down in step 4. If you want to test the PUT request right now, head down there and follow the instructions. If not, continue reading on how to setup a POST request for creating new pages. 3.2 create new pages with a POST request Final part of the coding part is creating new pages through our API. For this to work we need to implement a POST request that sends all the data that we need for page creation. We will do this at our endpoint: https://mysite.dev/api/pages/ Paste following code within the else statement that has the comment "// no url segment: handle POST requests": // POST request: create new page if(Rest\Request::is('post')) { // get data that was sent from the client in the request body + username and pass for authentication $params = Rest\Request::params(); // verify that this is an authorized request (kept very basic) $apiKey = $pages->get("template=api")->key; $apiUser = "myapiuser"; if($params["uname"] != $apiUser || $params["upass"] != $apiKey) { // unauthorized request $response["error"] = "Authorization failed"; $statuscode = 401; // Unauthorized (see /site/templates/inc/Rest.php) } else { // authorized request // create the new page $p = new Page(); $p->template = $sanitizer->text($params["template"]); $p->parent = $pages->get($sanitizer->text($params["parent"])); $p->name = $sanitizer->pageName($params["name"]); $p->title = $sanitizer->text($params["title"]); $p->save(); if($p->id) { $response["success"] = "Page created successfully"; $response["url"] = "https://mysite.dev/api/pages/{$p->id}"; } else { // page does not exist $response["error"] = "Something went wrong"; $statuscode = 404; // just as a dummy. Real error code depends on the type of error. } } } You already know what most of this code is doing (checking authorisation etc.). Here's what is new: We create a page through the PW API and assign it a template, a parent and basic content that was sent by the client. We check if the page has been saved and update our response body array with a success message and the URL that this page will be accessible at through the API for future requests. The client can store this URL for making GET or PUT requests to this page. If you're still reading, you have made it through the hard part of this tutorial. Congratulations. Having our code for reading, updating and creating pages, we now need a way to test the whole scenario. Read on to find out how this can be done. 4. test the whole setup with a Browser REST Client Addon The link in the heading will take you to a place from which you can install the very useful RESTClient addon to your favorite browser. I am using it with Firefox which is still the dev browser of my choice. Open a RESTClient session by clicking the little red square icon in the browsers addon bar. The UI is pretty straightforward and intuitive to use. 4.1 test the GET request Choose Method GET and fill in the URL to our endpoint. If you do not have a SSL setup for testing, just use http://yourrealtestdomain.dev/api/pages/1. If you happen to have a SSL test site with a self signed certificate, you need to point your browser to the URL https://yourrealtestdomain.dev/api/pages/ first in your test browser and add the security exception permanently. Otherwise RESTClient addon won't be able to retrieve data. If you have a test site with a 'real' SSL certificate, everything should be fine with using the https://... URL Hit send. In the Response Headers tab you should see a Status Code 200 and in the Response Body tabs a JSON string with data of your page. now change the 1 i the URL to some id that does not exist in your site and hit send again. You should get a 404 Status Code in the Response Headers tab and an error message "{"error":"The page does not exist"}" in the Response Body (Raw) tab. If you get these results, congrats! The GET request is working. For further testing you can save this request through the top menu Favorite Requests->Save Current Request. 4.1 test the PUT request Choose Method PUT and fill in the URL to our endpoint ending with 1 (http://yourrealtestdomain.dev/api/pages/1). In the top left click Headers->Content-Type: application/json to add the right content type to our request. If you miss this step, the request will not work. You will now see a "Headers" panel with all your headers for this request Click on Authentication->Basic Authentication. In the modal window that pops up, fill in user (myapiuser) and password (your API key). Check "Remember me" and hit Okay. You now should see Content-Type and Authorization headers in the "Headers" panel. Next, we need to send some data in the request body for updating our page title and name. Since we're using JSON, we need to create a JSON string that contains the data that we want to send. As I will update the home page for this example, my JSON reads { "title" : "Newhome", "name" : "newhome" } Be careful that you have a well formed string here. Otherwise you will get errors. Paste this into the "Body" panel of the REST Client addon. Hit send. In the Response Headers tab you should see a Status Code 200 and in the Response Body tabs a JSON string "{"success":"Page updated successfully"}". Now go to the PW backend and check if title and name of your page have been updated. If yes, congrats again. 4.2 test the POST request Choose Method POST and fill in the URL to our endpoint without any page id (http://yourrealtestdomain.dev/api/pages/). In the top left click Headers->Content-Type: application/json to add the right content type to our request. If you miss this step, the request will not work. You will now see a "Headers" panel with all your headers for this request Click on Authentication->Basic Authentication. In the modal window that pops up, fill in user (myapiuser) and password (your API key). Check "Remenber me" and hit Okay. You now should see Content-Type and Authorization headers in the "Headers" panel. Next, we need to send some data in the request body for updating our page title and name. Since we're using JSON, we need to create a JSON string that contains the data that we want to send. I will create a new page with template basic-page and parent /about/ for this example, my JSON reads { "template" : "basic-page", "parent" : "/about/", "title" : "New Page created through api", "name" : "newapipage" } Be careful that you have a well formed string here. Otherwise you will get errors. Paste this into the "Body" panel of the REST Client addon. Hit send. In the Response Headers tab you should see a Status Code 200 and in the Response Body tabs a JSON string "{"success":"Page created successfully","url":"https:\/\/mysite.dev\/api\/pages\/1019"}". Now go to the PW backend and check if title and name of your page have been updated. If yes, you're awesome! Summary By now you have learned how to build a simple REST API with ProcessWire for exchanging data with mobile devices or other websites. Notes I tested this on a fresh PW 2.7.2 stable install with the minimal site profile and can confirm the code is working. If you experience any difficulties in getting this to work for you, let me know and I will try to help. There purposely is quite a lot of repetion in the example code to make it easier to digest. In real life code you might not want to use procedural coding style but rather separate repeating logic out into classes/methods. Also, in life applications you should do more sanity checks for the authentication of clients with the API / for the data that is delivered by the client requests and more solid error handling. I skipped these to make the code shorter. RESTful services are by definition stateless (sessionless). My implementation within PW still opens a new session for each request and I haven't found a way around that yet. If anyone can help out this would be much appreciated. And finally big thanks to clsource for putting the Rest.php classes together. pages.php.zip1 point
-
Hi, So today I will writing a small tutorial on developing templates in Processwire using Twig Template, Processwire is a highly flexible CMS which gives developers/designers/users options and allows easy extension of the platform. So here goes the tutorial What is Twig Template ? Simply put in my own words, Twig is a modern templating engine that compiles down to PHP code, unlike PHP, Twig is clean on the eyes , flexible and also quite *easy* to have dynamic layout site with ease ,without pulling your hair out. Twig is trusted by various platforms. It was created by the guys behind Symfony. Take this code as an example {% for user in users %} <h1>* {{ user }}</h1> {% endfor %} This will simply be the equivalent in PHP World <?php $userArray = ["Nigeria","Russia"]; foreach($userArray as $user): ?> <h1><?= $user ?></h1> <?php endforeach; The PHP code though looks simple enough however, you start to notice that you have to be concerned about the PHP tags by ensuring they are closed properly , most times projects gets bigger and comes complex and harder to read/grasp, and also in PHP you can explicitly create variables in the template making it very hard to read as it grows and prone to getting messy WordPress is a major culprit when it comes to that regard. Have you ever wanted to created separate layouts for different pages and break your sites into different parts e.g Sidebar, Comment Section, Header Section ? the regular approach would be to create individual pages for each section and simply add them as templates for the pages and with time, you can end up having tons of templates, however Twig allows you to easily inherit templates and also override the templates where you can inject content into the block easily. Don't worry if you don't understand the concept, the following parts will explain with an example of how to easily inherit layouts and templates. Layout <!DOCTYPE html> <html lang="en"> <head> {{include("layout/elements/header.twig")}} </head> <body> <div class="container-fluid" id="minimal"> <header id="pageIntro"> <div class="bio_panel"> <div class="bio_section col-md-6"> <h1>Okeowo Aderemi</h1> <h2>{{ page.body }}</h2> </div> </div> <div class="clearfix"></div> </header> <section id="page-body"> <div class="container"> <div id="intro" class="col-md-7 col-lg-7"> <h1>About me</h1> <h2> {{ page.summary }} </h2> </div> {block name="content"}{/block} <a style="font-size:1.799783em; font-style:italic;color:#d29c23" href="{{pages.get('/notes').url }}">Read more articles</a> </div> <div class="clearfix"></div> </div> </section> </div> <footer> <div class="header-container headroom headroom--not-top headroom--pinned" id="header-container"> {{include("layout/elements/footer.twig")}} </div> </footer> </body> </html> This is basically a layout where we specify blocks and include other templates for the page, don't panic if you don't understand what is going on, I will simply break down the weird part as follows: Include This basically is similar to native PHP 'include', as it's name suggests it simply includes the templates and injects the content into the layout , nothing out of the ordinary here if you are already familiar with php's include function. {{ output }} This simply evaluates the expression and prints the value, this evaluate expressions, functions that return contents , in my own short words it's basically the same as <?= output ?> except for the fact that it's cleaner to read. {% expression %} unlike the previous this executes statements such as for loops and other Twig statements. {% for characters in attack_on_titans %} <h1> {{characters}} </h1> {% endfor %} This executes a for loop and within the for loop, it creates a context to which variables in that context can be referenced and evaluated, unlike dealing with the opening and closing PHP tags, Twig simply blends in with markup and makes it really quick to read. I will simply post the contents of both the header and footer so you can see the content of what is included in the layout header.php <meta charset="utf-8"/> <meta content="IE=edge" http-equiv="X-UA-Compatible"/> <meta content="width=device-width, initial-scale=1" name="viewport"/> <title> {{ page.title }} </title> <link href=" {{config.urls.templates }}assets/css/bootstrap.min.css" rel="stylesheet"/> <link href="{{config.urls.templates }}assets/css/main.min.css" rel="stylesheet"/> <link rel='stylesheet' type='text/css' href='{{config.urls.FieldtypeComments}}comments.css' /> <link rel="stylesheet" href="{{config.urls.siteModules}}InputfieldCKEditor/plugins/codesnippet/lib/highlight/styles/vs.css"> <script type="text/javascript" src="{{config.urls.siteModules}}InputfieldCKEditor/plugins/codesnippet/lib/highlight/highlight.pack.js"></script> <script src="{{config.urls.templates }}assets/js/vendors/jquery-1.11.3.min.js"> </script> <script src="{{config.urls.templates }}assets/js/vendors/bootstrap.min.js"> </script> <script src="{{config.urls.FieldtypeComments}}comments.js"></script> <link rel="stylesheet" type='text/css' href="{{config.urls.templates}}js/jquery.fancybox.min.css"> <script src="{{config.urls.templates}}js/jquery.fancybox.min.js"></script> {block name="javascriptcodes"}{/block} footer.php <nav class="site-nav pull-right"> <div class="trigger"> <a class="page-link" href="{{pages.get('/about').url}}"> <span>{</span> About <span>}</span> </a> <a class="page-link" href="{{pages.get('/notes').url}}"> <span>{</span> Journals <span>}</span> </a> <a class="page-link" target="_blank" href="https://ng.linkedin.com/in/okeowo-aderemi-82b75730"> <span>{</span> Linkedin <span>}</span> </a> <a class="twitter page-link" target="_blank" href="https://twitter.com/qtguru"> <span>{</span> Twitter <span>}</span> </a> </div> </nav> There's nothing special here, other than twig simply injecting these fragments into the main layout , the next part is the most interesting and important concept and benefit that Twig has to offer {% block content %}{% endblock %} This tag simply creates a placeholder in which the content would be provided by the template inheriting this layout, in lay terms it simply means child templates will provide content for that block, the 'content' simply uses the name 'content' to refer to that specific block, so assuming we were to inherit this template it would simply look like this. Inheriting Template Layout {% extends 'layout/blog.twig' %} {% block content %} <div class="container blog-container"> <section class="blog"> <header class="blog-header"> <h1> {{page.title}} </h1> <h5 class="blog_date"> {{page.published|date("F d, Y")}} </h5> <br> </br> </header> <div class="blog_content"> <hr class="small" /> {{page.body}} <hr class="small" /> </div> </section> </div> {% endblock %} {% block nav %} <div class="col-md-4 col-xs-4 col-sm-4 prev-nav"> <a href="{{page.prev.url}}"> ← Prev </a> </div> <div class="col-md-4 col-xs-4 col-sm-4 home-nav"> <a href="{{homepage.url}}"> Home </a> </div> <div class="col-md-4 col-xs-4 col-sm-4 next-nav"> <a href="{{page.next.url}}"> Next → </a> </div> {% endblock %} In this snippet you can easily notice how each blocks previously created in the header and layout are simply referenced by their names, by now you will notice that twig doesn't care how you arrange the order of each block, all Twig does is to get the contents for each blocks in the child templates and inject them in the layout theme, this allows flexible templating and also extending other layouts with ease. Twig in Processwire Thanks to @Wanze we have a Twig Module for Processwire and it's currently what i use to build PW solutions to clients https://modules.processwire.com/modules/template-engine-twig/ The Modules makes it easy to not only use Twig in PW but also specify folders to which it reads the twig templates, and also injects Processwire objects into it, which is why i can easily make reference to the Pages object, another useful feature in this module is that you can use your existing template files to serve as the data provider which will supply the data to be used for twig template. take for example, assuming I wanted the homepage to display the top six blog posts on it, TemplateEngineTwig will simply load the home.php ( Depending on what you set as the template), it is also important that your twig file bears the same name as your template name e.g home.php will render into home.twig here is an example to further explain my point. home.php <?php //Get the Top 6 Blog Posts $found=$pages->find("limit=6,include=hidden,template=blog-post,sort=-blog_date"); $view->set("posts",$found); The $view variable is the TemplateEngine which in this case would be Twig, the set method simply creates a variables posts which holds the data of the blog posts, the method allows our template 'blog.twig' to simply reference the 'posts' variable in Twig Context. Here is the content of the 'blog.twig' template blog.tpl {% extends 'layout/blog.twig' %} {% block content %} <div class="block_articles col-md-5 col-lg-5"> {% for post in posts %} <div class="article_listing"> <span class="article_date"> {{post.published}}</span> <h2 class="article_title"> <a href="{{post.url}}">{{post.title}}</a> </h2> </div> {% endfor %} {% endblock %} So home.php sets the data to be used in home.tpl once Twig processes the templates and generates the output, twig takes the output from the block and injects it in the appriopriate block in the layout, this makes Processwire templating more flexible and fun to work with. The major advantage this has; is that you can easily inherit layouts and provide contents for them with ease, without the need of running into confusions when handling complex layout issues,an example could be providing an administrator dashboard for users on the template side without allowing users into the Processwire back-end. You can also come up with several layouts and reusable templates. Feel free to ask questions and any concerns in this approach or any errors I might have made or overlooked. Thanks1 point
-
Hello, I recently posted in this topic, but I decided to start my own thread because while I believe my issue is related to the one in that thread, they are not exactly the same: I have created a custom User Template in the method outlined in the docs. I am creating a directory, so it made sense that every page in the directory was a Directory Member, so they could log in and edit their own information while also keeping the entire directory protected behind a login wall. So the new user type is created: "directory-member". I then created two new roles: "member" and "directory-admin": The "member" only has the ability to View directory-member pages, and "profile-edit", which allows them to manage their own information. The "directory-admin" has the ability to edit any directory-member pages, and administer users. Some Directory Members are both, but all have at least the "member" role. The first hint that something was wrong was when I was testing a "member" user and I could not add a new item to a repeater on that profile. The url for the profile edit (this will be important shortly) is site.dev/admin/profile. The repeater is set up to load new items through AJAX. If this option is turned off, the rest of this issue is no longer completely valid. But as I have found what I believe to be a pretty large issue in the Processwire codebase, I thought it worth bringing up. See, every page (even a user) has a $page->editUrl() method, and it returns a URL like this: site.dev/admin/access/users/edit/?id=2096. That's all good and fine for users that have page-edit permissions, but if they don't, that link will resolve to the admin's equivalent of a 404. So the way that Processwire currently gets around this is by creating a specific editing area for a user to interact with only their profile: /admin/profile. And that works pretty nicely, except for the fact that nowhere is editUrl() ever made aware of the difference. editUrl() is not hookable, and whether or not a page is editable is based on the PagePermissions module. On top of that, there are several core modules that hardcode a search-and-replace (see InputfieldRepeater.module:627) where the editing screen is for Users. This doesn't allow for a huge degree of flexibility that is offered in other places throughout Processwire. If line 627 of InputfieldRepeater is changed from this: $editorUrl = str_replace('/access/users/edit/', '/page/edit/', $editorUrl); to this: $editorUrl = str_replace('/access/users/edit/', '/profile/', $editorUrl); ...the AJAX repeaters work. It's maddening! As is brought up in the thread I attached above, a lot of the features of page editing are missing within /admin/profile/, and it just makes for an altogether strange editing experience. A user who has "page-edit" permissions for templates other than directory-member, but does have "profile-edit" permissions, will see their user in the Page List, but cannot edit their Page unless they hover over the wrench and click the "Profile" link. It just seems - off. I think what this hinges on for me is that the editUrl() of the user should be "/admin/profile/" if that user is logged in (and their page should be editable from the Page List), or the "/admin/access/users/edit/" url; regardless of the URL, both links should resolve to the Page Edit screen, as the Profile Edit screen seems to be a unnecessarily neutered version of Page Edit.1 point
-
More on this: https://processwire.com/blog/posts/processwire-3.x-api-reference/ https://processwire.com/blog/posts/processwire-3.0.16-continues-expanding-documentation-and-more/ https://processwire.com/blog/posts/processwire-3.0.41-and-a-look-at-api-explorer/ Write one then send a pm to Ryan Probably, see: https://processwire.com/blog/posts/processwire-3.0.60-core-updates-and-more/ "...With the API reference now up-to-date, we'll soon be looking at the cheatsheet and hopefully updating that to pull data from the API Explorer module as well." Also search for the word cheatsheet in these: https://processwire.com/blog/posts/processwire-3.x-api-reference/ https://processwire.com/blog/posts/growing-processwire-in-2016/#cheatsheet-and-documentation Same as the tutorials Yes, there is. It's too late here for me to compile a good list but I will try to do it in the following days if others do not provide something faster than me...1 point
-
https://www.google.at/search?q=site:processwire.com+save+page+multilanguage+api1 point
-
Well after your reading I went back to suggest to the "customer" who is a close friend of mine and shared the reading. So now we agreed to have the language names versus the flags or even add a dropdown box listing some countries that speak the selected languages. Thanks again for the sharing.1 point
-
Same here. But as long as this is not implemented I'll do it on my own for my own modules1 point
-
Actually, I think that is a great idea to track interest for modules, but it would be nice to have this feature and tracked information more centrally and open, so a potential user can see 'popularity' of modules that it gonna use.1 point
-
Thanks @Robin S - I am seeing it too - the solution however I am not seeing I'll try to take another look tomorrow. Hi @neosin - this is not related to Tracy. Have a read:1 point
-
Thanks BitPoet, man I already had it set up that way originally. I don't know why I changed it. My template already had: <?php $myPath = $config->urls->templates;?> at the top, and my links were already "<a href="/site-name/pagename"> So now it works again by reverting. Not sure why I changed it... I guess I just didn't comprehend what that <?php $myPath = $config->urls->templates;?> was doing... means my site name itself can be anything I want. Thanks!1 point
-
Don't use relative paths. They are an artifact of days long gone (at least in regular websites - there are use cases in routed app components) and they create more problems than they solve. If you worry what happens when you move a site to a deeper directory, use PHP to prefix your links with $config->urls->root.1 point
-
Update. It seems to go back to expected behavior if in the template code, you place the pw-append before the id but not after. The order matters. ie. This works: <div pw-append='main' id='main-content'> This does not: <div id='main-content' pw-append='main'>1 point
-
I also noticed if I remove the id tags off the code in the template file, it returns to the correct order. What the id tags are named does not seem to effect the order.1 point
-
I understand what you mean, I had the opportunity to rewrite an existing application to Processwire, that was going to be alot of work plus the site works fine already, as I know my way around WordPress Development, sometimes some decisions are not technical just business sense.1 point
-
https://ckeditor.com/cke4/addon/templates I've used this in a project and worked well, though it's not so simple to set up, and the client can easily mess things up.1 point
-
You could use CKEditor Templates. This would be a 2-Click Solution and you can even choose which block to insert:1 point
-
Have a read here: http://daily.unitedlanguagegroup.com/stories/editorials/inside-design-language-selector-no-flags1 point
-
Hi @adrian, I'm seeing an issue in the Console panel where the indentation (tabs) of saved snippets gets lost when the page is reloaded. In the screen capture below I save the snippet with the correct indentation and I can load that snippet again okay until I reload the page. After that the snippet doesn't load with the correct indentation.1 point
-
So if any documentation project aimed at creating a user/admin/modules/best practices manual(s) was started up, I would be happy to help however I could. I'm more of a back-end server person, not a web designer, and I really don't understand how to effectively use pw yet so maybe I could learn something from writing stuff.1 point
-
Your parent page is not assigned in a correct way. $form->parent = $page('template=contact-form'); // optional, saves form as page should be $form->parent = $pages->get('template=contact-form')->id; // optional, saves form as page1 point
-
More updates! Here is what was added since my last post: FieldtypeDatetime now supports format argument. Which allows you to pass PHP date format string and retrieve your datetime values formatted the way you want. Besides FieldtypeDatetime fields, the built-in fields created and modified are also support new format argument. FieldtypeOptions is now supported. first and last fields on PageArray types are now supported. As per @eelkenet's request. See above post for details on those. Finally, now there is a way to add support for any fieldtype you want via third-party module. What you need to do is create a module with the name exactly as the fieldtype you want to add support for with "GraphQL" prefix. So for FieldtypeMapMarker it would be GraphQLFieldtypeMapMarker. Then you need to add three required methods and install it. This will automatically add GraphQL support for your desired fieldtype and it will be available in your GraphQL api. Checkout the documentation and an example module for reference.1 point
-
I have to admit I like Umbraco as an ASP.Net CMS as it has a similar philosophy to Processwire where everything is customisable, and you can create templates with whatever fields you like out of the box. ASP.Net is both its strength and weakness though in that if you need an ASP.Net based solution, Umbraco is good, but most of the time a PHP based solution is fine. I think Processwire is easier for beginners without a lot of coding experience. I like Umbraco's permissions system better, and the separation between document types and templates is handy if you need multiple templates for the same data, but just creates extra work if you don't. I'd say Processwire's documentation is outstanding, and quite apart from the CMS itself is part of what sets it apart.1 point
-
Thanks @adrian! I rerecorded the video many times before I could make it watchable. Trust me, you wouldn't say the same thing for the very first ones About the field access rules. Yeah that's true. By default the behavior is the opposite to the one in ProcessWire. I think it would be better for security if the module initially treats everything private. But I get what you mean. In cases where you have dozens of fields in one template, it would be too tedious to configure access for each of them. That's why there is an option to reverse the behavior in the advanced section of the module configuration. You can learn more about it here. This option basically makes all fields without Access rules available to the public and you can restrict access by enabling rules only to couple ones.1 point
-
Or just untick "Enable CSS source maps" in the Console settings (F1 in the Console).1 point
-
I'd rather like to see the reference to the map being removed from the css file. Sass maps are there for development, but not for production usage.1 point
-
There is one thing that needs a bit attention when importing data. We need to check if a record was also imported by a previous run of the script, otherwise you may end up with numerous duplicates. while($row = $result->fetch_assoc()) { $title = wire("sanitizer")->text($row['title'], array("maxLength"=>128)); // fetch and sanitize the title $p = wire('pages')->get("title={$title}"); // check if it already exists if(0 < $p->id) continue; // move on to the next record if it exists $p = new Page(); $p->template = "dam"; $p->parent = "something"; $p->title = $title; // use the sanitized title $p->save(); }1 point
-
Based on what I'm understanding from your last message, I think you should skip keeping the separate table. It just sounds like extra, unnecessary work, unless there's something more to this project that I don't yet understand. Instead, I think you should have your cron job execute a script that bootstraps ProcessWire and takes care of all the adding, updating and deleting of records consistent with the web service you are reading from. This is something that I think ProcessWire is particularly good at, because it's been designed for this from the beginning (it's something I have to do with a lot of my client work). Whether XML or JSON doesn't matter much, as PHP includes the ability to read from either type quite easily. Though like the other guys here, I generally prefer JSON just because it's less verbose and less fuss. If JSON, you'll pull the feed and use PHP's json_decode() to convert it to an array. If XML, you'll use PHP's SimpleXML to convert it to an array. Once you've got the array of raw data, you'll iterate through it and add, update, or delete pages in ProcessWire to make it consistent with the data you are pulling from the web service. Live, working example I think that the best way to demonstrate it is with a live, working example. This one uses the existing modules.processwire.com/export-json/ feed. You might also want to see the feed in human-readable mode to get a better look at the format. Below is a shell script that bootstraps ProcessWire, reads from that feed and maintains a mini "modules directory" site, on your own site. I made this feed so that it can be tested and used on a brand new installation using the basic profile (included with PW). If left how it is, it'll create a mini modules directory site below the '/about/what/' page and use the template 'basic-page' for any pages it adds. But you can run this on any ProcessWire installation by just editing the script and changing the parent from '/about/what/' to something else, and changing the template from 'basic-page' to something else, if necessary. This script assumes that the template used has 3 fields: title, body, and summary. The 'basic-page' template in PW's default profile already has these. If you adapt this for your own use, you'd probably want to change it to use more specific fields consistent with what you need to store on your pages. In this example, I'm just building a 'body' field with some combined data in it, but that's just to minimize the amount of setup necessary for you or others to test this… The purpose is that this is something you can easily run in the default profile without adding any new templates, fields, pages, etc. 1. Paste the following script into the file import-json.php (or download the attachment below). For testing purposes, just put it in the same directory where you have ProcessWire installed. (If you place it elsewhere, update the include("./index.php"); line at the top to load ProcessWire's index.php file). 2. Edit the import-json.php file and update the first line: "#!/usr/bin/php", to point to where you have PHP installed (if not /usr/bin/php). Save. 3. Make the file executable as a shell script: chmod +x ./import-json.php 4. Run the file at the command line by typing "./import-json.php" and hit enter. It should create about 95 or so pages under /about/what/. Take a look at them. Run it again, and you'll find it reports no changes. Try making some changes to the text on 1 or 2 of the pages it added and run it again, it should update them. Try deleting some of it's pages, and it should add them back. Try adding some pages below /about/what/ on your own, run it again, and it should delete them. import-json.php #!/usr/bin/php <?php // replace the path in the shabang line above with the path to your PHP // bootstrap ProcessWire. Update the path in the include if this script is not in the same dir include("./index.php"); // if you want to run this as a PW page/template instead, remove everything above (except the PHP tag) // save our start time, so we can find which pages should be removed $started = time(); // keep track of how many changes we've made so we can report at the end $numChanged = 0; $numAdded = 0; $numTrashed = 0; // URL to our web service data $url = 'http://modules.processwire.com/export-json/?apikey=pw223&limit=100'; // get the data and decode it to an array $data = json_decode(file_get_contents($url), true); // if we couldn't load the data, then abort if(!$data || $data['status'] != 'success') throw new WireException("Can't load data from $url"); // the parent page of our items: /about/what/ is a page from the basic profile // update this to be whatever parent you want it to populate... $parent = wire('pages')->get('/about/what/'); if(!$parent->id) throw new WireException("Parent page does not exist"); // iterate each item in the feed and create or update pages with the data foreach($data['items'] as $item) { // see if we already have this item $page = $parent->child("name=$item[name]"); // if we don't have this item already then create it if(!$page->id) { $page = new Page(); $page->parent = $parent; $page->template = 'basic-page'; // template new pages should use $page->name = $item['name']; echo "\nAdding new page: $item[name]"; $numAdded++; } // now populate our page fields from data in the feed $page->of(false); // ensure output formatting is off $page->title = $item['title']; $page->summary = $item['summary']; // To keep it simple, we'll just populate our $page->body field with some combined // data from the feed. Outside of this example context, you'd probably want to // populate separate fields that you'd created on the page's template. $body = "<h2>$item[summary]</h2>"; $body .= "<p>Version: $item[module_version]</p>"; foreach($item['categories'] as $category) $body .= "<p>Category: $category[title]</p>"; $body .= "<p><a href='$item[download_url]'>Download</a> / <a href='$item[url]'>More Details</a></p>"; $page->body = $body; // print what changed $changes = $page->getChanges(); if(count($changes)) { $numChanged++; foreach($changes as $change) echo "\nUpdated '$change' on page: $page->name"; } // save the page $page->save(); } // now find pages that were not updated above, which indicates they // weren't in the feed and should probably be trashed $expired = $parent->children("modified<$started"); foreach($expired as $page) { echo "\nTrashing expired page: $page->name"; $page->trash(); // move to trash $numTrashed++; } echo "\n\n$numAdded page(s) were added"; echo "\n$numChanged page(s) were changed"; echo "\n$numTrashed page(s) were trashed\n"; import-json.php.txt Running the script as a cron job: You can instruct your cron job to run the script and it should be ready to go. You may want to move it to a non web accessible location for more permanent use. You'll also want to update your bootstrap "include()" line at the top to have the full path to your ProcessWire index.php file, as your cron job probably isn't executing it from the web root dir like you were manually. Running the script as a template file: You can run this script as a template file on a page by removing the include() line and everything above it with this line: <pre><?php Place it in your /site/templates/ directory, add the template from PW admin, and create a page that uses it, then view it.1 point
-
Nico, replace your $page->hidden = 'true'; with this: $page->addStatus(Page::statusHidden);1 point