Theme-Component v Plugin: What's the difference

Thanks, guys. This is helpful. I think the key distinction I am picking up:
–if you want to change something that is only about what’s on the front end, build a theme.
–if you want to change something that requires interaction with the backend, build a plugin.

Does that sound right?

Here’s a concrete example I have in mind that I’m trying to work out: I want to make it so that any category moderator can pin topics in that category. The broad strokes, I think, are:

  1. Evaluate whether the user is a moderator of the category (this requires going to the backend for information about the user and category)

  2. If user is moderator, show the pin button (this is front end)

  3. If user clicks the pin button, push that topic to the top (actually not sure where this happens in discourse code–maybe also front end and backend?)

Here, because I need to converse with the backend (probably in (1), maybe also in (3)?), i’d have to use a plugin. Does that sound right?

2 Likes

The superficial part might be, but it will involve the backend because it relates to rights and security. You can’t give the front end discretion on what privileges someone has.

1 Like

Do you mean that programmatically answering “Is this user a moderator of this category” is something that will involve multiple files across front and back end? Hmm…

1 Like

In a theme, you should be able to know if a user is moderator and also make backend calls if the discourse API exposes an endpoint that you can call from the front-end (after all the theme can use javascript), so you probably don’t need a plugin for [1]. You would need only if you need to change the backend behaviour or expose an API.

But you probably need for [3], because, like @merefield said, it relates to rights and security (if the backend blocks a moderator to pin a topic, you would have to change it to start allowing).

Like I said above, it probably won’t require a plugin just to make that validation (to know if the user is a moderator or not), but it will probably require because of the action of allowing him to pin the category. If the discourse has an option to allow any moderator to pin a topic (I don’t know if it has), then you wouldn’t need a plugin (but you probably wouldn’t need a theme too, unless the pin button is not shown to moderators, and you use a theme just to show it to moderators and call the endpoint in javascript when he clicks the pin button).

2 Likes

That’s very helpful regarding themes v plugins and also regarding the specific example I mentioned. Thanks.

So far my approach to entering changes has been to sort through the specifics of the Discourse code (where is this object defined, what controls this template, where is the logic that handles this action, etc…). But that has been slow going.

I’m thinking that probably a more efficient route would be to focus on the API. That way, I don’t need to sort through all the details of the mature Discourse code, and can also focus on building a theme instead of a plugin–or perhaps just entering changes in the “customize” dashboard.

Basically, figuring out how the API works seems a lot more manageable than figuring out the Discourse code base.

To stick with the example I mentioned: If user is a moderator of a category, allow that user to pin topics on the category page.

Could I do this without a plugin? Let me see if I can sketch it out:

1. Is a user a moderator of a category?

I currently don’t see an anything in the api that would tell me if a user is a moderator of a category. I’d expect it to be found in a GET call for retrieving a category, but I don’t see such a call there. Or maybe in the GET call for retrieving a user, but there I don’t see a listing of categories the user is a moderator of.

Could I add these in?

Or, alternatively, maybe I could create a custom_field on the user or the category to identify being a moderator, and then make an API call to that custom field when a category page loads.

2. If user is a moderator, show the pin button.

If I can answer (1), then I assume I can just add this button with front end javascript+css, showing it if a user is a moderator.

3. User (who is moderator) clicks button, and that pins the topic

On the API, topics do seem to have a “pinned” trait (a boolean). I assume this correlates to whether they are pinned in their category, as it looks like each topic only has one category.

So here, when the moderator hits the “pin” button, I could probably update the “pinned” status of the topic to True. If that doesn’t work, custom fields could also be a solution here too (although I am not seeing how to add custom fields to a topic).


So with that, or something like it, it seems like I could accomplish this task with the API that, were I to do it with a plugin, would require a lot of sorting through the files of the Discourse code base.

Does that sound right?

1 Like

Have you found: How to reverse engineer the Discourse API

1 Like

I have seen that previously, but I’m taking a closer look now. Thanks for the reminder.

Im trying to sort through:
–where in the API I would get the info about who the moderator of a given category is (I’m not seeing it in the info returned about categories or users–obviously it has to be somewhere)

–can you use the API to add new fields to an entry? For example, Could I use the API to add a custom field to a topic? (I don’t think topics generally come with custom fields)

1 Like

I would look in the database.

The database has a posts table and you could add a field there, but then you will be off the ranch and get no support.

1 Like

Thanks–this is all very helpful.

What database do you mean?


