Jump to content
MatthewSchenker

Create Pages (with File-Upload Field) via API

Recommended Posts

Greetings Everyone,

*************************************************
*************************************************
EDIT NOTE: This post started as a work-in-progress discussion as I was working out the elements of a successful form. After contributions from participants in this discussion, the code below has been tested and works well.

You can use the code as shown below in your ProcessWire templates!

Feel free to follow up with additional quesations/comments!
*************************************************
*************************************************

I have successfully built front-end forms with ProcessWire to add pages via the API. It works great -- until I had to include image uploads along with the "regular" form fields.  Then it temporarily got a bit complicated.

In this discussion, I show how to handle front-end submissions in ProcessWire with the goal of allowing us to create pages from custom forms.  I then go a step further and show how to use the same form to upload files (images and other files).

I'm hoping this discussion can illustrate the whole process. I know a lot of people are interested in using ProcessWire to do front-end submissions, and my goal for this discussion is to benefit others as well as myself!

First, here's my original contact form (no file uploads):

<form action="/customer-service/contact/contact-success/" method="post">
<p><label for="contactname">Name:</label></p>
<p><input type="text" name="contactname"></p>
<p><label for="email">E-Mail:</label></p>
<p><input type="email" name="email"></p>
<p><label for="comments">Comments:</label></p>
<p><textarea name="comments" cols="25" rows="6"></textarea></p>
<button type="submit">Submit</button></form>

And here's the "contact-success" page that picks up the form entry to create ProcessWire pages:

<?php // First, confirm that a submission has been made
if ($input->post->contactname) {

// Save in the ProcessWire page tree; map submission to the template fields
$np = new Page(); // create new page object
$np->template = $templates->get("contact_submission"); 
$np->parent = $pages->get("/customer-service/contact-us/contact-submission-listing/");

// Send all form submissions through ProcessWire sanitization
$title = $sanitizer->text($input->post->contactname);
$name = $sanitizer->text($input->post->contactname);
$contactname = $sanitizer->text($input->post->contactname);
$email = $sanitizer->email($input->post->email);
$comments = $sanitizer->textarea($input->post->comments);

// Match up the sanitized inputs we just got with the template fields
$np->of(false);
$np->title = $contactname;
$np->name = $contactname;
$np->contactname = $contactname;
$np->email = $email;
$np->comments = $comments;

// Save/create the page
$np->save();							    										    		

} ?>

This works great! After submitting the form, we go to the "Success" page, and new submissions show up in the ProcessWire page tree right away. Excellent!

Now I need to add a photo field. I altered the above form so it looks like this:

<form action="/customer-service/contact/contact-success/" method="post" enctype="multipart/form-data">
<p><label for="contactname">Name:</label></p>
<p><input type="text" name="contactname"></p>
<p><label for="email">E-Mail:</label></p>
<p><input type="email" name="email"></p>
<p><label for="comments">Comments:</label></p>
<p><textarea name="comments" cols="25" rows="6"></textarea></p>
<p>Click the "Select Files" button below to upload your photo.</p>
<input type="file" name="contact_photo" />
<button type="submit">Submit</button>
</form>

And here's the updated "contact-success" page:

<?php // First, confirm that a submission has been made
if($input->post->contactname) {

// Set a temporary upload location where the submitted files are stored during form processing
$upload_path = $config->paths->assets . "files/contact_files/";

// New wire upload
$contact_photo = new WireUpload('contact_photo'); // References the name of the field in the HTML form that uploads the photo
$contact_photo->setMaxFiles(5);
$contact_photo->setOverwrite(false);
$contact_photo->setDestinationPath($upload_path);
$contact_photo->setValidExtensions(array('jpg', 'jpeg', 'png', 'gif'));

// execute upload and check for errors
$files = $contact_photo->execute();

// Run a count($files) test to make sure there are actually files; if so, proceed; if not, generate getErrors()
if(!count($files)) {
$contact_photo->error("Sorry, but you need to add a photo!");
return false;
}

// Do an initial save in the ProcessWire page tree; set the necessary information (template, parent, title, and name)
$np = new Page(); // create new page object
$np->template = $templates->get("contact_submission"); // set the template that applies to pages created from form submissions
$np->parent = $pages->get("/customer-service/contact-us/contact-submission-listing/"); // set the parent for the page being created here

// Send all the form's $_POST submissions through ProcessWire's sanitization and/or map to a variable with the same name as the template fields we'll be populating
$np->title = $sanitizer->text($input->post->contactname);
$np->name = $np->title;
$np->contactname = $sanitizer->text($input->post->contactname);
$np->email = $sanitizer->email($input->post->email);
$np->comments = $sanitizer->textarea($input->post->comments);
$np->save();

// Run photo upload
foreach($files as $filename) {
$pathname = $upload_path . $filename;
$np->contact_photo->add($pathname);
$np->message("Added file: $filename");
unlink($pathname);
}

// Save page again
$np->save(); ?>

<p>Thank you for your contact information.</p>

<?php return true;
}

