Retrieve Topics based on custom field?

Using @angus’s very helpful guide, I have been able to add custom fields to topics and groups.

Once you successfully add a custom field, is there an (efficient) way to retrieve items based on that custom field ?

For example, let’s say I add the custom field fun_level to topics. So now there’s the field topic.fun_level (a string) added through my plugin.

Now, I want to retrieve and show a topic list of all topics where fun_level = “super-duper-fun”.

How would I do that?


If we want to retrieve all topics with, say, a certain tag, we can use ajax('/tags/tag-name.json').then(function(result))... , like used in this explanatory post.

For a custom field we’ve just created, this wouldn’t be available (I think). As this would require creating a controller for the custom field (like a controller for fun_level , with a show method that somehow retrieves all the topics where fun_level equals that :fun_level param value, or something like this).

I assume there is a more straightforward way?

1 Like

I think I have figured out one method to retrieve topics based on a custom field: search for them and retrieve the results.

Here’s an example, imagining you have a button with the action “searchForTopics()” somewhere, and you are trying to get back topics with the custom field fun_level equal to “super-duper-fun”:

(this would go in some js.es6 code, like an initializer):


import Topic from 'discourse/models/topic'; //not necessary based on the code below, but probably relevant for other related actions you'd want to take
import { ajax } from 'discourse/lib/ajax';

export default {
    actions: {
        checkTopic(){
            let custom_field_value = 'super-duper-fun'
            let searchTerm = 'fun_level:' + custom_field_value
            let args = { q: searchTerm }
            ajax("/search", { data: args }).then(results => {
                let topics = results.topics
                topics.forEach(topic => {
                   //just to get a list of the topic names
                    console.log('topic name = ')
                    console.log(topic)
                })
            })
        }
    }
}

This works. But is this the most efficient way to do it?

For example, is this method, using search, as efficient as the method discourse uses to show you topics that match a certain tag when you go to that tag page?

1 Like

The way to do this is

  1. On the client: Add a topic query param using api.addDiscoveryQueryParam

  2. On the server: Filter topic queries by the param using add_custom_filter in the TopicQuery class (see lib/topic_query)

The add_custom_filter callback will look a bit like this

::TopicQuery.add_custom_filter(:field_name) do |topics, query|
  if query.options[:field_name]
    topics.where("topics.id in (
      SELECT topic_id FROM topic_custom_fields
      WHERE (name = 'field_name')
      AND value = '#{query.options[:field_name]}'
    )")
  else
    topics
  end
end
3 Likes

EDIT: Having looked into api.addDiscoveryQueryParam more, I think I now get the general idea:

I want to programmatically retrieve all topics with the custom field fun_level = super-duper-fun. I think perhaps a controller method could do it? (still figuring that out).

An alternative is to do a search with ajax("/search") where I am searching all topics based on the custom field fun_level=super-duper-fun. But creating the custom field is not enough to enable this. I need to make the custom field fun_level one of the fields you can search against (just like you can search again certain categories, tags, etc), and that is not done automatically.

In some way, api.addDiscoveryQueryParam in a js file mixed with TopicQuery in plugin.rb is required to do that. But, tbh, I just haven’t gotten it to work. I’ve seen some plugins that use these methods, but I haven’t been able to sort out how they “bring it home”. I think some additional code is required, but I haven’t found it yet.

How you go from these methods to actually having the custom field available as a search term?

Earlier Reply

Thanks, @angus. To clarify the goal is not to have users manually enter search values in the search box. The goal is to go to programmatically retrieve topics based on a certain custom field. For example, the user would go to the page /fun_levels/super-duper-fun and load all topics where the field fun_level = ‘super-duper-fun’.

Is api.addDiscoveryQueryParam for that purpose?

Looking at examples like here, I’m not sure how addDiscoveryQueryParam works to actually retrieve the topics (I don’t think calling that method returns results that I can parse).

Maybe it is for potentially allowing the user to manually search by the term in the search box? That’s not the situation I am aiming at. (I could definitely be missing something).

I mentioned using ajax("/search…") earlier as that’s the best I’ve come up with so far to return topics, but I am wondering if there is a more efficient way to do it, even including setting up a model and controller method so show topics automatically, like how tags/:tag-name does it (that’s more complex, so I’m hoping to avoid it, but if it’s the best way I’ll consider it).

The best approach here depends on your final goal.

How are you envisaging this working? As an option in the sidebar in /search?

Hi @angus. The goal is not to add a query on the side search bar (that would be nice, but not the goal here). The goal is to programmatically load topics based on a custom field into a topic-list when the user visits a page. I think I’ve figured out the template/component (ie, view) piece. Now trying to figure out the logic that will load the topics.

The reason for talking about search was my thought that running ajax("/search") for custom_field=value when you visit the page might be a clean way of loading the topics. But I’m just trying to figure out a way that works best.


More details:

In my case, the first goal is to have the user go to a new template page I have created at a new path (/fun_levels/:fun_level), and load all topics with the custom field fun_level matching :fun_level.

I have separately figured out how to create the template and load it at the path. Now I want to programmatically load in the matching topics into the topic-list component I have on the page.

