Leaderboard
Popular Content
Showing content with the highest reputation on 03/13/2016 in all areas
-
4 points
-
@adrianmak, is it just a copy&paste mistake that your code accesses $category while you set outputFormatting to false on $page? @gebeer: Additionally, there are two other methods to get a page field's value in another language (with the first one being my favorite): // 1. Directly from the page: $title_en = $category->getLanguageValue($eng, 'title'); // 2. A bit more circumspect: $titleField = $category->getUnformatted('title'); $title_en = $titleField->getLanguageValue($eng);3 points
-
Support thread for WireMail Mailgun. For bug reports, please use the bug tracker on GitHub. https://github.com/plauclair/WireMailMailgun2 points
-
Composer is a package manager. If the package is a framework, a library or just a code snippet is not really relevant so much. And here you go about what things to use/do with composer. My goto libraries: Carbon, Nette/Forms, kahlan, faker, any of those when needed; As soon as I've more time to put into CI I'll certainly look into using phpdotenv. Probably every third party service integration like Mail / Calender / Paying Gateway should be managed by composer. Any finally if you ever need to use non-php software on your server like redis or elastic-search or others you can find php libraries to use those as well.2 points
-
If I've understood the question correctly, the answer is I don't know. But my guess so far is: I'm not sure you'll want Composer for this, as PW is still a system that needs to be installed and connected with a database, etc. Meaning, you can't use PW's API unless you've installed it, and that installation involves a few screens of configuring the DB, creating admin account, etc. Whereas, it seems that most of the things one pulls in with Composer are more PHP library specific. But it's a good question because we are going to be making the ProcessWire core available on Packagist, so it'll be interesting to find out exactly what one would do with it from there because ProcessWire is not something that will run out of a /vendor/ directory or just start working because one of its classes was autoloaded. I suspect we'll have some trial and error and things to discover here as we go through the process.2 points
-
I solved it by doing it a little bit differently - now sorting and filtering works: $siblings = $page->siblings("sort=date"); $prev = $siblings->getNext($page); $next = $siblings->getPrev($page); "Last" and "first" can be tested easily: if you reach the last or first item, $next or §prev return NULL (or have no ID) - LostKobrakai's code works great to catch that. So the difference was that I build my own pageArray ($siblings) instead of using $page, as suggested, to enable sorting. It was so easy to solve that I did not see it right before my eyes2 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
-
Hello, this module can publish content of a Processwire page on a Facebook page, triggered by saving the Processwire page. To set it up, configure the module with a Facebook app ID, secret and a Page ID. Following is additional configuration on Facebook for developers: Minimum Required Facebook App configuration: on Settings -> Basics, provide the App Domains, provide the Site URL, on Settings -> Advanced, set the API version (has been tested up to v3.3), add Product: Facebook Login, on Facebook Login -> Settings, set Client OAuth Login: Yes, set Web OAuth Login: Yes, set Enforce HTTPS: Yes, add "https://www.example.com/processwire/page/" to field Valid OAuth Redirect URIs. This module is configurable as follows: Templates: posts can take place only for pages with the defined templates. On/Off switch: specify a checkbox field that will not allow the post if checked. Specify a message and/or an image for the post. Usage edit the desired PW page and save; it will post right after the initial Facebook log in and permission granting. After that, an access token is kept. Download PW module directory: http://modules.processwire.com/modules/auto-fb-post/ Github: https://github.com/kastrind/AutoFbPost Note: Facebook SDK for PHP is utilized.1 point
-
This week we've been focused on one of the major 3.x roadmap items, which is to have strong Composer support in ProcessWire. In this post, we now have an outlined process and a full proof-of-concept module built to demonstrate it all, plus info on how other module authors can support Composer as an alternative installation method. To add to that, we get into Google's Client API with a new 3.x specific module and how to connect Google's Calendar services with ProcessWire. https://processwire.com/blog/posts/composer-google-calendars-and-processwire/1 point
-
Excellent walk-through on the blog, Ryan! I'm sure this will also broaden PW's appeal outside the current community. However, as a musician, every time I see Composer's logo I cringe...it's a conductor, not a composer FFS! Oh well, close enough for PHP, I guess.1 point
-
Ryan is not using a markdown parser for field descriptions/notes as they're only supporting a small subset of markdown (I think links/bold/italic and that's it) and it's faster this way, but that's probably the reason, why escaping doesn't work.1 point
-
I am getting the same error with multi language fields when I use getLanguageValue method. When I use the other approach it is working. Try this $categories = $pages->get(1060)->children(); $language = $user->language; // save the user's language foreach($categories as $category) { $user->language = $languages->get('default'); // you need to use default because English is your default language and set the $user language to it $content .= $category->title; } $user->language = $language; // restore the user's language If you want to show page titles in all languages, just loop through your languages inside the foreach: foreach($categories as $category) { foreach ($languages as $lang) { $user->language = $lang; $content .= $category->title; } }1 point
-
Thanks @horst - some great things for me to try there. I can see the sense of just having index.php in the physical document root! Cheers1 point
-
1 point
-
oh yeah, cool, for some reason i thought i fixed it, but i will take care of it and update - many thanks for reporting this!1 point
-
v0.4 release Small update with improvements on the addAttachment() method and an added validateEmail() method, using Mailgun's email validation service. https://github.com/plauclair/WireMailMailgun/releases/tag/0.41 point
-
I ended up making a ton of mods to this for things that were very specific to our needs. Coincidentally, I worked on cleaning it up and getting it back to a more releasable state earlier this week. I'll take a look at it on the devns branch on Monday/1 point
-
You need to create a file /site/modules/InputfieldCKEditor/contents.css with the styles you want to apply inside the Editor. And have a look at /site/modules/InputfieldCKEditor/README.txt on how to activate it: contents.css ============ Example CSS file for the admin editor. To make CKEditor use this file, go to your CKEditor field settings and specify /site/modules/InputfieldCKEditor/contents.css as the regular mode Contents CSS file.1 point
-
Hi Peter I can share with you my Mailgun journey.... I have: FormBuilder PW 3 latest dev Mailgun account Horst's wonderful WireMailSmtp module As you say mate, lots of moving static parts What stumped me was that I too was not getting any mails sent. Now I develop sites locally in MAMP and for some reason, the "sending of emails" did not work when I tried to get ProcessWire to send an email out from within my development environment. What I ended up doing was uploading the site to a live server and once I did that, mails were sent instantly! I am not sure what exactly causes the MAMP environment to have trouble sending things out Peter, but that just tells me maybe you should not trust your development environments settings to be able to send out emails. Try a live server Peter. The parts you have do work, as I have gotten them to wok and I'm just a dummy ( but a persistent one ) Hope that helps! For others that are looking into this in the future.. those of you that are using Digital Ocean, so much of the documentation for Digital Ocean and Mailgun focuses on using your existing DNS settings for your domain but adding records into it for a Mailgun subdomain. This approach really caused me lots of headaches so here is how I got it all working... My domain name is www.mysite.com. Let's use this as a base. First thing in Digital Ocean is to create a brand new domain to create a subdomain. This is not commonly mentioned in Digital Oceans docs or all the Digital Ocean - Mailgun tutorials out there. So, I have departed from everyone else by NOT touching my main website's ( www.mysite.com ) records. So I created a brand new domain called mail.mysite.com. The "mail" bit is my chosen subdomain. But as I said, I created a brand new domain to create that desired subdomain. Now, in Mailgun, I created an account with the domain mail.mysite.com to match the new domain I created in Digital Ocean. When you create the account in Mailgun, you will get two text values and one cname value. Head over to Digital Ocean and for your domain entry ( mine being the mail.mysite.com domain entry ), we edit the DNS entries and add the two text records and add a single cname record. Go back to Mailgun and hit the Check DNS Records Now button and the 3 entries mentioned earlier should light up green. Sometimes we may need to press the button two or three times Next in Mailgun go to Manage SMTP credentials and create a new user. Take note of this and the password it generates. We take these to our site and punch them into Horst's WireMail user credentials along with setting the Port to 587 and the SMTP hostname to be smtp.mailgun.org. There is also the local hostname field and I set that to www.mysite.com. After doing all this, it works great! Just a side note. What I have done is used a nice paid email service called FastMail. And so how I have set things up is I edit my main site's DNS records ( www.mysite.com ) and add two text records pointing to FastMail. So.... my domain mail.mysite.com sends out mails via Mailgun and by configuring things in WireMailSmtp I have set the sender address for these emails as: admin@mysite.com Should someone reply to one of these emails, then Fastmail picks that up via mysite.com's records and routes it to my email client. They have a really nice iOS app for business accounts by the way. And what is not apparent from signup is that you can pay for 6 months first. Apologies if the above helps no one and or contains lots of errors!1 point
-
You don't appear to be saving the page anywhere, so do this: $p->save('textJSon'); after you set the $value1 point
-
@OllieMackJames: Are you able to use PW 3.0.10 for your site, or do you need to stick with PW 2.7 ? If you can use PW 3, you simply may copy one of the ImageSizerEngines into your site modules directory, change the classname, and add the optimization functionality. To add the functionality, I highly would suggest to use an API image option, e.g. array("optimize" => true). This way you are able to control which variation should be passed to the optimisation service and which one not. At the end of the image manipulation, you can ask if optimize is set: if($this->optimize) { $optimus = new Optimus('<your_license_key>'); $result = $optimus->optimize($dstFilename); file_put_contents($dstFilename, $result); } That's all you need. You also may set the optimize default to true in site/config.php $config->imageSizerOptions = array(..., ..., ..., "optimize" => true)); and then explicitly disable it via individual API option settings in your template files. Just choose what fits best for your site.1 point
-
I agree it's not optimal either, but the best way to do this is to click on "Jo" to highlight it before dragging "Dateityp" under it. Think of it as expanding the tree below "Jo". Even though it has no children yet, you have to "open" them before you can put one in. It makes a lot of sense but I agree it's not the most intuitive experience. With Ryan adding long-click actions now, I think this would be a use case worth considering. I. e., dragging and holding a page on top of another page for one second could make it expand.1 point
-
Well spotted Willy! Cintia, before translating from portuguese to english, make sure you don't have mistakes on the portuguese words because it messes up the translation even more than usually. For instance, you missed the R in "Brasileia" and the G on "seuinte", and there are more. edit: Duh, you won't understand this probably , here you go: Cintia, antes de traduzires para inglês verifica que não tens erros no português. Isso vai tornar a tradução ainda mais caótica do que já fica normalmente. Por exemplo, falta o R em "Brasileia" e o G em "seuinte", e há mais assim.1 point
-
For me you two seem to speak the same language1 point
-
lost koberoaki bcoz he ask how.to do w/pw i.ansor more for "cylng lots pages" then "sum lotso pages" uses.either for sum butt uses my codes if u.might needs more todo1 point
-
If you want to avoid converting the selector to SQL you can let ProcessWire do it for you as teppo has shown in this post: $selectors = new Selectors("template=blog-post, limit=1000000"); $pagefinder = new PageFinder(); $sql = $pagefinder->getQuery($selectors->getArray(), array())->getQuery(); Then you can modify that or wrap your own select around it: select sum(data) as sum_amount from field_amount where pages_id in ($sql)1 point
-
1 point
-
I don't want to meshup (is that a word ?) with the great post of Radek. But I have good experience with mediaElement.js to use audio & video.1 point