else { ?>
<p> Sorry, your photo upload was not successful...</P>
<?php } ?>


Replace the field references with your own field names, make sure to change the various paths to match yours, and change the various messages to be what you need for your project.

Read the entire discussion to see how we worked through getting to the solution shown above.

Thanks,
Matthew

  • Like 9

Share this post


Link to post
Share on other sites

You have written contest_photo instead of contact_photo at the beginning. Not sure if this solves your issue. Sorry on mobile...

  • Like 2

Share this post


Link to post
Share on other sites

Hey Wanze,

Yes, that shows you have a good eye for sure!

Unfortunately, that was just an error from me transferring the text to the forum... I edited the forms in my first post and fixed that word.

In other words, it still doesn't work.

Any other ideas?

Thanks,

Matthew

Share this post


Link to post
Share on other sites

$contact_photo->setDestinationPath($upload_path);

I could be overlooking it (late here), but I don't see $upload_path defined anywhere.

Share this post


Link to post
Share on other sites

Hey renobird,

Late here too, but difficult to sleep when working on a ProcessWire site!

You are correct... I was missing that piece!  Definitely, that should be there.

I added it, and holding my breath I tried the form again.  But it still doesn't work.  Same result: no errors, and we see the "success" page, but the new page does not appear in the page tree.

Other suggestions or ideas?

Thanks,

Matthew

PS: I've edited my post above to keep track of changes.  That way, when this discussion is done, there will be a complete example for others to reference.

  • Like 1

Share this post


Link to post
Share on other sites
$upload_path = "/site/assets/files/temp/";

I think the path is not correct, it's more an "url".

The complete path usually starts with the folder your site's in (public_html/www).

Try this:

$upload_path = $config->paths->assets . 'files/temp/';

And have you debug mode turned on to see any errors?

Cheers

Share this post


Link to post
Share on other sites

Greetings,

To renobird: Yes, I am basing my work on Soma's code.  But because I have more fields than just a file upload, I have to re-work the original code.  In my code above, you can see that what I have looks like Soma's code, but with the other fields included.

$upload_path = "/site/assets/files/temp/";

I think the path is not correct, it's more an "url".

The complete path usually starts with the folder your site's in (public_html/www).

Try this:

$upload_path = $config->paths->assets . 'files/temp/';

And have you debug mode turned on to see any errors?

I fixed the path reference (updated my post above).  But still no luck.  The page is not being created.  And no errors are being reported.

Based on a comment from Ryan in a related discussion, I wonder if I am simply saving the page at the wrong stage?

Here's the related discussion: http://processwire.com/talk/topic/2997-uploading-a-new-photo-to-a-member-profile-avatar-field/#entry30621

I really want to have a full, public example of how to put file uploads in a form with other fields, so I am hoping this discussion can become a live working out of the details.

Thanks,

Matthew

Share this post


Link to post
Share on other sites

I have a working example - doing essentially what you are after. I'll compare to yours when I get back to the office (on mobile now) and see if can spot the issue.

Share this post


Link to post
Share on other sites

Not the ideal way to debug like this code in a forum. Nothing against anyone personally. Now the code lost intendation and no way to go through it.

Please post code to some snippet site like Gist or bitbucket.

In general, we should provide some more practice and best ways HOW TO DEBUG YOUR CODE. Good coding is learning how to debug. There's simple steps you can go through to test all vars and parts of code to see where the problem lays. Sometimes it's simple sometimes it can get hard, as you when you don't have errors.

Ok.

What I spotted too is the last bit, where is the $u->getErrors() coming from?

Also as you say the page isn't created at all? I would start there and get that right first and work you way up.

Thanks,

Soma

  • Like 2

Share this post


