When you click on "New Discussion", you are redirected to "/post/discussion/categoryname". So what you would have to do is a redirect to "/post/review/categoryname". Looking at that url, you see "/controller name/method name/arguments". By now if you would follow such a link, you would get a "Page not found", since there is no "review" method in post controller. That's where you have to start:
public function postController_review_create($sender, $args) {
echo 'Hello World';
decho($args);
}
Now you can go to /post/review and see a very simple output. Good start.
Next step is to render a view since you do not want to see that raw output. If you use the controllers render() method, there are some things taken for which are needed to get a nice Vanilla page. And we don't want to clobber the html in the plugin, so please create a folder called "views" and a file called "review.php" in there. Prefer Smarty? No problem, just use it and exchange every ".php" with ".tpl" in the next few lines.
Add some content to your file. Try <h1><?= $this->data('Title') ?></h1> for a beginning. In order to render this view, exchange the content of our self made method above to
This calls the controllers render method and passes our view to it. Simple as that.
Now you can take a look at a) postcontrollers method discussion() and the corresponding view to see what happens there and b) the QnA plugins method PostController_Question_Create and the view it uses. Imitate/copy the behavior as much as you need and see where it takes you.
But that said, I don't think that this is the best way. You didn't have said how much a Review should Differ from a discussion. How many additional fields shall be there? Of what type are they? If it is just one or two simple fields it might make sense to enhance discussion table and simply add the input fields to the view. Or you can reuse the discussion method and simply exchange the view. It really depends. There are several possibilities! If you tell me some more about the database entries (you don't have to name the fields if you do not want to), I could recommend better ways.
Example 1: Adding a field to database and discussion
Create a test plugin where you insert the following code. Then activate the plugin and create 2 new discussions. You'll see that there is a new form element. Look at how this will relate to the Discussion table entries for new discussions.
// Initiate db changes when plugin is enabled.
public function setup() {
$this->structure();
}
// Add an additional column to Discussion table
public function structure() {
Gdn::structure()->table('Discussion')
->column('IsReview', 'tinyint(1)', 0)
->set();
}
// Insert a form control for our additional field.
public function postController_beforeBodyInput_handler($sender) {
$this->structure();
echo '<div class="P">';
echo $sender->Form->checkBox('IsReview', 'This is a review');
echo '</div>';
}
We've added a form element with a name that exists in the database. Form is joined here with a database model for Discussion table and thus every form field is saved to database automatically.
Example 2: Add a form field and write information to discussions "meta data column Attributes":
Some tables have a column "Attributes" with serialized information. You can use this e.g. for meta information that shouldn't be search criteria.
// Insert new form field (this time not connected to db).
public function postController_beforeBodyInput_handler($sender, $args) {
echo '<div class="P">';
echo $sender->Form->checkBox('NewReview', 'This is a review');
echo '</div>';
}
// Add it to the attributes array of the discussion, before it is saved.
public function discussionModel_beforeSaveDiscussion_handler($sender, $args) {
$args['FormPostValues']['Attributes']['NewReview'] = $args['FormPostValues']['NewReview'];
}
// Ensure that the correct value is reflected when e.g. discussion is edited.
public function postController_beforeFormInputs_handler($sender, $args) {
$sender->Form->setValue('NewReview', $sender->DiscussionModel->EventArguments['Discussion']->Attributes['NewReview']);
}
This is a little advanced and would require further tweaking. But it shows you how you can reuse Vanilla.
// Create a new method which is available at /post/review.
public function postController_review_create($sender, $args = ['']) {
// Vanilla would assume mehtod name, so we have to correct this.
$sender->View = 'discussion';
// Pass category to dicscussion method.
$sender->discussion($args[0]);
}
// Add js which could change some more strings like "Discussion Title"
public function postController_render_before($sender) {
// Only add if we are at /post/review
if ($sender->RequestMethod === 'review') {
$sender->addJsFile('review', 'plugins/NewReview');
}
}
// Change what ever we can and then reuse the discussion method
public function postController_beforeDiscussionRender_handler($sender) {
if ($sender->RequestMethod === 'review') {
$sender->setData('Title', t('New Review'));
}
}
It might be used to add fields or something like that.
Basically the same as the last one, but this time, you can use your own view, although you reuse the discussion method.
// Create a new method which is available at /post/review.
public function postController_review_create($sender, $args = ['']) {
// Get the view "render.php" in plugins "views" folder
$sender->View = 'review';
$sender->ApplicationFolder = 'plugins/NewReview';
// Pass category to dicscussion method.
$sender->discussion($args[0]);
}
If you only use fields of the table Discussion, I'd assume that would be all you need. But you would have to implement something for "editreview".
Well, those were only examples of what is possible and maybe you do not need a complete new form. If you ask me, writing a plugin that re-uses as much of Vanilla as possible is the best way to write a good plugin.
With the little information available for the review functionality I am reduced to speculation - - what if the review "states" can merely be reflected with additional fields in the discussion table (as @R_J suggested above), user permissions controlling who can change the review "state", and lastly viewing the list of reviews (really discussions with specific state value) made visible with custom views?
Just a thought...
Essentially what I want is when the "New Review" discussion button is clicked it takes you to a new discussion where the form body has a drop-down list, user entry fields, etc.. The user then picks some elements from a drop down list which will populate some of the review and then the user fills out the entry fields for the rest of the review. It is finally posted and viewed by the members.
@R_J Thanks for the commits to the "New Review" plugin. Just read through it all and learned a lot. To start I will be going without restrictions for who can and cannot write reviews.
based on your description you can use the discussion prefix plugin as a model to create your plugin. You simply have more fields but the hooks are all there.
@rbrahmson said:
based on your description you can use the discussion prefix plugin as a model to create your plugin. You simply have more fields but the hooks are all there.
@rbrahmson I'm deciding between this and using the approach you suggested in the "HowTo: Form & Validation 0.2" plugin.
I've managed to make a form exactly as I like it using the "HowTo: Form & Validation 0.2" plugin but I'm not sure how to get that to appear every time the "New Review" button is pressed. Or how to insert the form into the body of the "New Review" plugin.
Given that you have quite a lot of extra fields for a review, I would create an extra table for that fields.
This should get you going (code is untested!), but it is missing some very important parts! You have to do some fail checking, validation and so on. When discussion is created successfully, but the additional fields could not be saved, you have to handle it somehow.
// This will be called when plugin is activated.
public function setup() {
$this->structure();
}
// Always do initial database changes this way! This will make your
// changes "visible" for /utility/structure calls
public function structure() {
// Create a table only for the additional fields.
Gdn::structure()
->Table('Review')
->primaryKey('ReviewID')
->column('ForeignID', 'int(11)')
->column('FieldA', 'varchar(50)', true)
->column('Recommended', 'tinyint(1)', '1')
// whatever...
->set();
}
public function postController_review_create($sender, $args = []) {
// Add a form to your page.
$sender->Form = new Gdn_Form();
// When a new discussion is created, the category name is passed as an
// argument. So when your new review button is clicked, the current category
// should be passed, too. And then you have to handle it here.
if (count($args) > 0) {
$sender->Form->setData(['Category' => $args[0]]);
}
// Check if the page is called with posted values.
if ($sender->Form->authenticatedPostBack()) {
$formPostValues = $sender->form->formValues();
// Save the discussion.
$discussionID = Gdn::discussionModel()->save($formPostValues);
// Save the rest of the fields to a separate table
$reviewModel = new Gdn_Model('Review');
$formPostValues['ForeignID'] = $discussionID;
$reviewModel->save($formPostValues);
}
$sender->render($this->getView('review.php'));
}
Well, I can take a look at the discussionController and compare it to the postController and tell you what the differences are, but to be honest: I don't care
If you try to recreate a similar action, keep as close to the original as you can. If you click on "New Discussion", a new page opens. Look at the url, it will read example.com/post/discussion(/category). The category will only be appended if you click on New Discussion while you e.g. looking at a specific discussion and not using the button while on recent discussions.
I have told you that the url has to be interpreted like that: /controller/method/argument(/argument/argument/argument/...). Clicking the "New Discussion" button obviously passes the action to the postControllers discussion method. And if you like to create something similar to a discussion, you should create a new method for the postController. That is the only reason why I did it like that.
If you compare postController and discussionController and end up with "Oh, there is no difference, I will choose discussionController" or even "What postController offers is nothing that I need so I will stay with discussionController", you will risk that some things might not work as expected in future times.
Moreover you have a separation of functions in there. The postControllers methods are all there for posting content (discussions and comments). The discussionController handles existing discussions. You could argue that editing a discussion whould be done in discussionController but not in postController, but if you see the postController as the HTTP POST, than it is better to be placed in the postController. But that hasn't been your question, I guess...
If you want to find out the differences, you have to look at a few places:
The class they extend class PostController extends VanillaController vs. class DiscussionController extends VanillaController no differences here.
functions __construct and initialize
postController:
public function initialize() {
parent::initialize();
$this->addModule('NewDiscussionModule');
}
discussionController:
public function initialize() {
parent::initialize();
$this->addDefinition('ConfirmDeleteCommentHeading', t('ConfirmDeleteCommentHeading', 'Delete Comment'));
$this->addDefinition('ConfirmDeleteCommentText', t('ConfirmDeleteCommentText', 'Are you sure you want to delete this comment?'));
$this->Menu->highlightRoute('/discussions');
}
Well, look at that! The NewDiscussionModule is added but I guess it is completely useless here. I'll make a pull request based on that. Many thanks for your curiousity!
But you can see that there is only very few differences. Mainly translation strings and a menu entry in the discussionController that you wouldn't need in postController.
Which classes/models they use and which properties they provide
postController:
/** @var DiscussionModel */
public $DiscussionModel;
/** @var Gdn_Form */
public $Form;
/** @var array An associative array of form types and their locations. */
public $FormCollection;
/** @var array Models to include. */
public $Uses = array('Form', 'Database', 'CommentModel', 'DiscussionModel', 'DraftModel');
/** @var bool Whether or not to show the category dropdown. */
public $ShowCategorySelector = true;
discussionController:
/** @var array Models to include. */
public $Uses = array('DiscussionModel', 'CommentModel', 'Form');
/** @var array Unique identifier. */
public $CategoryID;
/** @var DiscussionModel */
public $DiscussionModel;
Since those variables are not set in initialize() or __construct(), you only have to look at the $Uses array. postController gives you access to the database and the draftModel "out of the box" while you would have to instantiate them manually when using the discussionController.
The differences between both are really not important, it is just that using postController for creating your content type is the right way to do
Oh and I have to add that I'm no programmer at all so what you read above is what I've learned when playing around with Vanillas code. There might be other things of importance here that I do not know and maybe I even misinterpreted something. But take it as it is, the words of a non-professional coder, and it should help getting an impression.
@rbrahmson and @R_J , I was looking at your code for the 'PrefixDiscussion' plugin.
I am trying to mimic this section of code:
public function base_beforeDiscussionName_handler ($sender) { if (!checkPermission('Vanilla.PrefixDiscussion.View')) { return; } $prefix = $sender->EventArguments['Discussion']->Prefix; if ($prefix == '') { return; } $sender->EventArguments['Discussion']->Name = Wrap( $prefix, 'span', array('class' => 'PrefixDiscussion Sp'.str_replace(' ', '_', $prefix)) ).$sender->EventArguments['Discussion']->Name; }
But instead of appending the 'prefix' variable to the discussion title, I want to add it to the 'BodyBox'.
I've got a version of the fields I want popping up in my 'NewReview' plugin (thanks to you guys) but when I hit submit, obviously the field contents aren't shown on the body of the post.
I think my method should look like so:
public function base_beforeBodyContent_handler ($Sender){ $prefix = $sender->EventArguments['Discussion']->Prefix; if ($FieldA == '') { return; } $sender->EventArguments['Discussion']->Name = Wrap( $FieldA, 'span', //this is where I am stuck. ) }
Sorry, I can see no way of helping in that since I do not know if you only posted fragments because you only wanted to concentrate on the important things or if you simply left out the basics in your code and did not see some obvious errors:
In the snippet above $FieldA is simply not in the scope so it is always undefined.
Moreover you are defining $prefix without using it.
Changing the discussions name in this event will have no impact at all since it was already processed before the BeforeBodyContent event was fired.
And one big fault: you've started with ... function...($Sender) and used $sender afterwards...
If FieldA is a column of the discussion you would be able to use it like "prefix" in the snippet above.
When the BeforeBodyContent event is fired, the discussion is passed as an EventArgument. You can access all columns of the current discussion with $sender->EventArguments['Discussion']->WhateverColumn in there. But if FieldA is stored in an extra table, you might have to fetch that data (untested code):
By the way... You can save yourself some keystrokes if you use the EvenArguments like that:
public function base_beforeBodyContent_handler($sender, $args) {
// Now this is the same:
$discussion = $args['Discussion'];
$discussion = $sender->EventArguments['Discussion'];
@Rangerine - if you completed your extra field processing and they are now part of the discussion row in the discussion table, you can display these fields in the meta area, after the discussion title, etc. It requires different hooks as you can see from several plugins that do that. For example the DiscussionReaders, DiscussionExtract, and others in the directory.
I highly suggest looking at other plugin sources whenever you get stuck. Also search the forum - you will find that it is filled with useful code snippets.
Comments
See this plugin source: HowTo: Form & Validation 0.2
Will study this. Thanks, @rbrahmson !
When you click on "New Discussion", you are redirected to "/post/discussion/categoryname". So what you would have to do is a redirect to "/post/review/categoryname". Looking at that url, you see "/controller name/method name/arguments". By now if you would follow such a link, you would get a "Page not found", since there is no "review" method in post controller. That's where you have to start:
Now you can go to /post/review and see a very simple output. Good start.
Next step is to render a view since you do not want to see that raw output. If you use the controllers render() method, there are some things taken for which are needed to get a nice Vanilla page. And we don't want to clobber the html in the plugin, so please create a folder called "views" and a file called "review.php" in there. Prefer Smarty? No problem, just use it and exchange every ".php" with ".tpl" in the next few lines.
Add some content to your file. Try
<h1><?= $this->data('Title') ?></h1>
for a beginning. In order to render this view, exchange the content of our self made method above toThis calls the controllers render method and passes our view to it. Simple as that.
Now you can take a look at a) postcontrollers method discussion() and the corresponding view to see what happens there and b) the QnA plugins method PostController_Question_Create and the view it uses. Imitate/copy the behavior as much as you need and see where it takes you.
But that said, I don't think that this is the best way. You didn't have said how much a Review should Differ from a discussion. How many additional fields shall be there? Of what type are they? If it is just one or two simple fields it might make sense to enhance discussion table and simply add the input fields to the view. Or you can reuse the discussion method and simply exchange the view. It really depends. There are several possibilities! If you tell me some more about the database entries (you don't have to name the fields if you do not want to), I could recommend better ways.
Example 1: Adding a field to database and discussion
Create a test plugin where you insert the following code. Then activate the plugin and create 2 new discussions. You'll see that there is a new form element. Look at how this will relate to the Discussion table entries for new discussions.
We've added a form element with a name that exists in the database. Form is joined here with a database model for Discussion table and thus every form field is saved to database automatically.
Example 2: Add a form field and write information to discussions "meta data column Attributes":
Some tables have a column "Attributes" with serialized information. You can use this e.g. for meta information that shouldn't be search criteria.
Example 3:
This is a little advanced and would require further tweaking. But it shows you how you can reuse Vanilla.
It might be used to add fields or something like that.
Example 4:
Basically the same as the last one, but this time, you can use your own view, although you reuse the discussion method.
If you only use fields of the table Discussion, I'd assume that would be all you need. But you would have to implement something for "editreview".
Well, those were only examples of what is possible and maybe you do not need a complete new form. If you ask me, writing a plugin that re-uses as much of Vanilla as possible is the best way to write a good plugin.
With the little information available for the review functionality I am reduced to speculation - - what if the review "states" can merely be reflected with additional fields in the discussion table (as @R_J suggested above), user permissions controlling who can change the review "state", and lastly viewing the list of reviews (really discussions with specific state value) made visible with custom views?
Just a thought...
Essentially what I want is when the "New Review" discussion button is clicked it takes you to a new discussion where the form body has a drop-down list, user entry fields, etc.. The user then picks some elements from a drop down list which will populate some of the review and then the user fills out the entry fields for the rest of the review. It is finally posted and viewed by the members.
@R_J Thanks for the commits to the "New Review" plugin. Just read through it all and learned a lot. To start I will be going without restrictions for who can and cannot write reviews.
Here is an example of what would be outputted:
// the 3rd - 5th entries are populated after the user selects the 2nd item from a drop-down list
Product:
2nd Field:
3rd Field:
4th Field:
5th Field:
Field A:
Field B:
Field C:
Brief Description:
Then below that, the user writes the full review.
//Last entry in the review is a yes or no question as follows:
Do you recommend this product?:
based on your description you can use the discussion prefix plugin as a model to create your plugin. You simply have more fields but the hooks are all there.
@rbrahmson I'm deciding between this and using the approach you suggested in the "HowTo: Form & Validation 0.2" plugin.
I've managed to make a form exactly as I like it using the "HowTo: Form & Validation 0.2" plugin but I'm not sure how to get that to appear every time the "New Review" button is pressed. Or how to insert the form into the body of the "New Review" plugin.
Given that you have quite a lot of extra fields for a review, I would create an extra table for that fields.
This should get you going (code is untested!), but it is missing some very important parts! You have to do some fail checking, validation and so on. When discussion is created successfully, but the additional fields could not be saved, you have to handle it somehow.
@R_J What is the difference between
postController
and
discussionController
?
@R_J What is the difference between:
discussionController
and
postController
?
Thanks for the valuable info. I'm putting some things up on the GitHub in a bit for anyone to check out / test when they can.
Well, I can take a look at the discussionController and compare it to the postController and tell you what the differences are, but to be honest: I don't care
If you try to recreate a similar action, keep as close to the original as you can. If you click on "New Discussion", a new page opens. Look at the url, it will read example.com/post/discussion(/category). The category will only be appended if you click on New Discussion while you e.g. looking at a specific discussion and not using the button while on recent discussions.
I have told you that the url has to be interpreted like that: /controller/method/argument(/argument/argument/argument/...). Clicking the "New Discussion" button obviously passes the action to the postControllers discussion method. And if you like to create something similar to a discussion, you should create a new method for the postController. That is the only reason why I did it like that.
If you compare postController and discussionController and end up with "Oh, there is no difference, I will choose discussionController" or even "What postController offers is nothing that I need so I will stay with discussionController", you will risk that some things might not work as expected in future times.
Moreover you have a separation of functions in there. The postControllers methods are all there for posting content (discussions and comments). The discussionController handles existing discussions. You could argue that editing a discussion whould be done in discussionController but not in postController, but if you see the postController as the HTTP POST, than it is better to be placed in the postController. But that hasn't been your question, I guess...
If you want to find out the differences, you have to look at a few places:
The class they extend
class PostController extends VanillaController
vs.class DiscussionController extends VanillaController
no differences here.functions __construct and initialize
postController:
discussionController:
Well, look at that! The NewDiscussionModule is added but I guess it is completely useless here. I'll make a pull request based on that. Many thanks for your curiousity!
But you can see that there is only very few differences. Mainly translation strings and a menu entry in the discussionController that you wouldn't need in postController.
postController:
discussionController:
Since those variables are not set in initialize() or __construct(), you only have to look at the $Uses array. postController gives you access to the database and the draftModel "out of the box" while you would have to instantiate them manually when using the discussionController.
The differences between both are really not important, it is just that using postController for creating your content type is the right way to do
Oh and I have to add that I'm no programmer at all so what you read above is what I've learned when playing around with Vanillas code. There might be other things of importance here that I do not know and maybe I even misinterpreted something. But take it as it is, the words of a non-professional coder, and it should help getting an impression.
@R_J Thanks. Very insightful.
@rbrahmson and @R_J , I was looking at your code for the 'PrefixDiscussion' plugin.
I am trying to mimic this section of code:
public function base_beforeDiscussionName_handler ($sender) { if (!checkPermission('Vanilla.PrefixDiscussion.View')) { return; } $prefix = $sender->EventArguments['Discussion']->Prefix; if ($prefix == '') { return; } $sender->EventArguments['Discussion']->Name = Wrap( $prefix, 'span', array('class' => 'PrefixDiscussion Sp'.str_replace(' ', '_', $prefix)) ).$sender->EventArguments['Discussion']->Name; }
But instead of appending the 'prefix' variable to the discussion title, I want to add it to the 'BodyBox'.
I've got a version of the fields I want popping up in my 'NewReview' plugin (thanks to you guys) but when I hit submit, obviously the field contents aren't shown on the body of the post.
I think my method should look like so:
public function base_beforeBodyContent_handler ($Sender){ $prefix = $sender->EventArguments['Discussion']->Prefix; if ($FieldA == '') { return; } $sender->EventArguments['Discussion']->Name = Wrap( $FieldA, 'span', //this is where I am stuck. ) }
Sorry, I can see no way of helping in that since I do not know if you only posted fragments because you only wanted to concentrate on the important things or if you simply left out the basics in your code and did not see some obvious errors:
... function...($Sender)
and used$sender
afterwards...If FieldA is a column of the discussion you would be able to use it like "prefix" in the snippet above.
When the BeforeBodyContent event is fired, the discussion is passed as an EventArgument. You can access all columns of the current discussion with
$sender->EventArguments['Discussion']->WhateverColumn
in there. But if FieldA is stored in an extra table, you might have to fetch that data (untested code):By the way... You can save yourself some keystrokes if you use the EvenArguments like that:
@Rangerine - if you completed your extra field processing and they are now part of the discussion row in the discussion table, you can display these fields in the meta area, after the discussion title, etc. It requires different hooks as you can see from several plugins that do that. For example the DiscussionReaders, DiscussionExtract, and others in the directory.
I highly suggest looking at other plugin sources whenever you get stuck. Also search the forum - you will find that it is filled with useful code snippets.