Jump to content

abdus

Members
  • Posts

    743
  • Joined

  • Last visited

  • Days Won

    42

Everything posted by abdus

  1. That makes it clear. I've also found this part from the core inside Wire class. <?php // /wire/core/Wire.php public function wire($name = '', $value = null, $lock = false) { // ... if(is_object($name)) { // make an object wired (inject ProcessWire instance to object) if($name instanceof WireFuelable) { if($this->_wire) $name->setWire($wire); // inject fuel, PW 3.0 if(is_string($value) && $value) { // set as new API var if API var name specified in $value $wire->fuel()->set($value, $name, $lock); } $value = $name; // return the provided instance } else { throw new WireException("Wire::wire(\$o) expected WireFuelable for \$o and was given " . get_class($name)); } } // ... } Here, setWire() method sets the instance. /** * Set the current ProcessWire instance for this object (PW 3.0) * * Specify no arguments to get, or specify a ProcessWire instance to set. * * #pw-internal * * @param ProcessWire $wire * */ public function setWire(ProcessWire $wire) { $this->_wire = $wire; $this->getInstanceNum(); } Another question @LostKobrakai: Does every single object from PW core needs an instance to be set? What classes are instance-critical, so to speak?
  2. There's no need. I'll provide an option to remove local copy right away on successful upload, and a maintenance function to remove mirrored files from local drive as well. The module doesn't even need to be a custom fieldtype, it hooks into built-in modules to get file paths then upload to cloud.
  3. Yeah, WYSIWYG is very restrictive when you know what you're doing. Github Flavored Markdown + custom markup is a godsent for writings with lots of moving parts.
  4. Yes, that must be a given. I'll be sure to create a detailed summary of the writing with a clear list of outcomes to ensure a good transition from forum->blog->forum. I'd like hear those . In fact, in the future if this initiative gets popular, I'll probably create a post where people can make requests. That's a really good bonus that I didnt think about.
  5. Hey @joer80, I've been developing a module that mirrors every (or ones you specify) upload to S3 bucket. I've started with S3 and Cloudfront support and I'm planning to support other CDNs in the future. Backblaze seems like a great provider to include as well. Once it is finished, it will be a near complete CDN solution. Right now basic uploads are working fine, but it's not ready for production yet. However, I hope to complete and release it in a few weeks. Is there any feature that you think would be nice? Some features I'm planning to include before releasing: Full Amazon S3 support Origin pull support Process module/page for managing buckets and objects, such as clearing buckets, force uploading files & directories, maintenance mode etc. Configuration page Caching uploaded file paths in DB to keep track of uploaded files and fallback to local to local urls to prevent 404 errors. Textformatter module for rewriting local urls to CDN urls. Optional custom domain support etc. Form integration with AJAX for quick upload/upload all/force upload/delete/delete all etc buttons for file/image fields while editing a page/image. Hooks for custom file uploads Extendable/hookable API for new services Future considerations Other providers. Backblaze, MaxCDN etc. Wireshell integration, (not sure how extensible WS is, but it'd be nice to have) CSS rewrites? (for imports, images etc)
  6. Hey guys, I've recently started a blog on web development and ProcessWire, and I'm slowly building content to make it a habit and not let it become a failed experiment. I also believe that a good way to keep producing new content is to build an audience that cares about what you write and what you offer. So, I've started writing a series on module development, from basic to complex, where I explore core modules and unearth the valuable documentation inside PW core and put them into practice. I'll be stripping core modules to their essentials like blueprints that people can build upon, and construct various modules that replicate popular ones on other platforms, or ones that bring some of their eye-caching features. I haven't implemented a commenting system on my blog, because I want to share and collect feedback and recommendations here, in this forum. Is there an etiquette on promoting my own content? Is it welcome, or how much is OK, or how much is too much? Thanks in advance, Abdus.
  7. Without a backslash class names resolve to current namespace (ProcessWire), and since DateTime class is in global namespace, you should put \ before DateTime. $today = new \DateTime(); Also, I've realized that I am getting the same error, but $pages->find() is still returning a result (because when DateTime object is converted to string it turns into an empty string '', and the selector becomes "published>'' ", which returns all pages). You can fix the error using: $today = new \DateTime(); // use UNIX timestamps $aFewDaysAgo = $today->modify('-2 days')->getTimestamp(); $lastYear = $today->modify('-1 year')->getTimestamp(); // or convert to ISO format // $aFewDaysAgo = $today->modify('-2 days')->format('c'); // $lastYear = $today->modify('-1 year')->format('c'); $latestPages = $pages->find("published>=$aFewDaysAgo"); $pagesSinceLastYear = $pages->find("published>=$lastYear");
  8. You can use DateTime class. http://php.net/manual/en/class.datetime.php $today = new \DateTime(); $aFewDaysAgo = $today->modify('-2 days'); $lastYear = $today->modify('-1 year'); $latestPages = $pages->find("published>=$aFewDaysAgo"); $pagesSinceLastYear = $pages->find("published>=$lastYear");
  9. You shouldn't put $page-> in there, just use the name of the field. And your sort field must be an existing field. $items = $pages->find("template=recipes-inner, published>=$start, published<=$end, sort=-published, limit=9"); Also you don't have to manually build the boundaries of today, you can just use today $items = $pages->find("template=recipes-inner, published>=today, sort=-published, limit=9"); In fact, you can use any valid PHP date that you can use with DateTime constructor. http://php.net/manual/en/class.datetime.php published>=today published>=1365436783 published>=2013-04-08 published>=4/8/2013 published>=8.4.2013 published>="April 8, 2013"
  10. From the documentation for selectors <?php // single condition // pages published today, sorted new->old $recentItems = $pages->find("template=product, my_date_field>=today, sort=-my_date_field"); // multiple conditions with AND operator // pages published this year until today $thisYear = date('Y-01-01'); // 2017-01-01 or 2011-01-01 etc $recentItems = $pages->find("template=product, published>=$thisYear, published<=today");
  11. I'm building a module using Comments field as reference. Inside Comments module and many others, there's always this expression which is used to inject dependencies (according to Wire class documentation) $fieldset = $this->wire(new InputfieldWrapper()); Is there a reason for injecting API variables inside objects? Because everything seems to work fine without.
  12. @skylundy, turns out ProcessWire has a built-in class loader. It uses spl_autoload_register() internally. Given a folder structure: /site/templates/ components/ Composer.php Writer.php app.php where Composer.php and Writer.php are classes under \ProcessWire namespace You can autoload them using // /site/templates/app.php $loader = new WireClassLoader($wire); // or use $classLoader since it's already an API variable. // autoload classes inside /site/templates/components/ $componentsPath = wire()->config->paths->templates . 'components/'; $loader->addNamespace('ProcessWire', $componentsPath); // then you can reference classes just fine. $b = new Composer(); $w = new Writer(); $w->write($b->compose('hello')); http://processwire.com/api/ref/class-loader/
  13. Does every character light up when you paste your content here? Can you see all groups matched up correctly on the right? https://regex101.com/r/sZsQg5/2
  14. Make sure file starts with <?php namespace ProcessWire;
  15. You should check whether item has value inside the loop (where you're building $content) and add item to list only if its value is not empty foreach ($order as $o) { $item = $nutrition->get("name%=$o"); // skip item if its value is not populated if(empty($item->value)) continue; // item has value, add it to the list $content .= ...; } // now echo the $content echo $content
  16. You should check using if(!empty($item->value)) { // ... } Because, $item is a WireData object, it can't be empty. But, if value is not populated, $item->value will be empty, and you can check that.
  17. After this tutorial you'll have learned how to: Build a Process module Make an AJAX request to backend Serve JSON as response Let's say you want to display the latest orders in a dashboard that you can access from admin panel. And you want it to refresh its content with a button click. Most straightforward and proper way (that I know of) is to create a Process module, as they're built for this purpose. First, create a directory under /site/modules/, call it ProcessDashboard, and create a file named ProcessDashboard.module under that directory. Following is about the least amount of code you need to create a Process module. <?php namespace ProcessWire; class ProcessDashboard extends Process { public static function getModuleInfo() { return [ 'title' => 'Orders Dashboard', 'summary' => 'Shows latest orders', 'version' => '0.0.1', 'author' => 'abdus', 'autoload' => true, // to automatically create process page 'page' => [ 'name' => 'order-dashboard', 'title' => 'Orders', 'template' => 'admin' ] ]; } public function ___execute() { return 'hello'; } } Once you refresh module cache from Modules > Refresh, you'll see your module. Install it. It will create an admin page under admin (/processwire/) and will show up as a new item in top menu, and when you click on it, it will show the markup we've built in execute() function. All right, now let's make it do something useful. Let's add create a data list to display latest orders. We'll change execute() function to render a data table. public function ___execute() { /* @var $table MarkupAdminDataTable */ $table = $this->modules->MarkupAdminDataTable; $table->setID($this->className . 'Table'); // "#ProcessDashboardTable" $table->headerRow([ 'Product', 'Date', 'Total' ]); // fill the table foreach ($this->getLatest(10) as $order) { $table->row([ $order['title'], $order['date'], $order['total'] ]); } // to refresh items $refreshButton = $this->modules->InputfieldSubmit; $refreshButton->name = 'refresh'; $refreshButton->id = $this->className . 'Refresh'; // "#ProcessDashboardRefresh" $refreshButton->value = 'Refresh'; // label of the button return $table->render() . $refreshButton->render(); } where getLatest() function finds and returns the latest orders (with only title, date and total fields) protected function getLatest($limit = 5, $start = 0) { // find last $limit orders, starting from $start $orders = $this->pages->find("template=order, sort=-created, limit=$limit, start=$start"); // Only return what's necessary return $orders->explode(function ($order) { return [ 'title' => $order->title, 'date' => date('Y-m-d h:i:s', $order->created), 'total' => $order->total ]; }); } When you refresh the page, you should see a table like this Now we'll make that Refresh button work. When the button is clicked, it will make an AJAX request to ./latest endpoint, which will return a JSON of latest orders. We need some JS to make AJAX request and render new values. Create a JS file ./assets/dashboard.js inside the module directory. window.addEventListener('DOMContentLoaded', function () { let refresh = document.querySelector('#ProcessDashboardRefresh'); let table = document.querySelector('#ProcessDashboardTable'); refresh.addEventListener('click', function (e) { // https://developer.mozilla.org/en/docs/Web/API/Event/preventDefault e.preventDefault(); // Send a GET request to ./latest // http://api.jquery.com/jquery.getjson/ $.getJSON('./latest', { limit: 10 }, function (data) { // check if data is how we want it // if (data.length) {} etc // it's good to go, update the table updateTable(data); }); }); function renderRow(row) { return `<tr> <td>${row.title}</td> <td>${row.date}</td> <td>${row.total}</td> </tr>`; } function updateTable(rows) { table.tBodies[0].innerHTML = rows.map(renderRow).join(''); } }); And we'll add this to list of JS that runs on backend inside init() function public function init() { $scriptUrl = $this->urls->$this . 'assets/dashboard.js'; $this->config->scripts->add($scriptUrl); } Requests to ./latest will be handled by ___executeLatest() function inside the module, just creating the function is enough, PW will do the routing. Here you should notice how we're getting query parameters that are sent with the request. // handles ./latest endpoint public function ___executeLatest() { // get limit from request, if not provided, default to 10 $limit = $this->sanitizer->int($this->input->get->limit) ?? 10; return json_encode($this->getRandom($limit)); } Here getRandom() returns random orders to make it look like there's new orders coming in. protected function getRandom($limit = 5) { $orders = $this->pages->find("template=order, sort=random, limit=$limit"); return $orders->explode(function ($order) { return [ 'title' => $order->title, 'date' => date('Y-m-d h:i:s', $order->created), 'total' => $order->total ]; }); } And we're done. When refresh button is clicked, the table is refreshed with new data. Here it is in action: 2017-04-29_19-01-40.mp4 (227KB MP4, 0m4sec) Here's the source code: https://gist.github.com/abdusco/2bb649cd2fc181734a132b0e660f64a2 [Enhancement] Converting page titles to edit links If we checkout the source of MarkupAdminDataTable module, we can see we actually have several options on how columns are built. /** * Add a row to the table * * @param array $a Array of columns that will each be a `<td>`, where each element may be one of the following: * - `string`: converts to `<td>string</td>` * - `array('label' => 'url')`: converts to `<td><a href='url'>label</a></td>` * - `array('label', 'class')`: converts to `<td class='class'>label</td>` * @param array $options Optionally specify any one of the following: * - separator (bool): specify true to show a stronger visual separator above the column * - class (string): specify one or more class names to apply to the `<tr>` * - attrs (array): array of attr => value for attributes to add to the `<tr>` * @return $this * */ public function row(array $a, array $options = array()) {} This means, we can convert a column to link or add CSS classes to it. // (ProcessDashboard.module, inside ___execute() method) // fill the table foreach ($this->getLatest(10) as $order) { $table->row([ $order['title'] => $order['editUrl'], // associative -> becomes link $order['date'], // simple -> becomes text [$order['total'], 'some-class'] // array -> class is added ]); } Now, we need to get page edit urls. By changing getLatest() and getRandom() methods to return edit links in addition to previous fields protected function getLatest($limit = 5, $start = 0) { // find last $limit orders, starting from $offset $orders = $this->pages->find("template=order, sort=-created, limit=$limit, start=$start"); return $orders->explode(function ($order) { return [ 'title' => $order->title, 'date' => date('Y-m-d h:i:s', $order->created), 'total' => $order->total, 'editUrl' => $order->editUrl ]; }); } protected function getRandom($limit = 5) { $orders = $this->pages->find("template=order, sort=random, limit=$limit"); return $orders->explode(function ($order) { return [ 'title' => $order->title, 'date' => date('Y-m-d h:i:s', $order->created), 'total' => $order->total, 'editUrl' => $order->editUrl ]; }); } and tweaking JS file to render first column as links function renderRow(row) { return `<tr> <td><a href="${row.editUrl}">${row.title}</a></td> <td>${row.date}</td> <td>${row.total}</td> </tr>`; } we get a much more practical dashboard.
  18. AJAX or normal request doesn't really matter to backend. You just detect if request is an AJAX request and act accordingly. Because I had some things to do, I decided to procrastinate by building a simple module and write a tutorial for it.
  19. You're outputting $content with each iteration. Try putting echo $content after the closing brace } of foreach() {} foreach ($order as $o) { $item = $nutrition->get("name%=$o"); // $content .= ...; } echo $content;
  20. // $nutrition = new WireArray(); $order = [ 'Calories', 'Fat', 'Saturated', 'Salt', 'Carbs', 'Fibre', 'Sugar', 'Proteins', ]; $markup = ''; foreach ($order as $o) { $item = $nutrition->get("name%=$o"); // render item $markup .= $item->name; $markup .= $item->amount; $markup .= $item->unit; } // use markup echo $markup; A more flexible way would be to create a Page field with AsmSelect that you can add/remove and reorder items. But for a simple setup, this should suffice.
  21. Since $nutrition is a WireArray, you can get calories using selectors and echo it $cal = $nutrition->get('name%=cal'); echo $cal->name; // "Calorie" echo $cal->amount; // "650.2" echo $cal->unit; // "kcal" anywhere you like. Check out WireArray documentation https://processwire.com/api/ref/wire-array/
  22. Setting namespace should be enough, but try setting $config->compileModule = false https://github.com/processwire/processwire/blob/master/wire/config.php#L949
  23. You can use regex to split lines into parts (name, amount, unit) and use a combination of WireArray and WireData objects. In the end you'll have turned a list like this Proteins: 45.3 g Calories: 650 kcal Fat: 30g Carbs: 1.2g Sugar: 0.5g Salt: 1.1g Saturates: 15g Fibre: 0.2g Into one that looks like $text = $sanitizer->textarea($page->body); // remove markup from body $nutrition = new WireArray(); // regex pattern to extract values $pattern = '/^' . // beginning of each line '\s*' . // may start with some whitespace '(?P<name>[^:]+)' . // name (first part until colon) '\s*:\s*' . // a colon that may be surrounded by whitespaces '(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56) '\s*' . // after the number there may be some whitespace '(?P<unit>\w+)'. // unit (one or more letters) '\s*$/'; // there might be some whitespace before the end foreach (explode("\n", $text) as $line) { preg_match($pattern, $line, $parts); $nutrition->add((new WireData())->setArray([ 'name' => $parts['name'], 'amount' => floatval($parts['amount']), 'unit' => $parts['unit'] ])); } // Since $nutrition is a WireArray, you can do things like // $nutrition->sort('-amount'); // sort by amount descending // $cal = $nutrition->get('name=calorie'); // then create a list $content .= "<ul class='nutrition-list'>"; $content .= $nutrition->each('<li class="nutrition"><span class="nutrition__name">{name}</span> <span class="nutrition__value">{amount}</span> <span class="nutrition__unit">{unit}</span></li>'); $content .= "</ul>"; // Keep CSS out of PHP, in its own file $content .= "<style> .nutrition-list { list-style: none; display: flex; flex-wrap: wrap; } .nutrition { flex: 1 0 25%; } .nutrition__name { display: block; font-weight: bold; } .nutrition__value { font-size: 2em; } .nutrition__unit { opacity: 0.75; } </style>";
×
×
  • Create New...