Link to post
Share on other sites

Hey Soma,

I know what you're saying, but I wanted to use the forum as a way to work out what the general idea should be.  Ideally, it would be great to have things like this all in Github, but I think we often need to use a forum this way.  It's a more direct way -- in the end -- to have code that everyone here can use for their sites.

I think having a full working example of a form with various fields as well as file-uploads would be great for the forum.

Good catch on the $u->getErrors()... Yes, that should be $contest_photo->getErrors()

However... It still doesn't work.

I'm sure we'll solve this!

Thanks,

Matthew

Share this post


Link to post
Share on other sites

Matthew,

have you debug mode enabled to see PHP errors/warnings?

Also can you do a print_r($_FILES); to check if the PHP $_FILES array isn't empty?

Maybe the file isn't sent at all to the server.

Share this post


Link to post
Share on other sites

I also know what you're saying, but I have to strongly disagree. I could've write this in any other similar thread.

The problem is the code posted in a wysiwyg is "horrible" to read and even copy it.

Then after this is working (as with 1000 other code here in the forum), there comes the next who takes this code but need another feature.. then the game start again with same story same code but different bugs issues.

What I would like to do is helping how to code and debug, not have 1 million snippets that are sometimes very bad coded and not complete best practice or not even working spooking around the forum. It already happened many times and it will get worse by time. I know what you're wanting to do and I understand, but in the long run what we are practicing here since 2 years is very bad for newcomers. We better this energy and time to help with little tutorials and snippets that are good to start with in a dedicated site/place.

  • Like 3

Share this post


Link to post
Share on other sites

Hey Soma,

I get you!  I really do...  As someone who is attempting to document how to do things, it is difficult when there are pieces here and there for larger routines.  For example, with file-uploads and front-end forms in general, there are isolated chunks throughout the forum, and putting it all together as a single file can be difficult.  My goal with this discussion is to consolidate a whole routine in one coherent file.

For sure, your points about better ways to discuss such code is worthy of a larger discussion!  And yet, forums like this often have bits of code (many times, they are more than just "bits").  In fact, if you remove the code from a forum like this, what's left to discuss?

If I were to move this to GitHub, would it be as valuable to the ProcessWire community?

Thanks,

Matthew

Share this post


Link to post
Share on other sites

I didn't say move this discussion to github, but the snippet maybe?

Share this post


Link to post
Share on other sites

Im saying gist.github.com not github itself :-)

Share this post


Link to post
Share on other sites

Greetings,

OK, Soma, here's my gist for this subject: https://gist.github.com/MatthewSchenker/5205927

I hope this can make it easier and more productive to work this out -- and better for everyone to use.

Also, maybe this can be a spark that gets more of us to Github to work out our code?

Thanks,

Matthew

  • Like 1

Share this post


Link to post
Share on other sites

Much better... and just now I spoted (starting from top)  an error