Ideally, I’d avoid having to create a new “fun_level” model (which i have not done yet), just to keep things more straightforward and faster to implement. But I am open to that if inevitably that will be significantly more performant (this is a page that will be used a lot).


How to add the ability to have “fun_level” be an option on the search sidebar would be good to know too–as I’d expect to like to have that too. And maybe the best way to load topics based on the custom field is to add the custom field to the search options, and then call ajax("/search") with the query being “fun_level: super-duper-fun”.

So the search stuff could be important here. But the primary job right now is to load topics on a page based on a custom field when the user visits that page.

Is this page with a topic list meant to feel similar to existing topic list pages in Discourse, or is it substantively different?

2 Likes

Substantively different. But the focus here is just how to load topics that have a custom field value (say, fun_level = ‘super-duper-fun’) into the {{topic-list topics=selectedTopics showPosters=true}} component I am inserting into the page.

When you’re dealing with topic lists the question is whether you extend the existing Discourse discovery structure, or whether you create your own, which changes the implementation. There are many related questions to what you’re looking to do which we’re skipping over here.

So if you’re not extending the discovery structure, you won’t be doing the first part of what I suggested above. You should still do the second part though, adding the custom filter to TopicQuery. You’ll also need a client side route with an ajax call to an endpoint mapped to the list_controller.rb. You can find the list controller routes in config/routes.rb by doing a search for list#.

You should use the same topic list endpoint Discourse discovery uses, as you’ll then get things like pagination (handled by load-on-scroll in the topic list component), permission handling and many other things out of the box.

So you’ll need

  1. plugin.rb containing the custom filter
  2. client side route file
  3. client side template
1 Like

Thanks for the info.

I’m happy doing whatever way is easiest to just get the topics back that match the custom field value. I have a working route/path/template in place at /fun_levels/:fun_level, which loads the {{topic-list}} component. Again, if that way is doing a search for that custom field value (ajax(/search)), then that works too. I’m increasingly thinking that is the most straightforward way. I just haven’t gotten that to work yet.

And to clarify, my current method is to

  1. get back topics by ajax (just need to figure out what the right endpoint is/how to set that endpoint up–that’s the key),
  2. parse the result, and
  3. do component.set('showTopics', parsed-result), to load the topics into {{topic-list topics=showTopics}}.

This way seems a bit mysterious to me. I see the methods in the list_controller, such as def topics_by, but how would I take one of those methods, and alter it to return topics based on a custom field value?

I would advise agianst this for multiple reasons. Sorry to be mysterious, but it’d take a bit more time than I have right now to explain why in full.

The easiest way is to use one of the existing endpoints in the list controller. They’re already setup to serve lists of topics. You can find them in routes.rb, but in short, they’re the filters /latest, /top etc. For a list with a custom filter, You’ll want to use a query param like this

/latest?fun_level=5

Using the custom filter. You can follow the TopicQuery class through from the list_controller.rb to see how it works, e.g. it adds your custom filter as a supported parameter to the controller. The reason it feels mysterious is because that controller and class are handling a bunch of things for you like pagination and different filters which you’ll need to setup manually if you do it another way.

The “other way” which would make sense (not using /search mind you), would be to set up your own dedicated controller for the route, which uses the TopicQuery class like the list_controller.rb does. You’ll need to create a rails controller if you’re adding a completely new route in any event, so this is another plausible approach, albeit you’ll need to handle things like pagination yourself. You should still use a custom filter if you use this approach.

I understand some of this may seem opaque, however you’re dealing with complicated functionality here. To explain this in full I’d need to write up a 10 part course. Which I may do soon actually :slight_smile:

2 Likes

UPDATE: I think I’ve got it (mostly) working (!). Now, this will show the “latest” topics matching the custom field value. (the #latest method was the closest one I could find that made sense in the config/routes.rb file).

It’s important that, in fact, all topics that have the relevant custom field fun_level value are loaded onto the page. Is there something else I need to do to make that happen?


Here’s the code for my own notes and in case helpful to others:

–I’ve created the custom field :fun_level. Then:

plugin.rb

TopicQuery.add_custom_filter(:fun_level) do |topics, query|
  if query.options[:fun_level]
    topics.where("topics.id in (
      SELECT topic_id FROM topic_custom_fields
      WHERE (name = 'fun_level')
      AND value = '#{query.options[:fun_level]}'
    )")
  else
    topics
  end
end

/connectors/my-plugin-outlet/fun-level.js.es6 (a javascript file that is activated when going to relevant page. So this javascript could be in an initializer or in a connector linking up to a plugin outlet. I like to use code that goes with a connector, so I’ll use setup component here):

const ajax = require('discourse/lib/ajax')

export default {
    setupComponent(args, component) {
      let parsedResultArray = []
      var endPoint = '/latest?fun_level=' + funLevel  //funLevel = variable with value from the params   
      ajax(endPoint).then(function (result) {
            console.log('topic list result for topics matching that fun level = ')
            console.log(result.topic_list.topics)
            //parse results, and load them into parsedResultArray
            component.set('showTopics', parsedResultArray        
      })
   }
}

Now, the topics will be loaded into the {{topic-list topics=showTopics}} I have in the corresponding component, that’s put in the template through my-plugin-outlet.

This is a big step forward. Thank you very much, @angus.

2 Likes