Beginner's Guide to Creating Discourse Plugins Part 1: Creating a basic plugin

On Linux when I do this the file watcher does not work so bin/rake autospec is less useful. Not sure if kqueue is better here and still notifies.

It work! 2018-4-28.:grin:

at first it doesn’t work(the alert box don’t come out),
I just delete entire tmp/ folder as some comment suggest.
and then it work perfectly!

1. Code

2. Result


it pop up every time I refresh the page

Thanks!

4 Likes

I tried this guide and it worked, but I just don’t know what’s the relationship between the basic-plugin/plugin.rb and the alert.js.es6 ? What’s the point to make this happen with the file ‘plugin.rb’?

Hi Jordan_Smith welcome to the forum

My abridged unofficial explanation is that the plugin.rb file “sets things up” for serverside Ruby and allows it’s presence to display in the Admin -> Plugins page.

True, for something as trivial as an “alert”, perhaps a plugin is overkill and it would be better done using Admin -> Customize instead. Just as some things might be better done in a Theme.

I believe the intent of this topic was to provide a simple example to introduce beginning plugin authors to how plugins work, with the hope that it would be only a starting point.

Thank you for the reply. I didn’t question why plug.rb is there. What I wanted to know is how the plug.rb works with alert.js.es6 ? What’s the logic relation between these two files?

For a more complex plugin there would of course be a lot more going on. But in this example basically all the relation does is associate the JavaScript code with the plugin. Kind of like “If this plugin is installed, then include the JavaScript when the assets are compiled”.

1 Like

To expand on what @Mittineague has stated, it doesn’t. plugin.rb in the given example only exists because that is what is needed to tell Discourse what the plugin’s name is. description, version, and author. The plugin at this stage, is purely a JavaScript “enhancement” and doesn’t need any ruby code. Once you need ruby code to create new routes/endpoints, alter guardian gates, add SSO functionality, then plugin.rb becomes more than just a “file that tells discourse its name, description, version, and authors”

6 Likes

I’m experiencing the same issue with getting symlinks to work (I’m on Linux) - the ln -s appears to work, but from inside the docker container the path is inaccessible. The plugin works when it is directly copied into the plugins folder, but I like the symlink workflow as it enables a more sensible Git arrangment.

This SO question appears to suggest that symlinks won’t work this way unless we also add the linked-to plugin volume to the docker run command Mount host directory with a symbolic link inside in docker container - Stack Overflow

Anyone any thoughts on this? What are the pro plugin developers doing for a sensible Git workflow?

2 Likes

I’m using symlinks but my dev setup is Docker-frei.

Symlinks are a really nice way of being able to include or exclude plugins on a ‘build’ very quickly without disturbing the codebase.

2 Likes

Here’s what I’ve had to do in order to preserve a similar workflow to using symlinks (softlinks):

I edited /bin/docker/boot_dev in Discourse (outside the container) so that the plugin I want to work on is added as a volume and mounted in the /src/plugins/ directory (inside the container).

In my case it looks like this:

docker run -d -p 9405:9405 -p 1080:1080 -p 3000:3000 -p 9292:9292 \
-v "$DATA_DIR:/shared/postgres_data:delegated" \
-v "$SOURCE_DIR:/src:delegated" \ 
-v "/home/marcus/code/discourse/discourse-reflective-learning-plugin:/src/plugins/discourse-reflective-learning-plugin" \
$ENV_ARGS --hostname=discourse --name=discourse_dev --restart=always \
discourse/discourse_dev:release /sbin/boot

I’ve used absolute paths because I couldn’t be bothered to get into relative paths inside Docker and $SOURCE_DIR environment variables, but I’m sure there’s a cleverer way to do this, so that perhaps all your plugins could be in directories at the same level as discourse and it would automagically include them all. Hope this helps someone.

2 Likes

I am totally open to a PR that automatically follows symlinks in plugins/ dir and then smart mounts volumes.

4 Likes

Hmmmm. That’s a tricky one, because the symlink needed to be deleted, otherwise the Docker mount fails (as it cannot overwrite the existing file).

I had a bit of a google around for solutions, and found an interesting solution which would dereference the symlink and copy the symlink’s target into the specified directory, but couldn’t find anything that would create a Docker volume mount, thus preserving the dynamic link and bi-directional synchronisation. Happy to be steered towards a solution as this would be a neat feature to have.

Tell me how to add your validation to the app/models /user.rb model?
https://github.com/discourse/discourse/blob/8c2e27790cf7c5fcc883489a7bdf36008fda88ac/app/models/user.rb#L99-L110

1 Like

You can use regular Ruby code to do this in your plugin.rb:

class ::User < ActiveRecord::Base
  validates_presence_of :your_attribute
end
6 Likes

Just starting on my first small plugin :blush: I have a few questions I hope someone can help with…

First question is does it matter if we use what’s in this guide or should we really be aiming to use the plugin generator?

It wasn’t covered in this guide, but how can we make data available to our templates from the plugin.rb file?

Let’s say for example I want to display a random welcome message at the plugin-outlet topic-above-posts whenever somebody visits a topic, with the message being randomly selected from an array in plugin.rb… how would I send that data to the template? Which I also presume would go here:

plugins/my_new_plugin/assets/javascripts/discourse/templates/connectors/topic-above-posts/my_new_plugin.hbs (Is this correct?)

Any tips greatly appreciated :blush:

2 Likes

Using the plugin generator is generally a great way to get started and see how a plugin should be structured. I recommend it.

Now, regarding how to get ruby code into the front end: those are two different applications. The front end application (Ember) will have to request it from the server (Rails) somehow. Normally we do this via an AJAX call, but you could also do something like add to the SiteSerializer so that it’s sent automatically as part of the forum data.

Otherwise you will have to look up how to add a route/controller in rails and send the messages as JSON.

7 Likes

Thanks Robin!

Would you know if there are any tutorials on this? Or, is there a simple plugin (or even a dummy plugin) we can look at that points us in the right direction? (If not, is this something you could quickly put up for us somewhere please? I think it’ll help a lot of people :blush:)

Plus… any more thoughts on writing a book? I can put you in touch with someone if you fancy it :smiley:

1 Like

Thanks for the compliment but I’m definitely far too busy to consider writing a book. Plus tech books have SUPER limited range. Those I know who have written them say you do it for yourself, not for the money :slight_smile:

I can’t think of simple plugin for returning something from the server side. discourse-tooltips adds a route and then uses that to get previews of topics as you mouseover so that might be helpful.

8 Likes

Soon, I will start plugin development…

Question:

Is there a list of hooks where we actually can plugin?

For example, say we want to write a plugin to process the cooked data in a post before it is displayed, we would expect there to be at least one hook at the beginning and one at the end, like (for example):

  • display_post_start

  • display_post_complex

Then, our plugin would hook into the code at the hook location of our choice above.

Is there a list of these plugin hooks and what are these plugin hooks called in DiscourseWorld?

Update: Found this:

git grep "plugin-outlet" -- "*.hbs"

But there was no outlet for modifying cooked post content, that I can see.

Is there a tutorial on creating our own outlets, for example, an outlet to modify the cooked part of posts?

2 Likes

You’re looking for this guide, section decorateCooked():

5 Likes