if ($input->get->contactname){

Your form is method post and not get!

So you would have easily spoted this already if you add an pseudo echo "Hello" after the if() to see it gets to after the check. As I said it's really simple if you go step by step and output vars to test and see if it really get's there. 

Share this post


Link to post
Share on other sites

Hi Soma,

I used "GET" for the original form (the one with no file uploads).  I then switched to "POST" for the one with file uploads.  But to make it easier, I will switch all of them to the "POST" method.

Thanks,

Matthew

Share this post


Link to post
Share on other sites

Dont want to be mean but have to say that I already built multiple working front end form examples that work. The one Reno linked would be what you do. So this is not so much about making a working example but to help you debug your code you messed up. :-P #hides behind stone#

Of course yours has some more fields, but there's no validation happen on those, which I think should be the lesson here. To make it easy on this subject I would suggest looking at my other front end forms using PW forms and field that will make it more streamlined and easy. Its also in my gists examples.

Share this post


Link to post
Share on other sites

Hey Soma,

Yes, I admit the code I posted has problems.  I just can't seem to pinpoint the problem because building these kinds of forms gets into a lot of PW-specific steps.  I haven't seen any examples to follow for forms with image-upload fields along with "regular" fields.

I actually based my code above on your very helpful gist for uploading images to newly created pages (https://gist.github.com/somatonic/4150974).  But somewhere in the process of mixing in "regular" fields with the image upload requirements, something is off.

I realize I must be doing something wrong, but I can't tell what.

I'm hoping in the end we'll have a working example of how to do this that can be beneficial to others, since I know other need (or will need) to do this.

Thanks,

Matthew

Share this post


Link to post
Share on other sites

Hi Matthew,

have you tried my debugging steps? =)

Matthew,


have you debug mode enabled to see PHP errors/warnings?


Also can you do a print_r($_FILES); to check if the PHP $_FILES array isn't empty?


Maybe the file isn't sent at all to the server at all.

Share this post


Link to post
Share on other sites

Greetings Wanze,

Sorry, I meant to respond to that earlier.

Yes, I am seeing a result from that.  For example, I uploaded a photo called "cloudphoto.jpg.  Here's the message I'm seeing:

Array ( [contact_photo] => Array ( [name] => cloudphoto.jpg [type] => image/jpeg [tmp_name] => /tmp/phpdRhJLN [error] => 0 [size] => 1864092 ) ) 

Thanks,

Matthew

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By EyeDentify
      I have been experimenting with the new $page->meta() method and find it useful.

      Once i figured out that the data i "save" with it is tied to the page where i called the method from.

      So this is not obvious at least not for me in the documentation:
      https://processwire.com/api/ref/page/meta/
       
      So i just wanted to share that revelation with the community so you don´t get as confused as i was.

      Happy Coding Everyone.
    • By louisstephens
      Going through my long quest to get better with ajax and utilizing the api, I have hit yet another roadblock. I currently have a form with an image field (thanks to flydev for getting that sorted), "title" text input, and a select field set to multiple. In my ajax call, I added in:
      tags = $("#select-tags").val(); form_data.append('tags', tags); $.ajax({ type: 'POST', data: form_data, contentType: false, processData: false, url: '/ajax/upload-preview/', success: function(data) { console.log("Woo"); }, error: function(xhr, ajaxOptions, thrownError) { alert(xhr.responseText); } }); And in the ajax template: 
      $tags = $sanitizer->text($_POST['tags']); $image = $sanitizer->text($_POST['image']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->tags = $tags; $p->save(); If I select a "tag" from the select input and submit, it does indeed add it to the Page Reference field in the backend. However, this does not work with an array being passed to it of multiple options.

      So it does appear that my ajax call is trying to submit multiple options, but I am really just unsure how to get these two added in. I saw in other forums posts of add($page) and even add(array()). Do I need to handle this js array differently or do  I need to foreach through the $tags to add it like:
      foreach($tags as $tag) { $p->tags->add($tag); $p->save(); } I tried this approach, but apparently I am still missing something.
       
      Edit:
      I was doing some tweaking, and I know I can split the js array out like:
      for (i = 0, len = tags.length; i < len; i++) { console.log(tags[i]); } However, I am not sure then how to handle the POST in php if I were to split it out.
    • By louisstephens
      I have been messing around with creating pages from ajax requests, and it has gone swimmingly thus far. However, I am really struggling with creating a page and saving an image via ajax. 
      The form:
      <form action="./" role="form" method="post" enctype="multipart/form-data"> <div> <input type="text" id="preview" name="preview" placeholder="Image Title"> </div> <div> <input type="file" id="preview-name" name="preview-name"> </div> <div> <select id="select-tags" name="select-tags"> <?php $tags = $pages->find("template=tag"); ?> <option value="">Select Your Tags</option> <?php foreach ($tags as $tag) : ?> <option value="<?= $tag->name; ?>"><?= $tag->name; ?></option> <?php endforeach; ?> </select> </div> <div> <button type="button" id="submit-preview" name="submit" class="">Upload Images</button> </div> </form>  
      The ajax in my home template:
      $('#submit-preview').click(function(e) { e.preventDefault(); title = $("#preview").val(); image = $("input[name=preview-name]"); console.log(title); console.log(image); data = { title: title, image: image //not sure if this is actually needed }; $.ajax({ type: 'POST', data: data, url: '/development/upload-preview/', success: function(data) { console.log("Woo"); }, error: function(xhr, ajaxOptions, thrownError) { alert(xhr.responseText); } }); }); And finally in my ajax template:
      $imagePath = $config->paths->assets . "files/pdfs/"; //was from an older iteration $title = $sanitizer->text($_POST['title']); $image = $sanitizer->text($_POST['image']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $p->save(); I can complete the file upload but just using a simple post to the same page and it it works well, but I was really trying to work out the ajax on this so I could utilize some modals for success on creation (and to keep my templates a little cleaner). When I do run the code I have, a new/blank folder is created under assets, and a new page is created with the correct title entered. However, no image is being processed. I do get a 200 status in my console. I have searched google for help, but everything seems to be slightly off from my needs. If anyone could help point me in the right direction I would greatly appreciate it. 
    • By louisstephens
      This might be a completely dumb question, but I cant seem to wrap my head around it. I have a page reference field that allows users to select "Tags". In the front end I would like to use the titles as class names for a single item. ie:
      <?php $previews = $pages->find("template=preview"); ?> <?php foreach($previews as $preview): ?> <div class="tagOne TagTwo tagThree"> <!-- use another foreach to output--> <img src="<?=$preview->preview_image->url; ?>" /> </div> <?php endforeach; ?> I am little stumped as I know I need a foreach loop to produce each tag title, but how do I insert them all into one corresponding div class with spaces?
      Whelp, that was the easiest thing, but my brain just didnt "get it". Just put the foreach in the "class" inside of the overall foreach. Ugh 😓
    • By schwarzdesign
      We recently rebuilt the Architekturführer Köln (architectural guide Cologne) as a mobile-first JavaScript web app, powered by VueJS in the frontend and ProcessWire in the backend. Concept, design and implementation by schwarzdesign!
      The Architekturführer Köln is a guidebook and now a web application about architectural highlights in Cologne, Germany. It contains detailled information about around 100 objects (architectural landmarks) in Cologne. The web app offers multiple ways to search through all available objects, including:
      An interactive live map A list of object near the user's location Filtering based on architect, district and category Favourites saved by the user The frontend is written entirely in JavaScript, with the data coming from a ProcessWire-powered API-first backend.
      Frontend
      The app is built with the Vue framework and compiled with Webpack 4. As a learning exercise and for greater customizability we opted to not use Vue CLI, and instead wrote our own Webpack config with individually defined dependencies.
      The site is a SPA (Single Page Application), which means all internal links are intercepted by the Vue app and the corresponding routes (pages) are generated by the framework directly in the browser, using data retrieved from the API. It's also a PWA (Progressive Web App), the main feature of which is that you can install it to your home screen on your phone and launch it from there like a regular app. It also includes a service worker which catches requests to the API and returns cached responses when the network is not available. The Architekturführer is supposed to be taken with you on a walk through the city, and will keep working even if you are completely offline.
      Notable mentions from the tech stack:
      Vue Vue Router for the SPA functionality VueX for state management and storage / caching of the data returned through the API Leaflet (with Mapbox tiles) for the interactive maps Webpack 4 for compilation of the app into a single distributable Babel for transpilation of ES6+ SASS & PostCSS with Autoprefixer as a convenience for SASS in SFCs Google Workbox to generate the service worker instead of writing lots of boilerplate code Bootstrap 4 is barely used here, but we still included it's reboot and grid system Backend
      The ProcessWire backend is API-only, there are no server-side rendered templates, which means the only PHP template is the one used for the API. For this API, we used a single content type (template) with a couple of pre-defined endpoints (url segments); most importantly we built entdpoints to get a list of all objects (either including the full data, or only the data necessary to show teaser tiles), as well as individual objects and taxonomies. The API template which acts as a controller contains all the necessary switches and selectors to serve the correct response in <100 lines of code.
      Since we wanted some flexibility regarding the format in which different fields were transmitted over the api, we wrote a function to extract arbitrary page fields from ProcessWire pages and return them as serializable standard objects. There's also a function that takes a Pageimage object, creates multiple variants in different sizes and returns an object containing their base path and an array of variants (identified by their basename and width). We use that one to generate responsive images in the frontend. Check out the code for both functions in this gist.
      We used native ProcessWire data wherever possible, so as to not duplicate that work in the frontend app. For example:
      Page names from the backend translate to URLs in the frontend in the form of route parameters for the Vue Router Page IDs from ProcessWire are included in the API responses, we use those to identify objects across the app, for example to store the user's favourites, and as render keys for object lists Taxonomies have their own API endpoints, and objects contain their taxonomies only as IDs (in the same way ProcessWire uses Page References) Finally, the raw JSON data is cached using the cache API and this handy trick by @LostKobrakai to store raw JSON strings over the cache API.
      Screenshots














×
×
  • Create New...