To confirm, my idea here would be to use the API to do some of this stuff that might otherwise require a plugin. So, for example, my site itself would be making a call to the API to determine if a user (or group, as case may be) is a moderator of a category. The call would be hardcoded into the customize panel, or in a theme or plugin.

To make these kinds of calls, I’ll need to be authenticated, with I believe requires creating an API key from the admin panel. The admin panel API creation process requires a “Description” and a “User Level”–I’m not sure how that applies here. In my case, I want my app to make the API call. It’s not being made for a particular user. So I may be misunderstanding.

Do you know what User Level is appropriate for that kind of API call or what I should enter there?

1 Like

When Discourse is installed per the standard it is in a Docker container. Persistence of data is achieved using a PostgreSQL database.

See: Data Explorer Plugin

If you have admin rights you can grab one of the backups, it is SQL compressed into a tar.gz file. You can use the SQL to reload the data into another PostgreSQL database and do even more.

2 Likes

I have never created a theme, plugin, or used the API from Ruby or used the API with any other programming language. I do have admin access on a production site which is how I accessed the backup. I also program in Prolog which is how I am accessing the data and using it to parse the posts with DCGs. If you know BNF, DCGs are not much removed but you have to understand syntactic unification and backward chaining for the more sophisticated parts.

1 Like

Thanks. I’ll pursue this item separately.

1 Like

No, as access rights for each action are enforced by the server.

You’ll want to look at overriding the functions in lib/guardian/ and lib/guardian.rb to allow category-specific moderators to pin topics, then use the same mechanisms as Theme JS (except in the plugin) to change the UI so that the “pin topic” option appears when appropriate.

1 Like

Ahh–that makes sense. So sounds like using the API for that one would not work (your reply potentially saves me a lot of time).

I might try this in a way that’s slightly different than the normal pinning behavior. Instead, I’d give certain users “ownership” rights over a category, and that would let them highlight certain topics in a category.

The way I’d do it with the JSON API is–from my customize dashboard–give the relevant users a custom field (like category-name: owner) or something like that. And then, when the category page loads, call the API to evaluate the user; if they have that custom field, show the “highlight” button, and then if they hit the “highlight” button for a topic assign that topic into the category’s highlight group (also a custom field for that category).

This is a rough sketch–no need to go piece by piece to confirm these steps (when I code it I may have to adjust it anyway). But my question for now: can this way of using the JSON API–especially to create and retrieve custom fields from within my discourse app–work?

1 Like

Dear @JQ331,

But my question for now: can this way of using the JSON API–especially to create and retrieve custom fields from within my discourse app–work?

There has been some excellent replies to your questions about the different between a theme component, a plugin, and the discourse API and I doubt I can add much more value; but here is another way of looking at this, which you may or may not find helpful:

Both theme-components and plugins use template hooks (plugin hooks) to evaluate code in the Ember.js lifecycle (which is good to learn about, BTW).

In addition, the DIscourse API is also available to both theme-components and plugins.

The API basically exposes a subset, but not all, of the data in the underlying PostgreSQL DB.

When you are developing functionality, it would be a good idea to start with the API and determine if the data you need is available from the API.

If there is some data which you need which is not in the API, you need to examine the PostgreSQL DB to see if the data exists in the DB.

If the additional data you need exists in the DB, then you need to expose that data, and in general this means adding data to the Discourse data serializer and extending the API.

The data serializer is simply the process of creating the JSON object which is exposed by the API. This can be extended to add more objects.

To examine the PostgreSQL DB, I assume there are many ways to do this (reading the code on GitHub for example), but I do this by logging into the DB directly and looking at the tables in the DB and study what are the structure of these tables and what fields are in each table (using basic SQL).

So, to summarize (and keep this short), we need to have both the API and the DB tables as references, and in general, when you want to “extend the API” by adding data which is not provided by the API OOTB, we create a plugin for that; and then that data will be exposed along with the API (extended) and we can then use that data in both theme-components and plugins.

Hope this perspective was useful or helpful in some small way.

6 Likes

Outstanding explanation. Thanks very much for this response. It highlights something I had not realized before:

From these responses I am gathering that (like you say) interacting with the JSON API can be a good start in many cases, that could avoid the need for coding up a new theme or plugin. But there are some types of data that are not exposed by the API. To access and do stuff with those data types, you would need to use the Discourse data serializer to expose that data; and to do that serialization, you need to use a plugin.

Seems like one good example of data that is not available through the API are the group owners of a group. I say this because (regarding accessing group owners):

One point of confusion–in the Discourse API, when you get a specific group, one of the returned traits is listed as "is_group_owner": true, so not sure what that is supposed to mean…

But seems like to get the group owner I’d need to serialize the group owner trait.


Are there good examples of using the Discourse serializer? I’ve seen this, but given its importance a how-to with a few examples would be extremely helpful.

The closest example I’ve got is:

This is helpful, but not quite right (at least it gives me errors saying “invalid plugin”). I’m not sure how to adjust it so that on the group index page I can access the group owners for each group.

3 Likes

I’m not sure how you tried using the plugin example, but I had it running successfully on a development instance when using the file structure and code I posted in your other topic.

is_group_owner is used in the context of the current user viewing the groups.

You can get the owner information through an ajax call, but as far as I know it would need to be done for each individual group in the list, potentially resulting in a lot of requests. I think overall it would be challenging to get it to work using this method. In any case, if you want to experiment, you can try the following proof of concept code snippet in a theme. Just replace GROUP_NAME with the name of one of your groups. (EDIT: Here’s also a theme component with an example of how an ajax call can be utilized: https://github.com/awesomerobot/discourse-featured-topics/blob/master/common/head_tag.html)

<script type="text/discourse-plugin" version="0.8.40">
  const { ajax } = require("discourse/lib/ajax");
  ajax(`/groups/GROUP_NAME/members.json`).then(response => {
    console.log(response.owners.map(owner => owner.username))
  });
</script>

With all that said, using a plugin would definitely be the easiest, cleanest solution.

1 Like

Thanks, @tshenry. Not sure why the group owner code didn’t work for me–probably something to do with how I set up the plugin.

I have found the AJAX approach to work as an “MVP” method. BTW, I believe you can make an API call to [forum-url]/groups.json to get back all site groups, and then you can loop through from there, so no need to make multiple calls.

I was hoping to ask:

–For the AJAX/JSON API approach, do you know how to make it so that a function only runs when a user goes to a specific page? Right now, If I put the AJAX code in the </ head> section of my customize dashboard, I can get it to work, but it runs every time the site is loaded (when I only want it to run when the group index page is loaded)

–In my case, I’m using AJAX right now especially because I not only need to show group owners, but also a few other new traits of a group that I am adding. These would be like custom fields of the group that I am trying to retrieve and show. Right now, the “MVP” version (while I’m still learning how the discourse codebase works) is to save them to a separate non-discourse database that I pull from and add to the group index page.

Obviously, the cleaner solution would be to add the custom traits to the groups in the discourse database and call those back. Just trying to gauge the type of operation that would be required here. Would that require redoing a bunch of the discourse files (controllers, models, templates)?

1 Like

I can put it in a little GitHub repo when I get a chance.

I don’t think that gives you access to the owner data, but maybe I am missing something.

Regarding your questions:

  1. The theme component I linked to does something similar to make sure the ajax call only happens on /latest or the homepage. I would try to build off of that idea: https://github.com/awesomerobot/discourse-featured-topics/blob/ddf3d7e003423e2e5f83446a80cab78d51f09e2d/common/head_tag.html#L12

    Also if you haven’t already, definitely take a look at Developer’s guide to Discourse Themes

  2. There is no built-in concept of custom group field like there is with custom user fields. I believe you would need to build a plugin that adds all the necessary bits and pieces for that to work.

2 Likes

You’re right. I was forgetting–that is something I also store in a separate database that I call with AJAX (and some other magic) right now.

Great idea–I see what you did with if ((url == "/") || (url == "/latest") ) in that repo. I can do something similar.

Yes–I’ve gone through that guide as well as the other ones I’ve been able to find. It’s a great guide, but I have found the transition from that guide (and other ones on meta) to implementing this kind of customization to be tricky. These customizations require, as far as I can tell, really understanding how the templates, controllers, and models fit together in the Discourse codebase.

Digression

Ember may be a great system for building performant apps, but I do find it difficult to tease out how the different files are related. For example, finding the right template in a view in the Github repo does not tell you much about what other templates that one links to, and then especially which controllers and other models may be relevant. It’s possible, but slow going, and you really need to understand those relationships to do these customizations.

In Angular, as an alternative method, the pieces of the view components are normally grouped together with an html, typescript, and css file, and then other relevant files are clearly marked in these files (so services that are in use are identified in the typescript file and other components that are inserted are clearly marked in the html file). As far as I can see that’s not how the Discourse ember structure works (not to knock that structure–its a very stable, highly performant app–it just takes getting used to).

Hence my using stuff like AJAX to get at the same result while I continue to try to understand how the discourse pieces fit together.

2 Likes